This is an automated email from the ASF dual-hosted git repository.

wilfreds pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/yunikorn-web.git


The following commit(s) were added to refs/heads/master by this push:
     new d4e85f3  [YUNIKORN-325] add node resource utilization chart (#136)
d4e85f3 is described below

commit d4e85f35eb924d79b37f0b7331a5f270febe0741
Author: Cliff Su <[email protected]>
AuthorDate: Fri Oct 27 12:18:01 2023 +1100

    [YUNIKORN-325] add node resource utilization chart (#136)
    
    Add node utilisation to the dashboard in the form of a bar chart.
    New component for a bar chart, besides the donut chart added.
    Add nodes/utilization mock data to json db
    
    Closes: #136
    
    Signed-off-by: Wilfred Spiegelenburg <[email protected]>
---
 json-db.json                                       | 418 ++++++++++++++++++++-
 json-routes.json                                   |   1 +
 src/app/app.module.ts                              |  40 +-
 .../app-node-utilization.component.html}           |  27 +-
 .../app-node-utilization.component.scss}           |  14 +-
 .../app-node-utilization.component.spec.ts}        |  30 +-
 .../app-node-utilization.component.ts}             |  18 +-
 .../components/app-status/app-status.component.ts  |   5 +-
 .../bar-chart/bar-chart.component.html}            |  18 +-
 .../bar-chart/bar-chart.component.scss}            |  13 +-
 .../bar-chart.component.spec.ts}                   |  29 +-
 .../bar-chart.component.ts}                        |  53 +--
 .../container-status/container-status.component.ts |   5 +-
 .../components/dashboard/dashboard.component.html  |   5 +
 .../dashboard/dashboard.component.spec.ts          |   2 +
 .../components/dashboard/dashboard.component.ts    |  58 ++-
 .../donut-chart/donut-chart.component.ts           |   9 +-
 .../{donut-data.model.ts => chart-data.model.ts}   |   2 +-
 ...nut-data.model.ts => node-utilization.model.ts} |  19 +-
 src/app/services/scheduler/scheduler.service.ts    |   8 +-
 src/app/testing/mocks.ts                           |   1 +
 21 files changed, 604 insertions(+), 171 deletions(-)

diff --git a/json-db.json b/json-db.json
index 6144881..9ba1666 100644
--- a/json-db.json
+++ b/json-db.json
@@ -751,6 +751,422 @@
       "reservations": []
     }
   ],
