]>
Commit | Line | Data |
---|---|---|
9f95a23c | 1 | import { Component, Input, OnChanges, OnInit, TemplateRef, ViewChild } from '@angular/core'; |
39ae355f | 2 | import { AbstractControl, Validators } from '@angular/forms'; |
9f95a23c | 3 | |
9f95a23c TL |
4 | import { |
5 | ITreeOptions, | |
9f95a23c TL |
6 | TreeComponent, |
7 | TreeModel, | |
e306af50 TL |
8 | TreeNode, |
9 | TREE_ACTIONS | |
f67539c2 TL |
10 | } from '@circlon/angular-tree-component'; |
11 | import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; | |
12 | import _ from 'lodash'; | |
13 | import moment from 'moment'; | |
14 | ||
15 | import { CephfsService } from '~/app/shared/api/cephfs.service'; | |
16 | import { ConfirmationModalComponent } from '~/app/shared/components/confirmation-modal/confirmation-modal.component'; | |
17 | import { CriticalConfirmationModalComponent } from '~/app/shared/components/critical-confirmation-modal/critical-confirmation-modal.component'; | |
18 | import { FormModalComponent } from '~/app/shared/components/form-modal/form-modal.component'; | |
19 | import { ActionLabelsI18n } from '~/app/shared/constants/app.constants'; | |
20 | import { Icons } from '~/app/shared/enum/icons.enum'; | |
21 | import { NotificationType } from '~/app/shared/enum/notification-type.enum'; | |
22 | import { CdValidators } from '~/app/shared/forms/cd-validators'; | |
23 | import { CdFormModalFieldConfig } from '~/app/shared/models/cd-form-modal-field-config'; | |
24 | import { CdTableAction } from '~/app/shared/models/cd-table-action'; | |
25 | import { CdTableColumn } from '~/app/shared/models/cd-table-column'; | |
26 | import { CdTableSelection } from '~/app/shared/models/cd-table-selection'; | |
9f95a23c TL |
27 | import { |
28 | CephfsDir, | |
29 | CephfsQuotas, | |
30 | CephfsSnapshot | |
f67539c2 TL |
31 | } from '~/app/shared/models/cephfs-directory-models'; |
32 | import { Permission } from '~/app/shared/models/permissions'; | |
33 | import { CdDatePipe } from '~/app/shared/pipes/cd-date.pipe'; | |
34 | import { DimlessBinaryPipe } from '~/app/shared/pipes/dimless-binary.pipe'; | |
35 | import { AuthStorageService } from '~/app/shared/services/auth-storage.service'; | |
36 | import { ModalService } from '~/app/shared/services/modal.service'; | |
37 | import { NotificationService } from '~/app/shared/services/notification.service'; | |
9f95a23c TL |
38 | |
39 | class QuotaSetting { | |
40 | row: { | |
41 | // Used in quota table | |
42 | name: string; | |
43 | value: number | string; | |
44 | originPath: string; | |
45 | }; | |
46 | quotaKey: string; | |
47 | dirValue: number; | |
48 | nextTreeMaximum: { | |
49 | value: number; | |
50 | path: string; | |
51 | }; | |
52 | } | |
53 | ||
54 | @Component({ | |
55 | selector: 'cd-cephfs-directories', | |
56 | templateUrl: './cephfs-directories.component.html', | |
57 | styleUrls: ['./cephfs-directories.component.scss'] | |
58 | }) | |
59 | export class CephfsDirectoriesComponent implements OnInit, OnChanges { | |
f67539c2 | 60 | @ViewChild(TreeComponent) |
9f95a23c TL |
61 | treeComponent: TreeComponent; |
62 | @ViewChild('origin', { static: true }) | |
63 | originTmpl: TemplateRef<any>; | |
64 | ||
65 | @Input() | |
66 | id: number; | |
67 | ||
f67539c2 | 68 | private modalRef: NgbModalRef; |
9f95a23c TL |
69 | private dirs: CephfsDir[]; |
70 | private nodeIds: { [path: string]: CephfsDir }; | |
71 | private requestedPaths: string[]; | |
72 | private loadingTimeout: any; | |
73 | ||
74 | icons = Icons; | |
75 | loadingIndicator = false; | |
76 | loading = {}; | |
77 | treeOptions: ITreeOptions = { | |
78 | useVirtualScroll: true, | |
79 | getChildren: (node: TreeNode): Promise<any[]> => { | |
80 | return this.updateDirectory(node.id); | |
81 | }, | |
82 | actionMapping: { | |
83 | mouse: { | |
84 | click: this.selectAndShowNode.bind(this), | |
85 | expanderClick: this.selectAndShowNode.bind(this) | |
86 | } | |
87 | } | |
88 | }; | |
89 | ||
90 | permission: Permission; | |
91 | selectedDir: CephfsDir; | |
92 | settings: QuotaSetting[]; | |
93 | quota: { | |
94 | columns: CdTableColumn[]; | |
95 | selection: CdTableSelection; | |
96 | tableActions: CdTableAction[]; | |
97 | updateSelection: Function; | |
98 | }; | |
99 | snapshot: { | |
100 | columns: CdTableColumn[]; | |
101 | selection: CdTableSelection; | |
102 | tableActions: CdTableAction[]; | |
103 | updateSelection: Function; | |
104 | }; | |
105 | nodes: any[]; | |
39ae355f | 106 | alreadyExists: boolean; |
9f95a23c TL |
107 | |
108 | constructor( | |
109 | private authStorageService: AuthStorageService, | |
f67539c2 | 110 | private modalService: ModalService, |
9f95a23c TL |
111 | private cephfsService: CephfsService, |
112 | private cdDatePipe: CdDatePipe, | |
9f95a23c TL |
113 | private actionLabels: ActionLabelsI18n, |
114 | private notificationService: NotificationService, | |
115 | private dimlessBinaryPipe: DimlessBinaryPipe | |
116 | ) {} | |
117 | ||
118 | private selectAndShowNode(tree: TreeModel, node: TreeNode, $event: any) { | |
119 | TREE_ACTIONS.TOGGLE_EXPANDED(tree, node, $event); | |
120 | this.selectNode(node); | |
121 | } | |
122 | ||
123 | private selectNode(node: TreeNode) { | |
124 | TREE_ACTIONS.TOGGLE_ACTIVE(undefined, node, undefined); | |
125 | this.selectedDir = this.getDirectory(node); | |
126 | if (node.id === '/') { | |
127 | return; | |
128 | } | |
129 | this.setSettings(node); | |
130 | } | |
131 | ||
132 | ngOnInit() { | |
133 | this.permission = this.authStorageService.getPermissions().cephfs; | |
134 | this.setUpQuotaTable(); | |
135 | this.setUpSnapshotTable(); | |
136 | } | |
137 | ||
138 | private setUpQuotaTable() { | |
139 | this.quota = { | |
140 | columns: [ | |
141 | { | |
142 | prop: 'row.name', | |
f67539c2 | 143 | name: $localize`Name`, |
9f95a23c TL |
144 | flexGrow: 1 |
145 | }, | |
146 | { | |
147 | prop: 'row.value', | |
f67539c2 | 148 | name: $localize`Value`, |
9f95a23c TL |
149 | sortable: false, |
150 | flexGrow: 1 | |
151 | }, | |
152 | { | |
153 | prop: 'row.originPath', | |
f67539c2 | 154 | name: $localize`Origin`, |
9f95a23c TL |
155 | sortable: false, |
156 | cellTemplate: this.originTmpl, | |
157 | flexGrow: 1 | |
158 | } | |
159 | ], | |
160 | selection: new CdTableSelection(), | |
161 | updateSelection: (selection: CdTableSelection) => { | |
162 | this.quota.selection = selection; | |
163 | }, | |
164 | tableActions: [ | |
165 | { | |
166 | name: this.actionLabels.SET, | |
167 | icon: Icons.edit, | |
168 | permission: 'update', | |
169 | visible: (selection) => | |
170 | !selection.hasSelection || (selection.first() && selection.first().dirValue === 0), | |
171 | click: () => this.updateQuotaModal() | |
172 | }, | |
173 | { | |
174 | name: this.actionLabels.UPDATE, | |
175 | icon: Icons.edit, | |
176 | permission: 'update', | |
177 | visible: (selection) => selection.first() && selection.first().dirValue > 0, | |
178 | click: () => this.updateQuotaModal() | |
179 | }, | |
180 | { | |
181 | name: this.actionLabels.UNSET, | |
182 | icon: Icons.destroy, | |
183 | permission: 'update', | |
184 | disable: (selection) => | |
185 | !selection.hasSelection || (selection.first() && selection.first().dirValue === 0), | |
186 | click: () => this.unsetQuotaModal() | |
187 | } | |
188 | ] | |
189 | }; | |
190 | } | |
191 | ||
192 | private setUpSnapshotTable() { | |
193 | this.snapshot = { | |
194 | columns: [ | |
195 | { | |
196 | prop: 'name', | |
f67539c2 | 197 | name: $localize`Name`, |
9f95a23c TL |
198 | flexGrow: 1 |
199 | }, | |
200 | { | |
201 | prop: 'path', | |
f67539c2 | 202 | name: $localize`Path`, |
9f95a23c TL |
203 | isHidden: true, |
204 | flexGrow: 2 | |
205 | }, | |
206 | { | |
207 | prop: 'created', | |
f67539c2 | 208 | name: $localize`Created`, |
9f95a23c TL |
209 | flexGrow: 1, |
210 | pipe: this.cdDatePipe | |
211 | } | |
212 | ], | |
213 | selection: new CdTableSelection(), | |
214 | updateSelection: (selection: CdTableSelection) => { | |
215 | this.snapshot.selection = selection; | |
216 | }, | |
217 | tableActions: [ | |
218 | { | |
219 | name: this.actionLabels.CREATE, | |
220 | icon: Icons.add, | |
221 | permission: 'create', | |
222 | canBePrimary: (selection) => !selection.hasSelection, | |
522d829b TL |
223 | click: () => this.createSnapshot(), |
224 | disable: () => this.disableCreateSnapshot() | |
9f95a23c TL |
225 | }, |
226 | { | |
227 | name: this.actionLabels.DELETE, | |
228 | icon: Icons.destroy, | |
229 | permission: 'delete', | |
230 | click: () => this.deleteSnapshotModal(), | |
231 | canBePrimary: (selection) => selection.hasSelection, | |
232 | disable: (selection) => !selection.hasSelection | |
233 | } | |
234 | ] | |
235 | }; | |
236 | } | |
237 | ||
522d829b TL |
238 | private disableCreateSnapshot(): string | boolean { |
239 | const folders = this.selectedDir.path.split('/').slice(1); | |
240 | // With deph of 4 or more we have the subvolume files/folders for which we cannot create | |
241 | // a snapshot. Somehow, you can create a snapshot of the subvolume but not its files. | |
242 | if (folders.length >= 4 && folders[0] === 'volumes') { | |
243 | return $localize`Cannot create snapshots for files/folders in the subvolume ${folders[2]}`; | |
244 | } | |
245 | return false; | |
246 | } | |
247 | ||
9f95a23c TL |
248 | ngOnChanges() { |
249 | this.selectedDir = undefined; | |
250 | this.dirs = []; | |
251 | this.requestedPaths = []; | |
252 | this.nodeIds = {}; | |
253 | if (this.id) { | |
254 | this.setRootNode(); | |
255 | this.firstCall(); | |
256 | } | |
257 | } | |
258 | ||
259 | private setRootNode() { | |
260 | this.nodes = [ | |
261 | { | |
262 | name: '/', | |
263 | id: '/', | |
264 | isExpanded: true | |
265 | } | |
266 | ]; | |
267 | } | |
268 | ||
269 | private firstCall() { | |
270 | const path = '/'; | |
271 | setTimeout(() => { | |
272 | this.getNode(path).loadNodeChildren(); | |
273 | }, 10); | |
274 | } | |
275 | ||
276 | updateDirectory(path: string): Promise<any[]> { | |
277 | this.unsetLoadingIndicator(); | |
278 | if (!this.requestedPaths.includes(path)) { | |
279 | this.requestedPaths.push(path); | |
280 | } else if (this.loading[path] === true) { | |
281 | return undefined; // Path is currently fetched. | |
282 | } | |
283 | return new Promise((resolve) => { | |
284 | this.setLoadingIndicator(path, true); | |
285 | this.cephfsService.lsDir(this.id, path).subscribe((dirs) => { | |
286 | this.updateTreeStructure(dirs); | |
287 | this.updateQuotaTable(); | |
288 | this.updateTree(); | |
289 | resolve(this.getChildren(path)); | |
290 | this.setLoadingIndicator(path, false); | |
291 | }); | |
292 | }); | |
293 | } | |
294 | ||
295 | private setLoadingIndicator(path: string, loading: boolean) { | |
296 | this.loading[path] = loading; | |
297 | this.unsetLoadingIndicator(); | |
298 | } | |
299 | ||
300 | private getSubDirectories(path: string, tree: CephfsDir[] = this.dirs): CephfsDir[] { | |
301 | return tree.filter((d) => d.parent === path); | |
302 | } | |
303 | ||
304 | private getChildren(path: string): any[] { | |
305 | const subTree = this.getSubTree(path); | |
306 | return _.sortBy(this.getSubDirectories(path), 'path').map((dir) => | |
307 | this.createNode(dir, subTree) | |
308 | ); | |
309 | } | |
310 | ||
311 | private createNode(dir: CephfsDir, subTree?: CephfsDir[]): any { | |
312 | this.nodeIds[dir.path] = dir; | |
313 | if (!subTree) { | |
314 | this.getSubTree(dir.parent); | |
315 | } | |
316 | return { | |
317 | name: dir.name, | |
318 | id: dir.path, | |
319 | hasChildren: this.getSubDirectories(dir.path, subTree).length > 0 | |
320 | }; | |
321 | } | |
322 | ||
323 | private getSubTree(path: string): CephfsDir[] { | |
324 | return this.dirs.filter((d) => d.parent && d.parent.startsWith(path)); | |
325 | } | |
326 | ||
327 | private setSettings(node: TreeNode) { | |
328 | const readable = (value: number, fn?: (arg0: number) => number | string): number | string => | |
329 | value ? (fn ? fn(value) : value) : ''; | |
330 | ||
331 | this.settings = [ | |
332 | this.getQuota(node, 'max_files', readable), | |
333 | this.getQuota(node, 'max_bytes', (value) => | |
334 | readable(value, (v) => this.dimlessBinaryPipe.transform(v)) | |
335 | ) | |
336 | ]; | |
337 | } | |
338 | ||
339 | private getQuota( | |
340 | tree: TreeNode, | |
341 | quotaKey: string, | |
342 | valueConvertFn: (number: number) => number | string | |
343 | ): QuotaSetting { | |
344 | // Get current maximum | |
345 | const currentPath = tree.id; | |
346 | tree = this.getOrigin(tree, quotaKey); | |
347 | const dir = this.getDirectory(tree); | |
348 | const value = dir.quotas[quotaKey]; | |
349 | // Get next tree maximum | |
350 | // => The value that isn't changeable through a change of the current directories quota value | |
351 | let nextMaxValue = value; | |
352 | let nextMaxPath = dir.path; | |
353 | if (tree.id === currentPath) { | |
354 | if (tree.parent.id === '/') { | |
355 | // The value will never inherit any other value, so it has no maximum. | |
356 | nextMaxValue = 0; | |
357 | } else { | |
358 | const nextMaxDir = this.getDirectory(this.getOrigin(tree.parent, quotaKey)); | |
359 | nextMaxValue = nextMaxDir.quotas[quotaKey]; | |
360 | nextMaxPath = nextMaxDir.path; | |
361 | } | |
362 | } | |
363 | return { | |
364 | row: { | |
f67539c2 | 365 | name: quotaKey === 'max_bytes' ? $localize`Max size` : $localize`Max files`, |
9f95a23c TL |
366 | value: valueConvertFn(value), |
367 | originPath: value ? dir.path : '' | |
368 | }, | |
369 | quotaKey, | |
370 | dirValue: this.nodeIds[currentPath].quotas[quotaKey], | |
371 | nextTreeMaximum: { | |
372 | value: nextMaxValue, | |
373 | path: nextMaxValue ? nextMaxPath : '' | |
374 | } | |
375 | }; | |
376 | } | |
377 | ||
378 | /** | |
379 | * Get the node where the quota limit originates from in the current node | |
380 | * | |
381 | * Example as it's a recursive method: | |
382 | * | |
383 | * | Path + Value | Call depth | useOrigin? | Output | | |
384 | * |:-------------:|:----------:|:---------------------:|:------:| | |
385 | * | /a/b/c/d (15) | 1st | 2nd (5) < 15 => false | /a/b | | |
386 | * | /a/b/c (20) | 2nd | 3rd (5) < 20 => false | /a/b | | |
387 | * | /a/b (5) | 3rd | 4th (10) < 5 => true | /a/b | | |
388 | * | /a (10) | 4th | 10 => true | /a | | |
389 | * | |
390 | */ | |
391 | private getOrigin(tree: TreeNode, quotaSetting: string): TreeNode { | |
392 | if (tree.parent && tree.parent.id !== '/') { | |
393 | const current = this.getQuotaFromTree(tree, quotaSetting); | |
394 | ||
395 | // Get the next used quota and node above the current one (until it hits the root directory) | |
396 | const originTree = this.getOrigin(tree.parent, quotaSetting); | |
397 | const inherited = this.getQuotaFromTree(originTree, quotaSetting); | |
398 | ||
399 | // Select if the current quota is in use or the above | |
400 | const useOrigin = current === 0 || (inherited !== 0 && inherited < current); | |
401 | return useOrigin ? originTree : tree; | |
402 | } | |
403 | return tree; | |
404 | } | |
405 | ||
406 | private getQuotaFromTree(tree: TreeNode, quotaSetting: string): number { | |
407 | return this.getDirectory(tree).quotas[quotaSetting]; | |
408 | } | |
409 | ||
410 | private getDirectory(node: TreeNode): CephfsDir { | |
411 | const path = node.id as string; | |
412 | return this.nodeIds[path]; | |
413 | } | |
414 | ||
415 | selectOrigin(path: string) { | |
416 | this.selectNode(this.getNode(path)); | |
417 | } | |
418 | ||
419 | private getNode(path: string): TreeNode { | |
420 | return this.treeComponent.treeModel.getNodeById(path); | |
421 | } | |
422 | ||
423 | updateQuotaModal() { | |
424 | const path = this.selectedDir.path; | |
425 | const selection: QuotaSetting = this.quota.selection.first(); | |
426 | const nextMax = selection.nextTreeMaximum; | |
427 | const key = selection.quotaKey; | |
428 | const value = selection.dirValue; | |
429 | this.modalService.show(FormModalComponent, { | |
f67539c2 TL |
430 | titleText: this.getModalQuotaTitle( |
431 | value === 0 ? this.actionLabels.SET : this.actionLabels.UPDATE, | |
432 | path | |
433 | ), | |
434 | message: nextMax.value | |
435 | ? $localize`The inherited ${this.getQuotaValueFromPathMsg( | |
436 | nextMax.value, | |
437 | nextMax.path | |
438 | )} is the maximum value to be used.` | |
439 | : undefined, | |
440 | fields: [this.getQuotaFormField(selection.row.name, key, value, nextMax.value)], | |
441 | submitButtonText: $localize`Save`, | |
442 | onSubmit: (values: CephfsQuotas) => this.updateQuota(values) | |
9f95a23c TL |
443 | }); |
444 | } | |
445 | ||
446 | private getModalQuotaTitle(action: string, path: string): string { | |
f67539c2 | 447 | return $localize`${action} CephFS ${this.getQuotaName()} quota for '${path}'`; |
9f95a23c TL |
448 | } |
449 | ||
450 | private getQuotaName(): string { | |
f67539c2 | 451 | return this.isBytesQuotaSelected() ? $localize`size` : $localize`files`; |
9f95a23c TL |
452 | } |
453 | ||
454 | private isBytesQuotaSelected(): boolean { | |
455 | return this.quota.selection.first().quotaKey === 'max_bytes'; | |
456 | } | |
457 | ||
458 | private getQuotaValueFromPathMsg(value: number, path: string): string { | |
f67539c2 TL |
459 | value = this.isBytesQuotaSelected() ? this.dimlessBinaryPipe.transform(value) : value; |
460 | ||
461 | return $localize`${this.getQuotaName()} quota ${value} from '${path}'`; | |
9f95a23c TL |
462 | } |
463 | ||
464 | private getQuotaFormField( | |
465 | label: string, | |
466 | name: string, | |
467 | value: number, | |
468 | maxValue: number | |
469 | ): CdFormModalFieldConfig { | |
470 | const isBinary = name === 'max_bytes'; | |
471 | const formValidators = [isBinary ? CdValidators.binaryMin(0) : Validators.min(0)]; | |
472 | if (maxValue) { | |
473 | formValidators.push(isBinary ? CdValidators.binaryMax(maxValue) : Validators.max(maxValue)); | |
474 | } | |
475 | const field: CdFormModalFieldConfig = { | |
476 | type: isBinary ? 'binary' : 'number', | |
477 | label, | |
478 | name, | |
479 | value, | |
480 | validators: formValidators, | |
481 | required: true | |
482 | }; | |
483 | if (!isBinary) { | |
484 | field.errors = { | |
f67539c2 TL |
485 | min: $localize`Value has to be at least 0 or more`, |
486 | max: $localize`Value has to be at most ${maxValue} or less` | |
9f95a23c TL |
487 | }; |
488 | } | |
489 | return field; | |
490 | } | |
491 | ||
492 | private updateQuota(values: CephfsQuotas, onSuccess?: Function) { | |
493 | const path = this.selectedDir.path; | |
494 | const key = this.quota.selection.first().quotaKey; | |
495 | const action = | |
496 | this.selectedDir.quotas[key] === 0 | |
497 | ? this.actionLabels.SET | |
498 | : values[key] === 0 | |
499 | ? this.actionLabels.UNSET | |
f67539c2 TL |
500 | : $localize`Updated`; |
501 | this.cephfsService.quota(this.id, path, values).subscribe(() => { | |
9f95a23c TL |
502 | if (onSuccess) { |
503 | onSuccess(); | |
504 | } | |
505 | this.notificationService.show( | |
506 | NotificationType.success, | |
507 | this.getModalQuotaTitle(action, path) | |
508 | ); | |
509 | this.forceDirRefresh(); | |
510 | }); | |
511 | } | |
512 | ||
513 | unsetQuotaModal() { | |
514 | const path = this.selectedDir.path; | |
515 | const selection: QuotaSetting = this.quota.selection.first(); | |
516 | const key = selection.quotaKey; | |
517 | const nextMax = selection.nextTreeMaximum; | |
518 | const dirValue = selection.dirValue; | |
519 | ||
f67539c2 TL |
520 | const quotaValue = this.getQuotaValueFromPathMsg(nextMax.value, nextMax.path); |
521 | const conclusion = | |
522 | nextMax.value > 0 | |
523 | ? nextMax.value > dirValue | |
524 | ? $localize`in order to inherit ${quotaValue}` | |
525 | : $localize`which isn't used because of the inheritance of ${quotaValue}` | |
526 | : $localize`in order to have no quota on the directory`; | |
527 | ||
9f95a23c | 528 | this.modalRef = this.modalService.show(ConfirmationModalComponent, { |
f67539c2 TL |
529 | titleText: this.getModalQuotaTitle(this.actionLabels.UNSET, path), |
530 | buttonText: this.actionLabels.UNSET, | |
531 | description: $localize`${this.actionLabels.UNSET} ${this.getQuotaValueFromPathMsg( | |
532 | dirValue, | |
533 | path | |
534 | )} ${conclusion}.`, | |
535 | onSubmit: () => this.updateQuota({ [key]: 0 }, () => this.modalRef.close()) | |
9f95a23c TL |
536 | }); |
537 | } | |
538 | ||
539 | createSnapshot() { | |
540 | // Create a snapshot. Auto-generate a snapshot name by default. | |
541 | const path = this.selectedDir.path; | |
542 | this.modalService.show(FormModalComponent, { | |
f67539c2 TL |
543 | titleText: $localize`Create Snapshot`, |
544 | message: $localize`Please enter the name of the snapshot.`, | |
545 | fields: [ | |
546 | { | |
547 | type: 'text', | |
548 | name: 'name', | |
549 | value: `${moment().toISOString(true)}`, | |
39ae355f TL |
550 | required: true, |
551 | validators: [this.validateValue.bind(this)] | |
9f95a23c | 552 | } |
f67539c2 TL |
553 | ], |
554 | submitButtonText: $localize`Create Snapshot`, | |
555 | onSubmit: (values: CephfsSnapshot) => { | |
39ae355f TL |
556 | if (!this.alreadyExists) { |
557 | this.cephfsService.mkSnapshot(this.id, path, values.name).subscribe((name) => { | |
558 | this.notificationService.show( | |
559 | NotificationType.success, | |
560 | $localize`Created snapshot '${name}' for '${path}'` | |
561 | ); | |
562 | this.forceDirRefresh(); | |
563 | }); | |
564 | } else { | |
f67539c2 | 565 | this.notificationService.show( |
39ae355f TL |
566 | NotificationType.error, |
567 | $localize`Snapshot name '${values.name}' is already in use. Please use another name.` | |
f67539c2 | 568 | ); |
39ae355f | 569 | } |
9f95a23c TL |
570 | } |
571 | }); | |
572 | } | |
573 | ||
39ae355f TL |
574 | validateValue(control: AbstractControl) { |
575 | this.alreadyExists = this.selectedDir.snapshots.some((s) => s.name === control.value); | |
576 | } | |
577 | ||
9f95a23c TL |
578 | /** |
579 | * Forces an update of the current selected directory | |
580 | * | |
581 | * As all nodes point by their path on an directory object, the easiest way is to update | |
582 | * the objects by merge with their latest change. | |
583 | */ | |
584 | private forceDirRefresh(path?: string) { | |
585 | if (!path) { | |
586 | const dir = this.selectedDir; | |
587 | if (!dir) { | |
588 | throw new Error('This function can only be called without path if an selection was made'); | |
589 | } | |
590 | // Parent has to be called in order to update the object referring | |
591 | // to the current selected directory | |
592 | path = dir.parent ? dir.parent : dir.path; | |
593 | } | |
594 | const node = this.getNode(path); | |
595 | node.loadNodeChildren(); | |
596 | } | |
597 | ||
598 | private updateTreeStructure(dirs: CephfsDir[]) { | |
599 | const getChildrenAndPaths = ( | |
600 | directories: CephfsDir[], | |
601 | parent: string | |
602 | ): { children: CephfsDir[]; paths: string[] } => { | |
603 | const children = directories.filter((d) => d.parent === parent); | |
604 | const paths = children.map((d) => d.path); | |
605 | return { children, paths }; | |
606 | }; | |
607 | ||
608 | const parents = _.uniq(dirs.map((d) => d.parent).sort()); | |
609 | parents.forEach((p) => { | |
610 | const received = getChildrenAndPaths(dirs, p); | |
611 | const cached = getChildrenAndPaths(this.dirs, p); | |
612 | ||
613 | cached.children.forEach((d) => { | |
614 | if (!received.paths.includes(d.path)) { | |
615 | this.removeOldDirectory(d); | |
616 | } | |
617 | }); | |
618 | received.children.forEach((d) => { | |
619 | if (cached.paths.includes(d.path)) { | |
620 | this.updateExistingDirectory(cached.children, d); | |
621 | } else { | |
622 | this.addNewDirectory(d); | |
623 | } | |
624 | }); | |
625 | }); | |
626 | } | |
627 | ||
628 | private removeOldDirectory(rmDir: CephfsDir) { | |
629 | const path = rmDir.path; | |
630 | // Remove directory from local variables | |
631 | _.remove(this.dirs, (d) => d.path === path); | |
632 | delete this.nodeIds[path]; | |
633 | this.updateDirectoriesParentNode(rmDir); | |
634 | } | |
635 | ||
636 | private updateDirectoriesParentNode(dir: CephfsDir) { | |
637 | const parent = dir.parent; | |
638 | if (!parent) { | |
639 | return; | |
640 | } | |
641 | const node = this.getNode(parent); | |
642 | if (!node) { | |
643 | // Node will not be found for new sub sub directories - this is the intended behaviour | |
644 | return; | |
645 | } | |
646 | const children = this.getChildren(parent); | |
647 | node.data.children = children; | |
648 | node.data.hasChildren = children.length > 0; | |
649 | this.treeComponent.treeModel.update(); | |
650 | } | |
651 | ||
652 | private addNewDirectory(newDir: CephfsDir) { | |
653 | this.dirs.push(newDir); | |
654 | this.nodeIds[newDir.path] = newDir; | |
655 | this.updateDirectoriesParentNode(newDir); | |
656 | } | |
657 | ||
658 | private updateExistingDirectory(source: CephfsDir[], updatedDir: CephfsDir) { | |
659 | const currentDirObject = source.find((sub) => sub.path === updatedDir.path); | |
660 | Object.assign(currentDirObject, updatedDir); | |
661 | } | |
662 | ||
663 | private updateQuotaTable() { | |
664 | const node = this.selectedDir ? this.getNode(this.selectedDir.path) : undefined; | |
665 | if (node && node.id !== '/') { | |
666 | this.setSettings(node); | |
667 | } | |
668 | } | |
669 | ||
670 | private updateTree(force: boolean = false) { | |
671 | if (this.loadingIndicator && !force) { | |
672 | // In order to make the page scrollable during load, the render cycle for each node | |
673 | // is omitted and only be called if all updates were loaded. | |
674 | return; | |
675 | } | |
676 | this.treeComponent.treeModel.update(); | |
677 | this.nodes = [...this.nodes]; | |
678 | this.treeComponent.sizeChanged(); | |
679 | } | |
680 | ||
681 | deleteSnapshotModal() { | |
682 | this.modalRef = this.modalService.show(CriticalConfirmationModalComponent, { | |
f67539c2 TL |
683 | itemDescription: $localize`CephFs Snapshot`, |
684 | itemNames: this.snapshot.selection.selected.map((snapshot: CephfsSnapshot) => snapshot.name), | |
685 | submitAction: () => this.deleteSnapshot() | |
9f95a23c TL |
686 | }); |
687 | } | |
688 | ||
689 | deleteSnapshot() { | |
690 | const path = this.selectedDir.path; | |
691 | this.snapshot.selection.selected.forEach((snapshot: CephfsSnapshot) => { | |
692 | const name = snapshot.name; | |
693 | this.cephfsService.rmSnapshot(this.id, path, name).subscribe(() => { | |
694 | this.notificationService.show( | |
695 | NotificationType.success, | |
f67539c2 | 696 | $localize`Deleted snapshot '${name}' for '${path}'` |
9f95a23c TL |
697 | ); |
698 | }); | |
699 | }); | |
f67539c2 | 700 | this.modalRef.close(); |
9f95a23c TL |
701 | this.forceDirRefresh(); |
702 | } | |
703 | ||
704 | refreshAllDirectories() { | |
705 | // In order to make the page scrollable during load, the render cycle for each node | |
706 | // is omitted and only be called if all updates were loaded. | |
707 | this.loadingIndicator = true; | |
708 | this.requestedPaths.map((path) => this.forceDirRefresh(path)); | |
709 | const interval = setInterval(() => { | |
710 | this.updateTree(true); | |
711 | if (!this.loadingIndicator) { | |
712 | clearInterval(interval); | |
713 | } | |
714 | }, 3000); | |
715 | } | |
716 | ||
717 | unsetLoadingIndicator() { | |
718 | if (!this.loadingIndicator) { | |
719 | return; | |
720 | } | |
721 | clearTimeout(this.loadingTimeout); | |
722 | this.loadingTimeout = setTimeout(() => { | |
723 | const loading = Object.values(this.loading).some((l) => l); | |
724 | if (loading) { | |
725 | return this.unsetLoadingIndicator(); | |
726 | } | |
727 | this.loadingIndicator = false; | |
728 | this.updateTree(); | |
729 | // The problem is that we can't subscribe to an useful updated tree event and the time | |
730 | // between fetching all calls and rebuilding the tree can take some time | |
731 | }, 3000); | |
732 | } | |
733 | } |