This is an automated email from the ASF dual-hosted git repository.
chia7712 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 69e0eb8 [YUNIKORN-2231] Show node list when hovering mouse over the
node utitutilization bar chart (#157)
69e0eb8 is described below
commit 69e0eb857e5009f9ecdfda920bbd3dfef88325eb
Author: Yu-Lin Chen <[email protected]>
AuthorDate: Fri Jan 12 21:02:25 2024 +0800
[YUNIKORN-2231] Show node list when hovering mouse over the node
utitutilization bar chart (#157)
1) Enable Tooltip for bar-chart/area-chart/donut-chart.
2) Show node list in tooltips’s footer. (No more than 15 nodes)
3) Adjust ‘Nodes Resource Utilization’ chart width size (was 35%, is 50%)
4) Use same color for bars in ‘Nodes Resource Utilization’ (Using
multi-clor for the same resource is meaningless.)
5) Expose domaint resource type in subtitle of ‘Nodes Resource Utilization’
bar cahrt.
6) ix wrong variable name in bar-chart. (was ‘donutChartData’, is
‘barChartData’)
7) Refactor SchedulerService.fetchNodeUtilization() - move data
transformation logic to NodeUtilization.toNodeUtilizationChartData() and add
test case
Closes: #157
Signed-off-by: Chia-Ping Tsai <[email protected]>
---
json-db.json | 33 ++++++++++++++---
.../app-node-utilization.component.html | 3 +-
.../app-node-utilization.component.spec.ts | 5 ++-
.../app-node-utilization.component.ts | 2 ++
.../components/area-chart/area-chart.component.ts | 5 ++-
.../components/bar-chart/bar-chart.component.ts | 34 +++++++++++-------
.../components/dashboard/dashboard.component.html | 4 +--
.../components/dashboard/dashboard.component.scss | 4 +++
.../components/dashboard/dashboard.component.ts | 29 ++++-----------
.../donut-chart/donut-chart.component.ts | 5 +--
src/app/models/chart-data.model.ts | 6 ++--
src/app/models/node-utilization.model.spec.ts | 42 ++++++++++++++++++++++
src/app/models/node-utilization.model.ts | 40 +++++++++++++++++++++
src/app/services/scheduler/scheduler.service.ts | 4 +--
src/app/testing/mocks.ts | 2 +-
src/app/utils/constants.ts | 1 +
16 files changed, 168 insertions(+), 51 deletions(-)
diff --git a/json-db.json b/json-db.json
index be29598..d495481 100644
--- a/json-db.json
+++ b/json-db.json
@@ -758,18 +758,43 @@
"bucketName": "0-10%",
"numOfNodes": 1,
"nodeNames": [
- "aethergpu"
+ "node-01"
]
},
{
"bucketName": "10-20%",
"numOfNodes": 3,
- "nodeNames": null
+ "nodeNames": [
+ "node-02",
+ "node-03",
+ "node-04"
+ ]
},
{
"bucketName": "20-30%",
- "numOfNodes": 0,
- "nodeNames": null
+ "numOfNodes": 20,
+ "nodeNames": [
+ "node-05",
+ "node-06",
+ "node-07",
+ "node-08",
+ "node-09",
+ "node-10",
+ "node-11",
+ "node-12",
+ "node-13",
+ "node-14",
+ "node-15",
+ "node-16",
+ "node-17",
+ "node-18",
+ "node-19",
+ "node-20",
+ "node-21",
+ "node-22",
+ "node-23",
+ "node-24"
+ ]
},
{
"bucketName": "30-40%",
diff --git
a/src/app/components/app-node-utilization/app-node-utilization.component.html
b/src/app/components/app-node-utilization/app-node-utilization.component.html
index 3137ce6..4407b5a 100644
---
a/src/app/components/app-node-utilization/app-node-utilization.component.html
+++
b/src/app/components/app-node-utilization/app-node-utilization.component.html
@@ -18,7 +18,8 @@
<mat-card appearance="outlined" class="box-card">
<mat-card-header>
- <mat-card-title>Nodes Resource Utilization</mat-card-title>
+ <mat-card-title>{{title}}</mat-card-title>
+ <mat-card-subtitle>{{subtitle}}</mat-card-subtitle>
</mat-card-header>
<mat-card-content>
<div class="status-wrapper flex-grid">
diff --git
a/src/app/components/app-node-utilization/app-node-utilization.component.spec.ts
b/src/app/components/app-node-utilization/app-node-utilization.component.spec.ts
index 237bf64..feb4663 100644
---
a/src/app/components/app-node-utilization/app-node-utilization.component.spec.ts
+++
b/src/app/components/app-node-utilization/app-node-utilization.component.spec.ts
@@ -16,8 +16,10 @@
* limitations under the License.
*/
+import { MatCardModule } from '@angular/material/card';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { AppNodeUtilizationComponent } from
'@app/components/app-node-utilization/app-node-utilization.component';
+import { BarChartComponent } from
'@app/components/bar-chart/bar-chart.component';
describe('AppNodeUtilizationComponent', () => {
let component: AppNodeUtilizationComponent;
@@ -25,7 +27,8 @@ describe('AppNodeUtilizationComponent', () => {
beforeEach(() => {
TestBed.configureTestingModule({
- declarations: [AppNodeUtilizationComponent]
+ declarations: [AppNodeUtilizationComponent, BarChartComponent],
+ imports: [MatCardModule],
});
fixture = TestBed.createComponent(AppNodeUtilizationComponent);
component = fixture.componentInstance;
diff --git
a/src/app/components/app-node-utilization/app-node-utilization.component.ts
b/src/app/components/app-node-utilization/app-node-utilization.component.ts
index 28b5046..4e7b134 100644
--- a/src/app/components/app-node-utilization/app-node-utilization.component.ts
+++ b/src/app/components/app-node-utilization/app-node-utilization.component.ts
@@ -26,4 +26,6 @@ import { ChartDataItem } from '@app/models/chart-data.model';
})
export class AppNodeUtilizationComponent {
@Input() chartData: ChartDataItem[] = [];
+ @Input() title: String = "";
+ @Input() subtitle: String = "";
}
diff --git a/src/app/components/area-chart/area-chart.component.ts
b/src/app/components/area-chart/area-chart.component.ts
index aeb394b..0b4f6ca 100644
--- a/src/app/components/area-chart/area-chart.component.ts
+++ b/src/app/components/area-chart/area-chart.component.ts
@@ -37,6 +37,7 @@ import {
Filler,
Legend,
Title,
+ Tooltip
} from 'chart.js';
import 'chartjs-adapter-date-fns';
@@ -52,7 +53,8 @@ Chart.register(
TimeSeriesScale,
Filler,
Legend,
- Title
+ Title,
+ Tooltip
);
@Component({
@@ -155,6 +157,7 @@ export class AreaChartComponent implements OnInit,
AfterViewInit, OnChanges, OnD
display: false,
},
tooltip: {
+ enabled: true,
position: 'nearest',
},
},
diff --git a/src/app/components/bar-chart/bar-chart.component.ts
b/src/app/components/bar-chart/bar-chart.component.ts
index c89b7c5..4ea3a57 100644
--- a/src/app/components/bar-chart/bar-chart.component.ts
+++ b/src/app/components/bar-chart/bar-chart.component.ts
@@ -20,10 +20,10 @@ import { AfterViewInit, Component, Input, OnChanges,
OnDestroy, OnInit, SimpleCh
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 { Chart, BarController, CategoryScale, BarElement, Tooltip} from
'chart.js';
import { Subject, takeUntil } from 'rxjs';
-Chart.register(BarElement, CategoryScale, BarController);
+Chart.register(BarElement, CategoryScale, BarController, Tooltip);
@Component({
selector: 'app-bar-chart',
@@ -33,8 +33,8 @@ Chart.register(BarElement, CategoryScale, BarController);
export class BarChartComponent implements OnInit, AfterViewInit, OnChanges,
OnDestroy {
destroy$ = new Subject<boolean>();
chartContainerId = '';
- donutChartData: ChartDataItem[] = [];
- donutChart: Chart<'bar', number[], string> | undefined;
+ barChartData: ChartDataItem[] = [];
+ barChart: Chart<'bar', number[], string> | undefined;
@Input() data: ChartDataItem[] = [];
constructor(private eventBus: EventBusService) { }
@@ -45,7 +45,7 @@ export class BarChartComponent implements OnInit,
AfterViewInit, OnChanges, OnDe
this.eventBus
.getEvent(EventMap.WindowResizedEvent)
.pipe(takeUntil(this.destroy$))
- .subscribe(() => this.renderChart(this.donutChartData));
+ .subscribe(() => this.renderChart(this.barChartData));
}
ngOnDestroy() {
@@ -65,8 +65,8 @@ export class BarChartComponent implements OnInit,
AfterViewInit, OnChanges, OnDe
changes['data'].currentValue &&
changes['data'].currentValue.length > 0
) {
- this.donutChartData = changes['data'].currentValue;
- this.renderChart(this.donutChartData);
+ this.barChartData = changes['data'].currentValue;
+ this.renderChart(this.barChartData);
}
}
@@ -81,18 +81,18 @@ export class BarChartComponent implements OnInit,
AfterViewInit, OnChanges, OnDe
const dataValues = chartData.map((d) => d.value);
const chartLabels = chartData.map((d) => d.name);
const colors = chartData.map((d) => d.color);
+ const descriptions = chartData.map((d) => d.description);
- if (this.donutChart) {
- this.donutChart.destroy();
+ if (this.barChart) {
+ this.barChart.destroy();
}
- this.donutChart = new Chart(ctx!, {
+ this.barChart = new Chart(ctx!, {
type: 'bar',
data: {
labels: chartLabels,
datasets: [
{
- label: 'My First Dataset',
data: dataValues,
backgroundColor: colors,
borderWidth: 1
@@ -109,12 +109,22 @@ export class BarChartComponent implements OnInit,
AfterViewInit, OnChanges, OnDe
display: false,
},
tooltip: {
+ enabled: true,
position: 'nearest',
+ callbacks: {
+ label: function(context) {
+ let unit = context.parsed.y > 1 ? 'nodes' : 'node';
+ return 'Total: ' + context.parsed.y + ' ' + unit;
+ },
+ footer: function(context) {
+ return descriptions[context[0].dataIndex]
+ }
+ }
},
},
},
});
- this.donutChart.update();
+ this.barChart.update();
}
}
diff --git a/src/app/components/dashboard/dashboard.component.html
b/src/app/components/dashboard/dashboard.component.html
index 852ad12..b493d92 100644
--- a/src/app/components/dashboard/dashboard.component.html
+++ b/src/app/components/dashboard/dashboard.component.html
@@ -94,8 +94,8 @@
</div>
</div>
<div class="flex-grid grid-row">
- <div class="left-col flex-primary">
- <app-node-utilization [chartData]="nodeUtilizationData" />
+ <div class="half-col flex-primary">
+ <app-node-utilization
[chartData]="nodeUtilizationChartData.chartDataItems" title="Nodes Resource
Utilization" [subtitle]="('Domaint Resource:' +
nodeUtilizationChartData.type)"/>
</div>
</div>
</div>
diff --git a/src/app/components/dashboard/dashboard.component.scss
b/src/app/components/dashboard/dashboard.component.scss
index fc121cc..eb4f1cf 100644
--- a/src/app/components/dashboard/dashboard.component.scss
+++ b/src/app/components/dashboard/dashboard.component.scss
@@ -39,6 +39,10 @@
width: 65%;
max-width: 65%;
}
+ .half-col {
+ width: 50%;
+ max-width: 50%;
+ }
.grid-row {
margin-top: 30px;
}
diff --git a/src/app/components/dashboard/dashboard.component.ts
b/src/app/components/dashboard/dashboard.component.ts
index f38ab9b..d73279a 100644
--- a/src/app/components/dashboard/dashboard.component.ts
+++ b/src/app/components/dashboard/dashboard.component.ts
@@ -27,8 +27,7 @@ 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'];
+import { NodeUtilization, NodeUtilizationChartData } from
'@app/models/node-utilization.model';
@Component({
selector: 'app-dashboard',
@@ -45,13 +44,13 @@ export class DashboardComponent implements OnInit {
totalContainers = '';
appStatusData: ChartDataItem[] = [];
containerStatusData: ChartDataItem[] = [];
- nodeUtilizationData: ChartDataItem[] = [];
appHistoryData: AreaDataItem[] = [];
containerHistoryData: AreaDataItem[] = [];
clusterInfo: ClusterInfo = this.getEmptyClusterInfo();
buildInfo: BuildInfo = this.getEmptyBuildInfo();
initialAppHistory: HistoryInfo[] = [];
initialContainerHistory: HistoryInfo[] = [];
+ nodeUtilizationChartData: NodeUtilizationChartData = new
NodeUtilizationChartData("NA", []);
constructor(
private scheduler: SchedulerService,
@@ -109,26 +108,10 @@ export class DashboardComponent implements OnInit {
this.initialAppHistory = data;
this.appHistoryData = this.getAreaChartData(data);
});
-
- this.scheduler.fetchNodeUtilization().subscribe((data) => {
- const utilizationData: Record<string, number> = {};
- data.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.fetchClusterNodeUtilization().subscribe((data) => {
+ let nodeUtilization = new NodeUtilization(data.type, data.utilization);
+ this.nodeUtilizationChartData =
nodeUtilization.toNodeUtilizationChartData();
});
this.scheduler.fetchContainerHistory().subscribe((data) => {
diff --git a/src/app/components/donut-chart/donut-chart.component.ts
b/src/app/components/donut-chart/donut-chart.component.ts
index 94d119f..17c0d35 100644
--- a/src/app/components/donut-chart/donut-chart.component.ts
+++ b/src/app/components/donut-chart/donut-chart.component.ts
@@ -27,12 +27,12 @@ import {
} from '@angular/core';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
-import { Chart, ArcElement, DoughnutController } from 'chart.js';
+import { Chart, ArcElement, DoughnutController, Tooltip } from 'chart.js';
import { CommonUtil } from '@app/utils/common.util';
import { ChartDataItem } from '@app/models/chart-data.model';
import { EventBusService, EventMap } from
'@app/services/event-bus/event-bus.service';
-Chart.register(ArcElement, DoughnutController);
+Chart.register(ArcElement, DoughnutController, Tooltip);
@Component({
selector: 'app-donut-chart',
@@ -121,6 +121,7 @@ export class DonutChartComponent implements OnInit,
AfterViewInit, OnChanges, On
display: false,
},
tooltip: {
+ enabled: true,
position: 'nearest',
},
},
diff --git a/src/app/models/chart-data.model.ts
b/src/app/models/chart-data.model.ts
index 4c7a9b0..b1f180e 100644
--- a/src/app/models/chart-data.model.ts
+++ b/src/app/models/chart-data.model.ts
@@ -19,11 +19,13 @@
export class ChartDataItem {
name: string;
value: number;
- color: string;
+ color?: string;
+ description?: string;
- constructor(name: string, value: number, color: string) {
+ constructor(name: string, value: number, color?: string, description?:
string) {
this.name = name;
this.value = value;
this.color = color;
+ this.description = description;
}
}
diff --git a/src/app/models/node-utilization.model.spec.ts
b/src/app/models/node-utilization.model.spec.ts
new file mode 100644
index 0000000..0a51c8b
--- /dev/null
+++ b/src/app/models/node-utilization.model.spec.ts
@@ -0,0 +1,42 @@
+/**
+ * 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 { ChartDataItem } from '@app/models/chart-data.model';
+import { DEFAULT_BAR_COLOR } from '@app/utils/constants';
+import { NodeUtilization, NodeUtilizationChartData } from
'@app/models/node-utilization.model';
+
+describe('NodeUtilization', () => {
+
+ it('test NodeUtilization.toNodeUtilizationChartData()', () => {
+ const nodeUtilization = new NodeUtilization('vcores', [
+ { bucketName: '0-10%', numOfNodes: 2, nodeNames: ['node1', 'node2'] },
+ { bucketName: '10-20%', numOfNodes: 3, nodeNames: ['node3', 'node4',
'node5'] },
+ { bucketName: '20-30%', numOfNodes: 16, nodeNames: ['node6', 'node7',
'node8','node9', 'node10', 'node11','node12', 'node13', 'node14','node15',
'node16', 'node17','node18', 'node19', 'node20', 'node21'] },
+ ]);
+
+ const result = nodeUtilization.toNodeUtilizationChartData();
+
+ expect(result).toBeInstanceOf(NodeUtilizationChartData);
+ expect(result.type).toBe('vcores');
+ expect(result.chartDataItems).toEqual([
+ new ChartDataItem('0-10%', 2, DEFAULT_BAR_COLOR, 'node1\nnode2'),
+ new ChartDataItem('10-20%', 3, DEFAULT_BAR_COLOR, 'node3\nnode4\nnode5'),
+ new ChartDataItem('20-30%', 16, DEFAULT_BAR_COLOR,
'node10\nnode11\nnode12\nnode13\nnode14\nnode15\nnode16\nnode17\nnode18\nnode19\nnode20\nnode6\nnode7\nnode8\nnode9\n...1
more'),
+ ]);
+ });
+});
diff --git a/src/app/models/node-utilization.model.ts
b/src/app/models/node-utilization.model.ts
index 529922f..a21c4ed 100644
--- a/src/app/models/node-utilization.model.ts
+++ b/src/app/models/node-utilization.model.ts
@@ -16,6 +16,9 @@
* limitations under the License.
*/
+import { ChartDataItem } from '@app/models/chart-data.model';
+import { DEFAULT_BAR_COLOR } from '@app/utils/constants';
+
export class NodeUtilization {
constructor(
public type: string,
@@ -25,4 +28,41 @@ export class NodeUtilization {
nodeNames: null | string[];
}[],
) {}
+
+ // transform NodeUtilization to NodeUtilizationChartData for NodeUtilization
bar chart
+ toNodeUtilizationChartData(){
+ const MAX_NODES_IN_DESCRIPTION = 15;
+ const backgroundColor = DEFAULT_BAR_COLOR;
+ let type = this.type;
+ let utilization = this.utilization;
+ // prepare data
+ let chartDataItems = new Array<ChartDataItem>();
+ utilization.forEach(({ bucketName, numOfNodes, nodeNames }) => {
+ const numOfNodesValue = numOfNodes === -1 ? 0 : numOfNodes;
+ 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;
+ }
+ chartDataItems.push(new ChartDataItem(
+ bucketName,
+ numOfNodesValue,
+ backgroundColor,
+ description
+ ));
+ });
+ return new NodeUtilizationChartData(type, chartDataItems)
+ }
+}
+
+export class NodeUtilizationChartData {
+ type: string;
+ chartDataItems: ChartDataItem[];
+
+ constructor(type: string, chartDataItems: ChartDataItem[]) {
+ this.type = type;
+ this.chartDataItems = chartDataItems;
+ }
}
diff --git a/src/app/services/scheduler/scheduler.service.ts
b/src/app/services/scheduler/scheduler.service.ts
index 044ea3b..993c2c1 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} from '@app/models/node-utilization.model';
+import {NodeUtilization, NodeUtilizationChartData} from
'@app/models/node-utilization.model';
import {Partition} from '@app/models/partition-info.model';
import {QueueInfo, QueuePropertyItem} from '@app/models/queue-info.model';
@@ -254,7 +254,7 @@ export class SchedulerService {
);
}
- fetchNodeUtilization(): Observable<NodeUtilization>{
+ fetchClusterNodeUtilization(): Observable<NodeUtilization>{
const nodeUtilizationUrl =
`${this.envConfig.getSchedulerWebAddress()}/ws/v1/scheduler/node-utilization`;
return this.httpClient.get(nodeUtilizationUrl).pipe(map((data: any) =>
data as NodeUtilization));
}
diff --git a/src/app/testing/mocks.ts b/src/app/testing/mocks.ts
index 805dadb..1e0f491 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([]),
- fetchNodeUtilization: () => of([]),
+ fetchClusterNodeUtilization: () => of([]),
fecthHealthchecks: () => of([]),
};
diff --git a/src/app/utils/constants.ts b/src/app/utils/constants.ts
index 93c7560..643e9eb 100644
--- a/src/app/utils/constants.ts
+++ b/src/app/utils/constants.ts
@@ -20,3 +20,4 @@ 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
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]