+  "utilization": [
+    {
+      "type": "ephemeral-storage",
+      "utilization": [
+        {
+          "bucketName": "0-10%",
+          "numOfNodes": -1,
+          "nodeNames": [
+            "N/A"
+          ]
+        },
+        {
+          "bucketName": "10-20%",
+          "numOfNodes": -1,
+          "nodeNames": [
+            "N/A"
+          ]
+        },
+        {
+          "bucketName": "20-30%",
+          "numOfNodes": -1,
+          "nodeNames": [
+            "N/A"
+          ]
+        },
+        {
+          "bucketName": "30-40%",
+          "numOfNodes": -1,
+          "nodeNames": [
+            "N/A"
+          ]
+        },
+        {
+          "bucketName": "40-50%",
+          "numOfNodes": -1,
+          "nodeNames": [
+            "N/A"
+          ]
+        },
+        {
+          "bucketName": "50-60%",
+          "numOfNodes": -1,
+          "nodeNames": [
+            "N/A"
+          ]
+        },
+        {
+          "bucketName": "60-70%",
+          "numOfNodes": -1,
+          "nodeNames": [
+            "N/A"
+          ]
+        },
+        {
+          "bucketName": "70-80%",
+          "numOfNodes": -1,
+          "nodeNames": [
+            "N/A"
+          ]
+        },
+        {
+          "bucketName": "80-90%",
+          "numOfNodes": -1,
+          "nodeNames": [
+            "N/A"
+          ]
+        },
+        {
+          "bucketName": "90-100%",
+          "numOfNodes": -1,
+          "nodeNames": [
+            "N/A"
+          ]
+        }
+      ]
+    },
+    {
+      "type": "hugepages-1Gi",
+      "utilization": [
+        {
+          "bucketName": "0-10%",
+          "numOfNodes": -1,
+          "nodeNames": [
+            "N/A"
+          ]
+        },
+        {
+          "bucketName": "10-20%",
+          "numOfNodes": -1,
+          "nodeNames": [
+            "N/A"
+          ]
+        },
+        {
+          "bucketName": "20-30%",
+          "numOfNodes": -1,
+          "nodeNames": [
+            "N/A"
+          ]
+        },
+        {
+          "bucketName": "30-40%",
+          "numOfNodes": -1,
+          "nodeNames": [
+            "N/A"
+          ]
+        },
+        {
+          "bucketName": "40-50%",
+          "numOfNodes": -1,
+          "nodeNames": [
+            "N/A"
+          ]
+        },
+        {
+          "bucketName": "50-60%",
+          "numOfNodes": -1,
+          "nodeNames": [
+            "N/A"
+          ]
+        },
+        {
+          "bucketName": "60-70%",
+          "numOfNodes": -1,
+          "nodeNames": [
+            "N/A"
+          ]
+        },
+        {
+          "bucketName": "70-80%",
+          "numOfNodes": -1,
+          "nodeNames": [
+            "N/A"
+          ]
+        },
+        {
+          "bucketName": "80-90%",
+          "numOfNodes": -1,
+          "nodeNames": [
+            "N/A"
+          ]
+        },
+        {
+          "bucketName": "90-100%",
+          "numOfNodes": -1,
+          "nodeNames": [
+            "N/A"
+          ]
+        }
+      ]
+    },
+    {
+      "type": "hugepages-2Mi",
+      "utilization": [
+        {
+          "bucketName": "0-10%",
+          "numOfNodes": -1,
+          "nodeNames": [
+            "N/A"
+          ]
+        },
+        {
+          "bucketName": "10-20%",
+          "numOfNodes": -1,
+          "nodeNames": [
+            "N/A"
+          ]
+        },
+        {
+          "bucketName": "20-30%",
+          "numOfNodes": -1,
+          "nodeNames": [
+            "N/A"
+          ]
+        },
+        {
+          "bucketName": "30-40%",
+          "numOfNodes": -1,
+          "nodeNames": [
+            "N/A"
+          ]
+        },
+        {
+          "bucketName": "40-50%",
+          "numOfNodes": -1,
+          "nodeNames": [
+            "N/A"
+          ]
+        },
+        {
+          "bucketName": "50-60%",
+          "numOfNodes": -1,
+          "nodeNames": [
+            "N/A"
+          ]
+        },
+        {
+          "bucketName": "60-70%",
+          "numOfNodes": -1,
+          "nodeNames": [
+            "N/A"
+          ]
+        },
+        {
+          "bucketName": "70-80%",
+          "numOfNodes": -1,
+          "nodeNames": [
+            "N/A"
+          ]
+        },
+        {
+          "bucketName": "80-90%",
+          "numOfNodes": -1,
+          "nodeNames": [
+            "N/A"
+          ]
+        },
+        {
+          "bucketName": "90-100%",
+          "numOfNodes": -1,
+          "nodeNames": [
+            "N/A"
+          ]
+        }
+      ]
+    },
+    {
+      "type": "memory",
+      "utilization": [
+        {
+          "bucketName": "0-10%",
+          "numOfNodes": 0,
+          "nodeNames": null
+        },
+        {
+          "bucketName": "10-20%",
+          "numOfNodes": 1,
+          "nodeNames": [
+            "aethergpu"
+          ]
+        },
+        {
+          "bucketName": "20-30%",
+          "numOfNodes": 0,
+          "nodeNames": null
+        },
+        {
+          "bucketName": "30-40%",
+          "numOfNodes": 0,
+          "nodeNames": null
+        },
+        {
+          "bucketName": "40-50%",
+          "numOfNodes": 0,
+          "nodeNames": null
+        },
+        {
+          "bucketName": "50-60%",
+          "numOfNodes": 0,
+          "nodeNames": null
+        },
+        {
+          "bucketName": "60-70%",
+          "numOfNodes": 0,
+          "nodeNames": null
+        },
+        {
+          "bucketName": "70-80%",
+          "numOfNodes": 0,
+          "nodeNames": null
+        },
+        {
+          "bucketName": "80-90%",
+          "numOfNodes": 0,
+          "nodeNames": null
+        },
+        {
+          "bucketName": "90-100%",
+          "numOfNodes": 0,
+          "nodeNames": null
+        }
+      ]
+    },
+    {
+      "type": "pods",
+      "utilization": [
+        {
+          "bucketName": "0-10%",
+          "numOfNodes": -1,
+          "nodeNames": [
+            "N/A"
+          ]
+        },
+        {
+          "bucketName": "10-20%",
+          "numOfNodes": -1,
+          "nodeNames": [
+            "N/A"
+          ]
+        },
+        {
+          "bucketName": "20-30%",
+          "numOfNodes": -1,
+          "nodeNames": [
+            "N/A"
+          ]
+        },
+        {
+          "bucketName": "30-40%",
+          "numOfNodes": -1,
+          "nodeNames": [
+            "N/A"
+          ]
+        },
+        {
+          "bucketName": "40-50%",
+          "numOfNodes": -1,
+          "nodeNames": [
+            "N/A"
+          ]
+        },
+        {
+          "bucketName": "50-60%",
+          "numOfNodes": -1,
+          "nodeNames": [
+            "N/A"
+          ]
+        },
+        {
+          "bucketName": "60-70%",
+          "numOfNodes": -1,
+          "nodeNames": [
+            "N/A"
+          ]
+        },
+        {
+          "bucketName": "70-80%",
+          "numOfNodes": -1,
+          "nodeNames": [
+            "N/A"
+          ]
+        },
+        {
+          "bucketName": "80-90%",
+          "numOfNodes": -1,
+          "nodeNames": [
+            "N/A"
+          ]
+        },
+        {
+          "bucketName": "90-100%",
+          "numOfNodes": -1,
+          "nodeNames": [
+            "N/A"
+          ]
+        }
+      ]
+    },
+    {
+      "type": "vcore",
+      "utilization": [
+        {
+          "bucketName": "0-10%",
+          "numOfNodes": 1,
+          "nodeNames": [
+            "aethergpu"
+          ]
+        },
+        {
+          "bucketName": "10-20%",
+          "numOfNodes": 0,
+          "nodeNames": null
+        },
+        {
+          "bucketName": "20-30%",
+          "numOfNodes": 0,
+          "nodeNames": null
+        },
+        {
+          "bucketName": "30-40%",
+          "numOfNodes": 0,
+          "nodeNames": null
+        },
+        {
+          "bucketName": "40-50%",
+          "numOfNodes": 0,
+          "nodeNames": null
+        },
+        {
+          "bucketName": "50-60%",
+          "numOfNodes": 0,
+          "nodeNames": null
+        },
+        {
+          "bucketName": "60-70%",
+          "numOfNodes": 0,
+          "nodeNames": null
+        },
+        {
+          "bucketName": "70-80%",
+          "numOfNodes": 0,
+          "nodeNames": null
+        },
+        {
+          "bucketName": "80-90%",
+          "numOfNodes": 0,
+          "nodeNames": null
+        },
+        {
+          "bucketName": "90-100%",
+          "numOfNodes": 0,
+          "nodeNames": null
+        }
+      ]
+    }
+  ],
   "partitions": [
     {
       "clusterId": "mycluster",
@@ -903,4 +1319,4 @@
       }
     ]
   }
