@openshift-migration-advisor/shared-components 0.27.0 → 0.28.0-16fc442157c1
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/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 +4 -2
- package/dist/report/index.d.ts.map +1 -1
- package/dist/report/index.js +2 -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/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/__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__/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 +12 -0
- package/src/report/pdfExport.ts +81 -2
|
@@ -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
|
+
};
|