"node_modules/fork-awesome/css/fork-awesome.css",
"node_modules/awesome-bootstrap-checkbox/awesome-bootstrap-checkbox.css",
"node_modules/ngx-bootstrap/datepicker/bs-datepicker.css",
- "src/styles.scss"
+ "src/styles.scss",
+ "node_modules/ng2-tree/styles.css"
],
"scripts": [
"node_modules/chart.js/dist/Chart.bundle.js"
"version": "github:zzakir/ng2-toastr#0eafd72f1581058113ca338218d77f5c27f5b630",
"from": "github:zzakir/ng2-toastr#0eafd72"
},
+ "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": "2.0.5",
"resolved": "https://registry.npmjs.org/ngx-bootstrap/-/ngx-bootstrap-2.0.5.tgz",
"moment": "2.22.2",
"ng2-charts": "1.6.0",
"ng2-toastr": "zzakir/ng2-toastr#0eafd72",
+ "ng2-tree": "2.0.0-rc.11",
"ngx-bootstrap": "2.0.5",
"rxjs": "6.2.2",
"rxjs-compat": "6.2.2",
import { CephfsListComponent } from './ceph/cephfs/cephfs-list/cephfs-list.component';
import { ConfigurationFormComponent } from './ceph/cluster/configuration/configuration-form/configuration-form.component';
import { ConfigurationComponent } from './ceph/cluster/configuration/configuration.component';
+import { CrushmapComponent } from './ceph/cluster/crushmap/crushmap.component';
import { HostsComponent } from './ceph/cluster/hosts/hosts.component';
import { MonitorComponent } from './ceph/cluster/monitor/monitor.component';
import { OsdListComponent } from './ceph/cluster/osd/osd-list/osd-list.component';
}
]
},
+ {
+ path: 'crush-map',
+ component: CrushmapComponent,
+ canActivate: [AuthGuardService],
+ data: { breadcrumbs: 'Cluster/CRUSH map' }
+ },
{
path: 'perf_counters/:type/:id',
component: PerformanceCounterComponent,
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { RouterModule } from '@angular/router';
+import { TreeModule } from 'ng2-tree';
import { AlertModule } from 'ngx-bootstrap/alert';
import { BsDropdownModule } from 'ngx-bootstrap/dropdown';
import { ModalModule } from 'ngx-bootstrap/modal';
import { ConfigurationDetailsComponent } from './configuration/configuration-details/configuration-details.component';
import { ConfigurationFormComponent } from './configuration/configuration-form/configuration-form.component';
import { ConfigurationComponent } from './configuration/configuration.component';
+import { CrushmapComponent } from './crushmap/crushmap.component';
import { HostDetailsComponent } from './hosts/host-details/host-details.component';
import { HostsComponent } from './hosts/hosts.component';
import { MonitorComponent } from './monitor/monitor.component';
BsDropdownModule.forRoot(),
ModalModule.forRoot(),
AlertModule.forRoot(),
- TooltipModule.forRoot()
+ TooltipModule.forRoot(),
+ TreeModule
],
declarations: [
HostsComponent,
HostDetailsComponent,
ConfigurationDetailsComponent,
ConfigurationFormComponent,
- OsdReweightModalComponent
+ OsdReweightModalComponent,
+ CrushmapComponent
]
})
export class ClusterModule {}
--- /dev/null
+<div class="row">
+ <div class="col-sm-12 col-lg-12">
+ <div class="panel panel-default">
+ <div class="panel-heading">
+ <h3 class="panel-title">
+ <span i18n>{{ panelTitle }}</span>
+ </h3>
+ </div>
+ <div class="panel-body">
+ <div class="col-sm-6 col-lg-6">
+ <tree [tree]="tree"
+ (nodeSelected)="onNodeSelected($event)"></tree>
+ </div>
+ <div class="col-sm-6 col-lg-6 metadata">
+ <cd-table-key-value *ngIf="metadata"
+ [data]="metadata">
+ </cd-table-key-value>
+ </div>
+ </div>
+ </div>
+ </div>
+</div>
\ No newline at end of file
--- /dev/null
+::ng-deep tree-internal .tree li {
+ cursor: pointer;
+}
--- /dev/null
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { DebugElement } from '@angular/core';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { of } from 'rxjs';
+
+import { TreeModule } from 'ng2-tree';
+import { TabsModule } from 'ngx-bootstrap/tabs';
+
+import { configureTestBed } from '../../../../testing/unit-test-helper';
+import { DashboardService } from '../../../shared/api/dashboard.service';
+import { SharedModule } from '../../../shared/shared.module';
+import { CrushmapComponent } from './crushmap.component';
+
+describe('CrushmapComponent', () => {
+ let component: CrushmapComponent;
+ let fixture: ComponentFixture<CrushmapComponent>;
+ let debugElement: DebugElement;
+ configureTestBed({
+ imports: [HttpClientTestingModule, TreeModule, TabsModule.forRoot(), SharedModule],
+ declarations: [CrushmapComponent],
+ providers: [DashboardService]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(CrushmapComponent);
+ component = fixture.componentInstance;
+ debugElement = fixture.debugElement;
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ it('should display right title', () => {
+ fixture.detectChanges();
+ const span = debugElement.nativeElement.querySelector('span');
+ expect(span.textContent).toContain(component.panelTitle);
+ });
+
+ describe('test tree', () => {
+ let dashboardService: DashboardService;
+ const prepareGetHealth = (nodes: object[]) => {
+ spyOn(dashboardService, 'getHealth').and.returnValue(
+ of({ osd_map: { tree: { nodes: nodes } } })
+ );
+ fixture.detectChanges();
+ };
+
+ beforeEach(() => {
+ dashboardService = debugElement.injector.get(DashboardService);
+ });
+
+ it('should display "No nodes!" if ceph tree nodes is empty array', () => {
+ prepareGetHealth([]);
+ expect(dashboardService.getHealth).toHaveBeenCalled();
+ expect(component.tree.value).toEqual('No nodes!');
+ });
+
+ describe('nodes not empty', () => {
+ beforeEach(() => {
+ prepareGetHealth([
+ { children: [-2], type: 'root', name: 'default', id: -1 },
+ { children: [1, 0, 2], type: 'host', name: 'my-host', id: -2 },
+ { status: 'up', type: 'osd', name: 'osd.0', id: 0 },
+ { status: 'down', type: 'osd', name: 'osd.1', id: 1 },
+ { status: 'up', type: 'osd', name: 'osd.2', id: 2 }
+ ]);
+ });
+
+ it('should have tree structure derived from a root', () => {
+ expect(component.tree.value).toBe('default (root)');
+ });
+
+ it('should have one host child with 3 osd children', () => {
+ expect(component.tree.children.length).toBe(1);
+ expect(component.tree.children[0].value).toBe('my-host (host)');
+ expect(component.tree.children[0].children.length).toBe(3);
+ });
+
+ it('should have 3 osds in orderd', () => {
+ expect(component.tree.children[0].children[0].value).toBe('osd.0 (osd)--up');
+ expect(component.tree.children[0].children[1].value).toBe('osd.1 (osd)--down');
+ expect(component.tree.children[0].children[2].value).toBe('osd.2 (osd)--up');
+ });
+ });
+ });
+});
--- /dev/null
+import { Component, OnInit } from '@angular/core';
+
+import { NodeEvent, TreeModel } from 'ng2-tree';
+
+import { DashboardService } from '../../../shared/api/dashboard.service';
+
+@Component({
+ selector: 'cd-crushmap',
+ templateUrl: './crushmap.component.html',
+ styleUrls: ['./crushmap.component.scss']
+})
+export class CrushmapComponent implements OnInit {
+ panelTitle: string;
+ tree: TreeModel;
+ metadata: any;
+ metadataKeyMap: { [key: number]: number } = {};
+
+ constructor(private dashboardService: DashboardService) {
+ this.panelTitle = 'CRUSH map viewer';
+ }
+
+ ngOnInit() {
+ this.dashboardService.getHealth().subscribe((data: any) => {
+ this.tree = this._abstractTreeData(data);
+ });
+ }
+
+ _abstractTreeData(data: any): TreeModel {
+ const nodes = data.osd_map.tree.nodes || [];
+ const treeNodeMap: { [key: number]: any } = {};
+
+ if (0 === nodes.length) {
+ return {
+ value: 'No nodes!',
+ settings: { static: true }
+ };
+ }
+
+ const rootNodeId = nodes[0].id || null;
+ nodes.reverse().forEach((node) => {
+ treeNodeMap[node.id] = this.generateTreeLeaf(node, treeNodeMap);
+ });
+
+ return treeNodeMap[rootNodeId];
+ }
+
+ private generateTreeLeaf(node: any, treeNodeMap) {
+ const id = node.id;
+ this.metadataKeyMap[id] = node;
+ const settings = { static: true };
+
+ let value: string = node.name + ' (' + node.type + ')';
+ if (node.status) {
+ value += '--' + node.status;
+ }
+
+ const children: any[] = [];
+ if (node.children) {
+ node.children.sort().forEach((childId) => {
+ children.push(treeNodeMap[childId]);
+ });
+
+ return { value, settings, id, children };
+ }
+
+ return { value, settings, id };
+ }
+
+ onNodeSelected(e: NodeEvent) {
+ this.metadata = this.metadataKeyMap[e.node.id];
+ }
+}
routerLink="/configuration">Configuration
</a>
</li>
+ <li routerLinkActive="active"
+ class="tc_submenuitem tc_submenuitem_crush"
+ *ngIf="permissions.hosts.read && permissions.osd.read">
+ <a i18n
+ class="dropdown-item"
+ routerLink="/crush-map">CRUSH map
+ </a>
+ </li>
</ul>
</li>