@openshift-migration-advisor/shared-components 0.27.0 → 0.28.0-09381b4d22e5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/charts/MigrationDonutChart.d.ts.map +1 -1
- package/dist/charts/MigrationDonutChart.js +7 -0
- package/dist/report/ChartExportProvider.d.ts +4 -3
- package/dist/report/ChartExportProvider.d.ts.map +1 -1
- package/dist/report/ChartExportProvider.js +7 -4
- package/dist/report/ClustersOverview.d.ts +24 -0
- package/dist/report/ClustersOverview.d.ts.map +1 -0
- package/dist/report/ClustersOverview.js +228 -0
- package/dist/report/ClustersOverviewStyles.d.ts +12 -0
- package/dist/report/ClustersOverviewStyles.d.ts.map +1 -0
- package/dist/report/ClustersOverviewStyles.js +101 -0
- package/dist/report/CpuAndMemoryOverview.d.ts +19 -0
- package/dist/report/CpuAndMemoryOverview.d.ts.map +1 -0
- package/dist/report/CpuAndMemoryOverview.js +112 -0
- package/dist/report/FeatureStatusBadge.js +1 -1
- package/dist/report/ReportExportMenu.d.ts +1 -0
- package/dist/report/ReportExportMenu.d.ts.map +1 -1
- package/dist/report/ReportExportMenu.js +38 -4
- package/dist/report/StorageOverview.d.ts +27 -0
- package/dist/report/StorageOverview.d.ts.map +1 -0
- package/dist/report/StorageOverview.js +319 -0
- package/dist/report/VMMigrationStatus.d.ts +33 -0
- package/dist/report/VMMigrationStatus.d.ts.map +1 -0
- package/dist/report/VMMigrationStatus.js +129 -0
- package/dist/report/captureChartElement.d.ts +6 -1
- package/dist/report/captureChartElement.d.ts.map +1 -1
- package/dist/report/captureChartElement.js +41 -12
- package/dist/report/chartExport.d.ts +23 -0
- package/dist/report/chartExport.d.ts.map +1 -1
- package/dist/report/chartExport.js +21 -1
- package/dist/report/chartExportContext.d.ts +6 -2
- package/dist/report/chartExportContext.d.ts.map +1 -1
- package/dist/report/dashboardStyles.d.ts +6 -0
- package/dist/report/dashboardStyles.d.ts.map +1 -1
- package/dist/report/dashboardStyles.js +7 -4
- package/dist/report/index.d.ts +7 -3
- package/dist/report/index.d.ts.map +1 -1
- package/dist/report/index.js +4 -0
- package/dist/report/pdfExport.d.ts +2 -2
- package/dist/report/pdfExport.d.ts.map +1 -1
- package/dist/report/pdfExport.js +52 -3
- package/dist/report/reportExportOptions.d.ts +13 -2
- package/dist/report/reportExportOptions.d.ts.map +1 -1
- package/dist/report/reportExportOptions.js +13 -5
- package/package.json +2 -2
- package/src/charts/MigrationDonutChart.tsx +9 -0
- package/src/report/ChartExportProvider.tsx +18 -6
- package/src/report/ClustersOverview.tsx +451 -0
- package/src/report/ClustersOverviewStyles.ts +110 -0
- package/src/report/CpuAndMemoryOverview.tsx +277 -0
- package/src/report/FeatureStatusBadge.tsx +1 -1
- package/src/report/ReportExportMenu.tsx +69 -13
- package/src/report/StorageOverview.tsx +629 -0
- package/src/report/VMMigrationStatus.tsx +358 -0
- package/src/report/__tests__/ChartExportProvider.test.tsx +110 -2
- package/src/report/__tests__/ClustersOverview.test.tsx +241 -0
- package/src/report/__tests__/CpuAndMemoryOverview.test.tsx +200 -0
- package/src/report/__tests__/StorageOverview.test.tsx +223 -0
- package/src/report/__tests__/VMMigrationStatus.test.tsx +178 -0
- package/src/report/__tests__/captureChartElement.test.ts +120 -0
- package/src/report/__tests__/chartExportPackagers.test.ts +75 -0
- package/src/report/captureChartElement.ts +55 -10
- package/src/report/chartExport.ts +41 -1
- package/src/report/chartExportContext.tsx +9 -1
- package/src/report/dashboardStyles.ts +7 -4
- package/src/report/index.ts +24 -0
- package/src/report/pdfExport.ts +81 -2
- package/src/report/reportExportOptions.ts +28 -5
|
@@ -6,11 +6,13 @@ import {
|
|
|
6
6
|
useRef,
|
|
7
7
|
useState,
|
|
8
8
|
} from "react";
|
|
9
|
+
import { CHART_CAPTURE_SCALE_SINGLE } from "./captureChartElement.js";
|
|
9
10
|
import {
|
|
10
11
|
type ChartCaptureSource,
|
|
11
12
|
type ChartExportFile,
|
|
12
13
|
canvasToBlob,
|
|
13
14
|
downloadBlob,
|
|
15
|
+
type PdfTextPage,
|
|
14
16
|
type RegisteredChart,
|
|
15
17
|
releaseCanvas,
|
|
16
18
|
restoreChartExportViews,
|
|
@@ -28,6 +30,7 @@ import { getChartExportFilename } from "./chartExportFilenames.js";
|
|
|
28
30
|
|
|
29
31
|
export type CaptureChartElement = (
|
|
30
32
|
element: HTMLElement,
|
|
33
|
+
options?: import("./captureChartElement.js").CaptureChartElementOptions,
|
|
31
34
|
) => Promise<HTMLCanvasElement>;
|
|
32
35
|
export type ZipChartFiles = (files: ChartExportFile[]) => Promise<Blob>;
|
|
33
36
|
export type EncodeChartPng = (canvas: HTMLCanvasElement) => Promise<Blob>;
|
|
@@ -36,6 +39,12 @@ export type BuildChartDocument = (
|
|
|
36
39
|
documentTitle: string,
|
|
37
40
|
) => Promise<Blob>;
|
|
38
41
|
|
|
42
|
+
export type BuildPdfDocument = (
|
|
43
|
+
charts: ChartCaptureSource[],
|
|
44
|
+
documentTitle: string,
|
|
45
|
+
extraPages?: PdfTextPage[],
|
|
46
|
+
) => Promise<Blob>;
|
|
47
|
+
|
|
39
48
|
export type {
|
|
40
49
|
ChartExportApi,
|
|
41
50
|
ChartExportFormat,
|
|
@@ -47,7 +56,7 @@ export interface ChartExportProviderProps {
|
|
|
47
56
|
capture?: CaptureChartElement;
|
|
48
57
|
zipFiles?: ZipChartFiles;
|
|
49
58
|
encodePng?: EncodeChartPng;
|
|
50
|
-
buildPdf?:
|
|
59
|
+
buildPdf?: BuildPdfDocument;
|
|
51
60
|
buildHtml?: BuildChartDocument;
|
|
52
61
|
downloadFile?: (blob: Blob, filename: string) => void;
|
|
53
62
|
getZipFilename?: () => string;
|
|
@@ -96,8 +105,8 @@ async function resolveZip(override?: ZipChartFiles): Promise<ZipChartFiles> {
|
|
|
96
105
|
}
|
|
97
106
|
|
|
98
107
|
async function resolvePdf(
|
|
99
|
-
override?:
|
|
100
|
-
): Promise<
|
|
108
|
+
override?: BuildPdfDocument,
|
|
109
|
+
): Promise<BuildPdfDocument> {
|
|
101
110
|
if (override) {
|
|
102
111
|
return override;
|
|
103
112
|
}
|
|
@@ -220,7 +229,9 @@ export const ChartExportProvider: FC<ChartExportProviderProps> = ({
|
|
|
220
229
|
throw new Error("The chart is not ready to export yet.");
|
|
221
230
|
}
|
|
222
231
|
const captureFn = await resolveCapture(capture);
|
|
223
|
-
const canvas = await captureFn(chart.element
|
|
232
|
+
const canvas = await captureFn(chart.element, {
|
|
233
|
+
scale: CHART_CAPTURE_SCALE_SINGLE,
|
|
234
|
+
});
|
|
224
235
|
try {
|
|
225
236
|
downloadFile(await encodePng(canvas), chart.filename);
|
|
226
237
|
} finally {
|
|
@@ -281,11 +292,11 @@ export const ChartExportProvider: FC<ChartExportProviderProps> = ({
|
|
|
281
292
|
]);
|
|
282
293
|
|
|
283
294
|
const downloadPdf = useCallback(
|
|
284
|
-
async (documentTitle: string) => {
|
|
295
|
+
async (documentTitle: string, extraPages?: PdfTextPage[]) => {
|
|
285
296
|
await runCapture(
|
|
286
297
|
async () => {
|
|
287
298
|
const charts = listCharts();
|
|
288
|
-
if (charts.length === 0) {
|
|
299
|
+
if (charts.length === 0 && (extraPages?.length ?? 0) === 0) {
|
|
289
300
|
throw new Error("The report is not ready to export yet.");
|
|
290
301
|
}
|
|
291
302
|
const captureFn = await resolveCapture(capture);
|
|
@@ -296,6 +307,7 @@ export const ChartExportProvider: FC<ChartExportProviderProps> = ({
|
|
|
296
307
|
await build(
|
|
297
308
|
toCaptureSources(charts, captureFn, prepareElement),
|
|
298
309
|
documentTitle,
|
|
310
|
+
extraPages,
|
|
299
311
|
),
|
|
300
312
|
getPdfFilename(),
|
|
301
313
|
);
|
|
@@ -0,0 +1,451 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Card,
|
|
3
|
+
CardBody,
|
|
4
|
+
CardTitle,
|
|
5
|
+
Dropdown,
|
|
6
|
+
DropdownItem,
|
|
7
|
+
DropdownList,
|
|
8
|
+
Flex,
|
|
9
|
+
FlexItem,
|
|
10
|
+
MenuToggle,
|
|
11
|
+
type MenuToggleElement,
|
|
12
|
+
} from "@patternfly/react-core";
|
|
13
|
+
import { DatabaseIcon } from "@patternfly/react-icons";
|
|
14
|
+
import {
|
|
15
|
+
chart_color_black_500,
|
|
16
|
+
chart_color_blue_300,
|
|
17
|
+
chart_color_green_300,
|
|
18
|
+
chart_color_orange_100,
|
|
19
|
+
chart_color_purple_100,
|
|
20
|
+
chart_color_purple_300,
|
|
21
|
+
chart_color_red_orange_200,
|
|
22
|
+
chart_color_teal_200,
|
|
23
|
+
chart_color_teal_400,
|
|
24
|
+
chart_color_yellow_400,
|
|
25
|
+
} from "@patternfly/react-tokens";
|
|
26
|
+
import type { FC, Ref } from "react";
|
|
27
|
+
import { useMemo, useState } from "react";
|
|
28
|
+
import {
|
|
29
|
+
MigrationDonutChart,
|
|
30
|
+
type MigrationDonutChartDatum,
|
|
31
|
+
type MigrationDonutChartLegendVariant,
|
|
32
|
+
} from "../charts/MigrationDonutChart.js";
|
|
33
|
+
import { CardEmptyState } from "./CardEmptyState.js";
|
|
34
|
+
import { ChartHeaderActions } from "./ChartDownloadButton.js";
|
|
35
|
+
import { ChartExportSurface } from "./ChartExportSurface.js";
|
|
36
|
+
import { clustersOverviewStyles } from "./ClustersOverviewStyles.js";
|
|
37
|
+
import { chartExportViewsFromLabels } from "./chartExport.js";
|
|
38
|
+
import { REPORT_CARD_EMPTY_STATE_TITLES } from "./constants.js";
|
|
39
|
+
import { dashboardStyles } from "./dashboardStyles.js";
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Cluster inventory fields this card reads.
|
|
43
|
+
* agent-sdk and planner-sdk inventories both satisfy it. `cpuOverCommitment`
|
|
44
|
+
* stays `unknown` because generated models type it as a number while older
|
|
45
|
+
* payloads send a ratio string such as `"1:4"`.
|
|
46
|
+
*/
|
|
47
|
+
export interface InventoryDataLike {
|
|
48
|
+
infra?: {
|
|
49
|
+
cpuOverCommitment?: unknown;
|
|
50
|
+
};
|
|
51
|
+
vms?: {
|
|
52
|
+
total?: number;
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface ClustersOverviewProps {
|
|
57
|
+
clustersPerDatacenter?: number[];
|
|
58
|
+
clusters?: Record<string, InventoryDataLike>;
|
|
59
|
+
/** `"html"` in agent-ui; ui-app passes `"chart"`. */
|
|
60
|
+
legendVariant?: MigrationDonutChartLegendVariant;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
type ViewMode = "dataCenterDistribution" | "vmByCluster" | "cpuOverCommitment";
|
|
64
|
+
|
|
65
|
+
interface ClusterChart {
|
|
66
|
+
chartData: MigrationDonutChartDatum[];
|
|
67
|
+
legend: Record<string, string>;
|
|
68
|
+
title: string;
|
|
69
|
+
subTitle: string;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const VIEW_MODE_LABELS: Record<ViewMode, string> = {
|
|
73
|
+
dataCenterDistribution: "Cluster distribution by data center",
|
|
74
|
+
vmByCluster: "VM distribution by cluster",
|
|
75
|
+
cpuOverCommitment: "Cluster CPU over commitment",
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const TOP_CLUSTERS = 4;
|
|
79
|
+
const TOP_CPU_RATIOS = 5;
|
|
80
|
+
const REST_OF_CLUSTERS_LABEL = "Rest of clusters";
|
|
81
|
+
const REST_OF_DATACENTERS_LABEL = "Rest of datacenters";
|
|
82
|
+
const CHART_ID = "clusters-overview";
|
|
83
|
+
const CHART_TITLE = "Clusters";
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Same categorical order as the previous cluster hex palette, mapped onto
|
|
87
|
+
* PatternFly `chart_color_*` tokens (nearest token where the old hex was
|
|
88
|
+
* not itself a token).
|
|
89
|
+
*/
|
|
90
|
+
const CLUSTER_COLORS = [
|
|
91
|
+
chart_color_blue_300.value,
|
|
92
|
+
chart_color_purple_300.value,
|
|
93
|
+
chart_color_purple_100.value,
|
|
94
|
+
chart_color_teal_200.value,
|
|
95
|
+
chart_color_yellow_400.value,
|
|
96
|
+
chart_color_green_300.value,
|
|
97
|
+
chart_color_orange_100.value,
|
|
98
|
+
chart_color_red_orange_200.value,
|
|
99
|
+
chart_color_teal_400.value,
|
|
100
|
+
chart_color_black_500.value,
|
|
101
|
+
];
|
|
102
|
+
|
|
103
|
+
const EMPTY_CHART: ClusterChart = {
|
|
104
|
+
chartData: [],
|
|
105
|
+
legend: {},
|
|
106
|
+
title: "",
|
|
107
|
+
subTitle: "",
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const isViewMode = (value: string | number | undefined): value is ViewMode =>
|
|
111
|
+
value === "dataCenterDistribution" ||
|
|
112
|
+
value === "vmByCluster" ||
|
|
113
|
+
value === "cpuOverCommitment";
|
|
114
|
+
|
|
115
|
+
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
116
|
+
typeof value === "object" && value !== null && !Array.isArray(value);
|
|
117
|
+
|
|
118
|
+
const legendFor = (categories: string[]): Record<string, string> => {
|
|
119
|
+
const legend: Record<string, string> = {};
|
|
120
|
+
categories.forEach((category, index) => {
|
|
121
|
+
const color = CLUSTER_COLORS[index % CLUSTER_COLORS.length];
|
|
122
|
+
if (color) {
|
|
123
|
+
legend[category] = color;
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
return legend;
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const readCounts = (values: unknown): number[] => {
|
|
130
|
+
if (!Array.isArray(values)) {
|
|
131
|
+
return [];
|
|
132
|
+
}
|
|
133
|
+
return values.filter(
|
|
134
|
+
(value): value is number =>
|
|
135
|
+
typeof value === "number" && Number.isFinite(value) && value >= 0,
|
|
136
|
+
);
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
const readClusters = (clusters: unknown): unknown[] => {
|
|
140
|
+
if (!isRecord(clusters)) {
|
|
141
|
+
return [];
|
|
142
|
+
}
|
|
143
|
+
return Object.values(clusters);
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
const readVmTotal = (cluster: unknown): number => {
|
|
147
|
+
if (!isRecord(cluster) || !isRecord(cluster.vms)) {
|
|
148
|
+
return 0;
|
|
149
|
+
}
|
|
150
|
+
const total = cluster.vms.total;
|
|
151
|
+
return typeof total === "number" && Number.isFinite(total) && total >= 0
|
|
152
|
+
? total
|
|
153
|
+
: 0;
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Read `infra.cpuOverCommitment` without assuming either SDK's generated
|
|
158
|
+
* shape. Missing infra, a non-object infra, or an absent property yield
|
|
159
|
+
* `undefined` instead of throwing.
|
|
160
|
+
*/
|
|
161
|
+
const readCpuOverCommitment = (cluster: unknown): unknown => {
|
|
162
|
+
if (!isRecord(cluster) || !isRecord(cluster.infra)) {
|
|
163
|
+
return undefined;
|
|
164
|
+
}
|
|
165
|
+
if (!("cpuOverCommitment" in cluster.infra)) {
|
|
166
|
+
return undefined;
|
|
167
|
+
}
|
|
168
|
+
return cluster.infra.cpuOverCommitment;
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
const parseCpuOverCommitment = (raw: unknown): number => {
|
|
172
|
+
if (typeof raw === "number") {
|
|
173
|
+
return Number.isFinite(raw) ? raw : Number.NaN;
|
|
174
|
+
}
|
|
175
|
+
if (typeof raw !== "string") {
|
|
176
|
+
return Number.NaN;
|
|
177
|
+
}
|
|
178
|
+
const trimmed = raw.trim();
|
|
179
|
+
if (trimmed === "") {
|
|
180
|
+
return Number.NaN;
|
|
181
|
+
}
|
|
182
|
+
const match = trimmed.match(/^\d+\s*:\s*(\d+(?:\.\d+)?)$/);
|
|
183
|
+
if (match?.[1] != null) {
|
|
184
|
+
return Number(match[1]);
|
|
185
|
+
}
|
|
186
|
+
const asNum = Number(trimmed);
|
|
187
|
+
return Number.isFinite(asNum) ? asNum : Number.NaN;
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
const formatRatio = (ratio: number): string => {
|
|
191
|
+
const formatted = ratio % 1 === 0 ? ratio.toFixed(0) : ratio.toFixed(2);
|
|
192
|
+
return formatted.replace(/(?:\.0+|(\.\d*[1-9]))0+$/, "$1");
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
const buildRankedDonut = (
|
|
196
|
+
counts: number[],
|
|
197
|
+
options: {
|
|
198
|
+
sliceName: (rank: number) => string;
|
|
199
|
+
countSuffix: string;
|
|
200
|
+
restName: string;
|
|
201
|
+
subTitle: string;
|
|
202
|
+
},
|
|
203
|
+
): ClusterChart => {
|
|
204
|
+
const total = counts.reduce((sum, count) => sum + count, 0);
|
|
205
|
+
const ranked = counts
|
|
206
|
+
.map((count, index) => ({ count, index }))
|
|
207
|
+
.sort((a, b) => b.count - a.count);
|
|
208
|
+
const top = ranked.slice(0, TOP_CLUSTERS);
|
|
209
|
+
const restSum = ranked
|
|
210
|
+
.slice(TOP_CLUSTERS)
|
|
211
|
+
.reduce((sum, item) => sum + item.count, 0);
|
|
212
|
+
|
|
213
|
+
const slices: MigrationDonutChartDatum[] = top.map((item, index) => {
|
|
214
|
+
const name = options.sliceName(index + 1);
|
|
215
|
+
return {
|
|
216
|
+
name,
|
|
217
|
+
count: item.count,
|
|
218
|
+
countDisplay: `${item.count} ${options.countSuffix}`,
|
|
219
|
+
legendCategory: name,
|
|
220
|
+
};
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
if (restSum > 0) {
|
|
224
|
+
slices.push({
|
|
225
|
+
name: options.restName,
|
|
226
|
+
count: restSum,
|
|
227
|
+
countDisplay: `${restSum} ${options.countSuffix}`,
|
|
228
|
+
legendCategory: options.restName,
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return {
|
|
233
|
+
chartData: slices,
|
|
234
|
+
legend: legendFor(slices.map((slice) => slice.legendCategory)),
|
|
235
|
+
title: `${total}`,
|
|
236
|
+
subTitle: options.subTitle,
|
|
237
|
+
};
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
const buildVmByCluster = (clusters: unknown): ClusterChart => {
|
|
241
|
+
const counts = readClusters(clusters).map(readVmTotal);
|
|
242
|
+
if (counts.length === 0) {
|
|
243
|
+
return EMPTY_CHART;
|
|
244
|
+
}
|
|
245
|
+
return buildRankedDonut(counts, {
|
|
246
|
+
sliceName: (rank) => `Cluster ${rank}`,
|
|
247
|
+
countSuffix: "VMs",
|
|
248
|
+
restName: REST_OF_CLUSTERS_LABEL,
|
|
249
|
+
subTitle: "VMs",
|
|
250
|
+
});
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
const buildDatacenterDistribution = (
|
|
254
|
+
clustersPerDatacenter: unknown,
|
|
255
|
+
): ClusterChart => {
|
|
256
|
+
const counts = readCounts(clustersPerDatacenter);
|
|
257
|
+
if (counts.length === 0) {
|
|
258
|
+
return EMPTY_CHART;
|
|
259
|
+
}
|
|
260
|
+
return buildRankedDonut(counts, {
|
|
261
|
+
sliceName: (rank) => `Data center ${rank}`,
|
|
262
|
+
countSuffix: "clusters",
|
|
263
|
+
restName: REST_OF_DATACENTERS_LABEL,
|
|
264
|
+
subTitle: "Clusters",
|
|
265
|
+
});
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
const buildCpuOverCommitment = (clusters: unknown): ClusterChart => {
|
|
269
|
+
const ranked = readClusters(clusters)
|
|
270
|
+
.map((cluster) => parseCpuOverCommitment(readCpuOverCommitment(cluster)))
|
|
271
|
+
.filter((value) => Number.isFinite(value) && value >= 0)
|
|
272
|
+
.sort((a, b) => b - a)
|
|
273
|
+
.slice(0, TOP_CPU_RATIOS);
|
|
274
|
+
|
|
275
|
+
const slices: MigrationDonutChartDatum[] = ranked.map((value, index) => {
|
|
276
|
+
const legendCategory = `Cluster ${index + 1}`;
|
|
277
|
+
const display = formatRatio(value);
|
|
278
|
+
return {
|
|
279
|
+
name: display,
|
|
280
|
+
count: value,
|
|
281
|
+
countDisplay: display,
|
|
282
|
+
legendCategory,
|
|
283
|
+
};
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
return {
|
|
287
|
+
chartData: slices,
|
|
288
|
+
legend: legendFor(slices.map((slice) => slice.legendCategory)),
|
|
289
|
+
title: "",
|
|
290
|
+
subTitle: "",
|
|
291
|
+
};
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
export const ClustersOverview: FC<ClustersOverviewProps> = ({
|
|
295
|
+
clustersPerDatacenter,
|
|
296
|
+
clusters,
|
|
297
|
+
legendVariant = "html",
|
|
298
|
+
}) => {
|
|
299
|
+
const [viewMode, setViewMode] = useState<ViewMode>("vmByCluster");
|
|
300
|
+
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
|
|
301
|
+
|
|
302
|
+
const chart = useMemo(() => {
|
|
303
|
+
if (viewMode === "cpuOverCommitment") {
|
|
304
|
+
return buildCpuOverCommitment(clusters);
|
|
305
|
+
}
|
|
306
|
+
if (viewMode === "vmByCluster") {
|
|
307
|
+
return buildVmByCluster(clusters);
|
|
308
|
+
}
|
|
309
|
+
return buildDatacenterDistribution(clustersPerDatacenter);
|
|
310
|
+
}, [viewMode, clustersPerDatacenter, clusters]);
|
|
311
|
+
|
|
312
|
+
const chartTitle = `${CHART_TITLE} — ${VIEW_MODE_LABELS[viewMode]}`;
|
|
313
|
+
const emptyTitle =
|
|
314
|
+
viewMode === "cpuOverCommitment"
|
|
315
|
+
? REPORT_CARD_EMPTY_STATE_TITLES.cpuOvercommitment
|
|
316
|
+
: REPORT_CARD_EMPTY_STATE_TITLES.clusters;
|
|
317
|
+
|
|
318
|
+
return (
|
|
319
|
+
<ChartExportSurface
|
|
320
|
+
id={CHART_ID}
|
|
321
|
+
title={chartTitle}
|
|
322
|
+
exportViews={chartExportViewsFromLabels(CHART_TITLE, VIEW_MODE_LABELS)}
|
|
323
|
+
activeExportViewId={viewMode}
|
|
324
|
+
onExportViewChange={(viewId) => {
|
|
325
|
+
if (isViewMode(viewId)) {
|
|
326
|
+
setViewMode(viewId);
|
|
327
|
+
}
|
|
328
|
+
}}
|
|
329
|
+
>
|
|
330
|
+
<Card className={dashboardStyles.card}>
|
|
331
|
+
<CardTitle>
|
|
332
|
+
<Flex
|
|
333
|
+
className={clustersOverviewStyles.titleRow}
|
|
334
|
+
justifyContent={{ default: "justifyContentSpaceBetween" }}
|
|
335
|
+
alignItems={{ default: "alignItemsCenter" }}
|
|
336
|
+
>
|
|
337
|
+
<FlexItem>
|
|
338
|
+
<div>
|
|
339
|
+
<div>
|
|
340
|
+
<DatabaseIcon /> {CHART_TITLE}
|
|
341
|
+
</div>
|
|
342
|
+
<div className={clustersOverviewStyles.cardSubtitle}>
|
|
343
|
+
{viewMode === "dataCenterDistribution"
|
|
344
|
+
? "Top 5 datacenters"
|
|
345
|
+
: "Top 5 clusters"}
|
|
346
|
+
</div>
|
|
347
|
+
</div>
|
|
348
|
+
</FlexItem>
|
|
349
|
+
<ChartHeaderActions chartId={CHART_ID} title={chartTitle}>
|
|
350
|
+
<Dropdown
|
|
351
|
+
isOpen={isDropdownOpen}
|
|
352
|
+
onSelect={(_event, value) => {
|
|
353
|
+
if (isViewMode(value)) {
|
|
354
|
+
setViewMode(value);
|
|
355
|
+
}
|
|
356
|
+
setIsDropdownOpen(false);
|
|
357
|
+
}}
|
|
358
|
+
onOpenChange={setIsDropdownOpen}
|
|
359
|
+
toggle={(toggleRef: Ref<MenuToggleElement>) => (
|
|
360
|
+
<MenuToggle
|
|
361
|
+
ref={toggleRef}
|
|
362
|
+
onClick={() => setIsDropdownOpen((open) => !open)}
|
|
363
|
+
isExpanded={isDropdownOpen}
|
|
364
|
+
className={clustersOverviewStyles.menuToggleMinWidth}
|
|
365
|
+
>
|
|
366
|
+
{VIEW_MODE_LABELS[viewMode]}
|
|
367
|
+
</MenuToggle>
|
|
368
|
+
)}
|
|
369
|
+
>
|
|
370
|
+
<DropdownList>
|
|
371
|
+
<DropdownItem value="vmByCluster" key="vmByCluster">
|
|
372
|
+
{VIEW_MODE_LABELS.vmByCluster}
|
|
373
|
+
</DropdownItem>
|
|
374
|
+
<DropdownItem
|
|
375
|
+
value="cpuOverCommitment"
|
|
376
|
+
key="cpuOverCommitment"
|
|
377
|
+
>
|
|
378
|
+
{VIEW_MODE_LABELS.cpuOverCommitment}
|
|
379
|
+
</DropdownItem>
|
|
380
|
+
<DropdownItem
|
|
381
|
+
value="dataCenterDistribution"
|
|
382
|
+
key="dataCenterDistribution"
|
|
383
|
+
>
|
|
384
|
+
{VIEW_MODE_LABELS.dataCenterDistribution}
|
|
385
|
+
</DropdownItem>
|
|
386
|
+
</DropdownList>
|
|
387
|
+
</Dropdown>
|
|
388
|
+
</ChartHeaderActions>
|
|
389
|
+
</Flex>
|
|
390
|
+
</CardTitle>
|
|
391
|
+
<CardBody className={dashboardStyles.cardBodyScrollable}>
|
|
392
|
+
{chart.chartData.length === 0 ? (
|
|
393
|
+
<CardEmptyState title={emptyTitle} />
|
|
394
|
+
) : viewMode === "cpuOverCommitment" ? (
|
|
395
|
+
<>
|
|
396
|
+
<div className={clustersOverviewStyles.cpuOvercommitBoxes}>
|
|
397
|
+
{chart.chartData.map((item) => (
|
|
398
|
+
<div
|
|
399
|
+
key={`cpu-box-${item.legendCategory}`}
|
|
400
|
+
className={clustersOverviewStyles.cpuOvercommitBox}
|
|
401
|
+
style={{ background: chart.legend[item.legendCategory] }}
|
|
402
|
+
>
|
|
403
|
+
{item.countDisplay}
|
|
404
|
+
</div>
|
|
405
|
+
))}
|
|
406
|
+
</div>
|
|
407
|
+
<div className={clustersOverviewStyles.cpuOvercommitLegend}>
|
|
408
|
+
{chart.chartData.map((item) => (
|
|
409
|
+
<div
|
|
410
|
+
key={`cpu-legend-${item.legendCategory}`}
|
|
411
|
+
className={clustersOverviewStyles.cpuOvercommitLegendItem}
|
|
412
|
+
>
|
|
413
|
+
<span
|
|
414
|
+
className={
|
|
415
|
+
clustersOverviewStyles.cpuOvercommitLegendSwatch
|
|
416
|
+
}
|
|
417
|
+
style={{ background: chart.legend[item.legendCategory] }}
|
|
418
|
+
/>
|
|
419
|
+
<span
|
|
420
|
+
className={clustersOverviewStyles.cpuOvercommitLegendText}
|
|
421
|
+
>
|
|
422
|
+
{item.legendCategory} ({item.countDisplay})
|
|
423
|
+
</span>
|
|
424
|
+
</div>
|
|
425
|
+
))}
|
|
426
|
+
</div>
|
|
427
|
+
</>
|
|
428
|
+
) : (
|
|
429
|
+
<MigrationDonutChart
|
|
430
|
+
legendVariant={legendVariant}
|
|
431
|
+
data={chart.chartData}
|
|
432
|
+
height={300}
|
|
433
|
+
width={420}
|
|
434
|
+
donutThickness={18}
|
|
435
|
+
titleFontSize={34}
|
|
436
|
+
legend={chart.legend}
|
|
437
|
+
title={chart.title}
|
|
438
|
+
subTitle={chart.subTitle}
|
|
439
|
+
subTitleColor="var(--pf-t--global--text--color--subtle)"
|
|
440
|
+
tooltipLabelFormatter={({ datum, percent }) =>
|
|
441
|
+
`${datum.countDisplay}\n${percent.toFixed(1)}%`
|
|
442
|
+
}
|
|
443
|
+
/>
|
|
444
|
+
)}
|
|
445
|
+
</CardBody>
|
|
446
|
+
</Card>
|
|
447
|
+
</ChartExportSurface>
|
|
448
|
+
);
|
|
449
|
+
};
|
|
450
|
+
|
|
451
|
+
ClustersOverview.displayName = "ClustersOverview";
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { css } from "@emotion/css";
|
|
2
|
+
|
|
3
|
+
export const clustersOverviewStyles = {
|
|
4
|
+
titleRow: css`
|
|
5
|
+
width: 100%;
|
|
6
|
+
`,
|
|
7
|
+
|
|
8
|
+
cardSubtitle: css`
|
|
9
|
+
color: var(--pf-t--global--text--color--subtle);
|
|
10
|
+
font-size: 0.85rem;
|
|
11
|
+
`,
|
|
12
|
+
|
|
13
|
+
menuToggleMinWidth: css`
|
|
14
|
+
min-width: 250px;
|
|
15
|
+
`,
|
|
16
|
+
|
|
17
|
+
cpuOvercommitBoxes: css`
|
|
18
|
+
display: flex;
|
|
19
|
+
justify-content: center;
|
|
20
|
+
gap: 12px;
|
|
21
|
+
margin: 124px 0 16px;
|
|
22
|
+
flex-wrap: wrap;
|
|
23
|
+
|
|
24
|
+
@media (max-width: 1200px) {
|
|
25
|
+
margin: 72px 0 12px;
|
|
26
|
+
gap: 20px;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
@media (max-width: 768px) {
|
|
30
|
+
margin: 40px 0 8px;
|
|
31
|
+
gap: 16px;
|
|
32
|
+
}
|
|
33
|
+
`,
|
|
34
|
+
|
|
35
|
+
cpuOvercommitBox: css`
|
|
36
|
+
color: #000;
|
|
37
|
+
min-width: 120px;
|
|
38
|
+
height: 64px;
|
|
39
|
+
border-radius: 12px;
|
|
40
|
+
display: flex;
|
|
41
|
+
align-items: center;
|
|
42
|
+
justify-content: center;
|
|
43
|
+
font-size: 22px;
|
|
44
|
+
padding: 0 16px;
|
|
45
|
+
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
|
46
|
+
|
|
47
|
+
@media (max-width: 768px) {
|
|
48
|
+
min-width: 96px;
|
|
49
|
+
height: 56px;
|
|
50
|
+
font-size: 18px;
|
|
51
|
+
padding: 0 12px;
|
|
52
|
+
border-radius: 10px;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
@media (max-width: 480px) {
|
|
56
|
+
min-width: 88px;
|
|
57
|
+
height: 48px;
|
|
58
|
+
font-size: 16px;
|
|
59
|
+
border-radius: 8px;
|
|
60
|
+
}
|
|
61
|
+
`,
|
|
62
|
+
|
|
63
|
+
cpuOvercommitLegend: css`
|
|
64
|
+
display: flex;
|
|
65
|
+
justify-content: center;
|
|
66
|
+
gap: 6px 24px;
|
|
67
|
+
flex-wrap: wrap;
|
|
68
|
+
margin-top: 20%;
|
|
69
|
+
|
|
70
|
+
@media (max-width: 1200px) {
|
|
71
|
+
margin-top: 20%;
|
|
72
|
+
gap: 6px 20px;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
@media (max-width: 768px) {
|
|
76
|
+
margin-top: 20%;
|
|
77
|
+
gap: 6px 16px;
|
|
78
|
+
}
|
|
79
|
+
`,
|
|
80
|
+
|
|
81
|
+
cpuOvercommitLegendItem: css`
|
|
82
|
+
display: flex;
|
|
83
|
+
align-items: center;
|
|
84
|
+
gap: 12px;
|
|
85
|
+
`,
|
|
86
|
+
|
|
87
|
+
cpuOvercommitLegendSwatch: css`
|
|
88
|
+
display: inline-block;
|
|
89
|
+
width: 12px;
|
|
90
|
+
height: 12px;
|
|
91
|
+
border-radius: 2px;
|
|
92
|
+
|
|
93
|
+
@media (max-width: 768px) {
|
|
94
|
+
width: 10px;
|
|
95
|
+
height: 10px;
|
|
96
|
+
}
|
|
97
|
+
`,
|
|
98
|
+
|
|
99
|
+
cpuOvercommitLegendText: css`
|
|
100
|
+
font-size: 16px;
|
|
101
|
+
|
|
102
|
+
@media (max-width: 768px) {
|
|
103
|
+
font-size: 16px;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
@media (max-width: 480px) {
|
|
107
|
+
font-size: 14px;
|
|
108
|
+
}
|
|
109
|
+
`,
|
|
110
|
+
};
|