You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@helix.apache.org by jx...@apache.org on 2018/08/18 00:20:07 UTC
[07/14] helix git commit: Introduce Helix cluster dashboard
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 <vx...@linkedin.com>
Authored: Thu Dec 21 15:38:07 2017 -0800
Committer: Vivo Xu <vx...@linkedin.com>
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": {