]> git.apps.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: Replace ng2-tree with angular-tree-component 33758/head
authorTiago Melo <tmelo@suse.com>
Thu, 12 Mar 2020 21:31:48 +0000 (20:31 -0100)
committerTiago Melo <tmelo@suse.com>
Thu, 12 Mar 2020 21:45:54 +0000 (20:45 -0100)
Fixes: https://tracker.ceph.com/issues/44450
Signed-off-by: Tiago Melo <tmelo@suse.com>
20 files changed:
src/pybind/mgr/dashboard/frontend/angular.json
src/pybind/mgr/dashboard/frontend/package-lock.json
src/pybind/mgr/dashboard/frontend/package.json
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/block.module.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-target-details/iscsi-target-details.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-target-details/iscsi-target-details.component.scss
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-target-details/iscsi-target-details.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-target-details/iscsi-target-details.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-target-list/iscsi-target-list.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/mirroring.module.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-directories/cephfs-directories.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-directories/cephfs-directories.component.scss
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/cluster.module.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/crushmap/crushmap.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/crushmap/crushmap.component.scss
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/crushmap/crushmap.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/crushmap/crushmap.component.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/enum/icons.enum.ts
src/pybind/mgr/dashboard/frontend/src/styles.scss
src/pybind/mgr/dashboard/frontend/src/styles/ng2-tree.scss [deleted file]

index 56a25e515b55c2ff65f61c79beea584598f379e0..c54fb5f553dc8ea5d7d350a03a4c8138aa0acf75 100644 (file)
@@ -37,8 +37,7 @@
               "node_modules/ngx-toastr/toastr.css",
               "node_modules/ngx-bootstrap/datepicker/bs-datepicker.css",
               "src/styles.scss",
-              "src/styles/vendor.overrides.scss",
-              "node_modules/ng2-tree/styles.css"
+              "src/styles/vendor.overrides.scss"
             ],
             "scripts": [
               "node_modules/chart.js/dist/Chart.bundle.js"
index 63d29ad9658bea0cd0de681fa86800dc8a4e185b..a042d28572f609c7148caaa213e08019501e56c2 100644 (file)
       "dev": true
     },
     "angular-tree-component": {
-      "version": "8.5.2",
-      "resolved": "https://registry.npmjs.org/angular-tree-component/-/angular-tree-component-8.5.2.tgz",
-      "integrity": "sha512-3NwMB+vLq1+WHz2UVgsZA73E1LmIIWJlrrasCKXbLJ3S7NmY9O/wKcolji3Vp2W//5KQ33RXu1jiPXCOQdRzVA==",
+      "version": "8.5.6",
+      "resolved": "https://registry.npmjs.org/angular-tree-component/-/angular-tree-component-8.5.6.tgz",
+      "integrity": "sha512-cxNem6872diZz9kIGqrjSJbKt0P3WSq9wTqZIeVJ8zsddI4Y6ShAVZlZNXUMRyJq246c9pJ6JJEAOzKVLk9xgA==",
       "requires": {
         "lodash": "^4.17.11",
-        "mobx": "^5.14.2",
-        "mobx-angular": "3.0.3",
-        "opencollective-postinstall": "^2.0.2"
+        "mobx": "^4.15.1"
       }
     },
     "ansi-colors": {
       }
     },
     "mobx": {
-      "version": "5.15.2",
-      "resolved": "https://registry.npmjs.org/mobx/-/mobx-5.15.2.tgz",
-      "integrity": "sha512-eVmHGuSYd0ZU6x8gYMdgLEnCC9kfBJaf7/qJt+/yIxczVVUiVzHvTBjZH3xEa5FD5VJJSh1/Ba4SThE4ErEGjw=="
-    },
-    "mobx-angular": {
-      "version": "3.0.3",
-      "resolved": "https://registry.npmjs.org/mobx-angular/-/mobx-angular-3.0.3.tgz",
-      "integrity": "sha512-mZuuose70V+Sd0hMWDElpRe3mA6GhYjSQN3mHzqk2XWXRJ+eWQa/f3Lqhw+Me/Xd2etWsGR1hnRa1BfQ2ZDtpw=="
+      "version": "4.15.4",
+      "resolved": "https://registry.npmjs.org/mobx/-/mobx-4.15.4.tgz",
+      "integrity": "sha512-nyuHPqmKnVOnbvkjR8OrijBtovxAHYC+JU8/qBqvBw4Dez/n+zzxqNHbZNFy7/07+wwc/Qz7JS9WSfy1LcYISA=="
     },
     "moment": {
       "version": "2.24.0",
         }
       }
     },
-    "ng2-tree": {
-      "version": "2.0.0-rc.11",
-      "resolved": "https://registry.npmjs.org/ng2-tree/-/ng2-tree-2.0.0-rc.11.tgz",
-      "integrity": "sha512-COGMatd+YrwJb3LSobagDC+t2PlSh4GkgG75Akh9QbXOSdFFPkbGmZvILg2xO4Hc+xicacvHp+6GINvjIeJwkA=="
-    },
     "ngx-bootstrap": {
       "version": "5.1.2",
       "resolved": "https://registry.npmjs.org/ngx-bootstrap/-/ngx-bootstrap-5.1.2.tgz",
     "opencollective-postinstall": {
       "version": "2.0.2",
       "resolved": "https://registry.npmjs.org/opencollective-postinstall/-/opencollective-postinstall-2.0.2.tgz",
-      "integrity": "sha512-pVOEP16TrAO2/fjej1IdOyupJY8KDUM1CvsaScRbw6oddvpQoOfGk4ywha0HKKVAD6RkW4x6Q+tNBwhf3Bgpuw=="
+      "integrity": "sha512-pVOEP16TrAO2/fjej1IdOyupJY8KDUM1CvsaScRbw6oddvpQoOfGk4ywha0HKKVAD6RkW4x6Q+tNBwhf3Bgpuw==",
+      "dev": true
     },
     "opn": {
       "version": "5.5.0",
         "tslib": "^1.9.0"
       }
     },
