]> git.proxmox.com Git - ceph.git/blame - ceph/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-directories/cephfs-directories.component.ts
import ceph quincy 17.2.6
[ceph.git] / ceph / src / pybind / mgr / dashboard / frontend / src / app / ceph / cephfs / cephfs-directories / cephfs-directories.component.ts
CommitLineData
9f95a23c 1import { Component, Input, OnChanges, OnInit, TemplateRef, ViewChild } from '@angular/core';
39ae355f 2import { AbstractControl, Validators } from '@angular/forms';
9f95a23c 3
9f95a23c
TL
4import {
5 ITreeOptions,
9f95a23c
TL
6 TreeComponent,
7 TreeModel,
e306af50
TL
8 TreeNode,
9 TREE_ACTIONS
f67539c2
TL
10} from '@circlon/angular-tree-component';
11import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap';
12import _ from 'lodash';
13import moment from 'moment';
14
15import { CephfsService } from '~/app/shared/api/cephfs.service';
16import { ConfirmationModalComponent } from '~/app/shared/components/confirmation-modal/confirmation-modal.component';
17import { CriticalConfirmationModalComponent } from '~/app/shared/components/critical-confirmation-modal/critical-confirmation-modal.component';
18import { FormModalComponent } from '~/app/shared/components/form-modal/form-modal.component';
19import { ActionLabelsI18n } from '~/app/shared/constants/app.constants';
20import { Icons } from '~/app/shared/enum/icons.enum';
21import { NotificationType } from '~/app/shared/enum/notification-type.enum';
22import { CdValidators } from '~/app/shared/forms/cd-validators';
23import { CdFormModalFieldConfig } from '~/app/shared/models/cd-form-modal-field-config';
24import { CdTableAction } from '~/app/shared/models/cd-table-action';
25import { CdTableColumn } from '~/app/shared/models/cd-table-column';
26import { CdTableSelection } from '~/app/shared/models/cd-table-selection';
9f95a23c
TL
27import {
28 CephfsDir,
29 CephfsQuotas,
30 CephfsSnapshot
f67539c2
TL
31} from '~/app/shared/models/cephfs-directory-models';
32import { Permission } from '~/app/shared/models/permissions';
33import { CdDatePipe } from '~/app/shared/pipes/cd-date.pipe';
34import { DimlessBinaryPipe } from '~/app/shared/pipes/dimless-binary.pipe';
35import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
36import { ModalService } from '~/app/shared/services/modal.service';
37import { NotificationService } from '~/app/shared/services/notification.service';
9f95a23c
TL
38
39class 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})
59export 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}