]> git.proxmox.com Git - ceph.git/blob - ceph/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-directories/cephfs-directories.component.ts
import 15.2.0 Octopus source
[ceph.git] / ceph / src / pybind / mgr / dashboard / frontend / src / app / ceph / cephfs / cephfs-directories / cephfs-directories.component.ts
1 import { Component, Input, OnChanges, OnInit, TemplateRef, ViewChild } from '@angular/core';
2 import { Validators } from '@angular/forms';
3
4 import { I18n } from '@ngx-translate/i18n-polyfill';
5 import {
6 ITreeOptions,
7 TREE_ACTIONS,
8 TreeComponent,
9 TreeModel,
10 TreeNode
11 } from 'angular-tree-component';
12 import * as _ from 'lodash';
13 import * as moment from 'moment';
14 import { BsModalRef, BsModalService } from 'ngx-bootstrap/modal';
15
16 import { CephfsService } from '../../../shared/api/cephfs.service';
17 import { ConfirmationModalComponent } from '../../../shared/components/confirmation-modal/confirmation-modal.component';
18 import { CriticalConfirmationModalComponent } from '../../../shared/components/critical-confirmation-modal/critical-confirmation-modal.component';
19 import { FormModalComponent } from '../../../shared/components/form-modal/form-modal.component';
20 import { ActionLabelsI18n } from '../../../shared/constants/app.constants';
21 import { Icons } from '../../../shared/enum/icons.enum';
22 import { NotificationType } from '../../../shared/enum/notification-type.enum';
23 import { CdValidators } from '../../../shared/forms/cd-validators';
24 import { CdFormModalFieldConfig } from '../../../shared/models/cd-form-modal-field-config';
25 import { CdTableAction } from '../../../shared/models/cd-table-action';
26 import { CdTableColumn } from '../../../shared/models/cd-table-column';
27 import { CdTableSelection } from '../../../shared/models/cd-table-selection';
28 import {
29 CephfsDir,
30 CephfsQuotas,
31 CephfsSnapshot
32 } from '../../../shared/models/cephfs-directory-models';
33 import { Permission } from '../../../shared/models/permissions';
34 import { CdDatePipe } from '../../../shared/pipes/cd-date.pipe';
35 import { DimlessBinaryPipe } from '../../../shared/pipes/dimless-binary.pipe';
36 import { AuthStorageService } from '../../../shared/services/auth-storage.service';
37 import { NotificationService } from '../../../shared/services/notification.service';
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 {
60 @ViewChild(TreeComponent, { static: false })
61 treeComponent: TreeComponent;
62 @ViewChild('origin', { static: true })
63 originTmpl: TemplateRef<any>;
64
65 @Input()
66 id: number;
67
68 private modalRef: BsModalRef;
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[];
106
107 constructor(
108 private authStorageService: AuthStorageService,
109 private modalService: BsModalService,
110 private cephfsService: CephfsService,
111 private cdDatePipe: CdDatePipe,
112 private i18n: I18n,
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',
143 name: this.i18n('Name'),
144 flexGrow: 1
145 },
146 {
147 prop: 'row.value',
148 name: this.i18n('Value'),
149 sortable: false,
150 flexGrow: 1
151 },
152 {
153 prop: 'row.originPath',
154 name: this.i18n('Origin'),
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',
197 name: this.i18n('Name'),
198 flexGrow: 1
199 },
200 {
201 prop: 'path',
202 name: this.i18n('Path'),
203 isHidden: true,
204 flexGrow: 2
205 },
206 {
207 prop: 'created',
208 name: this.i18n('Created'),
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,
223 click: () => this.createSnapshot()
224 },
225 {
226 name: this.actionLabels.DELETE,
227 icon: Icons.destroy,
228 permission: 'delete',
229 click: () => this.deleteSnapshotModal(),
230 canBePrimary: (selection) => selection.hasSelection,
231 disable: (selection) => !selection.hasSelection
232 }
233 ]
234 };
235 }
236
237 ngOnChanges() {
238 this.selectedDir = undefined;
239 this.dirs = [];
240 this.requestedPaths = [];
241 this.nodeIds = {};
242 if (this.id) {
243 this.setRootNode();
244 this.firstCall();
245 }
246 }
247
248 private setRootNode() {
249 this.nodes = [
250 {
251 name: '/',
252 id: '/',
253 isExpanded: true
254 }
255 ];
256 }
257
258 private firstCall() {
259 const path = '/';
260 setTimeout(() => {
261 this.getNode(path).loadNodeChildren();
262 }, 10);
263 }
264
265 updateDirectory(path: string): Promise<any[]> {
266 this.unsetLoadingIndicator();
267 if (!this.requestedPaths.includes(path)) {
268 this.requestedPaths.push(path);
269 } else if (this.loading[path] === true) {
270 return undefined; // Path is currently fetched.
271 }
272 return new Promise((resolve) => {
273 this.setLoadingIndicator(path, true);
274 this.cephfsService.lsDir(this.id, path).subscribe((dirs) => {
275 this.updateTreeStructure(dirs);
276 this.updateQuotaTable();
277 this.updateTree();
278 resolve(this.getChildren(path));
279 this.setLoadingIndicator(path, false);
280 });
281 });
282 }
283
284 private setLoadingIndicator(path: string, loading: boolean) {
285 this.loading[path] = loading;
286 this.unsetLoadingIndicator();
287 }
288
289 private getSubDirectories(path: string, tree: CephfsDir[] = this.dirs): CephfsDir[] {
290 return tree.filter((d) => d.parent === path);
291 }
292
293 private getChildren(path: string): any[] {
294 const subTree = this.getSubTree(path);
295 return _.sortBy(this.getSubDirectories(path), 'path').map((dir) =>
296 this.createNode(dir, subTree)
297 );
298 }
299
300 private createNode(dir: CephfsDir, subTree?: CephfsDir[]): any {
301 this.nodeIds[dir.path] = dir;
302 if (!subTree) {
303 this.getSubTree(dir.parent);
304 }
305 return {
306 name: dir.name,
307 id: dir.path,
308 hasChildren: this.getSubDirectories(dir.path, subTree).length > 0
309 };
310 }
311
312 private getSubTree(path: string): CephfsDir[] {
313 return this.dirs.filter((d) => d.parent && d.parent.startsWith(path));
314 }
315
316 private setSettings(node: TreeNode) {
317 const readable = (value: number, fn?: (arg0: number) => number | string): number | string =>
318 value ? (fn ? fn(value) : value) : '';
319
320 this.settings = [
321 this.getQuota(node, 'max_files', readable),
322 this.getQuota(node, 'max_bytes', (value) =>
323 readable(value, (v) => this.dimlessBinaryPipe.transform(v))
324 )
325 ];
326 }
327
328 private getQuota(
329 tree: TreeNode,
330 quotaKey: string,
331 valueConvertFn: (number: number) => number | string
332 ): QuotaSetting {
333 // Get current maximum
334 const currentPath = tree.id;
335 tree = this.getOrigin(tree, quotaKey);
336 const dir = this.getDirectory(tree);
337 const value = dir.quotas[quotaKey];
338 // Get next tree maximum
339 // => The value that isn't changeable through a change of the current directories quota value
340 let nextMaxValue = value;
341 let nextMaxPath = dir.path;
342 if (tree.id === currentPath) {
343 if (tree.parent.id === '/') {
344 // The value will never inherit any other value, so it has no maximum.
345 nextMaxValue = 0;
346 } else {
347 const nextMaxDir = this.getDirectory(this.getOrigin(tree.parent, quotaKey));
348 nextMaxValue = nextMaxDir.quotas[quotaKey];
349 nextMaxPath = nextMaxDir.path;
350 }
351 }
352 return {
353 row: {
354 name: quotaKey === 'max_bytes' ? this.i18n('Max size') : this.i18n('Max files'),
355 value: valueConvertFn(value),
356 originPath: value ? dir.path : ''
357 },
358 quotaKey,
359 dirValue: this.nodeIds[currentPath].quotas[quotaKey],
360 nextTreeMaximum: {
361 value: nextMaxValue,
362 path: nextMaxValue ? nextMaxPath : ''
363 }
364 };
365 }
366
367 /**
368 * Get the node where the quota limit originates from in the current node
369 *
370 * Example as it's a recursive method:
371 *
372 * | Path + Value | Call depth | useOrigin? | Output |
373 * |:-------------:|:----------:|:---------------------:|:------:|
374 * | /a/b/c/d (15) | 1st | 2nd (5) < 15 => false | /a/b |
375 * | /a/b/c (20) | 2nd | 3rd (5) < 20 => false | /a/b |
376 * | /a/b (5) | 3rd | 4th (10) < 5 => true | /a/b |
377 * | /a (10) | 4th | 10 => true | /a |
378 *
379 */
380 private getOrigin(tree: TreeNode, quotaSetting: string): TreeNode {
381 if (tree.parent && tree.parent.id !== '/') {
382 const current = this.getQuotaFromTree(tree, quotaSetting);
383
384 // Get the next used quota and node above the current one (until it hits the root directory)
385 const originTree = this.getOrigin(tree.parent, quotaSetting);
386 const inherited = this.getQuotaFromTree(originTree, quotaSetting);
387
388 // Select if the current quota is in use or the above
389 const useOrigin = current === 0 || (inherited !== 0 && inherited < current);
390 return useOrigin ? originTree : tree;
391 }
392 return tree;
393 }
394
395 private getQuotaFromTree(tree: TreeNode, quotaSetting: string): number {
396 return this.getDirectory(tree).quotas[quotaSetting];
397 }
398
399 private getDirectory(node: TreeNode): CephfsDir {
400 const path = node.id as string;
401 return this.nodeIds[path];
402 }
403
404 selectOrigin(path: string) {
405 this.selectNode(this.getNode(path));
406 }
407
408 private getNode(path: string): TreeNode {
409 return this.treeComponent.treeModel.getNodeById(path);
410 }
411
412 updateQuotaModal() {
413 const path = this.selectedDir.path;
414 const selection: QuotaSetting = this.quota.selection.first();
415 const nextMax = selection.nextTreeMaximum;
416 const key = selection.quotaKey;
417 const value = selection.dirValue;
418 this.modalService.show(FormModalComponent, {
419 initialState: {
420 titleText: this.getModalQuotaTitle(
421 value === 0 ? this.actionLabels.SET : this.actionLabels.UPDATE,
422 path
423 ),
424 message: nextMax.value
425 ? this.i18n('The inherited {{quotaValue}} is the maximum value to be used.', {
426 quotaValue: this.getQuotaValueFromPathMsg(nextMax.value, nextMax.path)
427 })
428 : undefined,
429 fields: [this.getQuotaFormField(selection.row.name, key, value, nextMax.value)],
430 submitButtonText: this.i18n('Save'),
431 onSubmit: (values: CephfsQuotas) => this.updateQuota(values)
432 }
433 });
434 }
435
436 private getModalQuotaTitle(action: string, path: string): string {
437 return this.i18n("{{action}} CephFS {{quotaName}} quota for '{{path}}'", {
438 action,
439 quotaName: this.getQuotaName(),
440 path
441 });
442 }
443
444 private getQuotaName(): string {
445 return this.isBytesQuotaSelected() ? this.i18n('size') : this.i18n('files');
446 }
447
448 private isBytesQuotaSelected(): boolean {
449 return this.quota.selection.first().quotaKey === 'max_bytes';
450 }
451
452 private getQuotaValueFromPathMsg(value: number, path: string): string {
453 return this.i18n("{{quotaName}} quota {{value}} from '{{path}}'", {
454 value: this.isBytesQuotaSelected() ? this.dimlessBinaryPipe.transform(value) : value,
455 quotaName: this.getQuotaName(),
456 path
457 });
458 }
459
460 private getQuotaFormField(
461 label: string,
462 name: string,
463 value: number,
464 maxValue: number
465 ): CdFormModalFieldConfig {
466 const isBinary = name === 'max_bytes';
467 const formValidators = [isBinary ? CdValidators.binaryMin(0) : Validators.min(0)];
468 if (maxValue) {
469 formValidators.push(isBinary ? CdValidators.binaryMax(maxValue) : Validators.max(maxValue));
470 }
471 const field: CdFormModalFieldConfig = {
472 type: isBinary ? 'binary' : 'number',
473 label,
474 name,
475 value,
476 validators: formValidators,
477 required: true
478 };
479 if (!isBinary) {
480 field.errors = {
481 min: this.i18n(`Value has to be at least {{value}} or more`, { value: 0 }),
482 max: this.i18n(`Value has to be at most {{value}} or less`, { value: maxValue })
483 };
484 }
485 return field;
486 }
487
488 private updateQuota(values: CephfsQuotas, onSuccess?: Function) {
489 const path = this.selectedDir.path;
490 const key = this.quota.selection.first().quotaKey;
491 const action =
492 this.selectedDir.quotas[key] === 0
493 ? this.actionLabels.SET
494 : values[key] === 0
495 ? this.actionLabels.UNSET
496 : this.i18n('Updated');
497 this.cephfsService.updateQuota(this.id, path, values).subscribe(() => {
498 if (onSuccess) {
499 onSuccess();
500 }
501 this.notificationService.show(
502 NotificationType.success,
503 this.getModalQuotaTitle(action, path)
504 );
505 this.forceDirRefresh();
506 });
507 }
508
509 unsetQuotaModal() {
510 const path = this.selectedDir.path;
511 const selection: QuotaSetting = this.quota.selection.first();
512 const key = selection.quotaKey;
513 const nextMax = selection.nextTreeMaximum;
514 const dirValue = selection.dirValue;
515
516 this.modalRef = this.modalService.show(ConfirmationModalComponent, {
517 initialState: {
518 titleText: this.getModalQuotaTitle(this.actionLabels.UNSET, path),
519 buttonText: this.actionLabels.UNSET,
520 description: this.i18n(`{{action}} {{quotaValue}} {{conclusion}}.`, {
521 action: this.actionLabels.UNSET,
522 quotaValue: this.getQuotaValueFromPathMsg(dirValue, path),
523 conclusion:
524 nextMax.value > 0
525 ? nextMax.value > dirValue
526 ? this.i18n('in order to inherit {{quotaValue}}', {
527 quotaValue: this.getQuotaValueFromPathMsg(nextMax.value, nextMax.path)
528 })
529 : this.i18n("which isn't used because of the inheritance of {{quotaValue}}", {
530 quotaValue: this.getQuotaValueFromPathMsg(nextMax.value, nextMax.path)
531 })
532 : this.i18n('in order to have no quota on the directory')
533 }),
534 onSubmit: () => this.updateQuota({ [key]: 0 }, () => this.modalRef.hide())
535 }
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, {
543 initialState: {
544 titleText: this.i18n('Create Snapshot'),
545 message: this.i18n('Please enter the name of the snapshot.'),
546 fields: [
547 {
548 type: 'text',
549 name: 'name',
550 value: `${moment().toISOString(true)}`,
551 required: true
552 }
553 ],
554 submitButtonText: this.i18n('Create Snapshot'),
555 onSubmit: (values: CephfsSnapshot) => {
556 this.cephfsService.mkSnapshot(this.id, path, values.name).subscribe((name) => {
557 this.notificationService.show(
558 NotificationType.success,
559 this.i18n('Created snapshot "{{name}}" for "{{path}}"', {
560 name: name,
561 path: path
562 })
563 );
564 this.forceDirRefresh();
565 });
566 }
567 }
568 });
569 }
570
571 /**
572 * Forces an update of the current selected directory
573 *
574 * As all nodes point by their path on an directory object, the easiest way is to update
575 * the objects by merge with their latest change.
576 */
577 private forceDirRefresh(path?: string) {
578 if (!path) {
579 const dir = this.selectedDir;
580 if (!dir) {
581 throw new Error('This function can only be called without path if an selection was made');
582 }
583 // Parent has to be called in order to update the object referring
584 // to the current selected directory
585 path = dir.parent ? dir.parent : dir.path;
586 }
587 const node = this.getNode(path);
588 node.loadNodeChildren();
589 }
590
591 private updateTreeStructure(dirs: CephfsDir[]) {
592 const getChildrenAndPaths = (
593 directories: CephfsDir[],
594 parent: string
595 ): { children: CephfsDir[]; paths: string[] } => {
596 const children = directories.filter((d) => d.parent === parent);
597 const paths = children.map((d) => d.path);
598 return { children, paths };
599 };
600
601 const parents = _.uniq(dirs.map((d) => d.parent).sort());
602 parents.forEach((p) => {
603 const received = getChildrenAndPaths(dirs, p);
604 const cached = getChildrenAndPaths(this.dirs, p);
605
606 cached.children.forEach((d) => {
607 if (!received.paths.includes(d.path)) {
608 this.removeOldDirectory(d);
609 }
610 });
611 received.children.forEach((d) => {
612 if (cached.paths.includes(d.path)) {
613 this.updateExistingDirectory(cached.children, d);
614 } else {
615 this.addNewDirectory(d);
616 }
617 });
618 });
619 }
620
621 private removeOldDirectory(rmDir: CephfsDir) {
622 const path = rmDir.path;
623 // Remove directory from local variables
624 _.remove(this.dirs, (d) => d.path === path);
625 delete this.nodeIds[path];
626 this.updateDirectoriesParentNode(rmDir);
627 }
628
629 private updateDirectoriesParentNode(dir: CephfsDir) {
630 const parent = dir.parent;
631 if (!parent) {
632 return;
633 }
634 const node = this.getNode(parent);
635 if (!node) {
636 // Node will not be found for new sub sub directories - this is the intended behaviour
637 return;
638 }
639 const children = this.getChildren(parent);
640 node.data.children = children;
641 node.data.hasChildren = children.length > 0;
642 this.treeComponent.treeModel.update();
643 }
644
645 private addNewDirectory(newDir: CephfsDir) {
646 this.dirs.push(newDir);
647 this.nodeIds[newDir.path] = newDir;
648 this.updateDirectoriesParentNode(newDir);
649 }
650
651 private updateExistingDirectory(source: CephfsDir[], updatedDir: CephfsDir) {
652 const currentDirObject = source.find((sub) => sub.path === updatedDir.path);
653 Object.assign(currentDirObject, updatedDir);
654 }
655
656 private updateQuotaTable() {
657 const node = this.selectedDir ? this.getNode(this.selectedDir.path) : undefined;
658 if (node && node.id !== '/') {
659 this.setSettings(node);
660 }
661 }
662
663 private updateTree(force: boolean = false) {
664 if (this.loadingIndicator && !force) {
665 // In order to make the page scrollable during load, the render cycle for each node
666 // is omitted and only be called if all updates were loaded.
667 return;
668 }
669 this.treeComponent.treeModel.update();
670 this.nodes = [...this.nodes];
671 this.treeComponent.sizeChanged();
672 }
673
674 deleteSnapshotModal() {
675 this.modalRef = this.modalService.show(CriticalConfirmationModalComponent, {
676 initialState: {
677 itemDescription: this.i18n('CephFs Snapshot'),
678 itemNames: this.snapshot.selection.selected.map(
679 (snapshot: CephfsSnapshot) => snapshot.name
680 ),
681 submitAction: () => this.deleteSnapshot()
682 }
683 });
684 }
685
686 deleteSnapshot() {
687 const path = this.selectedDir.path;
688 this.snapshot.selection.selected.forEach((snapshot: CephfsSnapshot) => {
689 const name = snapshot.name;
690 this.cephfsService.rmSnapshot(this.id, path, name).subscribe(() => {
691 this.notificationService.show(
692 NotificationType.success,
693 this.i18n('Deleted snapshot "{{name}}" for "{{path}}"', {
694 name: name,
695 path: path
696 })
697 );
698 });
699 });
700 this.modalRef.hide();
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 }