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

ccondit 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 6cac541  [YUNIKORN-2325] Add a chart to display multi-type resource 
utilization (#160)
6cac541 is described below

commit 6cac541d54c54aab621ae9503c275b8995b2a198
Author: Yu-Lin Chen <[email protected]>
AuthorDate: Fri Feb 2 12:22:51 2024 -0600

    [YUNIKORN-2325] Add a chart to display multi-type resource utilization 
(#160)
    
    Closes: #160
    
    Signed-off-by: Craig Condit <[email protected]>
---
 json-db.json                                       | 470 +++++++++++++++++++++
 json-routes.json                                   |   1 +
 src/app/app.module.ts                              |   4 +
 .../app-node-utilizations.component.html}          |  29 +-
 .../app-node-utilizations.component.scss}          |   8 +-
 .../app-node-utilizations.component.spec.ts        | 114 +++++
 .../app-node-utilizations.component.ts             | 150 +++++++
 .../components/dashboard/dashboard.component.html  |   2 +-
 .../components/dashboard/dashboard.component.ts    |   2 +-
 .../nodes-view/nodes-view.component.html           |   5 +
 .../nodes-view/nodes-view.component.spec.ts        |   9 +-
 .../vertical-bar-chart.component.html}             |  16 +-
 .../vertical-bar-chart.component.scss}             |   9 +-
 .../vertical-bar-chart.component.spec.ts}          |  31 +-
 .../vertical-bar-chart.component.ts                | 201 +++++++++
 src/app/models/chart-data.model.ts                 |  18 +
 src/app/models/node-utilization.model.ts           |  16 +-
 src/app/services/scheduler/scheduler.service.ts    |  29 +-
 src/app/testing/mocks.ts                           |   2 +-
 src/app/utils/common.util.ts                       |  18 +
 src/app/utils/constants.ts                         |   3 +-
 21 files changed, 1058 insertions(+), 79 deletions(-)

diff --git a/json-db.json b/json-db.json
index 4dede38..cdecd82 100644
--- a/json-db.json
+++ b/json-db.json
@@ -963,6 +963,476 @@
       }
     ]
   },