-    "rxjs-compat": {
-      "version": "6.5.3",
-      "resolved": "https://registry.npmjs.org/rxjs-compat/-/rxjs-compat-6.5.3.tgz",
-      "integrity": "sha512-BIJX2yovz3TBpjJoAZyls2QYuU6ZiCaZ+U96SmxQpuSP/qDUfiXPKOVLbThBB2WZijNHkdTTJXKRwvv5Y48H7g=="
-    },
     "safe-buffer": {
       "version": "5.1.2",
       "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
index a6763dbd91ea7949a1ee86a09bf7be281f20b203..7890e3560203d48a26d3773e1d63a1b38bd72828 100644 (file)
@@ -90,7 +90,7 @@
     "@auth0/angular-jwt": "2.1.1",
     "@ngx-translate/i18n-polyfill": "1.0.0",
     "@swimlane/ngx-datatable": "15.0.2",
-    "angular-tree-component": "8.5.2",
+    "angular-tree-component": "8.5.6",
     "async-mutex": "0.1.4",
     "bootstrap": "4.3.1",
     "chart.js": "2.8.0",
     "ng-bootstrap-form-validation": "5.0.0",
     "ng-click-outside": "5.3.0",
     "ng2-charts": "2.3.0",
-    "ng2-tree": "2.0.0-rc.11",
     "ngx-bootstrap": "5.1.2",
     "ngx-toastr": "11.0.0",
     "rxjs": "6.5.3",
-    "rxjs-compat": "6.5.3",
     "simplebar-angular": "2.0.1",
     "swagger-ui-dist": "3.23.11",
     "tslib": "1.10.0",
index c592e29386b755631a09a4e24e2e052a8daad48a..7db6861101db3edb5572e1a784c71e5d9ca538be 100644 (file)
@@ -3,8 +3,8 @@ import { NgModule } from '@angular/core';
 import { FormsModule, ReactiveFormsModule } from '@angular/forms';
 import { RouterModule, Routes } from '@angular/router';
 
+import { TreeModule } from 'angular-tree-component';
 import { NgBootstrapFormValidationModule } from 'ng-bootstrap-form-validation';
-import { TreeModule } from 'ng2-tree';
 import { BsDatepickerModule } from 'ngx-bootstrap/datepicker';
 import { BsDropdownModule } from 'ngx-bootstrap/dropdown';
 import { ModalModule } from 'ngx-bootstrap/modal';
@@ -67,8 +67,8 @@ import { RbdTrashRestoreModalComponent } from './rbd-trash-restore-modal/rbd-tra
     ModalModule.forRoot(),
     SharedModule,
     RouterModule,
-    TreeModule,
-    NgBootstrapFormValidationModule
+    NgBootstrapFormValidationModule,
+    TreeModule.forRoot()
   ],
   declarations: [
     RbdListComponent,
index ed5ecec46c3a6d39257a35c5c867f9f6d770e613..29d91ef472ccdde53d9ea741156741c857ed9944 100644 (file)
@@ -1,19 +1,23 @@
 <div class="row">
   <div class="col-6">
     <legend i18n>iSCSI Topology</legend>
-    <tree [tree]="tree"
-          (nodeSelected)="onNodeSelected($event)">
-      <ng-template let-node>
-        <span class="node-name"
-              [innerHTML]="node.value"></span>
-        <span>&nbsp;</span>
 
+    <tree-root #tree
+               [nodes]="nodes"
+               [options]="treeOptions"
+               (updateData)="onUpdateData()">
+      <ng-template #treeNodeTemplate
+                   let-node
+                   let-index="index">
+        <i [class]="node.data.cdIcon"></i>
+        <span>{{ node.data.name }}</span>
+        &nbsp;
         <span class="badge"
-              [ngClass]="{'badge-success': ['logged_in'].includes(node.status), 'badge-danger': ['logged_out'].includes(node.status)}">
-          {{ node.status }}
+              [ngClass]="{'badge-success': ['logged_in'].includes(node.data.status), 'badge-danger': ['logged_out'].includes(node.data.status)}">
+          {{ node.data.status }}
         </span>
       </ng-template>
-    </tree>
+    </tree-root>
   </div>
 
   <div class="col-6 metadata"
index 95fb5b9589549d735119cfdf9e2420fbaf3c3e34..e072facaa1d0f9bf39595a4b71a4f147dca06aa6 100644 (file)
@@ -1,22 +1,20 @@
 import { ComponentFixture, TestBed } from '@angular/core/testing';
 
-import { NodeEvent, Tree, TreeModule } from 'ng2-tree';
+import { TreeModel, TreeModule } from 'angular-tree-component';
+import * as _ from 'lodash';
 
 import { configureTestBed, i18nProviders } from '../../../../testing/unit-test-helper';
 import { CdTableSelection } from '../../../shared/models/cd-table-selection';
 import { SharedModule } from '../../../shared/shared.module';
 import { IscsiTargetDetailsComponent } from './iscsi-target-details.component';
 
-import * as _ from 'lodash';
-import { Icons } from '../../../shared/enum/icons.enum';
-
 describe('IscsiTargetDetailsComponent', () => {
   let component: IscsiTargetDetailsComponent;
   let fixture: ComponentFixture<IscsiTargetDetailsComponent>;
 
   configureTestBed({
     declarations: [IscsiTargetDetailsComponent],
-    imports: [TreeModule, SharedModule],
+    imports: [TreeModule.forRoot(), SharedModule],
     providers: [i18nProviders]
   });
 
@@ -89,7 +87,7 @@ describe('IscsiTargetDetailsComponent', () => {
 
     expect(component.data).toEqual(tempData);
     expect(component.metadata).toEqual({});
-    expect(component.tree).toEqual(undefined);
+    expect(component.nodes).toEqual([]);
 
     component.ngOnChanges();
 
@@ -104,89 +102,79 @@ describe('IscsiTargetDetailsComponent', () => {
       disk_rbd_disk_1: { backstore: 'backstore:1', controls: { hw_max_sectors: 1 } },
       root: { dataout_timeout: 2 }
     });
-    expect(component.tree).toEqual({
-      children: [
-        {
-          children: [{ id: 'disk_rbd_disk_1', value: 'rbd/disk_1' }],
-          settings: {
-            cssClasses: {
-              expanded: _.join([Icons.large, Icons.disk], ' '),
-              leaf: _.join([Icons.disk], ' ')
-            },
-            selectionAllowed: false
-          },
-          value: 'Disks'
-        },
-        {
-          children: [{ value: 'node1:192.168.100.201' }],
-          settings: {
-            cssClasses: {
-              expanded: _.join([Icons.large, Icons.server], ' '),
-              leaf: _.join([Icons.large, Icons.server], ' ')
-            },
-            selectionAllowed: false
+    expect(component.nodes).toEqual([
+      {
+        cdIcon: 'fa fa-lg fa fa-bullseye',
+        cdId: 'root',
+        children: [
+          {
+            cdIcon: 'fa fa-lg fa fa-hdd-o',
+            children: [
+              {
+                cdIcon: 'fa fa-hdd-o',
+                cdId: 'disk_rbd_disk_1',
+                name: 'rbd/disk_1'
+              }
+            ],
+            isExpanded: true,
+            name: 'Disks'
           },
-          value: 'Portals'
-        },
-        {
-          children: [
-            {
-              children: [
-                {
-                  id: 'disk_rbd_disk_1',
-                  settings: {
-                    cssClasses: {
-                      expanded: _.join([Icons.large, Icons.disk], ' '),
-                      leaf: _.join([Icons.disk], ' ')
-                    }
-                  },
-                  value: 'rbd/disk_1'
-                }
-              ],
-              id: 'client_iqn.1994-05.com.redhat:rh7-client',
-              status: 'logged_in',
-              value: 'iqn.1994-05.com.redhat:rh7-client'
-            }
-          ],
-          settings: {
-            cssClasses: {
-              expanded: _.join([Icons.large, Icons.user], ' '),
-              leaf: _.join([Icons.user], ' ')
-            },
-            selectionAllowed: false
+          {
+            cdIcon: 'fa fa-lg fa fa-server',
+            children: [
+              {
+                cdIcon: 'fa fa-server',
+                name: 'node1:192.168.100.201'
+              }
+            ],
+            isExpanded: true,
+            name: 'Portals'
           },
-          value: 'Initiators'
-        },
-        {
-          children: [],
-          settings: {
-            cssClasses: {
-              expanded: _.join([Icons.large, Icons.user], ' '),
-              leaf: _.join([Icons.user], ' ')
-            },
-            selectionAllowed: false
+          {
+            cdIcon: 'fa fa-lg fa fa-user',
+            children: [
+              {
+                cdIcon: 'fa fa-user',
+                cdId: 'client_iqn.1994-05.com.redhat:rh7-client',
+                children: [
+                  {
+                    cdIcon: 'fa fa-hdd-o',
+                    cdId: 'disk_rbd_disk_1',
+                    name: 'rbd/disk_1'
+                  }
+                ],
+                name: 'iqn.1994-05.com.redhat:rh7-client',
+                status: 'logged_in'
+              }
+            ],
+            isExpanded: true,
+            name: 'Initiators'
           },
-          value: 'Groups'
-        }
-      ],
-      id: 'root',
-      settings: {
-        cssClasses: { expanded: _.join([Icons.large, Icons.bullseye], ' ') },
-        static: true
-      },
-      value: 'iqn.2003-01.com.redhat.iscsi-gw:iscsi-igw'
-    });
+          {
+            cdIcon: 'fa fa-lg fa fa-users',
+            children: [],
+            isExpanded: true,
+            name: 'Groups'
+          }
+        ],
+        isExpanded: true,
+        name: 'iqn.2003-01.com.redhat.iscsi-gw:iscsi-igw'
+      }
+    ]);
   });
 
   describe('should update data when onNodeSelected is called', () => {
+    let tree: TreeModel;
+
     beforeEach(() => {
       component.ngOnChanges();
+      tree = component.tree.treeModel;
+      fixture.detectChanges();
     });
 
     it('with target selected', () => {
-      const tree = new Tree(component.tree);
-      const node = new NodeEvent(tree);
-      component.onNodeSelected(node);
+      const node = tree.getNodeBy({ data: { cdId: 'root' } });
+      component.onNodeSelected(tree, node);
       expect(component.data).toEqual([
         { current: 128, default: 128, displayName: 'cmdsn_depth' },
         { current: 2, default: 20, displayName: 'dataout_timeout' }
@@ -194,9 +182,8 @@ describe('IscsiTargetDetailsComponent', () => {
     });
 
     it('with disk selected', () => {
-      const tree = new Tree(component.tree.children[0].children[0]);
-      const node = new NodeEvent(tree);
-      component.onNodeSelected(node);
+      const node = tree.getNodeBy({ data: { cdId: 'disk_rbd_disk_1' } });
+      component.onNodeSelected(tree, node);
       expect(component.data).toEqual([
         { current: 1, default: 1024, displayName: 'hw_max_sectors' },
         { current: 8, default: 8, displayName: 'max_data_area_mb' },
@@ -205,9 +192,8 @@ describe('IscsiTargetDetailsComponent', () => {
     });
 
     it('with initiator selected', () => {
-      const tree = new Tree(component.tree.children[2].children[0]);
-      const node = new NodeEvent(tree);
-      component.onNodeSelected(node);
+      const node = tree.getNodeBy({ data: { cdId: 'client_iqn.1994-05.com.redhat:rh7-client' } });
+      component.onNodeSelected(tree, node);
       expect(component.data).toEqual([
         { current: 'myiscsiusername', default: undefined, displayName: 'user' },
         { current: 'myhost', default: undefined, displayName: 'alias' },
@@ -217,9 +203,8 @@ describe('IscsiTargetDetailsComponent', () => {
     });
 
     it('with any other selected', () => {
-      const tree = new Tree(component.tree.children[1].children[0]);
-      const node = new NodeEvent(tree);
-      component.onNodeSelected(node);
+      const node = tree.getNodeBy({ data: { name: 'Disks' } });
+      component.onNodeSelected(tree, node);
       expect(component.data).toBeUndefined();
     });
   });
index 929340a15b6ece7049b094a714ec701911320efd..b989c9c1921bd0076d6bf33ade1f91eccdcbe84d 100644 (file)
@@ -1,8 +1,14 @@
 import { Component, Input, OnChanges, OnInit, TemplateRef, ViewChild } from '@angular/core';
 
 import { I18n } from '@ngx-translate/i18n-polyfill';
+import {
+  ITreeOptions,
+  TREE_ACTIONS,
+  TreeComponent,
+  TreeModel,
+  TreeNode
+} from 'angular-tree-component';
 import * as _ from 'lodash';
-import { NodeEvent, TreeModel } from 'ng2-tree';
 
 import { TableComponent } from '../../../shared/datatable/table/table.component';
 import { Icons } from '../../../shared/enum/icons.enum';
@@ -36,12 +42,24 @@ export class IscsiTargetDetailsComponent implements OnChanges, OnInit {
     }
   }
 
+  @ViewChild('tree', { static: false }) tree: TreeComponent;
+
+  icons = Icons;
   columns: CdTableColumn[];
   data: any;
   metadata: any = {};
   selectedItem: any;
   title: string;
-  tree: TreeModel;
+
+  nodes: any[] = [];
+  treeOptions: ITreeOptions = {
+    useVirtualScroll: true,
+    actionMapping: {
+      mouse: {
+        click: this.onNodeSelected.bind(this)
+      }
+    }
+  };
 
   constructor(
     private i18n: I18n,
@@ -102,8 +120,8 @@ export class IscsiTargetDetailsComponent implements OnChanges, OnInit {
         leaf: _.join([Icons.user], ' ')
       },
       groups: {
-        expanded: _.join([Icons.large, Icons.user], ' '),
-        leaf: _.join([Icons.user], ' ')
+        expanded: _.join([Icons.large, Icons.users], ' '),
+        leaf: _.join([Icons.users], ' ')
       },
       disks: {
         expanded: _.join([Icons.large, Icons.disk], ' '),
@@ -111,31 +129,35 @@ export class IscsiTargetDetailsComponent implements OnChanges, OnInit {
       },
       portals: {
         expanded: _.join([Icons.large, Icons.server], ' '),
-        leaf: _.join([Icons.large, Icons.server], ' ')
+        leaf: _.join([Icons.server], ' ')
       }
     };
 
     const disks: any[] = [];
     _.forEach(this.selectedItem.disks, (disk) => {
-      const id = 'disk_' + disk.pool + '_' + disk.image;
-      this.metadata[id] = {
+      const cdId = 'disk_' + disk.pool + '_' + disk.image;
+      this.metadata[cdId] = {
         controls: disk.controls,
         backstore: disk.backstore
       };
       ['wwn', 'lun'].forEach((k) => {
         if (k in disk) {
-          this.metadata[id][k] = disk[k];
+          this.metadata[cdId][k] = disk[k];
         }
       });
       disks.push({
-        value: `${disk.pool}/${disk.image}`,
-        id: id
+        name: `${disk.pool}/${disk.image}`,
+        cdId: cdId,
+        cdIcon: cssClasses.disks.leaf
       });
     });
 
     const portals: any[] = [];
     _.forEach(this.selectedItem.portals, (portal) => {
-      portals.push({ value: `${portal.host}:${portal.ip}` });
+      portals.push({
+        name: `${portal.host}:${portal.ip}`,
+        cdIcon: cssClasses.portals.leaf
+      });
     });
 
     const clients: any[] = [];
@@ -153,11 +175,9 @@ export class IscsiTargetDetailsComponent implements OnChanges, OnInit {
       const luns: any[] = [];
       client.luns.forEach((lun: Record<string, any>) => {
         luns.push({
-          value: `${lun.pool}/${lun.image}`,
-          id: 'disk_' + lun.pool + '_' + lun.image,
-          settings: {
-            cssClasses: cssClasses.disks
-          }
+          name: `${lun.pool}/${lun.image}`,
+          cdId: 'disk_' + lun.pool + '_' + lun.image,
+          cdIcon: cssClasses.disks.leaf
         });
       });
 
@@ -166,10 +186,11 @@ export class IscsiTargetDetailsComponent implements OnChanges, OnInit {
         status = Object.keys(client.info.state).includes('LOGGED_IN') ? 'logged_in' : 'logged_out';
       }
       clients.push({
-        value: client.client_iqn,
+        name: client.client_iqn,
         status: status,
-        id: 'client_' + client.client_iqn,
-        children: luns
+        cdId: 'client_' + client.client_iqn,
+        children: luns,
+        cdIcon: cssClasses.initiators.leaf
       });
     });
 
@@ -178,84 +199,72 @@ export class IscsiTargetDetailsComponent implements OnChanges, OnInit {
       const luns: any[] = [];
       group.disks.forEach((disk: Record<string, any>) => {
         luns.push({
-          value: `${disk.pool}/${disk.image}`,
-          id: 'disk_' + disk.pool + '_' + disk.image
+          name: `${disk.pool}/${disk.image}`,
+          cdId: 'disk_' + disk.pool + '_' + disk.image,
+          cdIcon: cssClasses.disks.leaf
         });
       });
 
       const initiators: any[] = [];
       group.members.forEach((member: string) => {
         initiators.push({
-          value: member,
-          id: 'client_' + member
+          name: member,
+          cdId: 'client_' + member
         });
       });
 
       groups.push({
-        value: group.group_id,
+        name: group.group_id,
+        cdIcon: cssClasses.groups.leaf,
         children: [
           {
-            value: 'Disks',
+            name: 'Disks',
             children: luns,
-            settings: {
-              selectionAllowed: false,
-              cssClasses: cssClasses.disks
-            }
+            cdIcon: cssClasses.disks.expanded
           },
           {
-            value: 'Initiators',
+            name: 'Initiators',
             children: initiators,
-            settings: {
-              selectionAllowed: false,
-              cssClasses: cssClasses.initiators
-            }
+            cdIcon: cssClasses.initiators.expanded
           }
         ]
       });
     });
 
-    this.tree = {
-      value: this.selectedItem.target_iqn,
-      id: 'root',
-      settings: {
-        static: true,
-        cssClasses: cssClasses.target
-      },
-      children: [
-        {
-          value: 'Disks',
-          children: disks,
-          settings: {
-            selectionAllowed: false,
-            cssClasses: cssClasses.disks
-          }
-        },
-        {
-          value: 'Portals',
-          children: portals,
-          settings: {
-            selectionAllowed: false,
-            cssClasses: cssClasses.portals
-          }
-        },
-        {
-          value: 'Initiators',
-          children: clients,
-          settings: {
-            selectionAllowed: false,
-            cssClasses: cssClasses.initiators
-          }
-        },
-        {
-          value: 'Groups',
-          children: groups,
-          settings: {
-            selectionAllowed: false,
-            cssClasses: cssClasses.groups
+    this.nodes = [
+      {
+        name: this.selectedItem.target_iqn,
+        cdId: 'root',
+        isExpanded: true,
+        cdIcon: cssClasses.target.expanded,
+        children: [
+          {
+            name: 'Disks',
+            isExpanded: true,
+            children: disks,
+            cdIcon: cssClasses.disks.expanded
+          },
+          {
+            name: 'Portals',
+            isExpanded: true,
+            children: portals,
+            cdIcon: cssClasses.portals.expanded
+          },
+          {
+            name: 'Initiators',
+            isExpanded: true,
+            children: clients,
+            cdIcon: cssClasses.initiators.expanded
+          },
+          {
+            name: 'Groups',
+            isExpanded: true,
+            children: groups,
+            cdIcon: cssClasses.groups.expanded
           }
-        }
-      ]
-    };
+        ]
+      }
+    ];
   }
 
   private format(value: any) {
@@ -265,12 +274,13 @@ export class IscsiTargetDetailsComponent implements OnChanges, OnInit {
     return value;
   }
 
-  onNodeSelected(e: NodeEvent) {
-    if (e.node.id) {
-      this.title = e.node.value;
-      const tempData = this.metadata[e.node.id] || {};
+  onNodeSelected(tree: TreeModel, node: TreeNode) {
+    TREE_ACTIONS.ACTIVATE(tree, node, true);
+    if (node.data.cdId) {
+      this.title = node.data.name;
+      const tempData = this.metadata[node.data.cdId] || {};
 
-      if (e.node.id === 'root') {
+      if (node.data.cdId === 'root') {
         this.columns[2].isHidden = false;
         this.data = _.map(this.settings.target_default_controls, (value, key) => {
           value = this.format(value);
@@ -290,7 +300,7 @@ export class IscsiTargetDetailsComponent implements OnChanges, OnInit {
             });
           });
         }
-      } else if (e.node.id.toString().startsWith('disk_')) {
+      } else if (node.data.cdId.toString().startsWith('disk_')) {
         this.columns[2].isHidden = false;
         this.data = _.map(this.settings.disk_default_controls[tempData.backstore], (value, key) => {
           value = this.format(value);
@@ -334,4 +344,8 @@ export class IscsiTargetDetailsComponent implements OnChanges, OnInit {
       this.detailTable.updateColumns();
     }
   }
+
+  onUpdateData() {
+    this.tree.treeModel.expandAll();
+  }
 }
index c1413f96a7ef25df6a352f52e34610144f91a9f6..91bcc1f7db2e7ebb2771316abe02a223fd0be391 100644 (file)
@@ -2,7 +2,7 @@ import { HttpClientTestingModule } from '@angular/common/http/testing';
 import { ComponentFixture, TestBed } from '@angular/core/testing';
 import { RouterTestingModule } from '@angular/router/testing';
 
-import { TreeModule } from 'ng2-tree';
+import { TreeModule } from 'angular-tree-component';
 import { TabsModule } from 'ngx-bootstrap/tabs';
 import { ToastrModule } from 'ngx-toastr';
 import { BehaviorSubject, of } from 'rxjs';
index e5c8d240c991d0ec5901fa4d54ad5a6d14df6630..a051cf99b0f4ff84f7f58226944728c3942fa056 100644 (file)
@@ -4,7 +4,6 @@ import { FormsModule, ReactiveFormsModule } from '@angular/forms';
 import { RouterModule } from '@angular/router';
 
 import { NgBootstrapFormValidationModule } from 'ng-bootstrap-form-validation';
-import { TreeModule } from 'ng2-tree';
 import { AlertModule } from 'ngx-bootstrap/alert';
 import { BsDropdownModule } from 'ngx-bootstrap/dropdown';
 import { ModalModule } from 'ngx-bootstrap/modal';
@@ -46,7 +45,6 @@ import { PoolListComponent } from './pool-list/pool-list.component';
     ModalModule.forRoot(),
     AlertModule.forRoot(),
     TooltipModule.forRoot(),
-    TreeModule,
     NgBootstrapFormValidationModule
   ],
   declarations: [
index 7753d599bb6e0977ae2a9437402d8abfe6faaa8e..1d4b2ece85b3affcba16e598c1bcd526ead285c2 100644 (file)
         </button>
       </div>
       <div class="card-body">
-        <!--
-          ng2-tree can't be used here as it cannot handle the reloading of all nodes
-          without loosing all states of the current tree. The difference of both tree components is
-          that ng2-tree is defined and configured by each node where as angular-tree
-          is configured by a tree structure and consist of nodes that mainly hold data.
-          Angular-tree is a lot better for dynamically loaded trees. The downside is that it's not
-          possible to set individual icons for each node.
-        -->
         <tree-root *ngIf="nodes"
                    [nodes]="nodes"
                    [options]="treeOptions">
index bc6646e5f376f6aba54ddfa94134a18cea863d50..33d580283df6bfc6f321f1e93600f2d5c4696ea8 100644 (file)
@@ -1,8 +1,5 @@
 // Angular2-Tree Component
 ::ng-deep tree-root {
-  tree-viewport {
-    padding-bottom: 1.5em;
-  }
   .tree-children {
     overflow: inherit;
   }
index a08d669044b7e529354ba62cf6bb3019115921e9..e1634f1e4d17611ecad7fa73ac2e93b894823800 100644 (file)
@@ -3,8 +3,8 @@ import { NgModule } from '@angular/core';
 import { FormsModule, ReactiveFormsModule } from '@angular/forms';
 import { RouterModule } from '@angular/router';
 
+import { TreeModule } from 'angular-tree-component';
 import { NgBootstrapFormValidationModule } from 'ng-bootstrap-form-validation';
-import { TreeModule } from 'ng2-tree';
 import { AlertModule } from 'ngx-bootstrap/alert';
 import { BsDatepickerModule } from 'ngx-bootstrap/datepicker';
 import { BsDropdownModule } from 'ngx-bootstrap/dropdown';
@@ -79,10 +79,10 @@ import { ServicesComponent } from './services/services.component';
     ModalModule.forRoot(),
     AlertModule.forRoot(),
     TooltipModule.forRoot(),
-    TreeModule,
     MgrModulesModule,
     TypeaheadModule.forRoot(),
     TimepickerModule.forRoot(),
+    TreeModule.forRoot(),
     BsDatepickerModule.forRoot(),
     NgBootstrapFormValidationModule,
     CephSharedModule
index a3961e433a9132f43b01459a4220c6b35cd4e514..d56a8e37eb9860c63a337f2a2082029af88de3d1 100644 (file)
@@ -6,20 +6,26 @@
       <div class="card-body">
         <div class="row">
           <div class="col-sm-6 col-lg-6">
-            <tree [tree]="tree"
-                  [settings]="{rootIsVisible: false}"
-                  (nodeSelected)="onNodeSelected($event)">
-              <ng-template let-node>
-                <span class="badge"
-                      [ngClass]="{'badge-success': ['in', 'up'].includes(node.status), 'badge-danger': ['down', 'out', 'destroyed'].includes(node.status)}">
-                  {{ node.status }}
+            <i *ngIf="loadingIndicator"
+               [ngClass]="[icons.large, icons.spinner, icons.spin]"></i>
+
+            <tree-root #tree
+                       [nodes]="nodes"
+                       [options]="treeOptions"
+                       (updateData)="onUpdateData()">
+              <ng-template #treeNodeTemplate
+                           let-node>
+                <span *ngIf="node.data.status"
+                      class="badge"
+                      [ngClass]="{'badge-success': ['in', 'up'].includes(node.data.status), 'badge-danger': ['down', 'out', 'destroyed'].includes(node.data.status)}">
+                  {{ node.data.status }}
                 </span>
                 <span>&nbsp;</span>
                 <span class="node-name"
-                      [ngClass]="{'type-osd': node.type === 'osd'}"
-                      [innerHTML]="node.value"></span>
+                      [ngClass]="{'type-osd': node.data.type === 'osd'}"
+                      [innerHTML]="node.data.name"></span>
               </ng-template>
-            </tree>
+            </tree-root>
           </div>
           <div class="col-sm-6 col-lg-6 metadata"
                *ngIf="metadata">
index 025791b5e247c9c4c21b99ec448e5ab632048d2f..a6706ddfebf22cded7cb840ddb2ee7878ff2cb67 100644 (file)
@@ -2,7 +2,7 @@ import { HttpClientTestingModule } from '@angular/common/http/testing';
 import { DebugElement } from '@angular/core';
 import { ComponentFixture, TestBed } from '@angular/core/testing';
 
-import { TreeModule } from 'ng2-tree';
+import { TreeModule } from 'angular-tree-component';
 import { TabsModule } from 'ngx-bootstrap/tabs';
 import { of } from 'rxjs';
 
@@ -16,7 +16,7 @@ describe('CrushmapComponent', () => {
   let fixture: ComponentFixture<CrushmapComponent>;
   let debugElement: DebugElement;
   configureTestBed({
-    imports: [HttpClientTestingModule, TreeModule, TabsModule.forRoot(), SharedModule],
+    imports: [HttpClientTestingModule, TreeModule.forRoot(), TabsModule.forRoot(), SharedModule],
     declarations: [CrushmapComponent],
     providers: [HealthService]
   });
@@ -53,7 +53,7 @@ describe('CrushmapComponent', () => {
     it('should display "No nodes!" if ceph tree nodes is empty array', () => {
       prepareGetHealth([]);
       expect(healthService.getFullHealth).toHaveBeenCalled();
-      expect(component.tree.value).toEqual('No nodes!');
+      expect(component.nodes[0].name).toEqual('No nodes!');
     });
 
     describe('nodes not empty', () => {
@@ -71,86 +71,70 @@ describe('CrushmapComponent', () => {
       });
 
       it('should have two root nodes', () => {
-        expect(component.tree.children).toEqual([
+        expect(component.nodes).toEqual([
           {
+            cdId: -3,
             children: [
               {
                 children: [
                   {
-                    id: 4,
-                    settings: {
-                      static: true
-                    },
+                    id: component.nodes[0].children[0].children[0].id,
+                    cdId: 4,
                     status: 'up',
                     type: 'osd',
-                    value: 'osd.0-2 (osd)'
+                    name: 'osd.0-2 (osd)'
                   }
                 ],
-                id: -4,
-                settings: {
-                  static: true
-                },
+                id: component.nodes[0].children[0].id,
+                cdId: -4,
                 status: undefined,
                 type: 'host',
-                value: 'my-host-2 (host)'
+                name: 'my-host-2 (host)'
               }
             ],
-            id: -3,
-            settings: {
-              static: true
-            },
+            id: component.nodes[0].id,
             status: undefined,
             type: 'root',
-            value: 'default-2 (root)'
+            name: 'default-2 (root)'
           },
           {
             children: [
               {
                 children: [
                   {
-                    id: 0,
-                    settings: {
-                      static: true
-                    },
+                    id: component.nodes[1].children[0].children[0].id,
+                    cdId: 0,
                     status: 'up',
                     type: 'osd',
-                    value: 'osd.0 (osd)'
+                    name: 'osd.0 (osd)'
                   },
                   {
-                    id: 1,
-                    settings: {
-                      static: true
-                    },
+                    id: component.nodes[1].children[0].children[1].id,
+                    cdId: 1,
                     status: 'down',
                     type: 'osd',
-                    value: 'osd.1 (osd)'
+                    name: 'osd.1 (osd)'
                   },
                   {
-                    id: 2,
-                    settings: {
-                      static: true
-                    },
+                    id: component.nodes[1].children[0].children[2].id,
+                    cdId: 2,
                     status: 'up',
                     type: 'osd',
-                    value: 'osd.2 (osd)'
+                    name: 'osd.2 (osd)'
                   }
                 ],
-                id: -2,
-                settings: {
-                  static: true
-                },
+                id: component.nodes[1].children[0].id,
+                cdId: -2,
                 status: undefined,
                 type: 'host',
-                value: 'my-host (host)'
+                name: 'my-host (host)'
               }
             ],
-            id: -1,
-            settings: {
-              static: true
-            },
+            id: component.nodes[1].id,
+            cdId: -1,
             status: undefined,
             type: 'root',
-            value: 'default (root)'
+            name: 'default (root)'
           }
         ]);
       });
index 52359f269d6f80bfdd1668d849cff56f218b53ae..42add6f2c039b6b00fc1c67e07e4cd9f885cd6a5 100644 (file)
@@ -1,8 +1,15 @@
-import { Component, OnInit } from '@angular/core';
+import { Component, OnInit, ViewChild } from '@angular/core';
 
-import { NodeEvent, TreeModel } from 'ng2-tree';
+import {
+  ITreeOptions,
+  TREE_ACTIONS,
+  TreeComponent,
+  TreeModel,
+  TreeNode
+} from 'angular-tree-component';
 
 import { HealthService } from '../../../shared/api/health.service';
+import { Icons } from '../../../shared/enum/icons.enum';
 
 @Component({
   selector: 'cd-crushmap',
@@ -10,7 +17,20 @@ import { HealthService } from '../../../shared/api/health.service';
   styleUrls: ['./crushmap.component.scss']
 })
 export class CrushmapComponent implements OnInit {
-  tree: TreeModel;
+  @ViewChild('tree', { static: false }) tree: TreeComponent;
+
+  icons = Icons;
+  loadingIndicator = true;
+  nodes: any[] = [];
+  treeOptions: ITreeOptions = {
+    useVirtualScroll: true,
+    actionMapping: {
+      mouse: {
+        click: this.onNodeSelected.bind(this)
+      }
+    }
+  };
+
   metadata: any;
   metadataTitle: string;
   metadataKeyMap: { [key: number]: any } = {};
@@ -19,19 +39,21 @@ export class CrushmapComponent implements OnInit {
 
   ngOnInit() {
     this.healthService.getFullHealth().subscribe((data: any) => {
-      this.tree = this._abstractTreeData(data);
+      this.loadingIndicator = false;
+      this.nodes = this.abstractTreeData(data);
     });
   }
 
-  _abstractTreeData(data: any): TreeModel {
+  private abstractTreeData(data: any): any[] {
     const nodes = data.osd_map.tree.nodes || [];
     const treeNodeMap: { [key: number]: any } = {};
 
     if (0 === nodes.length) {
-      return {
-        value: 'No nodes!',
-        settings: { static: true }
-      };
+      return [
+        {
+          name: 'No nodes!'
+        }
+      ];
     }
 
     const roots: any[] = [];
@@ -46,22 +68,18 @@ export class CrushmapComponent implements OnInit {
       return treeNodeMap[id];
     });
 
-    return {
-      value: 'CRUSH map',
-      children: children
-    };
+    return children;
   }
 
   private generateTreeLeaf(node: any, treeNodeMap: any) {
-    const id = node.id;
-    this.metadataKeyMap[id] = node;
-    const settings = { static: true };
+    const cdId = node.id;
+    this.metadataKeyMap[cdId] = node;
 
-    const value: string = node.name + ' (' + node.type + ')';
+    const name: string = node.name + ' (' + node.type + ')';
     const status: string = node.status;
 
     const children: any[] = [];
-    const resultNode = { value, status, settings, id, type: node.type };
+    const resultNode = { name, status, cdId, type: node.type };
     if (node.children) {
       node.children.sort().forEach((childId: any) => {
         children.push(treeNodeMap[childId]);
@@ -73,9 +91,19 @@ export class CrushmapComponent implements OnInit {
     return resultNode;
   }
 
-  onNodeSelected(e: NodeEvent) {
-    const { name, type, status, ...remain } = this.metadataKeyMap[e.node.id];
-    this.metadata = remain;
-    this.metadataTitle = name + ' (' + type + ')';
+  onNodeSelected(tree: TreeModel, node: TreeNode) {
+    TREE_ACTIONS.ACTIVATE(tree, node, true);
+    if (node.data.cdId !== undefined) {
+      const { name, type, status, ...remain } = this.metadataKeyMap[node.data.cdId];
+      this.metadata = remain;
+      this.metadataTitle = name + ' (' + type + ')';
+    } else {
+      delete this.metadata;
+      delete this.metadataTitle;
+    }
+  }
+
+  onUpdateData() {
+    this.tree.treeModel.expandAll();
   }
 }
index 5fb20ad604a7b71f01ab22e8d9937749000a1840..8fc034b0d0ff026cc259b5c71457ebcb6a266680 100644 (file)
@@ -26,6 +26,7 @@ export enum Icons {
   down = 'fa fa-arrow-down', // Mark Down
   erase = 'fa fa-eraser', // Purge
   user = 'fa fa-user', // User, Initiators
+  users = 'fa fa-users', // Users, Groups
   share = 'fa fa-share-alt', // share
   key = 'fa fa-key-modern', // S3 Keys, Swift Keys, Authentication
   warning = 'fa fa-exclamation-triangle', // Notification warning
index b5d80130a0829b3f2de8d20a5f2c8f0ff4b1a495..79367b7191343a54ab7f6ce12307b88c3079d20f 100644 (file)
@@ -495,3 +495,11 @@ bfv-messages {
   color: $color-solid-gray;
   background-color: $color-light-shade-gray;
 }
+
+// angular-tree-component
+tree-root {
+  tree-viewport {
+    // Fix visual bug when tree is empty
+    min-height: 1em;
+  }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/styles/ng2-tree.scss b/src/pybind/mgr/dashboard/frontend/src/styles/ng2-tree.scss
deleted file mode 100644 (file)
index 62164c0..0000000
+++ /dev/null
@@ -1,25 +0,0 @@
-@import 'defaults';
-
-/* ng2-tree */
-::ng-deep tree-internal .tree {
-  li {
-    cursor: pointer;
-  }
-  .node-value {
-    &:hover {
-      color: #212121;
-    }
-    &:after {
-      height: 0;
-    }
-    color: #2b99a8;
-    border-radius: 5px;
-  }
-  .node-selected {
-    background-color: #d9edf7;
-    color: #212121;
-  }
-  .loading-children {
-    display: none;
-  }
-}