-}
+}
\ No newline at end of file
diff --git a/json-routes.json b/json-routes.json
index fe19605..7bb5032 100644
--- a/json-routes.json
+++ b/json-routes.json
@@ -1,4 +1,5 @@
 {
+  "/ws/v1/nodes/utilization": "/utilization",
   "/ws/v1/*": "/$1",
   "/history/apps": "/appHistory",
   "/history/containers": "/containerHistory",
diff --git a/src/app/app.module.ts b/src/app/app.module.ts
index 48ed265..6cc8fc8 100644
--- a/src/app/app.module.ts
+++ b/src/app/app.module.ts
@@ -40,24 +40,26 @@ import { MatButtonModule } from '@angular/material/button';
 import { MatExpansionModule } from '@angular/material/expansion';
 import {MatIconModule} from '@angular/material/icon';
 
-import { AppRoutingModule } from './app-routing.module';
-import { envConfigFactory, EnvconfigService } from 
'./services/envconfig/envconfig.service';
-import { ApiErrorInterceptor } from 
'./interceptors/api-error/api-error.interceptor';
-import { AppComponent } from './app.component';
-import { DashboardComponent } from 
'./components/dashboard/dashboard.component';
-import { QueuesViewComponent } from 
'./components/queues-view/queues-view.component';
-import { DonutChartComponent } from 
'./components/donut-chart/donut-chart.component';
-import { AreaChartComponent } from 
'./components/area-chart/area-chart.component';
-import { AppStatusComponent } from 
'./components/app-status/app-status.component';
-import { AppHistoryComponent } from 
'./components/app-history/app-history.component';
-import { ContainerStatusComponent } from 
'./components/container-status/container-status.component';
-import { ContainerHistoryComponent } from 
'./components/container-history/container-history.component';
-import { QueueRackComponent } from 
'./components/queue-rack/queue-rack.component';
-import { AppsViewComponent } from './components/apps-view/apps-view.component';
-import { NodesViewComponent } from 
'./components/nodes-view/nodes-view.component';
-import { ErrorViewComponent } from 
'./components/error-view/error-view.component';
-import { StatusViewComponent } from 
'./components/status-view/status-view.component';
-import { HealthchecksComponent } from 
'./components/healthchecks/healthchecks.component';
+import { AppRoutingModule } from '@app/app-routing.module';
+import { envConfigFactory, EnvconfigService } from 
'@app/services/envconfig/envconfig.service';
+import { ApiErrorInterceptor } from 
'@app/interceptors/api-error/api-error.interceptor';
+import { AppComponent } from '@app/app.component';
+import { DashboardComponent } from 
'@app/components/dashboard/dashboard.component';
+import { QueuesViewComponent } from 
'@app/components/queues-view/queues-view.component';
+import { DonutChartComponent } from 
'@app/components/donut-chart/donut-chart.component';
+import { AreaChartComponent } from 
'@app/components/area-chart/area-chart.component';
+import { AppStatusComponent } from 
'@app/components/app-status/app-status.component';
+import { AppHistoryComponent } from 
'@app/components/app-history/app-history.component';
+import { ContainerStatusComponent } from 
'@app/components/container-status/container-status.component';
+import { ContainerHistoryComponent } from 
'@app/components/container-history/container-history.component';
+import { QueueRackComponent } from 
'@app/components/queue-rack/queue-rack.component';
+import { AppsViewComponent } from 
'@app/components/apps-view/apps-view.component';
+import { NodesViewComponent } from 
'@app/components/nodes-view/nodes-view.component';
+import { ErrorViewComponent } from 
'@app/components/error-view/error-view.component';
+import { StatusViewComponent } from 
'@app/components/status-view/status-view.component';
+import { HealthchecksComponent } from 
'@app/components/healthchecks/healthchecks.component';
+import { AppNodeUtilizationComponent } from 
'@app/components/app-node-utilization/app-node-utilization.component';
+import { BarChartComponent } from 
'@app/components/bar-chart/bar-chart.component';
 
 @NgModule({
   declarations: [
@@ -76,6 +78,8 @@ import { HealthchecksComponent } from 
'./components/healthchecks/healthchecks.co
     ErrorViewComponent,
     StatusViewComponent,
     HealthchecksComponent,
+    AppNodeUtilizationComponent,
+    BarChartComponent,
   ],
   imports: [
     BrowserModule,
diff --git a/src/app/models/donut-data.model.ts 
b/src/app/components/app-node-utilization/app-node-utilization.component.html
similarity index 67%
copy from src/app/models/donut-data.model.ts
copy to 
src/app/components/app-node-utilization/app-node-utilization.component.html
index 09457ac..3137ce6 100644
--- a/src/app/models/donut-data.model.ts
+++ 
b/src/app/components/app-node-utilization/app-node-utilization.component.html
@@ -1,4 +1,4 @@
-/**
+<!--
  * Licensed to the Apache Software Foundation (ASF) under one
  * or more contributor license agreements.  See the NOTICE file
  * distributed with this work for additional information
@@ -14,16 +14,17 @@
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * See the License for the specific language governing permissions and
  * limitations under the License.
- */
+ -->
 
-export class DonutDataItem {
-  name: string;
-  value: number;
-  color: string;
-
-  constructor(name: string, value: number, color: string) {
-    this.name = name;
-    this.value = value;
-    this.color = color;
-  }
-}
+<mat-card appearance="outlined" class="box-card">
+  <mat-card-header>
+    <mat-card-title>Nodes Resource Utilization</mat-card-title>
+  </mat-card-header>
+  <mat-card-content>
+    <div class="status-wrapper flex-grid">
+      <div class="chart-wrapper flex-primary">
+        <app-bar-chart [data]="chartData" />
+      </div>
+    </div>
+  </mat-card-content>
+</mat-card>
diff --git a/src/app/models/donut-data.model.ts 
b/src/app/components/app-node-utilization/app-node-utilization.component.scss
similarity index 79%
copy from src/app/models/donut-data.model.ts
copy to 
src/app/components/app-node-utilization/app-node-utilization.component.scss
index 09457ac..9dc82d0 100644
--- a/src/app/models/donut-data.model.ts
+++ 
b/src/app/components/app-node-utilization/app-node-utilization.component.scss
@@ -16,14 +16,8 @@
  * limitations under the License.
  */
 
-export class DonutDataItem {
-  name: string;
-  value: number;
-  color: string;
-
-  constructor(name: string, value: number, color: string) {
-    this.name = name;
-    this.value = value;
-    this.color = color;
-  }
+.status-wrapper {
+  width: 100%;
+  height: 100%;
+  align-items: center;
 }
diff --git a/src/app/components/container-status/container-status.component.ts 
b/src/app/components/app-node-utilization/app-node-utilization.component.spec.ts
similarity index 54%
copy from src/app/components/container-status/container-status.component.ts
copy to 
src/app/components/app-node-utilization/app-node-utilization.component.spec.ts
index 2390d59..237bf64 100644
--- a/src/app/components/container-status/container-status.component.ts
+++ 
b/src/app/components/app-node-utilization/app-node-utilization.component.spec.ts
@@ -16,19 +16,23 @@
  * limitations under the License.
  */
 
-import { Component, OnInit, Input } from '@angular/core';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { AppNodeUtilizationComponent } from 
'@app/components/app-node-utilization/app-node-utilization.component';
 
-import { DonutDataItem } from '@app/models/donut-data.model';
+describe('AppNodeUtilizationComponent', () => {
+  let component: AppNodeUtilizationComponent;
+  let fixture: ComponentFixture<AppNodeUtilizationComponent>;
 
-@Component({
-  selector: 'app-container-status',
-  templateUrl: './container-status.component.html',
-  styleUrls: ['./container-status.component.scss'],
-})
-export class ContainerStatusComponent implements OnInit {
-  @Input() chartData: DonutDataItem[] = [];
+  beforeEach(() => {
+    TestBed.configureTestingModule({
+      declarations: [AppNodeUtilizationComponent]
+    });
+    fixture = TestBed.createComponent(AppNodeUtilizationComponent);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  });
 
-  constructor() {}
-
-  ngOnInit() {}
-}
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+});
diff --git a/src/app/models/donut-data.model.ts 
b/src/app/components/app-node-utilization/app-node-utilization.component.ts
similarity index 69%
copy from src/app/models/donut-data.model.ts
copy to 
src/app/components/app-node-utilization/app-node-utilization.component.ts
index 09457ac..28b5046 100644
--- a/src/app/models/donut-data.model.ts
+++ b/src/app/components/app-node-utilization/app-node-utilization.component.ts
@@ -16,14 +16,14 @@
  * limitations under the License.
  */
 
-export class DonutDataItem {
-  name: string;
-  value: number;
-  color: string;
+import { Component, Input } from '@angular/core';
+import { ChartDataItem } from '@app/models/chart-data.model';
 
-  constructor(name: string, value: number, color: string) {
-    this.name = name;
-    this.value = value;
-    this.color = color;
-  }
+@Component({
+  selector: 'app-node-utilization',
+  templateUrl: './app-node-utilization.component.html',
+  styleUrls: ['./app-node-utilization.component.scss']
+})
+export class AppNodeUtilizationComponent {
+  @Input() chartData: ChartDataItem[] = [];
 }
diff --git a/src/app/components/app-status/app-status.component.ts 
b/src/app/components/app-status/app-status.component.ts
index 91fa178..e936156 100644
--- a/src/app/components/app-status/app-status.component.ts
+++ b/src/app/components/app-status/app-status.component.ts
@@ -17,8 +17,7 @@
  */
 
 import { Component, OnInit, Input } from '@angular/core';
-
-import { DonutDataItem } from '@app/models/donut-data.model';
+import { ChartDataItem } from '@app/models/chart-data.model';
 
 @Component({
   selector: 'app-application-status',
@@ -26,7 +25,7 @@ import { DonutDataItem } from '@app/models/donut-data.model';
   styleUrls: ['./app-status.component.scss'],
 })
 export class AppStatusComponent implements OnInit {
-  @Input() chartData: DonutDataItem[] = [];
+  @Input() chartData: ChartDataItem[] = [];
 
   constructor() {}
 
diff --git a/src/app/models/donut-data.model.ts 
b/src/app/components/bar-chart/bar-chart.component.html
similarity index 78%
copy from src/app/models/donut-data.model.ts
copy to src/app/components/bar-chart/bar-chart.component.html
index 09457ac..5d08ea8 100644
--- a/src/app/models/donut-data.model.ts
+++ b/src/app/components/bar-chart/bar-chart.component.html
@@ -1,4 +1,4 @@
-/**
+<!--
  * Licensed to the Apache Software Foundation (ASF) under one
  * or more contributor license agreements.  See the NOTICE file
  * distributed with this work for additional information
@@ -14,16 +14,8 @@
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * See the License for the specific language governing permissions and
  * limitations under the License.
- */
+ -->
 
-export class DonutDataItem {
-  name: string;
-  value: number;
-  color: string;
-
-  constructor(name: string, value: number, color: string) {
-    this.name = name;
-    this.value = value;
-    this.color = color;
-  }
-}
+<div style="width: 100%; height: 100%">
+  <canvas class="bar-chart" id="{{ chartContainerId }}"></canvas>
+</div>
diff --git a/src/app/models/donut-data.model.ts 
b/src/app/components/bar-chart/bar-chart.component.scss
similarity index 79%
copy from src/app/models/donut-data.model.ts
copy to src/app/components/bar-chart/bar-chart.component.scss
index 09457ac..6a5db9a 100644
--- a/src/app/models/donut-data.model.ts
+++ b/src/app/components/bar-chart/bar-chart.component.scss
@@ -16,14 +16,7 @@
  * limitations under the License.
  */
 
-export class DonutDataItem {
-  name: string;
-  value: number;
-  color: string;
-
-  constructor(name: string, value: number, color: string) {
-    this.name = name;
-    this.value = value;
-    this.color = color;
-  }
+.bar-chart {
+  width: 100%;
+  height: 100%;
 }
diff --git a/src/app/components/container-status/container-status.component.ts 
b/src/app/components/bar-chart/bar-chart.component.spec.ts
similarity index 58%
copy from src/app/components/container-status/container-status.component.ts
copy to src/app/components/bar-chart/bar-chart.component.spec.ts
index 2390d59..49e06be 100644
--- a/src/app/components/container-status/container-status.component.ts
+++ b/src/app/components/bar-chart/bar-chart.component.spec.ts
@@ -16,19 +16,24 @@
  * limitations under the License.
  */
 
-import { Component, OnInit, Input } from '@angular/core';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
 
-import { DonutDataItem } from '@app/models/donut-data.model';
+import { BarChartComponent } from './bar-chart.component';
 
-@Component({
-  selector: 'app-container-status',
-  templateUrl: './container-status.component.html',
-  styleUrls: ['./container-status.component.scss'],
-})
-export class ContainerStatusComponent implements OnInit {
-  @Input() chartData: DonutDataItem[] = [];
+describe('BarChartComponent', () => {
+  let component: BarChartComponent;
+  let fixture: ComponentFixture<BarChartComponent>;
 
-  constructor() {}
+  beforeEach(() => {
+    TestBed.configureTestingModule({
+      declarations: [BarChartComponent]
+    });
+    fixture = TestBed.createComponent(BarChartComponent);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  });
 
-  ngOnInit() {}
-}
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+});
diff --git a/src/app/components/donut-chart/donut-chart.component.ts 
b/src/app/components/bar-chart/bar-chart.component.ts
similarity index 71%
copy from src/app/components/donut-chart/donut-chart.component.ts
copy to src/app/components/bar-chart/bar-chart.component.ts
index e7bb71e..c89b7c5 100644
--- a/src/app/components/donut-chart/donut-chart.component.ts
+++ b/src/app/components/bar-chart/bar-chart.component.ts
@@ -16,42 +16,31 @@
  * limitations under the License.
  */
 
-import {
-  Component,
-  OnInit,
-  AfterViewInit,
-  Input,
-  OnChanges,
-  SimpleChanges,
-  OnDestroy,
-} from '@angular/core';
-import { Subject } from 'rxjs';
-import { takeUntil } from 'rxjs/operators';
-import { Chart, ArcElement, DoughnutController } from 'chart.js';
-
-import { CommonUtil } from '@app/utils/common.util';
-import { DonutDataItem } from '@app/models/donut-data.model';
+import { AfterViewInit, Component, Input, OnChanges, OnDestroy, OnInit, 
SimpleChanges } from '@angular/core';
+import { ChartDataItem } from '@app/models/chart-data.model';
 import { EventBusService, EventMap } from 
'@app/services/event-bus/event-bus.service';
+import { CommonUtil } from '@app/utils/common.util';
+import { Chart, BarController, CategoryScale, BarElement } from 'chart.js';
+import { Subject, takeUntil } from 'rxjs';
 
-Chart.register(ArcElement, DoughnutController);
+Chart.register(BarElement, CategoryScale, BarController);
 
 @Component({
-  selector: 'app-donut-chart',
-  templateUrl: './donut-chart.component.html',
-  styleUrls: ['./donut-chart.component.scss'],
+  selector: 'app-bar-chart',
+  templateUrl: './bar-chart.component.html',
+  styleUrls: ['./bar-chart.component.scss']
 })
-export class DonutChartComponent implements OnInit, AfterViewInit, OnChanges, 
OnDestroy {
+export class BarChartComponent implements OnInit, AfterViewInit, OnChanges, 
OnDestroy {
   destroy$ = new Subject<boolean>();
   chartContainerId = '';
-  donutChartData: DonutDataItem[] = [];
-  donutChart: Chart<'doughnut', number[], string> | undefined;
+  donutChartData: ChartDataItem[] = [];
+  donutChart: Chart<'bar', number[], string> | undefined;
 
-  @Input() data: DonutDataItem[] = [];
-
-  constructor(private eventBus: EventBusService) {}
+  @Input() data: ChartDataItem[] = [];
+  constructor(private eventBus: EventBusService) { }
 
   ngOnInit() {
-    this.chartContainerId = CommonUtil.createUniqId('donut_chart_');
+    this.chartContainerId = CommonUtil.createUniqId('bar_chart_');
 
     this.eventBus
       .getEvent(EventMap.WindowResizedEvent)
@@ -81,7 +70,7 @@ export class DonutChartComponent implements OnInit, 
AfterViewInit, OnChanges, On
     }
   }
 
-  renderChart(chartData: DonutDataItem[] = []) {
+  renderChart(chartData: ChartDataItem[] = []) {
     if (!this.chartContainerId) {
       return;
     }
@@ -98,22 +87,20 @@ export class DonutChartComponent implements OnInit, 
AfterViewInit, OnChanges, On
     }
 
     this.donutChart = new Chart(ctx!, {
-      type: 'doughnut',
+      type: 'bar',
       data: {
         labels: chartLabels,
         datasets: [
           {
+            label: 'My First Dataset',
             data: dataValues,
             backgroundColor: colors,
-          },
+            borderWidth: 1
+          }
         ],
       },
       options: {
         responsive: true,
-        animation: {
-          animateScale: true,
-          animateRotate: true,
-        },
         plugins: {
           legend: {
             display: false,
diff --git a/src/app/components/container-status/container-status.component.ts 
b/src/app/components/container-status/container-status.component.ts
index 2390d59..e8f5f42 100644
--- a/src/app/components/container-status/container-status.component.ts
+++ b/src/app/components/container-status/container-status.component.ts
@@ -17,8 +17,7 @@
  */
 
 import { Component, OnInit, Input } from '@angular/core';
-
-import { DonutDataItem } from '@app/models/donut-data.model';
+import { ChartDataItem } from '@app/models/chart-data.model';
 
 @Component({
   selector: 'app-container-status',
@@ -26,7 +25,7 @@ import { DonutDataItem } from '@app/models/donut-data.model';
   styleUrls: ['./container-status.component.scss'],
 })
 export class ContainerStatusComponent implements OnInit {
-  @Input() chartData: DonutDataItem[] = [];
+  @Input() chartData: ChartDataItem[] = [];
 
   constructor() {}
 
diff --git a/src/app/components/dashboard/dashboard.component.html 
b/src/app/components/dashboard/dashboard.component.html
index 7cc895f..852ad12 100644
--- a/src/app/components/dashboard/dashboard.component.html
+++ b/src/app/components/dashboard/dashboard.component.html
@@ -93,4 +93,9 @@
       <app-container-history 
[chartData]="containerHistoryData"></app-container-history>
     </div>
   </div>
+  <div class="flex-grid grid-row">
+    <div class="left-col flex-primary">
+      <app-node-utilization [chartData]="nodeUtilizationData" />
+    </div>
+  </div>
 </div>
diff --git a/src/app/components/dashboard/dashboard.component.spec.ts 
b/src/app/components/dashboard/dashboard.component.spec.ts
index e3c3fdb..43efdd2 100644
--- a/src/app/components/dashboard/dashboard.component.spec.ts
+++ b/src/app/components/dashboard/dashboard.component.spec.ts
@@ -31,6 +31,7 @@ import { ContainerStatusComponent } from 
'@app/components/container-status/conta
 import { ContainerHistoryComponent } from 
'@app/components/container-history/container-history.component';
 import { SchedulerService } from '@app/services/scheduler/scheduler.service';
 import { EventBusService } from '@app/services/event-bus/event-bus.service';
+import { AppNodeUtilizationComponent } from 
'@app/components/app-node-utilization/app-node-utilization.component';
 import {
   MockSchedulerService,
   MockNgxSpinnerService,
@@ -49,6 +50,7 @@ describe('DashboardComponent', () => {
         MockComponent(AppHistoryComponent),
         MockComponent(ContainerStatusComponent),
         MockComponent(ContainerHistoryComponent),
+        MockComponent(AppNodeUtilizationComponent),
       ],
       imports: [MatCardModule, MatMenuModule, RouterTestingModule],
       providers: [
diff --git a/src/app/components/dashboard/dashboard.component.ts 
b/src/app/components/dashboard/dashboard.component.ts
index 2cf141c..e633659 100644
--- a/src/app/components/dashboard/dashboard.component.ts
+++ b/src/app/components/dashboard/dashboard.component.ts
@@ -21,13 +21,15 @@ import { NgxSpinnerService } from 'ngx-spinner';
 import { finalize } from 'rxjs/operators';
 import { SchedulerService } from '@app/services/scheduler/scheduler.service';
 import { BuildInfo, ClusterInfo } from '@app/models/cluster-info.model';
-import { DonutDataItem } from '@app/models/donut-data.model';
+import { ChartDataItem } from '@app/models/chart-data.model';
 import { AreaDataItem } from '@app/models/area-data.model';
 import { HistoryInfo } from '@app/models/history-info.model';
 import { Applications, Partition } from '@app/models/partition-info.model';
 import { EventBusService, EventMap } from 
'@app/services/event-bus/event-bus.service';
 import { NOT_AVAILABLE } from '@app/utils/constants';
 
+const CHART_COLORS = ['#4285f4', '#db4437', '#f4b400', '#0f9d58', '#ff6d00', 
'#3949ab', '#facc54', '#26bbf0', '#cc6164', '#60cea5'];
+
 @Component({
   selector: 'app-dashboard',
   templateUrl: './dashboard.component.html',
@@ -41,8 +43,9 @@ export class DashboardComponent implements OnInit {
   totalNodes = '';
   totalApplications = '';
   totalContainers = '';
-  appStatusData: DonutDataItem[] = [];
-  containerStatusData: DonutDataItem[] = [];
+  appStatusData: ChartDataItem[] = [];
+  containerStatusData: ChartDataItem[] = [];
+  nodeUtilizationData: ChartDataItem[] = [];
   appHistoryData: AreaDataItem[] = [];
   containerHistoryData: AreaDataItem[] = [];
   clusterInfo: ClusterInfo = this.getEmptyClusterInfo();
@@ -54,7 +57,7 @@ export class DashboardComponent implements OnInit {
     private scheduler: SchedulerService,
     private spinner: NgxSpinnerService,
     private eventBus: EventBusService
-  ) {}
+  ) { }
 
   ngOnInit() {
     this.spinner.show();
@@ -107,6 +110,29 @@ export class DashboardComponent implements OnInit {
       this.appHistoryData = this.getAreaChartData(data);
     });
 
+    this.scheduler.fetchNodeUtilization().subscribe((data) => {
+      const utilizationData: Record<string, number> = {};
+      data.forEach((utilizationInfo) => {
+        utilizationInfo.utilization.forEach(({ bucketName, numOfNodes }) => {
+          const numOfNodesValue = numOfNodes === -1 ? 0 : numOfNodes;
+          if (utilizationData[bucketName]) {
+            utilizationData[bucketName] += numOfNodesValue;
+          } else {
+            utilizationData[bucketName] = numOfNodesValue;
+          }
+        });
+      });
+
+      this.nodeUtilizationData = [];
+      Object.keys(utilizationData).forEach((name, index) => {
+        this.nodeUtilizationData.push(new ChartDataItem(
+          name,
+          utilizationData[name],
+          CHART_COLORS[index],
+        ));
+      });
+    });
+
     this.scheduler.fetchContainerHistory().subscribe((data) => {
       this.initialContainerHistory = data;
       this.containerHistoryData = this.getAreaChartData(data);
@@ -122,22 +148,22 @@ export class DashboardComponent implements OnInit {
 
   updateAppStatusData(applications: Applications) {
     this.appStatusData = []
-    if (applications.New) this.appStatusData.push(new DonutDataItem('New', 
applications.New, '#facc54'))
-    if (applications.Accepted) this.appStatusData.push(new 
DonutDataItem('Accepted', applications.Accepted, '#facc54'))
-    if (applications.Starting) this.appStatusData.push(new 
DonutDataItem('Starting', applications.Starting, '#26bbf0'))
-    if (applications.Running) this.appStatusData.push(new 
DonutDataItem('Running', applications.Running, '#26bbf0'))
-    if (applications.Rejected) this.appStatusData.push(new 
DonutDataItem('Rejected', applications.Rejected, '#cc6164'))
-    if (applications.Completing) this.appStatusData.push(new 
DonutDataItem('Completing', applications.Completing, '#60cea5'))
-    if (applications.Completed) this.appStatusData.push(new 
DonutDataItem('Completed', applications.Completed, '#60cea5'))
-    if (applications.Failing) this.appStatusData.push(new 
DonutDataItem('Failing', applications.Failing, '#cc6164'))
-    if (applications.Failed) this.appStatusData.push(new 
DonutDataItem('Failed', applications.Failed, '#cc6164'))
-    if (applications.Expired) this.appStatusData.push(new 
DonutDataItem('Expired', applications.Expired, '#cc6164'))
-    if (applications.Resuming) this.appStatusData.push(new 
DonutDataItem('Resuming', applications.Resuming, '#facc54'))
+    if (applications.New) this.appStatusData.push(new ChartDataItem('New', 
applications.New, '#facc54'))
+    if (applications.Accepted) this.appStatusData.push(new 
ChartDataItem('Accepted', applications.Accepted, '#facc54'))
+    if (applications.Starting) this.appStatusData.push(new 
ChartDataItem('Starting', applications.Starting, '#26bbf0'))
+    if (applications.Running) this.appStatusData.push(new 
ChartDataItem('Running', applications.Running, '#26bbf0'))
+    if (applications.Rejected) this.appStatusData.push(new 
ChartDataItem('Rejected', applications.Rejected, '#cc6164'))
+    if (applications.Completing) this.appStatusData.push(new 
ChartDataItem('Completing', applications.Completing, '#60cea5'))
+    if (applications.Completed) this.appStatusData.push(new 
ChartDataItem('Completed', applications.Completed, '#60cea5'))
+    if (applications.Failing) this.appStatusData.push(new 
ChartDataItem('Failing', applications.Failing, '#cc6164'))
+    if (applications.Failed) this.appStatusData.push(new 
ChartDataItem('Failed', applications.Failed, '#cc6164'))
+    if (applications.Expired) this.appStatusData.push(new 
ChartDataItem('Expired', applications.Expired, '#cc6164'))
+    if (applications.Resuming) this.appStatusData.push(new 
ChartDataItem('Resuming', applications.Resuming, '#facc54'))
   }
 
   updateContainerStatusData(info: Partition) {
     this.containerStatusData = [
-      new DonutDataItem('Running', +info.totalContainers, '#26bbf0'),
+      new ChartDataItem('Running', +info.totalContainers, '#26bbf0'),
     ];
   }
 
diff --git a/src/app/components/donut-chart/donut-chart.component.ts 
b/src/app/components/donut-chart/donut-chart.component.ts
index e7bb71e..94d119f 100644
--- a/src/app/components/donut-chart/donut-chart.component.ts
+++ b/src/app/components/donut-chart/donut-chart.component.ts
@@ -28,9 +28,8 @@ import {
 import { Subject } from 'rxjs';
 import { takeUntil } from 'rxjs/operators';
 import { Chart, ArcElement, DoughnutController } from 'chart.js';
-
 import { CommonUtil } from '@app/utils/common.util';
-import { DonutDataItem } from '@app/models/donut-data.model';
+import { ChartDataItem } from '@app/models/chart-data.model';
 import { EventBusService, EventMap } from 
'@app/services/event-bus/event-bus.service';
 
 Chart.register(ArcElement, DoughnutController);
@@ -43,10 +42,10 @@ Chart.register(ArcElement, DoughnutController);
 export class DonutChartComponent implements OnInit, AfterViewInit, OnChanges, 
OnDestroy {
   destroy$ = new Subject<boolean>();
   chartContainerId = '';
-  donutChartData: DonutDataItem[] = [];
+  donutChartData: ChartDataItem[] = [];
   donutChart: Chart<'doughnut', number[], string> | undefined;
 
-  @Input() data: DonutDataItem[] = [];
+  @Input() data: ChartDataItem[] = [];
 
   constructor(private eventBus: EventBusService) {}
 
@@ -81,7 +80,7 @@ export class DonutChartComponent implements OnInit, 
AfterViewInit, OnChanges, On
     }
   }
 
-  renderChart(chartData: DonutDataItem[] = []) {
+  renderChart(chartData: ChartDataItem[] = []) {
     if (!this.chartContainerId) {
       return;
     }
diff --git a/src/app/models/donut-data.model.ts 
b/src/app/models/chart-data.model.ts
similarity index 97%
copy from src/app/models/donut-data.model.ts
copy to src/app/models/chart-data.model.ts
index 09457ac..4c7a9b0 100644
--- a/src/app/models/donut-data.model.ts
+++ b/src/app/models/chart-data.model.ts
@@ -16,7 +16,7 @@
  * limitations under the License.
  */
 
-export class DonutDataItem {
+export class ChartDataItem {
   name: string;
   value: number;
   color: string;
diff --git a/src/app/models/donut-data.model.ts 
b/src/app/models/node-utilization.model.ts
similarity index 79%
rename from src/app/models/donut-data.model.ts
rename to src/app/models/node-utilization.model.ts
index 09457ac..529922f 100644
--- a/src/app/models/donut-data.model.ts
+++ b/src/app/models/node-utilization.model.ts
@@ -16,14 +16,13 @@
  * limitations under the License.
  */
 
-export class DonutDataItem {
-  name: string;
-  value: number;
-  color: string;
-
-  constructor(name: string, value: number, color: string) {
-    this.name = name;
-    this.value = value;
-    this.color = color;
-  }
+export class NodeUtilization {
+  constructor(
+    public type: string,
+    public utilization: {
+      bucketName: string;
+      numOfNodes: number;
+      nodeNames: null | string[];
+    }[],
+  ) {}
 }
diff --git a/src/app/services/scheduler/scheduler.service.ts 
b/src/app/services/scheduler/scheduler.service.ts
index 251b5e2..e8803a9 100644
--- a/src/app/services/scheduler/scheduler.service.ts
+++ b/src/app/services/scheduler/scheduler.service.ts
@@ -18,7 +18,7 @@
 
 import { Injectable } from '@angular/core';
 import { HttpClient } from '@angular/common/http';
-import { Observable, queueScheduler } from 'rxjs';
+import { Observable } from 'rxjs';
 import { map } from 'rxjs/operators';
 
 import { QueueInfo, QueuePropertyItem } from '@app/models/queue-info.model';
@@ -33,6 +33,7 @@ import { NodeInfo } from '@app/models/node-info.model';
 import { NOT_AVAILABLE } from '@app/utils/constants';
 import { Partition } from '@app/models/partition-info.model';
 import { SchedulerHealthInfo } from "@app/models/scheduler-health-info.model";
+import { NodeUtilization } from '@app/models/node-utilization.model';
 
 @Injectable({
   providedIn: 'root',
@@ -253,6 +254,11 @@ export class SchedulerService {
     );
   }
 
+  fetchNodeUtilization(): Observable<NodeUtilization[]>{
+    const nodeUtilizationUrl = 
`${this.envConfig.getSchedulerWebAddress()}/ws/v1/nodes/utilization`;
+    return this.httpClient.get(nodeUtilizationUrl).pipe(map((data: any) => 
data as NodeUtilization[]));
+  }
+
   fecthHealthchecks(): Observable<SchedulerHealthInfo> {
     const healthCheckUrl = 
`${this.envConfig.getSchedulerWebAddress()}/ws/v1/scheduler/healthcheck`;
     return this.httpClient.get(healthCheckUrl).pipe(map((data: any) => data as 
SchedulerHealthInfo));
diff --git a/src/app/testing/mocks.ts b/src/app/testing/mocks.ts
index 45efbd4..805dadb 100644
--- a/src/app/testing/mocks.ts
+++ b/src/app/testing/mocks.ts
@@ -31,6 +31,7 @@ export const MockSchedulerService = {
   fetchAppHistory: () => of([]),
   fetchContainerHistory: () => of([]),
   fetchNodeList: () => of([]),
+  fetchNodeUtilization: () => of([]),
   fecthHealthchecks: () => of([]),
 };
 


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]


Reply via email to