+  "node-utilizations": [
+    {
+      "clusterId": "mycluster",
+      "partition": "default",
+      "utilizations": [
+        {
+          "type": "ephemeral-storagesss",
+          "utilization": [
+            {
+              "bucketName": "0-10%",
+              "numOfNodes": 1,
+              "nodeNames": [
+                "node-01"
+              ]
+            },
+            {
+              "bucketName": "10-20%",
+              "numOfNodes": 2,
+              "nodeNames": [
+                "node-02",
+                "node-03"
+              ]
+            },
+            {
+              "bucketName": "20-30%",
+              "numOfNodes": 3,
+              "nodeNames": [
+                "node-04",
+                "node-05",
+                "node-06"
+              ]
+            },
+            {
+              "bucketName": "30-40%",
+              "numOfNodes": 4,
+              "nodeNames": [
+                "node-07",
+                "node-08",
+                "node-09",
+                "node-10"
+              ]
+            },
+            {
+              "bucketName": "40-50%"
+            },
+            {
+              "bucketName": "50-60%"
+            },
+            {
+              "bucketName": "60-70%"
+            },
+            {
+              "bucketName": "70-80%"
+            },
+            {
+              "bucketName": "80-90%"
+            },
+            {
+              "bucketName": "90-100%"
+            }
+          ]
+        },
+        {
+          "type": "hugepages-1Gi",
+          "utilization": [
+            {
+              "bucketName": "0-10%"
+            },
+            {
+              "bucketName": "10-20%",
+              "numOfNodes": 1,
+              "nodeNames": [
+                "node-01"
+              ]
+            },
+            {
+              "bucketName": "20-30%",
+              "numOfNodes": 2,
+              "nodeNames": [
+                "node-02",
+                "node-03"
+              ]
+            },
+            {
+              "bucketName": "30-40%",
+              "numOfNodes": 3,
+              "nodeNames": [
+                "node-04",
+                "node-05",
+                "node-06"
+              ]
+            },
+            {
+              "bucketName": "40-50%",
+              "numOfNodes": 4,
+              "nodeNames": [
+                "node-07",
+                "node-08",
+                "node-09",
+                "node-10"
+              ]
+            },
+            {
+              "bucketName": "50-60%"
+            },
+            {
+              "bucketName": "60-70%"
+            },
+            {
+              "bucketName": "70-80%"
+            },
+            {
+              "bucketName": "80-90%"
+            },
+            {
+              "bucketName": "90-100%"
+            }
+          ]
+        },
+        {
+          "type": "hugepages-2Mi",
+          "utilization": [
+            {
+              "bucketName": "0-10%"
+            },
+            {
+              "bucketName": "10-20%"
+            },
+            {
+              "bucketName": "20-30%",
+              "numOfNodes": 1,
+              "nodeNames": [
+                "node-01"
+              ]
+            },
+            {
+              "bucketName": "30-40%",
+              "numOfNodes": 2,
+              "nodeNames": [
+                "node-02",
+                "node-03"
+              ]
+            },
+            {
+              "bucketName": "40-50%",
+              "numOfNodes": 3,
+              "nodeNames": [
+                "node-04",
+                "node-05",
+                "node-06"
+              ]
+            },
+            {
+              "bucketName": "50-60%",
+              "numOfNodes": 4,
+              "nodeNames": [
+                "node-07",
+                "node-08",
+                "node-09",
+                "node-10"
+              ]
+            },
+            {
+              "bucketName": "60-70%"
+            },
+            {
+              "bucketName": "70-80%"
+            },
+            {
+              "bucketName": "80-90%"
+            },
+            {
+              "bucketName": "90-100%"
+            }
+          ]
+        },
+        {
+          "type": "memory",
+          "utilization": [
+            {
+              "bucketName": "0-10%"
+            },
+            {
+              "bucketName": "10-20%"
+            },
+            {
+              "bucketName": "20-30%"
+            },
+            {
+              "bucketName": "30-40%",
+              "numOfNodes": 1,
+              "nodeNames": [
+                "node-01"
+              ]
+            },
+            {
+              "bucketName": "40-50%",
+              "numOfNodes": 2,
+              "nodeNames": [
+                "node-02",
+                "node-03"
+              ]
+            },
+            {
+              "bucketName": "50-60%",
+              "numOfNodes": 3,
+              "nodeNames": [
+                "node-04",
+                "node-05",
+                "node-06"
+              ]
+            },
+            {
+              "bucketName": "60-70%",
+              "numOfNodes": 4,
+              "nodeNames": [
+                "node-07",
+                "node-08",
+                "node-09",
+                "node-10"
+              ]
+            },
+            {
+              "bucketName": "70-80%"
+            },
+            {
+              "bucketName": "80-90%"
+            },
+            {
+              "bucketName": "90-100%"
+            }
+          ]
+        },
+        {
+          "type": "pods",
+          "utilization": [
+            {
+              "bucketName": "0-10%"
+            },
+            {
+              "bucketName": "10-20%"
+            },
+            {
+              "bucketName": "20-30%"
+            },
+            {
+              "bucketName": "30-40%"
+            },
+            {
+              "bucketName": "40-50%"
+            },
+            {
+              "bucketName": "50-60%",
+              "numOfNodes": 1,
+              "nodeNames": [
+                "node-01"
+              ]
+            },
+            {
+              "bucketName": "60-70%",
+              "numOfNodes": 2,
+              "nodeNames": [
+                "node-02",
+                "node-03"
+              ]
+            },
+            {
+              "bucketName": "70-80%",
+              "numOfNodes": 3,
+              "nodeNames": [
+                "node-04",
+                "node-05",
+                "node-06"
+              ]
+            },
+            {
+              "bucketName": "80-90%",
+              "numOfNodes": 4,
+              "nodeNames": [
+                "node-07",
+                "node-08",
+                "node-09",
+                "node-10"
+              ]
+            },
+            {
+              "bucketName": "90-100%"
+            }
+          ]
+        },
+        {
+          "type": "vcore",
+          "utilization": [
+            {
+              "bucketName": "0-10%"
+            },
+            {
+              "bucketName": "10-20%"
+            },
+            {
+              "bucketName": "20-30%"
+            },
+            {
+              "bucketName": "30-40%"
+            },
+            {
+              "bucketName": "40-50%"
+            },
+            {
+              "bucketName": "50-60%"
+            },
+            {
+              "bucketName": "60-70%",
+              "numOfNodes": 1,
+              "nodeNames": [
+                "node-01"
+              ]
+            },
+            {
+              "bucketName": "70-80%",
+              "numOfNodes": 2,
+              "nodeNames": [
+                "node-02",
+                "node-03"
+              ]
+            },
+            {
+              "bucketName": "80-90%",
+              "numOfNodes": 3,
+              "nodeNames": [
+                "node-04",
+                "node-05",
+                "node-06"
+              ]
+            },
+            {
+              "bucketName": "90-100%",
+              "numOfNodes": 4,
+              "nodeNames": [
+                "node-07",
+                "node-08",
+                "node-09",
+                "node-10"
+              ]
+            }
+          ]
+        }
+      ]
+    },
+    {
+      "clusterId": "mycluster",
+      "partition": "cluster-2",
+      "utilizations": [
+        {
+          "type": "memory",
+          "utilization": [
+            {
+              "bucketName": "0-10%"
+            },
+            {
+              "bucketName": "10-20%"
+            },
+            {
+              "bucketName": "20-30%"
+            },
+            {
+              "bucketName": "30-40%",
+              "numOfNodes": 1,
+              "nodeNames": [
+                "node-01"
+              ]
+            },
+            {
+              "bucketName": "40-50%",
+              "numOfNodes": 2,
+              "nodeNames": [
+                "node-02",
+                "node-03"
+              ]
+            },
+            {
+              "bucketName": "50-60%",
+              "numOfNodes": 3,
+              "nodeNames": [
+                "node-04",
+                "node-05",
+                "node-06"
+              ]
+            },
+            {
+              "bucketName": "60-70%",
+              "numOfNodes": 4,
+              "nodeNames": [
+                "node-07",
+                "node-08",
+                "node-09",
+                "node-10"
+              ]
+            },
+            {
+              "bucketName": "70-80%"
+            },
+            {
+              "bucketName": "80-90%"
+            },
+            {
+              "bucketName": "90-100%"
+            }
+          ]
+        },
+        {
+          "type": "pods",
+          "utilization": [
+            {
+              "bucketName": "0-10%"
+            },
+            {
+              "bucketName": "10-20%"
+            },
+            {
+              "bucketName": "20-30%"
+            },
+            {
+              "bucketName": "30-40%"
+            },
+            {
+              "bucketName": "40-50%"
+            },
+            {
+              "bucketName": "50-60%",
+              "numOfNodes": 1,
+              "nodeNames": [
+                "node-01"
+              ]
+            },
+            {
+              "bucketName": "60-70%",
+              "numOfNodes": 2,
+              "nodeNames": [
+                "node-02",
+                "node-03"
+              ]
+            },
+            {
+              "bucketName": "70-80%",
+              "numOfNodes": 3,
+              "nodeNames": [
+                "node-04",
+                "node-05",
+                "node-06"
+              ]
+            },
+            {
+              "bucketName": "80-90%",
+              "numOfNodes": 4,
+              "nodeNames": [
+                "node-07",
+                "node-08",
+                "node-09",
+                "node-10"
+              ]
+            },
+            {
+              "bucketName": "90-100%"
+            }
+          ]
+        }
+      ]
+    }
+  ],
   "partitions": [
     {
       "clusterId": "mycluster",
diff --git a/json-routes.json b/json-routes.json
index 7b2086a..068091b 100644
--- a/json-routes.json
+++ b/json-routes.json
@@ -1,5 +1,6 @@
 {
   "/ws/v1/scheduler/node-utilization": "/node-utilization",
+  "/ws/v1/scheduler/node-utilizations": "/node-utilizations",
   "/ws/v1/*": "/$1",
   "/history/apps": "/appHistory",
   "/history/containers": "/containerHistory",
diff --git a/src/app/app.module.ts b/src/app/app.module.ts
index a325942..ced1b06 100644
--- a/src/app/app.module.ts
+++ b/src/app/app.module.ts
@@ -61,6 +61,8 @@ import { StatusViewComponent } from 
'@app/components/status-view/status-view.com
 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';
+import { AppNodeUtilizationsComponent } from 
'@app/components/app-node-utilizations/app-node-utilizations.component';
+import { VerticalBarChartComponent } from 
'@app/components/vertical-bar-chart/vertical-bar-chart.component';
 
 @NgModule({
   declarations: [
@@ -82,6 +84,8 @@ import { BarChartComponent } from 
'@app/components/bar-chart/bar-chart.component
     HealthchecksComponent,
     AppNodeUtilizationComponent,
     BarChartComponent,
+    AppNodeUtilizationsComponent,
+    VerticalBarChartComponent,
   ],
   imports: [
     BrowserModule,
diff --git a/src/app/models/chart-data.model.ts 
b/src/app/components/app-node-utilizations/app-node-utilizations.component.html
similarity index 64%
copy from src/app/models/chart-data.model.ts
copy to 
src/app/components/app-node-utilizations/app-node-utilizations.component.html
index b1f180e..69f1f65 100644
--- a/src/app/models/chart-data.model.ts
+++ 
b/src/app/components/app-node-utilizations/app-node-utilizations.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,18 +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 ChartDataItem {
-  name: string;
-  value: number;
-  color?: string;
-  description?: string;
-
-  constructor(name: string, value: number, color?: string, description?: 
string) {
-    this.name = name;
-    this.value = value;
-    this.color = color;
-    this.description = description;
-  }
-}
+<mat-card appearance="outlined" class="box-card">
+  <mat-card-header>
+    <mat-card-title>Node Resource Utilization</mat-card-title>
+  </mat-card-header>
+  <mat-card-content>
+    <div class="status-wrapper flex-grid">
+      <div class="chart-wrapper flex-primary">
+        <app-vertical-bar-chart [bucketList]="bucketList" 
[barChartDataSets]="barChartDataSets" />
+      </div>
+    </div>
+  </mat-card-content>
+</mat-card>
\ No newline at end of file
diff --git a/src/app/utils/constants.ts 
b/src/app/components/app-node-utilizations/app-node-utilizations.component.scss
similarity index 79%
copy from src/app/utils/constants.ts
copy to 
src/app/components/app-node-utilizations/app-node-utilizations.component.scss
index 643e9eb..a3f036c 100644
--- a/src/app/utils/constants.ts
+++ 
b/src/app/components/app-node-utilizations/app-node-utilizations.component.scss
@@ -14,10 +14,4 @@
  * 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 const DEFAULT_PARTITION_VALUE = '';
-export const DEFAULT_PROTOCOL = 'http:';
-export const NOT_AVAILABLE = 'n/a';
-export const PARTITION_DEFAULT = 'default';
-export const DEFAULT_BAR_COLOR = '#4285f4';
\ No newline at end of file
+ */
\ No newline at end of file
diff --git 
a/src/app/components/app-node-utilizations/app-node-utilizations.component.spec.ts
 
b/src/app/components/app-node-utilizations/app-node-utilizations.component.spec.ts
new file mode 100644
index 0000000..08e749f
--- /dev/null
+++ 
b/src/app/components/app-node-utilizations/app-node-utilizations.component.spec.ts
@@ -0,0 +1,114 @@
+/**
+ * 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
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * 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.
+ */
+
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { MatCardModule } from '@angular/material/card';
+import { AppNodeUtilizationsComponent } from 
'@app/components/app-node-utilizations/app-node-utilizations.component';
+import { VerticalBarChartComponent } from 
'@app/components/vertical-bar-chart/vertical-bar-chart.component';
+import { CHART_COLORS } from '@app/utils/constants';
+
+describe('AppNodeUtilizationsComponent', () => {
+  let component: AppNodeUtilizationsComponent;
+  let fixture: ComponentFixture<AppNodeUtilizationsComponent>;
+
+  beforeEach(() => {
+    TestBed.configureTestingModule({
+      imports: [HttpClientTestingModule, MatCardModule],
+      declarations: [AppNodeUtilizationsComponent, VerticalBarChartComponent]
+    });
+
+    fixture = TestBed.createComponent(AppNodeUtilizationsComponent);
+    component = fixture.componentInstance;
+  });
+
+  it('test AppNodeUtilizationsComponent.calculateAvgUtilization()', () => {
+    type TestCase = {
+      description: string;
+      nodeNumInBuckets: number[];
+      expected: number;
+    };
+    const testCases: TestCase[] = [
+      {
+        description: 'Test 2 nodes, 1 node in 0~10%, 1 node in 10~20%',
+        nodeNumInBuckets: [1, 1, 0, 0, 0, 0, 0, 0, 0, 0],
+        expected: 0.1
+      },
+      {
+        description: 'Test 10 nodes, 1 node in each bucket',
+        nodeNumInBuckets: [1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
+        expected: 0.5
+      },
+      {
+        description: 'Test zero node in buckets',
+        nodeNumInBuckets: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
+        expected: 0
+      },
+    ]
+
+    testCases.forEach((testCase: TestCase) => {
+      const result = 
component.calculateAvgUtilization(testCase.nodeNumInBuckets);
+      expect(result).toEqual(testCase.expected);
+    });
+  });
+
+  it('test AppNodeUtilizationsComponent.generateColorMapping()', () => {
+    const types = [
+      'type03', 'type01', 'type02', 'type04', 'type05',
+      'type06', 'type07', 'type08', 'type09', 'type10', 'type11'
+    ];
+    const colorMapping = component.generateColorMapping(types);
+
+    expect(colorMapping.size).toBe(11);
+    expect(colorMapping.get('type01')).toBe(CHART_COLORS[0]);
+    expect(colorMapping.get('type02')).toBe(CHART_COLORS[1]);
+    expect(colorMapping.get('type03')).toBe(CHART_COLORS[2]);
+    expect(colorMapping.get('type11')).toBe(CHART_COLORS[0]);
+  });
+
+  it('test AppNodeUtilizationsComponent.getBarDescription()', () => {
+    type TestCase = {
+      description: string;
+      nodeNames: string[];
+      expected: string;
+    };
+    const testCases: TestCase[] = [
+      {
+        description: 'Test single node',
+        nodeNames: [""],
+        expected: ""
+      },
+      {
+        description: 'Test unordered multi-nodes',
+        nodeNames: ["node02", "node01"],
+        expected: "node01\nnode02"
+      },
+      {
+        description: 'Test over than MAX_NODES_IN_DESCRIPTION nodes',
+        nodeNames: ["node01", "node02", "node03", "node04", "node05", 
"node06", "node07", "node08", "node09", "node10", "node11", "node12", "node13", 
"node14", "node15", "node16"],
+        expected: 
"node01\nnode02\nnode03\nnode04\nnode05\nnode06\nnode07\nnode08\nnode09\nnode10\nnode11\nnode12\nnode13\nnode14\nnode15\n...1
 more"
+      },
+    ]
+
+    testCases.forEach((testCase: TestCase) => {
+      const result = component.getBarDescription(testCase.nodeNames);
+      expect(result).toEqual(testCase.expected);
+    });
+  });
+});
diff --git 
a/src/app/components/app-node-utilizations/app-node-utilizations.component.ts 
b/src/app/components/app-node-utilizations/app-node-utilizations.component.ts
new file mode 100644
index 0000000..2f16df2
--- /dev/null
+++ 
b/src/app/components/app-node-utilizations/app-node-utilizations.component.ts
@@ -0,0 +1,150 @@
+/**
+ * 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
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * 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.
+ */
+
+import { Component, Input, OnChanges, OnInit, SimpleChanges } from 
'@angular/core';
+import { BarChartDataSet } from '@app/models/chart-data.model';
+import { CHART_COLORS, DEFAULT_BAR_COLOR } from '@app/utils/constants';
+import { CommonUtil } from '@app/utils/common.util';
+import { NodeUtilization, NodeUtilizationsInfo } from 
'@app/models/node-utilization.model';
+import { SchedulerService } from '@app/services/scheduler/scheduler.service';
+
+
+@Component({
+  selector: 'app-node-utilizations',
+  templateUrl: './app-node-utilizations.component.html',
+  styleUrls: ['./app-node-utilizations.component.scss']
+})
+export class AppNodeUtilizationsComponent implements OnInit, OnChanges {
+  nodeUtilizations: NodeUtilization[] = [];
+
+  // input data for vertical bar chart, key is resource type
+  bucketList: string[] = [];                                          // one 
bucket list for all resource types, length should be exactly 10
+  barChartDataSets: BarChartDataSet[] = new Array<BarChartDataSet>(); // one 
dataset for each type
+
+  @Input() partitionSelected: string = "";
+
+  constructor(
+    private scheduler: SchedulerService
+  ) { }
+
+  ngOnInit() {
+    this.reloadBarChartData()
+  }
+
+  ngOnChanges(changes: SimpleChanges) {
+    if (
+      changes['partitionSelected']
+    ) {
+      this.reloadBarChartData()
+    }
+  }
+
+  reloadBarChartData() {
+    this.scheduler.fetchNodeUtilizationsInfo().subscribe((data) => {
+      let nodeUtilizationsInfo: NodeUtilizationsInfo[] = data
+      for (let i = 0; i < nodeUtilizationsInfo.length; i++) {
+        if (nodeUtilizationsInfo[i].partition === this.partitionSelected) {
+          let nodeUtilizations = nodeUtilizationsInfo[i].utilizations
+          this.fetchBarChartData(nodeUtilizations)
+          break;
+        }
+      }
+    });
+  }
+
+  fetchBarChartData(nodeUtilizations: NodeUtilization[]) {
+    let barChartDataSets = new Array<BarChartDataSet>();
+    if (nodeUtilizations.length === 0) {
+      // clean bar chart data
+      this.barChartDataSets = barChartDataSets;
+      return;
+    }
+
+    let colorMapping = this.generateColorMapping(
+      nodeUtilizations.map((nodeUtilization) => (nodeUtilization.type))
+    );
+
+    for (let i = 0; i < nodeUtilizations.length; i++) {
+      let type = nodeUtilizations[i].type;
+      let utilization = nodeUtilizations[i].utilization
+      let borderWidth = 1
+
+      if (i === 0) {
+        // get bucketList only from the first type of node utilization
+        // should always be 10 buckets. (ranging from 0% to 100%).
+        this.bucketList = utilization.map((item) => item.bucketName);
+      }
+      let bucketValues = utilization.map((item) => item.numOfNodes);
+      barChartDataSets.push(new BarChartDataSet(
+        type,
+        bucketValues,
+        this.calculateAvgUtilization(bucketValues),
+        colorMapping.get(type) ?? DEFAULT_BAR_COLOR,
+        borderWidth,
+        utilization.map((item) => this.getBarDescription(item.nodeNames))
+      ))
+    }
+
+    // sort by resource type first, then sort by avg utilization rate
+    barChartDataSets.sort((a, b) => CommonUtil.resourcesCompareFn(a.label, 
b.label));
+    barChartDataSets.sort((a, b) => b.avgUtilizationRate - 
a.avgUtilizationRate);
+    barChartDataSets = barChartDataSets.slice(0, 10); // only show top 10 
resources
+
+    // refresh bar chart data
+    this.barChartDataSets = barChartDataSets;
+  }
+
+  calculateAvgUtilization(nodeNumInBuckets: number[]): number {
+    // Calculates the average utilization of nodes based on a distribution of 
node utilizations.
+    // Note: It not a precise average.
+    // value of nodeCounts[0] means node count of 0%~10%, take 5% as the 
utilization of node in bucket
+    // value of nodeCounts[1] means node count of 10%~20, take 15% as the 
utilization of node in bucket
+    // value of nodeCounts[9] means node count of 90%~100%, take 95% as the 
utilization of node in bucket
+    let totalNodes = 0;
+    let weightedSum = 0;
+    for (let i = 0; i < 10; i++) { //buckets have fixed length 10
+      if (nodeNumInBuckets[i] != undefined) {
+        totalNodes += nodeNumInBuckets[i];
+        weightedSum += nodeNumInBuckets[i] * (5 + 10 * i);
+      }
+    }
+    return totalNodes ? weightedSum / totalNodes / 100 : 0;
+  }
+
+  generateColorMapping(types: string[]): Map<string, string> {
+    // give each resource type a color based on its index after 
lexicographically sorting
+    types.sort();
+    let colorMapping = new Map<string, string>();
+    for (let i = 0; i < types.length; i++) {
+      colorMapping.set(types[i], CHART_COLORS[i % 10])
+    }
+    return colorMapping
+  }
+
+  getBarDescription(nodeNames: string[] | null): string {
+    let MAX_NODES_IN_DESCRIPTION = 15;
+    let description: string | undefined;
+    if (nodeNames && nodeNames.length > MAX_NODES_IN_DESCRIPTION) {
+      // only put MAX_NODES_IN_DESCRIPTION nodes in description, others will 
be replaced by '...N more'
+      description = nodeNames.slice(0, 
MAX_NODES_IN_DESCRIPTION).sort().join("\n") + "\n..." + (nodeNames.length - 
MAX_NODES_IN_DESCRIPTION) + " more";
+    } else {
+      description = nodeNames ? nodeNames.sort().join("\n") : undefined;
+    }
+    return description || ""
+  }
+}
diff --git a/src/app/components/dashboard/dashboard.component.html 
b/src/app/components/dashboard/dashboard.component.html
index b493d92..e1a5f3b 100644
--- a/src/app/components/dashboard/dashboard.component.html
+++ b/src/app/components/dashboard/dashboard.component.html
@@ -95,7 +95,7 @@
   </div>
   <div class="flex-grid grid-row">
     <div class="half-col flex-primary">
-      <app-node-utilization 
[chartData]="nodeUtilizationChartData.chartDataItems" title="Nodes Resource 
Utilization" [subtitle]="('Domaint Resource:' + 
nodeUtilizationChartData.type)"/>
+      <app-node-utilization 
[chartData]="nodeUtilizationChartData.chartDataItems" title="Node Resource 
Utilization" [subtitle]="('Dominant Resource:' + 
nodeUtilizationChartData.type)"/>
     </div>
   </div>
 </div>
diff --git a/src/app/components/dashboard/dashboard.component.ts 
b/src/app/components/dashboard/dashboard.component.ts
index d73279a..c02083c 100644
--- a/src/app/components/dashboard/dashboard.component.ts
+++ b/src/app/components/dashboard/dashboard.component.ts
@@ -109,7 +109,7 @@ export class DashboardComponent implements OnInit {
       this.appHistoryData = this.getAreaChartData(data);
     });
     
-    this.scheduler.fetchClusterNodeUtilization().subscribe((data) => {
+    this.scheduler.fetchNodeUtilization().subscribe((data) => {
       let nodeUtilization = new NodeUtilization(data.type, data.utilization);
       this.nodeUtilizationChartData = 
nodeUtilization.toNodeUtilizationChartData();
     });
diff --git a/src/app/components/nodes-view/nodes-view.component.html 
b/src/app/components/nodes-view/nodes-view.component.html
index 17b8f8b..66c2f81 100644
--- a/src/app/components/nodes-view/nodes-view.component.html
+++ b/src/app/components/nodes-view/nodes-view.component.html
@@ -197,4 +197,9 @@
       ></mat-paginator>
     </div>
   </div>
+  <div class="flex-grid">
+    <div class="flex-primary">
+      <app-node-utilizations [partitionSelected]="partitionSelected" />
+    </div>
+  </div>
 </div>
diff --git a/src/app/components/nodes-view/nodes-view.component.spec.ts 
b/src/app/components/nodes-view/nodes-view.component.spec.ts
index 17ca95c..97a147f 100644
--- a/src/app/components/nodes-view/nodes-view.component.spec.ts
+++ b/src/app/components/nodes-view/nodes-view.component.spec.ts
@@ -21,9 +21,13 @@ import { NoopAnimationsModule } from 
'@angular/platform-browser/animations';
 import { NgxSpinnerService } from 'ngx-spinner';
 
 import { NodesViewComponent } from './nodes-view.component';
+import { AppNodeUtilizationsComponent } from 
'@app/components/app-node-utilizations/app-node-utilizations.component';
+import { VerticalBarChartComponent } from 
'@app/components/vertical-bar-chart/vertical-bar-chart.component';
+
 import { SchedulerService } from '@app/services/scheduler/scheduler.service';
 import { HAMMER_LOADER } from '@angular/platform-browser';
 import { MockSchedulerService, MockNgxSpinnerService } from 
'@app/testing/mocks';
+import { MatCardModule } from '@angular/material/card';
 import { MatTableModule } from '@angular/material/table';
 import { MatPaginatorModule } from '@angular/material/paginator';
 import { MatDividerModule } from '@angular/material/divider';
@@ -58,13 +62,12 @@ describe('NodesViewComponent', () => {
 
   beforeEach(() => {
     TestBed.configureTestingModule({
-      declarations: [NodesViewComponent],
-      imports: [MatFormFieldModule, MatInputModule, ReactiveFormsModule],
+      declarations: [NodesViewComponent, AppNodeUtilizationsComponent, 
VerticalBarChartComponent],
+      imports: [MatFormFieldModule, MatInputModule, ReactiveFormsModule, 
MatCardModule],
     })
     .compileComponents();
     fixture = TestBed.createComponent(NodesViewComponent);
     component = fixture.componentInstance;
-    fixture.detectChanges();
   });
 
   it('should create the component', () => {
diff --git a/src/app/utils/constants.ts 
b/src/app/components/vertical-bar-chart/vertical-bar-chart.component.html
similarity index 78%
copy from src/app/utils/constants.ts
copy to src/app/components/vertical-bar-chart/vertical-bar-chart.component.html
index 643e9eb..c5cf1e2 100644
--- a/src/app/utils/constants.ts
+++ b/src/app/components/vertical-bar-chart/vertical-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,10 +14,10 @@
  * 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 const DEFAULT_PARTITION_VALUE = '';
-export const DEFAULT_PROTOCOL = 'http:';
-export const NOT_AVAILABLE = 'n/a';
-export const PARTITION_DEFAULT = 'default';
-export const DEFAULT_BAR_COLOR = '#4285f4';
\ No newline at end of file
+ -->
+<!-- <div class="chart-container"> -->
+<div class="flex-grid">
+    <div class="canvas-div">
+        <canvas class="vertical-bar-chart" id="{{ chartContainerId 
}}"></canvas>
+    </div>
+</div>
\ No newline at end of file
diff --git a/src/app/utils/constants.ts 
b/src/app/components/vertical-bar-chart/vertical-bar-chart.component.scss
similarity index 79%
copy from src/app/utils/constants.ts
copy to src/app/components/vertical-bar-chart/vertical-bar-chart.component.scss
index 643e9eb..ebd91ee 100644
--- a/src/app/utils/constants.ts
+++ b/src/app/components/vertical-bar-chart/vertical-bar-chart.component.scss
@@ -16,8 +16,7 @@
  * limitations under the License.
  */
 
-export const DEFAULT_PARTITION_VALUE = '';
-export const DEFAULT_PROTOCOL = 'http:';
-export const NOT_AVAILABLE = 'n/a';
-export const PARTITION_DEFAULT = 'default';
-export const DEFAULT_BAR_COLOR = '#4285f4';
\ No newline at end of file
+.canvas-div {
+    height: 50vh;
+    width: 100%;
+}
diff --git a/src/app/models/chart-data.model.ts 
b/src/app/components/vertical-bar-chart/vertical-bar-chart.component.spec.ts
similarity index 56%
copy from src/app/models/chart-data.model.ts
copy to 
src/app/components/vertical-bar-chart/vertical-bar-chart.component.spec.ts
index b1f180e..2293138 100644
--- a/src/app/models/chart-data.model.ts
+++ b/src/app/components/vertical-bar-chart/vertical-bar-chart.component.spec.ts
@@ -16,16 +16,23 @@
  * limitations under the License.
  */
 
-export class ChartDataItem {
-  name: string;
-  value: number;
-  color?: string;
-  description?: string;
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { VerticalBarChartComponent } from './vertical-bar-chart.component';
 
-  constructor(name: string, value: number, color?: string, description?: 
string) {
-    this.name = name;
-    this.value = value;
-    this.color = color;
-    this.description = description;
-  }
-}
+describe('VerticalBarChartComponent', () => {
+  let component: VerticalBarChartComponent;
+  let fixture: ComponentFixture<VerticalBarChartComponent>;
+
+  beforeEach(() => {
+    TestBed.configureTestingModule({
+      declarations: [VerticalBarChartComponent]
+    });
+    fixture = TestBed.createComponent(VerticalBarChartComponent);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+});
diff --git 
a/src/app/components/vertical-bar-chart/vertical-bar-chart.component.ts 
b/src/app/components/vertical-bar-chart/vertical-bar-chart.component.ts
new file mode 100644
index 0000000..424e655
--- /dev/null
+++ b/src/app/components/vertical-bar-chart/vertical-bar-chart.component.ts
@@ -0,0 +1,201 @@
+/**
+ * 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
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * 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.
+ */
+
+import { AfterViewInit, Component, Input, OnChanges, OnDestroy, OnInit, 
SimpleChanges } from '@angular/core';
+import { BarChartDataSet } 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, Tooltip } from 'chart.js';
+import { Subject, takeUntil } from 'rxjs';
+
+Chart.register(Tooltip);
+
+@Component({
+  selector: 'app-vertical-bar-chart',
+  templateUrl: './vertical-bar-chart.component.html',
+  styleUrls: ['./vertical-bar-chart.component.scss']
+})
+export class VerticalBarChartComponent implements OnInit, AfterViewInit, 
OnChanges, OnDestroy {
+  destroy$ = new Subject<boolean>();
+  chartContainerId = '';
+  barChart: Chart<'bar' | 'line', number[], string> | undefined;
+
+  @Input() bucketList: string[] = [];                                          
 // one bucket list for all resource types, length should be exactly 10
+  @Input() barChartDataSets: BarChartDataSet[] = new Array<BarChartDataSet>(); 
 // one dataset for each type
+
+  constructor(private eventBus: EventBusService) { }
+
+  ngOnInit() {
+    this.chartContainerId = CommonUtil.createUniqId('vertical_bar_chart_');
+    this.eventBus
+      .getEvent(EventMap.WindowResizedEvent)
+      .pipe(takeUntil(this.destroy$))
+      .subscribe(() => {
+        this.renderChart(this.bucketList, this.barChartDataSets);
+      });
+  }
+
+  ngOnDestroy() {
+    this.destroy$.next(true);
+    this.destroy$.unsubscribe();
+  }
+
+  ngAfterViewInit() {
+    if (this.barChartDataSets) {
+      this.renderChart(this.bucketList, this.barChartDataSets);
+    }
+  }
+
+  ngOnChanges(changes: SimpleChanges) {
+    if (
+      changes['barChartDataSets'] &&
+      changes['barChartDataSets'].currentValue
+    ) {
+      this.barChartDataSets = changes['barChartDataSets'].currentValue;
+      this.renderChart(this.bucketList, this.barChartDataSets);
+    }
+  }
+
+  renderChart(bucketList: string[], barChartDataSets: BarChartDataSet[]) {
+    if (!this.chartContainerId) {
+      return;
+    }
+    const ctx = (document.getElementById(this.chartContainerId) as 
HTMLCanvasElement).getContext(
+      '2d'
+    );
+
+    if (this.barChart) {
+      this.barChart.destroy();
+    }
+
+    this.barChart = new Chart(ctx!, {
+      type: 'bar',
+      data: {
+        labels: this.bucketList,
+        datasets: barChartDataSets.map((item, index) => {
+          return {
+            label: item.label,
+            data: item.data,
+            backgroundColor: item.backgroundColor,
+            borderWidth: item.borderWidth,
+          }
+        })
+      },
+      options: {
+        responsive: true,
+        plugins: {
+          legend: {
+            display: true,
+            position: 'left',
+            align: 'start',
+            onClick: (e) => {}, // disable legend click event
+            onHover: (event, legendItem, legend) => {
+              let datasetIndex = legendItem.datasetIndex
+              if (this.barChart != undefined && datasetIndex !== undefined) {
+                this.barChart.data.datasets[datasetIndex].backgroundColor = 
this.adjustOpacity(this.barChartDataSets[datasetIndex].backgroundColor, 0.5);
+              }
+              this.barChart?.update("resize");
+            },
+            onLeave: (event, legendItem, legend) => {
+              let datasetIndex = legendItem.datasetIndex
+              if (this.barChart != undefined && datasetIndex !== undefined) {
+                this.barChart.data.datasets[datasetIndex].backgroundColor = 
this.barChartDataSets[datasetIndex].backgroundColor;
+              }
+              this.barChart?.update("resize");
+            },
+          },
+          title: {
+            display: false,
+          },
+          tooltip: {
+            enabled: true,
+            position: 'nearest',
+            callbacks: {
+              label: function (context) {
+                return barChartDataSets[context.datasetIndex].label;
+              },
+              labelColor: function (context) {
+                return {
+                  borderColor: 
barChartDataSets[context.datasetIndex].backgroundColor,
+                  backgroundColor: 
barChartDataSets[context.datasetIndex].backgroundColor,
+                };
+              },
+              footer: function (context) {
+                // show bar description on tooltip footer
+                let datasetIndex = context[0].datasetIndex;
+                let dataIndex = context[0].dataIndex;
+                let nodeCount = context[0].parsed.y
+                let unit = nodeCount > 1 ? 'nodes' : 'node';
+                return "Total: " + nodeCount + " " + unit + "\n\n" + 
barChartDataSets[datasetIndex].description[dataIndex];
+              }
+            }
+          },
+        },
+        scales: {
+          y: {
+            ticks: {
+              stepSize: 1,
+              precision: 0
+            }
+          }
+        },
+        onHover: (event, chartElement) => {
+          if (this.barChartDataSets.length > 0) {
+            if (this.barChart != undefined) {
+              if (chartElement.length > 0) {
+                // Reset datasets background color
+                this.barChart?.data.datasets?.forEach((dataset, i) => {
+                  this.barChartDataSets.forEach((item, index) => {
+                    if (this.barChart != undefined) {
+                      this.barChart.data.datasets[index].backgroundColor = 
this.barChartDataSets[index].backgroundColor;
+                    }
+                  });
+                });
+                //Update the target dataset's background color
+                const datasetIndex = chartElement[0].datasetIndex;
+                if (this.barChart != undefined) {
+                  this.barChart.data.datasets[datasetIndex].backgroundColor = 
this.adjustOpacity(this.barChartDataSets[datasetIndex].backgroundColor, 0.5);
+                }
+              } else {
+                // Reset datasets background color
+                this.barChart?.data.datasets?.forEach((dataset, i) => {
+                  this.barChartDataSets.forEach((item, datasetIndex) => {
+                    if (this.barChart != undefined) {
+                      
this.barChart.data.datasets[datasetIndex].backgroundColor = 
this.barChartDataSets[datasetIndex].backgroundColor;
+                    }
+                  });
+                });
+              }
+              this.barChart?.update("resize");
+            }
+          }
+        },
+      },
+    });
+    this.barChart.update();
+  }
+
+  adjustOpacity(rgba: string, opacity: number) {
+    const rgbaValues = rgba.match(/[\d.]+/g);
+    if (!rgbaValues || rgbaValues.length < 4) {
+      return rgba;
+    }
+    rgbaValues[3] = String(opacity);
+    return `rgba(${rgbaValues.join(',')})`;
+  }
+}
diff --git a/src/app/models/chart-data.model.ts 
b/src/app/models/chart-data.model.ts
index b1f180e..81fd3a6 100644
--- a/src/app/models/chart-data.model.ts
+++ b/src/app/models/chart-data.model.ts
@@ -29,3 +29,21 @@ export class ChartDataItem {
     this.description = description;
   }
 }
+
+export class BarChartDataSet {
+  label: string;
+  data: number[];
+  avgUtilizationRate: number;
+  backgroundColor: string;
+  borderWidth: number;
+  description: string[];
+
+  constructor(label: string, data: number[], avgUtilizationRate: number, 
backgroundColor: string, borderWidth: number, description: string[]) {
+    this.label = label;
+    this.data = data;
+    this.avgUtilizationRate = avgUtilizationRate;
+    this.backgroundColor = backgroundColor;
+    this.borderWidth = borderWidth;
+    this.description = description;
+  }
+}
diff --git a/src/app/models/node-utilization.model.ts 
b/src/app/models/node-utilization.model.ts
index a21c4ed..d56a722 100644
--- a/src/app/models/node-utilization.model.ts
+++ b/src/app/models/node-utilization.model.ts
@@ -27,10 +27,10 @@ export class NodeUtilization {
       numOfNodes: number;
       nodeNames: null | string[];
     }[],
-  ) {}
+  ) { }
 
   // transform NodeUtilization to NodeUtilizationChartData for NodeUtilization 
bar chart
-  toNodeUtilizationChartData(){
+  toNodeUtilizationChartData() {
     const MAX_NODES_IN_DESCRIPTION = 15;
     const backgroundColor = DEFAULT_BAR_COLOR;
     let type = this.type;
@@ -42,9 +42,9 @@ export class NodeUtilization {
       let description: string | undefined;
       if (nodeNames && nodeNames.length > MAX_NODES_IN_DESCRIPTION) {
         // only put MAX_NODES_IN_DESCRIPTION nodes in description, others will 
be replaced by '...N more'
-        description = nodeNames.slice(0, 
MAX_NODES_IN_DESCRIPTION).sort().join("\n") + "\n..."+ 
(nodeNames.length-MAX_NODES_IN_DESCRIPTION) +" more";
+        description = nodeNames.slice(0, 
MAX_NODES_IN_DESCRIPTION).sort().join("\n") + "\n..." + (nodeNames.length - 
MAX_NODES_IN_DESCRIPTION) + " more";
       } else {
-        description = nodeNames?nodeNames.sort().join("\n"): undefined;
+        description = nodeNames ? nodeNames.sort().join("\n") : undefined;
       }
       chartDataItems.push(new ChartDataItem(
         bucketName,
@@ -57,6 +57,14 @@ export class NodeUtilization {
   }
 }
 
+export class NodeUtilizationsInfo {
+  constructor(
+    public clusterId: string,
+    public partition: string,
+    public utilizations: NodeUtilization[],
+  ) { }
+}
+
 export class NodeUtilizationChartData {
   type: string;
   chartDataItems: ChartDataItem[];
diff --git a/src/app/services/scheduler/scheduler.service.ts 
b/src/app/services/scheduler/scheduler.service.ts
index 012f307..f848bb3 100644
--- a/src/app/services/scheduler/scheduler.service.ts
+++ b/src/app/services/scheduler/scheduler.service.ts
@@ -23,7 +23,7 @@ import {AppInfo} from '@app/models/app-info.model';
 import {ClusterInfo} from '@app/models/cluster-info.model';
 import {HistoryInfo} from '@app/models/history-info.model';
 import {NodeInfo} from '@app/models/node-info.model';
-import {NodeUtilization, NodeUtilizationChartData} from 
'@app/models/node-utilization.model';
+import {NodeUtilization, NodeUtilizationsInfo} from 
'@app/models/node-utilization.model';
 import {Partition} from '@app/models/partition-info.model';
 
 import {QueueInfo, QueuePropertyItem} from '@app/models/queue-info.model';
@@ -255,11 +255,16 @@ export class SchedulerService {
     );
   }
 
-  fetchClusterNodeUtilization(): Observable<NodeUtilization>{
+  fetchNodeUtilization(): Observable<NodeUtilization>{
     const nodeUtilizationUrl = 
`${this.envConfig.getSchedulerWebAddress()}/ws/v1/scheduler/node-utilization`;
     return this.httpClient.get(nodeUtilizationUrl).pipe(map((data: any) => 
data as NodeUtilization));
   }
 
+  fetchNodeUtilizationsInfo(): Observable<NodeUtilizationsInfo[]>{
+    const nodeUtilizationsUrl = 
`${this.envConfig.getSchedulerWebAddress()}/ws/v1/scheduler/node-utilizations`;
+    return this.httpClient.get(nodeUtilizationsUrl).pipe(map((data: any) => 
data as NodeUtilizationsInfo[]));
+  }
+
   fecthHealthchecks(): Observable<SchedulerHealthInfo> {
     const healthCheckUrl = 
`${this.envConfig.getSchedulerWebAddress()}/ws/v1/scheduler/healthcheck`;
     return this.httpClient.get(healthCheckUrl).pipe(map((data: any) => data as 
SchedulerHealthInfo));
@@ -329,7 +334,7 @@ export class SchedulerService {
     const formatted: string[] = [];
     if (resource) {
       // Object.keys() didn't guarantee the order of keys, sort it before 
iterate.
-      Object.keys(resource).sort(this.resourcesCompareFn).forEach((key) => {
+      Object.keys(resource).sort(CommonUtil.resourcesCompareFn).forEach((key) 
=> {
         let value = resource[key];
         let formattedKey = key;
         let formattedValue : string;
@@ -364,24 +369,6 @@ export class SchedulerService {
     return formatted.join(', ');
   } 
 
-  private resourcesCompareFn(a: string, b: string): number {
-    // define the order of resources
-    const resourceOrder: { [key: string]: number } = {
-      "memory": 1,
-      "vcore": 2,
-      "pods": 3,
-      "ephemeral-storage": 4
-    };
-    const orderA = a in resourceOrder ? resourceOrder[a] : 
Number.MAX_SAFE_INTEGER;
-    const orderB = b in resourceOrder ? resourceOrder[b] : 
Number.MAX_SAFE_INTEGER;
-  
-    if (orderA !== orderB) {
-      return orderA - orderB;  // Resources in the order defined above
-    } else {
-      return a.localeCompare(b);  // Other resources will be in lexicographic 
order
-    }
-  }
-
   private formatPercent(resource: SchedulerResourceInfo): string {
     const formatted = [];
 
diff --git a/src/app/testing/mocks.ts b/src/app/testing/mocks.ts
index 1e0f491..805dadb 100644
--- a/src/app/testing/mocks.ts
+++ b/src/app/testing/mocks.ts
@@ -31,7 +31,7 @@ export const MockSchedulerService = {
   fetchAppHistory: () => of([]),
   fetchContainerHistory: () => of([]),
   fetchNodeList: () => of([]),
-  fetchClusterNodeUtilization: () => of([]),
+  fetchNodeUtilization: () => of([]),
   fecthHealthchecks: () => of([]),
 };
 
diff --git a/src/app/utils/common.util.ts b/src/app/utils/common.util.ts
index 4c71722..98f2a18 100644
--- a/src/app/utils/common.util.ts
+++ b/src/app/utils/common.util.ts
@@ -97,4 +97,22 @@ export class CommonUtil {
     }
     return NOT_AVAILABLE;
   }
+
+  static resourcesCompareFn(a: string, b: string): number {
+    // define the order of resources
+    const resourceOrder: { [key: string]: number } = {
+      "memory": 1,
+      "vcore": 2,
+      "pods": 3,
+      "ephemeral-storage": 4
+    };
+    const orderA = a in resourceOrder ? resourceOrder[a] : 
Number.MAX_SAFE_INTEGER;
+    const orderB = b in resourceOrder ? resourceOrder[b] : 
Number.MAX_SAFE_INTEGER;
+  
+    if (orderA !== orderB) {
+      return orderA - orderB;  // Resources in the order defined above
+    } else {
+      return a.localeCompare(b);  // Other resources will be in lexicographic 
order
+    }
+  }
 }
diff --git a/src/app/utils/constants.ts b/src/app/utils/constants.ts
index 643e9eb..da3d13d 100644
--- a/src/app/utils/constants.ts
+++ b/src/app/utils/constants.ts
@@ -20,4 +20,5 @@ export const DEFAULT_PARTITION_VALUE = '';
 export const DEFAULT_PROTOCOL = 'http:';
 export const NOT_AVAILABLE = 'n/a';
 export const PARTITION_DEFAULT = 'default';
-export const DEFAULT_BAR_COLOR = '#4285f4';
\ No newline at end of file
+export const DEFAULT_BAR_COLOR = 'rgba(66, 133, 244, 1)';
+export const CHART_COLORS = ['rgba(66, 133, 244, 1)', 'rgb(219, 68, 55, 1)', 
'rgb(244, 180, 0, 1)', 'rgb(15, 157, 88, 1)', 'rgb(255, 109, 0, 1)', 'rgb(57, 
73, 171, 1)', 'rgb(250, 204, 84, 1)', 'rgb(38, 187, 240, 1)', 'rgb(204, 97, 
100, 1)', 'rgb(96, 206, 165, 1)']


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

Reply via email to