]> git.apps.os.sepia.ceph.com Git - ceph.git/blob
e9db99eed52ffb1b2c85f02356a4623b07b575f1
[ceph.git] /
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   TreeComponent,
8   TreeModel,
9   TreeNode,
10   TREE_ACTIONS
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)
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 }