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": {