"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"
"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",
"@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",
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';
ModalModule.forRoot(),
SharedModule,
RouterModule,
- TreeModule,
- NgBootstrapFormValidationModule
+ NgBootstrapFormValidationModule,
+ TreeModule.forRoot()
],
declarations: [
RbdListComponent,
<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> </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>
+
<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"
-@import 'ng2-tree.scss';
-
-::ng-deep tree .fa {
- font-weight: unset !important;
-}
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]
});
expect(component.data).toEqual(tempData);
expect(component.metadata).toEqual({});
- expect(component.tree).toEqual(undefined);
+ expect(component.nodes).toEqual([]);
component.ngOnChanges();
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' }
});
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' },
});
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' },
});
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();
});
});
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';
}
}
+ @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,
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], ' '),
},
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[] = [];
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
});
});
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
});
});
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) {
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);
});
});
}
- } 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);
this.detailTable.updateColumns();
}
}
+
+ onUpdateData() {
+ this.tree.treeModel.expandAll();
+ }
}
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';
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';
ModalModule.forRoot(),
AlertModule.forRoot(),
TooltipModule.forRoot(),
- TreeModule,
NgBootstrapFormValidationModule
],
declarations: [
</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">
// Angular2-Tree Component
::ng-deep tree-root {
- tree-viewport {
- padding-bottom: 1.5em;
- }
.tree-children {
overflow: inherit;
}
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';
ModalModule.forRoot(),
AlertModule.forRoot(),
TooltipModule.forRoot(),
- TreeModule,
MgrModulesModule,
TypeaheadModule.forRoot(),
TimepickerModule.forRoot(),
+ TreeModule.forRoot(),
BsDatepickerModule.forRoot(),
NgBootstrapFormValidationModule,
CephSharedModule
<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> </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">
-@import 'ng2-tree.scss';
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';
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]
});
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', () => {
});
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)'
}
]);
});
-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',
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 } = {};
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[] = [];
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]);
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();
}
}
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
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;
+ }
+}
+++ /dev/null
-@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;
- }
-}