Introduce Helix cluster dashboard
Project: http://git-wip-us.apache.org/repos/asf/helix/repo Commit: http://git-wip-us.apache.org/repos/asf/helix/commit/a97f1580 Tree: http://git-wip-us.apache.org/repos/asf/helix/tree/a97f1580 Diff: http://git-wip-us.apache.org/repos/asf/helix/diff/a97f1580 Branch: refs/heads/master Commit: a97f15809efe6e325b17ff38f040b693f9ad94ad Parents: d192afc Author: Vivo Xu <[email protected]> Authored: Thu Dec 21 15:38:07 2017 -0800 Committer: Vivo Xu <[email protected]> Committed: Wed Aug 8 15:36:53 2018 -0700 ---------------------------------------------------------------------- helix-front/client/app/app-routing.module.ts | 5 + helix-front/client/app/app.module.ts | 4 +- .../cluster-detail/cluster-detail.component.ts | 1 + .../app/dashboard/dashboard.component.html | 29 ++ .../app/dashboard/dashboard.component.scss | 43 +++ .../app/dashboard/dashboard.component.spec.ts | 31 +++ .../client/app/dashboard/dashboard.component.ts | 274 +++++++++++++++++++ .../client/app/dashboard/dashboard.module.ts | 21 ++ helix-front/client/styles.scss | 3 + helix-front/package.json | 4 +- 10 files changed, 413 insertions(+), 2 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/helix/blob/a97f1580/helix-front/client/app/app-routing.module.ts ---------------------------------------------------------------------- diff --git a/helix-front/client/app/app-routing.module.ts b/helix-front/client/app/app-routing.module.ts index f32a793..3e34485 100644 --- a/helix-front/client/app/app-routing.module.ts +++ b/helix-front/client/app/app-routing.module.ts @@ -16,6 +16,7 @@ import { InstanceDetailComponent } from './instance/instance-detail/instance-det import { WorkflowListComponent } from './workflow/workflow-list/workflow-list.component'; import { WorkflowDetailComponent } from './workflow/workflow-detail/workflow-detail.component'; import { HelixListComponent } from './chooser/helix-list/helix-list.component'; +import { DashboardComponent } from './dashboard/dashboard.component'; const HELIX_ROUTES: Routes = [ { @@ -55,6 +56,10 @@ const HELIX_ROUTES: Routes = [ { path: 'workflows', component: WorkflowListComponent + }, + { + path: 'dashboard', + component: DashboardComponent } ] }, http://git-wip-us.apache.org/repos/asf/helix/blob/a97f1580/helix-front/client/app/app.module.ts ---------------------------------------------------------------------- diff --git a/helix-front/client/app/app.module.ts b/helix-front/client/app/app.module.ts index 0b5fd25..b781eee 100644 --- a/helix-front/client/app/app.module.ts +++ b/helix-front/client/app/app.module.ts @@ -17,6 +17,7 @@ import { HistoryModule } from './history/history.module'; import { AppComponent } from './app.component'; import { WorkflowModule } from './workflow/workflow.module'; import { ChooserModule } from './chooser/chooser.module'; +import { DashboardModule } from './dashboard/dashboard.module'; @NgModule({ declarations: [ @@ -37,7 +38,8 @@ import { ChooserModule } from './chooser/chooser.module'; ControllerModule, HistoryModule, WorkflowModule, - ChooserModule + ChooserModule, + DashboardModule ], providers: [], bootstrap: [AppComponent] http://git-wip-us.apache.org/repos/asf/helix/blob/a97f1580/helix-front/client/app/cluster/cluster-detail/cluster-detail.component.ts ---------------------------------------------------------------------- diff --git a/helix-front/client/app/cluster/cluster-detail/cluster-detail.component.ts b/helix-front/client/app/cluster/cluster-detail/cluster-detail.component.ts index 1308b4d..d2a3bb3 100644 --- a/helix-front/client/app/cluster/cluster-detail/cluster-detail.component.ts +++ b/helix-front/client/app/cluster/cluster-detail/cluster-detail.component.ts @@ -18,6 +18,7 @@ import { InputDialogComponent } from '../../shared/dialog/input-dialog/input-dia export class ClusterDetailComponent implements OnInit { readonly tabLinks = [ + { label: 'Dashboard (beta)', link: 'dashboard' }, { label: 'Resources', link: 'resources' }, { label: 'Workflows', link: 'workflows' }, { label: 'Instances', link: 'instances' }, http://git-wip-us.apache.org/repos/asf/helix/blob/a97f1580/helix-front/client/app/dashboard/dashboard.component.html ---------------------------------------------------------------------- diff --git a/helix-front/client/app/dashboard/dashboard.component.html b/helix-front/client/app/dashboard/dashboard.component.html new file mode 100644 index 0000000..85989f1 --- /dev/null +++ b/helix-front/client/app/dashboard/dashboard.component.html @@ -0,0 +1,29 @@ +<section fxLayout="column" fxFlex> + <section class="info" fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="10px"> + <div class="oval">Resource</div> + <div class="rectangle">Instance</div> + <div class="hint"> + (Scroll to zoom; Drag to move; Click for details) + </div> + <span fxFlex="1 1 auto"></span> + <button mat-button (click)="updateResources()"> + <mat-icon>refresh</mat-icon> + Refresh Status + </button> + <button + mat-button + *ngIf="selectedResource || selectedInstance" + color="accent" + [routerLink]="['../', selectedResource ? 'resources' : 'instances', selectedResource || selectedInstance]"> + {{ selectedResource || selectedInstance }} + <mat-icon>arrow_forward</mat-icon> + </button> + </section> + <section + class="cluster-dashboard" + [visNetwork]="visNetwork" + [visNetworkData]="visNetworkData" + [visNetworkOptions]="visNetworkOptions" + (initialized)="networkInitialized()"> + </section> +</section> http://git-wip-us.apache.org/repos/asf/helix/blob/a97f1580/helix-front/client/app/dashboard/dashboard.component.scss ---------------------------------------------------------------------- diff --git a/helix-front/client/app/dashboard/dashboard.component.scss b/helix-front/client/app/dashboard/dashboard.component.scss new file mode 100644 index 0000000..dd14878 --- /dev/null +++ b/helix-front/client/app/dashboard/dashboard.component.scss @@ -0,0 +1,43 @@ +:host { + width: 100%; + height: 100%; +} + +.cluster-dashboard { + width: 800px; + height: 600px; +} + +.info { + font-size: 12px; + height: 36px; + background-color: #fff; + border-bottom: 1px solid #ccc; + text-align: center; + vertical-align: middle; + line-height: 24px; +} + +.oval { + width: 80px; + height: 24px; + background-color: #7FCAC3; + border: 1px solid #65A19C; + -moz-border-radius: 80px / 24px; + -webkit-border-radius: 80px / 24px; + border-raius: 80px / 24px; +} + +.rectangle { + width: 80px; + height: 24px; + background-color: #90CAF9; + border: 1px solid #73A1C7; + -moz-border-radius: 5px; + -webkit-border-radius: 5px; + border-raius: 5px; +} + +.hint { + color: gray; +} http://git-wip-us.apache.org/repos/asf/helix/blob/a97f1580/helix-front/client/app/dashboard/dashboard.component.spec.ts ---------------------------------------------------------------------- diff --git a/helix-front/client/app/dashboard/dashboard.component.spec.ts b/helix-front/client/app/dashboard/dashboard.component.spec.ts new file mode 100644 index 0000000..2c1c53a --- /dev/null +++ b/helix-front/client/app/dashboard/dashboard.component.spec.ts @@ -0,0 +1,31 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; + +import { VisModule } from 'ngx-vis'; + +import { TestingModule } from '../../testing/testing.module'; +import { DashboardComponent } from './dashboard.component'; + +describe('DashboardComponent', () => { + let component: DashboardComponent; + let fixture: ComponentFixture<DashboardComponent>; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ TestingModule, VisModule ], + schemas: [ NO_ERRORS_SCHEMA ], + declarations: [ DashboardComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(DashboardComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should be created', () => { + expect(component).toBeTruthy(); + }); +}); http://git-wip-us.apache.org/repos/asf/helix/blob/a97f1580/helix-front/client/app/dashboard/dashboard.component.ts ---------------------------------------------------------------------- diff --git a/helix-front/client/app/dashboard/dashboard.component.ts b/helix-front/client/app/dashboard/dashboard.component.ts new file mode 100644 index 0000000..dbe32a3 --- /dev/null +++ b/helix-front/client/app/dashboard/dashboard.component.ts @@ -0,0 +1,274 @@ +import { + Component, + ElementRef, + OnInit, + AfterViewInit, + OnDestroy +} from '@angular/core'; +import { Router, ActivatedRoute } from '@angular/router'; +import { Observable, Subscription } from 'rxjs'; + +import * as _ from 'lodash'; +import { VisNode, VisNodes, VisEdges, VisNetworkService, VisNetworkData, VisNetworkOptions } from 'ngx-vis'; + +import { ResourceService } from '../resource/shared/resource.service'; +import { InstanceService } from '../instance/shared/instance.service'; +import { HelperService } from '../shared/helper.service'; + +class DashboardNetworkData implements VisNetworkData { + public nodes: VisNodes; + public edges: VisEdges; +} + +@Component({ + selector: 'hi-dashboard', + templateUrl: './dashboard.component.html', + styleUrls: ['./dashboard.component.scss'], + providers: [ResourceService, InstanceService] +}) +export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy { + + visNetwork = 'cluster-dashboard'; + visNetworkData: DashboardNetworkData; + visNetworkOptions: VisNetworkOptions; + + clusterName: string; + isLoading = false; + resourceToId = {}; + instanceToId = {}; + selectedResource: any; + selectedInstance: any; + updateSubscription: Subscription; + updateInterval = 3000; + + constructor( + private el:ElementRef, + private route: ActivatedRoute, + protected visService: VisNetworkService, + protected resourceService: ResourceService, + protected instanceService: InstanceService, + protected helper: HelperService + ) { } + + networkInitialized() { + this.visService.on(this.visNetwork, 'click'); + this.visService.on(this.visNetwork, 'zoom'); + + this.visService.click + .subscribe((eventData: any[]) => { + if (eventData[0] === this.visNetwork) { + // clear the edges first + this.visNetworkData.edges.clear(); + this.selectedResource = null; + this.selectedInstance = null; + + // + if (eventData[1].nodes.length) { + const id = eventData[1].nodes[0]; + this.onNodeSelected(id); + } + } + }); + + this.visService.zoom + .subscribe((eventData: any) => { + if (eventData[0] === this.visNetwork) { + const scale = eventData[1].scale; + if (scale == 10) { + // too big + } else if (scale < 0.3) { + // small enough + } + } + }); + } + + ngOnInit() { + const nodes = new VisNodes(); + const edges = new VisEdges(); + this.visNetworkData = { nodes, edges }; + + this.visNetworkOptions = { + interaction: { + navigationButtons: true, + keyboard: true + }, + layout: { + // layout will be the same every time the nodes are settled + randomSeed: 7 + }, + physics: { + // default is barnesHut + solver: 'forceAtlas2Based', + forceAtlas2Based: { + // default: -50 + gravitationalConstant: -30, + // default: 0 + // avoidOverlap: 0.3 + } + }, + groups: { + resource: { + color: '#7FCAC3', + shape: 'ellipse', + widthConstraint: { maximum: 140 } + }, + instance: { + color: '#90CAF9', + shape: 'box', + widthConstraint: { maximum: 140 } + }, + instance_bad: { + color: '#CA7F86', + shape: 'box', + widthConstraint: { maximum: 140 } + }, + partition: { + color: '#98D4B1', + shape: 'ellipse', + widthConstraint: { maximum: 140 } + } + } + }; + } + + initDashboard() { + // resize container according to the parent + let width = this.el.nativeElement.offsetWidth; + let height = this.el.nativeElement.offsetHeight - 36; + let dashboardDom = this.el.nativeElement.getElementsByClassName(this.visNetwork)[0]; + dashboardDom.style.width = `${ width }px`; + dashboardDom.style.height = `${ height }px`; + + // load data + this.route.parent.params + .map(p => p.name) + .subscribe(name => { + this.clusterName = name; + this.fetchResources(); + // this.updateResources(); + }); + } + + ngAfterViewInit() { + setTimeout(_ => this.initDashboard()); + } + + ngOnDestroy(): void { + if (this.updateSubscription) { + this.updateSubscription.unsubscribe(); + } + + this.visService.off(this.visNetwork, 'zoom'); + this.visService.off(this.visNetwork, 'click'); + } + + protected fetchResources() { + this.isLoading = true; + + this.resourceService + .getAll(this.clusterName) + .subscribe( + result => { + _.forEach(result, (resource) => { + const newId = this.visNetworkData.nodes.getLength() + 1; + this.resourceToId[resource.name] = newId; + this.visNetworkData.nodes.add({ + id: newId, + label: resource.name, + group: 'resource', + title: JSON.stringify(resource) + }); + }); + + this.visService.fit(this.visNetwork); + }, + error => this.helper.showError(error), + () => this.isLoading = false + ); + + this.instanceService + .getAll(this.clusterName) + .subscribe( + result => { + _.forEach(result, (instance) => { + const newId = this.visNetworkData.nodes.getLength() + 1; + this.instanceToId[instance.name] = newId; + this.visNetworkData.nodes.add({ + id: newId, + label: instance.name, + group: instance.healthy ? 'instance' : 'instance_bad', + title: JSON.stringify(instance), + }); + }); + + this.visService.fit(this.visNetwork); + }, + error => this.helper.showError(error), + () => this.isLoading = false + ); + } + + updateResources() { + /* disable auto-update for now + this.updateSubscription = Observable + .interval(this.updateInterval) + .flatMap(i => this.instanceService.getAll(this.clusterName))*/ + this.instanceService.getAll(this.clusterName) + .subscribe( + result => { + _.forEach(result, instance => { + const id = this.instanceToId[instance.name]; + this.visNetworkData.nodes.update([{ + id: id, + group: instance.healthy ? 'instance' : 'instance_bad' + }]); + }); + } + ); + } + + protected onNodeSelected(id) { + const instanceName = _.findKey(this.instanceToId, value => value === id); + if (instanceName) { + this.selectedInstance = instanceName; + // fetch relationships + this.resourceService + .getAllOnInstance(this.clusterName, instanceName) + .subscribe( + resources => { + _.forEach(resources, (resource) => { + this.visNetworkData.edges.add({ + from: id, + to: this.resourceToId[resource.name] + }); + }); + }, + error => this.helper.showError(error) + ); + } else { + const resourceName = _.findKey(this.resourceToId, value => value === id); + if (resourceName) { + this.selectedResource = resourceName; + this.resourceService + .get(this.clusterName, resourceName) + .subscribe( + resource => { + _(resource.partitions) + .flatMap('replicas') + .unionBy('instanceName') + .map('instanceName') + .forEach((instanceName) => { + this.visNetworkData.edges.add({ + from: this.instanceToId[instanceName], + to: this.resourceToId[resourceName] + }); + }); + }, + error => this.helper.showError(error) + ); + } + } + } + +} http://git-wip-us.apache.org/repos/asf/helix/blob/a97f1580/helix-front/client/app/dashboard/dashboard.module.ts ---------------------------------------------------------------------- diff --git a/helix-front/client/app/dashboard/dashboard.module.ts b/helix-front/client/app/dashboard/dashboard.module.ts new file mode 100644 index 0000000..e048327 --- /dev/null +++ b/helix-front/client/app/dashboard/dashboard.module.ts @@ -0,0 +1,21 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; + +import { VisModule } from 'ngx-vis'; +import { NgxChartsModule } from '@swimlane/ngx-charts'; + +import { SharedModule } from '../shared/shared.module'; +import { DashboardComponent } from './dashboard.component'; + +@NgModule({ + imports: [ + CommonModule, + SharedModule, + VisModule, + NgxChartsModule + ], + declarations: [ + DashboardComponent + ] +}) +export class DashboardModule { } http://git-wip-us.apache.org/repos/asf/helix/blob/a97f1580/helix-front/client/styles.scss ---------------------------------------------------------------------- diff --git a/helix-front/client/styles.scss b/helix-front/client/styles.scss index 9a64f47..820de3a 100644 --- a/helix-front/client/styles.scss +++ b/helix-front/client/styles.scss @@ -2,6 +2,9 @@ @import url('https://fonts.googleapis.com/icon?family=Material+Icons'); @import url('https://fonts.googleapis.com/css?family=Roboto:300,400,500,700,400italic'); +// ngx-vis styles (vis styles) +@import '~vis/dist/vis-network.min.css'; + // ngx-datatable styles @import '~@swimlane/ngx-datatable/release/index.css'; @import '~@swimlane/ngx-datatable/release/themes/material.css'; http://git-wip-us.apache.org/repos/asf/helix/blob/a97f1580/helix-front/package.json ---------------------------------------------------------------------- diff --git a/helix-front/package.json b/helix-front/package.json index 626fedd..846ff78 100644 --- a/helix-front/package.json +++ b/helix-front/package.json @@ -27,7 +27,7 @@ "@angular/common": "^5.1.1", "@angular/compiler": "^5.1.1", "@angular/core": "^5.1.1", - "@angular/flex-layout": "^2.0.0-beta.12", + "@angular/flex-layout": "2.0.0-beta.12", "@angular/forms": "^5.1.1", "@angular/http": "^5.1.1", "@angular/material": "^5.0.1", @@ -50,9 +50,11 @@ "ngx-clipboard": "^9.0.0", "ngx-dag": "0.0.2", "ngx-json-viewer": "^2.3.0", + "ngx-vis": "^0.1.0", "node-sass": "4.5.3", "request": "^2.81.0", "rxjs": "^5.5.5", + "vis": "^4.21.0", "zone.js": "^0.8.4" }, "devDependencies": {
