@openshift-migration-advisor/shared-components 0.27.0 → 0.28.0

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.
Files changed (40) hide show
  1. package/dist/report/ChartExportProvider.d.ts +3 -2
  2. package/dist/report/ChartExportProvider.d.ts.map +1 -1
  3. package/dist/report/ChartExportProvider.js +3 -3
  4. package/dist/report/ClustersOverview.d.ts +24 -0
  5. package/dist/report/ClustersOverview.d.ts.map +1 -0
  6. package/dist/report/ClustersOverview.js +228 -0
  7. package/dist/report/ClustersOverviewStyles.d.ts +12 -0
  8. package/dist/report/ClustersOverviewStyles.d.ts.map +1 -0
  9. package/dist/report/ClustersOverviewStyles.js +101 -0
  10. package/dist/report/CpuAndMemoryOverview.d.ts +19 -0
  11. package/dist/report/CpuAndMemoryOverview.d.ts.map +1 -0
  12. package/dist/report/CpuAndMemoryOverview.js +112 -0
  13. package/dist/report/captureChartElement.d.ts.map +1 -1
  14. package/dist/report/captureChartElement.js +28 -8
  15. package/dist/report/chartExport.d.ts +23 -0
  16. package/dist/report/chartExport.d.ts.map +1 -1
  17. package/dist/report/chartExport.js +20 -0
  18. package/dist/report/chartExportContext.d.ts +6 -2
  19. package/dist/report/chartExportContext.d.ts.map +1 -1
  20. package/dist/report/index.d.ts +4 -2
  21. package/dist/report/index.d.ts.map +1 -1
  22. package/dist/report/index.js +2 -0
  23. package/dist/report/pdfExport.d.ts +2 -2
  24. package/dist/report/pdfExport.d.ts.map +1 -1
  25. package/dist/report/pdfExport.js +52 -3
  26. package/package.json +2 -2
  27. package/src/report/ChartExportProvider.tsx +13 -5
  28. package/src/report/ClustersOverview.tsx +451 -0
  29. package/src/report/ClustersOverviewStyles.ts +110 -0
  30. package/src/report/CpuAndMemoryOverview.tsx +277 -0
  31. package/src/report/__tests__/ChartExportProvider.test.tsx +110 -2
  32. package/src/report/__tests__/ClustersOverview.test.tsx +241 -0
  33. package/src/report/__tests__/CpuAndMemoryOverview.test.tsx +200 -0
  34. package/src/report/__tests__/captureChartElement.test.ts +73 -0
  35. package/src/report/__tests__/chartExportPackagers.test.ts +75 -0
  36. package/src/report/captureChartElement.ts +37 -7
  37. package/src/report/chartExport.ts +40 -0
  38. package/src/report/chartExportContext.tsx +9 -1
  39. package/src/report/index.ts +12 -0
  40. package/src/report/pdfExport.ts +81 -2
@@ -1,9 +1,10 @@
1
1
  import { type FC, type ReactNode } from "react";
2
- import { type ChartCaptureSource, type ChartExportFile } from "./chartExport.js";
2
+ import { type ChartCaptureSource, type ChartExportFile, type PdfTextPage } from "./chartExport.js";
3
3
  export type CaptureChartElement = (element: HTMLElement) => Promise<HTMLCanvasElement>;
4
4
  export type ZipChartFiles = (files: ChartExportFile[]) => Promise<Blob>;
5
5
  export type EncodeChartPng = (canvas: HTMLCanvasElement) => Promise<Blob>;
6
6
  export type BuildChartDocument = (charts: ChartCaptureSource[], documentTitle: string) => Promise<Blob>;
7
+ export type BuildPdfDocument = (charts: ChartCaptureSource[], documentTitle: string, extraPages?: PdfTextPage[]) => Promise<Blob>;
7
8
  export type { ChartExportApi, ChartExportFormat, } from "./chartExportContext.js";
8
9
  export { useChartExport, useRegisterChart } from "./chartExportContext.js";
9
10
  export interface ChartExportProviderProps {
@@ -11,7 +12,7 @@ export interface ChartExportProviderProps {
11
12
  capture?: CaptureChartElement;
12
13
  zipFiles?: ZipChartFiles;
13
14
  encodePng?: EncodeChartPng;
14
- buildPdf?: BuildChartDocument;
15
+ buildPdf?: BuildPdfDocument;
15
16
  buildHtml?: BuildChartDocument;
16
17
  downloadFile?: (blob: Blob, filename: string) => void;
17
18
  getZipFilename?: () => string;
@@ -1 +1 @@
1
- {"version":3,"file":"ChartExportProvider.d.ts","sourceRoot":"","sources":["../../src/report/ChartExportProvider.tsx"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,EAAE,EACP,KAAK,SAAS,EAKf,MAAM,OAAO,CAAC;AACf,OAAO,EACL,KAAK,kBAAkB,EACvB,KAAK,eAAe,EAQrB,MAAM,kBAAkB,CAAC;AAU1B,MAAM,MAAM,mBAAmB,GAAG,CAChC,OAAO,EAAE,WAAW,KACjB,OAAO,CAAC,iBAAiB,CAAC,CAAC;AAChC,MAAM,MAAM,aAAa,GAAG,CAAC,KAAK,EAAE,eAAe,EAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;AACxE,MAAM,MAAM,cAAc,GAAG,CAAC,MAAM,EAAE,iBAAiB,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;AAC1E,MAAM,MAAM,kBAAkB,GAAG,CAC/B,MAAM,EAAE,kBAAkB,EAAE,EAC5B,aAAa,EAAE,MAAM,KAClB,OAAO,CAAC,IAAI,CAAC,CAAC;AAEnB,YAAY,EACV,cAAc,EACd,iBAAiB,GAClB,MAAM,yBAAyB,CAAC;AACjC,OAAO,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAE3E,MAAM,WAAW,wBAAwB;IACvC,QAAQ,EAAE,SAAS,CAAC;IACpB,OAAO,CAAC,EAAE,mBAAmB,CAAC;IAC9B,QAAQ,CAAC,EAAE,aAAa,CAAC;IACzB,SAAS,CAAC,EAAE,cAAc,CAAC;IAC3B,QAAQ,CAAC,EAAE,kBAAkB,CAAC;IAC9B,SAAS,CAAC,EAAE,kBAAkB,CAAC;IAC/B,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;IACtD,cAAc,CAAC,EAAE,MAAM,MAAM,CAAC;IAC9B,cAAc,CAAC,EAAE,MAAM,MAAM,CAAC;IAC9B,eAAe,CAAC,EAAE,MAAM,MAAM,CAAC;CAChC;AAsED,eAAO,MAAM,mBAAmB,EAAE,EAAE,CAAC,wBAAwB,CAgQ5D,CAAC"}
1
+ {"version":3,"file":"ChartExportProvider.d.ts","sourceRoot":"","sources":["../../src/report/ChartExportProvider.tsx"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,EAAE,EACP,KAAK,SAAS,EAKf,MAAM,OAAO,CAAC;AACf,OAAO,EACL,KAAK,kBAAkB,EACvB,KAAK,eAAe,EAGpB,KAAK,WAAW,EAMjB,MAAM,kBAAkB,CAAC;AAU1B,MAAM,MAAM,mBAAmB,GAAG,CAChC,OAAO,EAAE,WAAW,KACjB,OAAO,CAAC,iBAAiB,CAAC,CAAC;AAChC,MAAM,MAAM,aAAa,GAAG,CAAC,KAAK,EAAE,eAAe,EAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;AACxE,MAAM,MAAM,cAAc,GAAG,CAAC,MAAM,EAAE,iBAAiB,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;AAC1E,MAAM,MAAM,kBAAkB,GAAG,CAC/B,MAAM,EAAE,kBAAkB,EAAE,EAC5B,aAAa,EAAE,MAAM,KAClB,OAAO,CAAC,IAAI,CAAC,CAAC;AAEnB,MAAM,MAAM,gBAAgB,GAAG,CAC7B,MAAM,EAAE,kBAAkB,EAAE,EAC5B,aAAa,EAAE,MAAM,EACrB,UAAU,CAAC,EAAE,WAAW,EAAE,KACvB,OAAO,CAAC,IAAI,CAAC,CAAC;AAEnB,YAAY,EACV,cAAc,EACd,iBAAiB,GAClB,MAAM,yBAAyB,CAAC;AACjC,OAAO,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAE3E,MAAM,WAAW,wBAAwB;IACvC,QAAQ,EAAE,SAAS,CAAC;IACpB,OAAO,CAAC,EAAE,mBAAmB,CAAC;IAC9B,QAAQ,CAAC,EAAE,aAAa,CAAC;IACzB,SAAS,CAAC,EAAE,cAAc,CAAC;IAC3B,QAAQ,CAAC,EAAE,gBAAgB,CAAC;IAC5B,SAAS,CAAC,EAAE,kBAAkB,CAAC;IAC/B,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;IACtD,cAAc,CAAC,EAAE,MAAM,MAAM,CAAC;IAC9B,cAAc,CAAC,EAAE,MAAM,MAAM,CAAC;IAC9B,eAAe,CAAC,EAAE,MAAM,MAAM,CAAC;CAChC;AAsED,eAAO,MAAM,mBAAmB,EAAE,EAAE,CAAC,wBAAwB,CAiQ5D,CAAC"}
@@ -156,17 +156,17 @@ export const ChartExportProvider = ({ children, capture, zipFiles, encodePng = c
156
156
  runCapture,
157
157
  zipFiles,
158
158
  ]);
159
- const downloadPdf = useCallback(async (documentTitle) => {
159
+ const downloadPdf = useCallback(async (documentTitle, extraPages) => {
160
160
  await runCapture(async () => {
161
161
  const charts = listCharts();
162
- if (charts.length === 0) {
162
+ if (charts.length === 0 && (extraPages?.length ?? 0) === 0) {
163
163
  throw new Error("The report is not ready to export yet.");
164
164
  }
165
165
  const captureFn = await resolveCapture(capture);
166
166
  const prepareElement = await resolvePrepareElement(capture);
167
167
  const build = await resolvePdf(buildPdf);
168
168
  try {
169
- downloadFile(await build(toCaptureSources(charts, captureFn, prepareElement), documentTitle), getPdfFilename());
169
+ downloadFile(await build(toCaptureSources(charts, captureFn, prepareElement), documentTitle, extraPages), getPdfFilename());
170
170
  }
171
171
  finally {
172
172
  await restoreChartExportViews(charts);
@@ -0,0 +1,24 @@
1
+ import type { FC } from "react";
2
+ import { type MigrationDonutChartLegendVariant } from "../charts/MigrationDonutChart.js";
3
+ /**
4
+ * Cluster inventory fields this card reads.
5
+ * agent-sdk and planner-sdk inventories both satisfy it. `cpuOverCommitment`
6
+ * stays `unknown` because generated models type it as a number while older
7
+ * payloads send a ratio string such as `"1:4"`.
8
+ */
9
+ export interface InventoryDataLike {
10
+ infra?: {
11
+ cpuOverCommitment?: unknown;
12
+ };
13
+ vms?: {
14
+ total?: number;
15
+ };
16
+ }
17
+ export interface ClustersOverviewProps {
18
+ clustersPerDatacenter?: number[];
19
+ clusters?: Record<string, InventoryDataLike>;
20
+ /** `"html"` in agent-ui; ui-app passes `"chart"`. */
21
+ legendVariant?: MigrationDonutChartLegendVariant;
22
+ }
23
+ export declare const ClustersOverview: FC<ClustersOverviewProps>;
24
+ //# sourceMappingURL=ClustersOverview.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ClustersOverview.d.ts","sourceRoot":"","sources":["../../src/report/ClustersOverview.tsx"],"names":[],"mappings":"AAyBA,OAAO,KAAK,EAAE,EAAE,EAAO,MAAM,OAAO,CAAC;AAErC,OAAO,EAGL,KAAK,gCAAgC,EACtC,MAAM,kCAAkC,CAAC;AAS1C;;;;;GAKG;AACH,MAAM,WAAW,iBAAiB;IAChC,KAAK,CAAC,EAAE;QACN,iBAAiB,CAAC,EAAE,OAAO,CAAC;KAC7B,CAAC;IACF,GAAG,CAAC,EAAE;QACJ,KAAK,CAAC,EAAE,MAAM,CAAC;KAChB,CAAC;CACH;AAED,MAAM,WAAW,qBAAqB;IACpC,qBAAqB,CAAC,EAAE,MAAM,EAAE,CAAC;IACjC,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAAC;IAC7C,qDAAqD;IACrD,aAAa,CAAC,EAAE,gCAAgC,CAAC;CAClD;AAyOD,eAAO,MAAM,gBAAgB,EAAE,EAAE,CAAC,qBAAqB,CA2JtD,CAAC"}
@@ -0,0 +1,228 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { Card, CardBody, CardTitle, Dropdown, DropdownItem, DropdownList, Flex, FlexItem, MenuToggle, } from "@patternfly/react-core";
3
+ import { DatabaseIcon } from "@patternfly/react-icons";
4
+ import { chart_color_black_500, chart_color_blue_300, chart_color_green_300, chart_color_orange_100, chart_color_purple_100, chart_color_purple_300, chart_color_red_orange_200, chart_color_teal_200, chart_color_teal_400, chart_color_yellow_400, } from "@patternfly/react-tokens";
5
+ import { useMemo, useState } from "react";
6
+ import { MigrationDonutChart, } from "../charts/MigrationDonutChart.js";
7
+ import { CardEmptyState } from "./CardEmptyState.js";
8
+ import { ChartHeaderActions } from "./ChartDownloadButton.js";
9
+ import { ChartExportSurface } from "./ChartExportSurface.js";
10
+ import { clustersOverviewStyles } from "./ClustersOverviewStyles.js";
11
+ import { chartExportViewsFromLabels } from "./chartExport.js";
12
+ import { REPORT_CARD_EMPTY_STATE_TITLES } from "./constants.js";
13
+ import { dashboardStyles } from "./dashboardStyles.js";
14
+ const VIEW_MODE_LABELS = {
15
+ dataCenterDistribution: "Cluster distribution by data center",
16
+ vmByCluster: "VM distribution by cluster",
17
+ cpuOverCommitment: "Cluster CPU over commitment",
18
+ };
19
+ const TOP_CLUSTERS = 4;
20
+ const TOP_CPU_RATIOS = 5;
21
+ const REST_OF_CLUSTERS_LABEL = "Rest of clusters";
22
+ const REST_OF_DATACENTERS_LABEL = "Rest of datacenters";
23
+ const CHART_ID = "clusters-overview";
24
+ const CHART_TITLE = "Clusters";
25
+ /**
26
+ * Same categorical order as the previous cluster hex palette, mapped onto
27
+ * PatternFly `chart_color_*` tokens (nearest token where the old hex was
28
+ * not itself a token).
29
+ */
30
+ const CLUSTER_COLORS = [
31
+ chart_color_blue_300.value,
32
+ chart_color_purple_300.value,
33
+ chart_color_purple_100.value,
34
+ chart_color_teal_200.value,
35
+ chart_color_yellow_400.value,
36
+ chart_color_green_300.value,
37
+ chart_color_orange_100.value,
38
+ chart_color_red_orange_200.value,
39
+ chart_color_teal_400.value,
40
+ chart_color_black_500.value,
41
+ ];
42
+ const EMPTY_CHART = {
43
+ chartData: [],
44
+ legend: {},
45
+ title: "",
46
+ subTitle: "",
47
+ };
48
+ const isViewMode = (value) => value === "dataCenterDistribution" ||
49
+ value === "vmByCluster" ||
50
+ value === "cpuOverCommitment";
51
+ const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
52
+ const legendFor = (categories) => {
53
+ const legend = {};
54
+ categories.forEach((category, index) => {
55
+ const color = CLUSTER_COLORS[index % CLUSTER_COLORS.length];
56
+ if (color) {
57
+ legend[category] = color;
58
+ }
59
+ });
60
+ return legend;
61
+ };
62
+ const readCounts = (values) => {
63
+ if (!Array.isArray(values)) {
64
+ return [];
65
+ }
66
+ return values.filter((value) => typeof value === "number" && Number.isFinite(value) && value >= 0);
67
+ };
68
+ const readClusters = (clusters) => {
69
+ if (!isRecord(clusters)) {
70
+ return [];
71
+ }
72
+ return Object.values(clusters);
73
+ };
74
+ const readVmTotal = (cluster) => {
75
+ if (!isRecord(cluster) || !isRecord(cluster.vms)) {
76
+ return 0;
77
+ }
78
+ const total = cluster.vms.total;
79
+ return typeof total === "number" && Number.isFinite(total) && total >= 0
80
+ ? total
81
+ : 0;
82
+ };
83
+ /**
84
+ * Read `infra.cpuOverCommitment` without assuming either SDK's generated
85
+ * shape. Missing infra, a non-object infra, or an absent property yield
86
+ * `undefined` instead of throwing.
87
+ */
88
+ const readCpuOverCommitment = (cluster) => {
89
+ if (!isRecord(cluster) || !isRecord(cluster.infra)) {
90
+ return undefined;
91
+ }
92
+ if (!("cpuOverCommitment" in cluster.infra)) {
93
+ return undefined;
94
+ }
95
+ return cluster.infra.cpuOverCommitment;
96
+ };
97
+ const parseCpuOverCommitment = (raw) => {
98
+ if (typeof raw === "number") {
99
+ return Number.isFinite(raw) ? raw : Number.NaN;
100
+ }
101
+ if (typeof raw !== "string") {
102
+ return Number.NaN;
103
+ }
104
+ const trimmed = raw.trim();
105
+ if (trimmed === "") {
106
+ return Number.NaN;
107
+ }
108
+ const match = trimmed.match(/^\d+\s*:\s*(\d+(?:\.\d+)?)$/);
109
+ if (match?.[1] != null) {
110
+ return Number(match[1]);
111
+ }
112
+ const asNum = Number(trimmed);
113
+ return Number.isFinite(asNum) ? asNum : Number.NaN;
114
+ };
115
+ const formatRatio = (ratio) => {
116
+ const formatted = ratio % 1 === 0 ? ratio.toFixed(0) : ratio.toFixed(2);
117
+ return formatted.replace(/(?:\.0+|(\.\d*[1-9]))0+$/, "$1");
118
+ };
119
+ const buildRankedDonut = (counts, options) => {
120
+ const total = counts.reduce((sum, count) => sum + count, 0);
121
+ const ranked = counts
122
+ .map((count, index) => ({ count, index }))
123
+ .sort((a, b) => b.count - a.count);
124
+ const top = ranked.slice(0, TOP_CLUSTERS);
125
+ const restSum = ranked
126
+ .slice(TOP_CLUSTERS)
127
+ .reduce((sum, item) => sum + item.count, 0);
128
+ const slices = top.map((item, index) => {
129
+ const name = options.sliceName(index + 1);
130
+ return {
131
+ name,
132
+ count: item.count,
133
+ countDisplay: `${item.count} ${options.countSuffix}`,
134
+ legendCategory: name,
135
+ };
136
+ });
137
+ if (restSum > 0) {
138
+ slices.push({
139
+ name: options.restName,
140
+ count: restSum,
141
+ countDisplay: `${restSum} ${options.countSuffix}`,
142
+ legendCategory: options.restName,
143
+ });
144
+ }
145
+ return {
146
+ chartData: slices,
147
+ legend: legendFor(slices.map((slice) => slice.legendCategory)),
148
+ title: `${total}`,
149
+ subTitle: options.subTitle,
150
+ };
151
+ };
152
+ const buildVmByCluster = (clusters) => {
153
+ const counts = readClusters(clusters).map(readVmTotal);
154
+ if (counts.length === 0) {
155
+ return EMPTY_CHART;
156
+ }
157
+ return buildRankedDonut(counts, {
158
+ sliceName: (rank) => `Cluster ${rank}`,
159
+ countSuffix: "VMs",
160
+ restName: REST_OF_CLUSTERS_LABEL,
161
+ subTitle: "VMs",
162
+ });
163
+ };
164
+ const buildDatacenterDistribution = (clustersPerDatacenter) => {
165
+ const counts = readCounts(clustersPerDatacenter);
166
+ if (counts.length === 0) {
167
+ return EMPTY_CHART;
168
+ }
169
+ return buildRankedDonut(counts, {
170
+ sliceName: (rank) => `Data center ${rank}`,
171
+ countSuffix: "clusters",
172
+ restName: REST_OF_DATACENTERS_LABEL,
173
+ subTitle: "Clusters",
174
+ });
175
+ };
176
+ const buildCpuOverCommitment = (clusters) => {
177
+ const ranked = readClusters(clusters)
178
+ .map((cluster) => parseCpuOverCommitment(readCpuOverCommitment(cluster)))
179
+ .filter((value) => Number.isFinite(value) && value >= 0)
180
+ .sort((a, b) => b - a)
181
+ .slice(0, TOP_CPU_RATIOS);
182
+ const slices = ranked.map((value, index) => {
183
+ const legendCategory = `Cluster ${index + 1}`;
184
+ const display = formatRatio(value);
185
+ return {
186
+ name: display,
187
+ count: value,
188
+ countDisplay: display,
189
+ legendCategory,
190
+ };
191
+ });
192
+ return {
193
+ chartData: slices,
194
+ legend: legendFor(slices.map((slice) => slice.legendCategory)),
195
+ title: "",
196
+ subTitle: "",
197
+ };
198
+ };
199
+ export const ClustersOverview = ({ clustersPerDatacenter, clusters, legendVariant = "html", }) => {
200
+ const [viewMode, setViewMode] = useState("vmByCluster");
201
+ const [isDropdownOpen, setIsDropdownOpen] = useState(false);
202
+ const chart = useMemo(() => {
203
+ if (viewMode === "cpuOverCommitment") {
204
+ return buildCpuOverCommitment(clusters);
205
+ }
206
+ if (viewMode === "vmByCluster") {
207
+ return buildVmByCluster(clusters);
208
+ }
209
+ return buildDatacenterDistribution(clustersPerDatacenter);
210
+ }, [viewMode, clustersPerDatacenter, clusters]);
211
+ const chartTitle = `${CHART_TITLE} — ${VIEW_MODE_LABELS[viewMode]}`;
212
+ const emptyTitle = viewMode === "cpuOverCommitment"
213
+ ? REPORT_CARD_EMPTY_STATE_TITLES.cpuOvercommitment
214
+ : REPORT_CARD_EMPTY_STATE_TITLES.clusters;
215
+ return (_jsx(ChartExportSurface, { id: CHART_ID, title: chartTitle, exportViews: chartExportViewsFromLabels(CHART_TITLE, VIEW_MODE_LABELS), activeExportViewId: viewMode, onExportViewChange: (viewId) => {
216
+ if (isViewMode(viewId)) {
217
+ setViewMode(viewId);
218
+ }
219
+ }, children: _jsxs(Card, { className: dashboardStyles.card, children: [_jsx(CardTitle, { children: _jsxs(Flex, { className: clustersOverviewStyles.titleRow, justifyContent: { default: "justifyContentSpaceBetween" }, alignItems: { default: "alignItemsCenter" }, children: [_jsx(FlexItem, { children: _jsxs("div", { children: [_jsxs("div", { children: [_jsx(DatabaseIcon, {}), " ", CHART_TITLE] }), _jsx("div", { className: clustersOverviewStyles.cardSubtitle, children: viewMode === "dataCenterDistribution"
220
+ ? "Top 5 datacenters"
221
+ : "Top 5 clusters" })] }) }), _jsx(ChartHeaderActions, { chartId: CHART_ID, title: chartTitle, children: _jsx(Dropdown, { isOpen: isDropdownOpen, onSelect: (_event, value) => {
222
+ if (isViewMode(value)) {
223
+ setViewMode(value);
224
+ }
225
+ setIsDropdownOpen(false);
226
+ }, onOpenChange: setIsDropdownOpen, toggle: (toggleRef) => (_jsx(MenuToggle, { ref: toggleRef, onClick: () => setIsDropdownOpen((open) => !open), isExpanded: isDropdownOpen, className: clustersOverviewStyles.menuToggleMinWidth, children: VIEW_MODE_LABELS[viewMode] })), children: _jsxs(DropdownList, { children: [_jsx(DropdownItem, { value: "vmByCluster", children: VIEW_MODE_LABELS.vmByCluster }, "vmByCluster"), _jsx(DropdownItem, { value: "cpuOverCommitment", children: VIEW_MODE_LABELS.cpuOverCommitment }, "cpuOverCommitment"), _jsx(DropdownItem, { value: "dataCenterDistribution", children: VIEW_MODE_LABELS.dataCenterDistribution }, "dataCenterDistribution")] }) }) })] }) }), _jsx(CardBody, { className: dashboardStyles.cardBodyScrollable, children: chart.chartData.length === 0 ? (_jsx(CardEmptyState, { title: emptyTitle })) : viewMode === "cpuOverCommitment" ? (_jsxs(_Fragment, { children: [_jsx("div", { className: clustersOverviewStyles.cpuOvercommitBoxes, children: chart.chartData.map((item) => (_jsx("div", { className: clustersOverviewStyles.cpuOvercommitBox, style: { background: chart.legend[item.legendCategory] }, children: item.countDisplay }, `cpu-box-${item.legendCategory}`))) }), _jsx("div", { className: clustersOverviewStyles.cpuOvercommitLegend, children: chart.chartData.map((item) => (_jsxs("div", { className: clustersOverviewStyles.cpuOvercommitLegendItem, children: [_jsx("span", { className: clustersOverviewStyles.cpuOvercommitLegendSwatch, style: { background: chart.legend[item.legendCategory] } }), _jsxs("span", { className: clustersOverviewStyles.cpuOvercommitLegendText, children: [item.legendCategory, " (", item.countDisplay, ")"] })] }, `cpu-legend-${item.legendCategory}`))) })] })) : (_jsx(MigrationDonutChart, { legendVariant: legendVariant, data: chart.chartData, height: 300, width: 420, donutThickness: 18, titleFontSize: 34, legend: chart.legend, title: chart.title, subTitle: chart.subTitle, subTitleColor: "var(--pf-t--global--text--color--subtle)", tooltipLabelFormatter: ({ datum, percent }) => `${datum.countDisplay}\n${percent.toFixed(1)}%` })) })] }) }));
227
+ };
228
+ ClustersOverview.displayName = "ClustersOverview";
@@ -0,0 +1,12 @@
1
+ export declare const clustersOverviewStyles: {
2
+ titleRow: string;
3
+ cardSubtitle: string;
4
+ menuToggleMinWidth: string;
5
+ cpuOvercommitBoxes: string;
6
+ cpuOvercommitBox: string;
7
+ cpuOvercommitLegend: string;
8
+ cpuOvercommitLegendItem: string;
9
+ cpuOvercommitLegendSwatch: string;
10
+ cpuOvercommitLegendText: string;
11
+ };
12
+ //# sourceMappingURL=ClustersOverviewStyles.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ClustersOverviewStyles.d.ts","sourceRoot":"","sources":["../../src/report/ClustersOverviewStyles.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,sBAAsB;;;;;;;;;;CA2GlC,CAAC"}
@@ -0,0 +1,101 @@
1
+ import { css } from "@emotion/css";
2
+ export const clustersOverviewStyles = {
3
+ titleRow: css `
4
+ width: 100%;
5
+ `,
6
+ cardSubtitle: css `
7
+ color: var(--pf-t--global--text--color--subtle);
8
+ font-size: 0.85rem;
9
+ `,
10
+ menuToggleMinWidth: css `
11
+ min-width: 250px;
12
+ `,
13
+ cpuOvercommitBoxes: css `
14
+ display: flex;
15
+ justify-content: center;
16
+ gap: 12px;
17
+ margin: 124px 0 16px;
18
+ flex-wrap: wrap;
19
+
20
+ @media (max-width: 1200px) {
21
+ margin: 72px 0 12px;
22
+ gap: 20px;
23
+ }
24
+
25
+ @media (max-width: 768px) {
26
+ margin: 40px 0 8px;
27
+ gap: 16px;
28
+ }
29
+ `,
30
+ cpuOvercommitBox: css `
31
+ color: #000;
32
+ min-width: 120px;
33
+ height: 64px;
34
+ border-radius: 12px;
35
+ display: flex;
36
+ align-items: center;
37
+ justify-content: center;
38
+ font-size: 22px;
39
+ padding: 0 16px;
40
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
41
+
42
+ @media (max-width: 768px) {
43
+ min-width: 96px;
44
+ height: 56px;
45
+ font-size: 18px;
46
+ padding: 0 12px;
47
+ border-radius: 10px;
48
+ }
49
+
50
+ @media (max-width: 480px) {
51
+ min-width: 88px;
52
+ height: 48px;
53
+ font-size: 16px;
54
+ border-radius: 8px;
55
+ }
56
+ `,
57
+ cpuOvercommitLegend: css `
58
+ display: flex;
59
+ justify-content: center;
60
+ gap: 6px 24px;
61
+ flex-wrap: wrap;
62
+ margin-top: 20%;
63
+
64
+ @media (max-width: 1200px) {
65
+ margin-top: 20%;
66
+ gap: 6px 20px;
67
+ }
68
+
69
+ @media (max-width: 768px) {
70
+ margin-top: 20%;
71
+ gap: 6px 16px;
72
+ }
73
+ `,
74
+ cpuOvercommitLegendItem: css `
75
+ display: flex;
76
+ align-items: center;
77
+ gap: 12px;
78
+ `,
79
+ cpuOvercommitLegendSwatch: css `
80
+ display: inline-block;
81
+ width: 12px;
82
+ height: 12px;
83
+ border-radius: 2px;
84
+
85
+ @media (max-width: 768px) {
86
+ width: 10px;
87
+ height: 10px;
88
+ }
89
+ `,
90
+ cpuOvercommitLegendText: css `
91
+ font-size: 16px;
92
+
93
+ @media (max-width: 768px) {
94
+ font-size: 16px;
95
+ }
96
+
97
+ @media (max-width: 480px) {
98
+ font-size: 14px;
99
+ }
100
+ `,
101
+ };
@@ -0,0 +1,19 @@
1
+ import type { FC } from "react";
2
+ import { type MigrationDonutChartLegendVariant } from "../charts/MigrationDonutChart.js";
3
+ export interface CpuAndMemoryOverviewProps {
4
+ cpuTierDistribution?: Record<string, number>;
5
+ memoryTierDistribution?: Record<string, number>;
6
+ memoryTotalGB?: number;
7
+ cpuTotalCores?: number;
8
+ /**
9
+ * Memory-tier slice click. The argument is the distribution key.
10
+ * Agent-ui maps it to a VM memory filter; ui-app omits it.
11
+ */
12
+ onTierSelect?: (tierLabel: string) => void;
13
+ /** Center-title click. Agent-ui opens the VM list with no extra filters. */
14
+ onTitleClick?: () => void;
15
+ /** `"html"` in agent-ui; ui-app passes `"chart"`. */
16
+ legendVariant?: MigrationDonutChartLegendVariant;
17
+ }
18
+ export declare const CpuAndMemoryOverview: FC<CpuAndMemoryOverviewProps>;
19
+ //# sourceMappingURL=CpuAndMemoryOverview.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CpuAndMemoryOverview.d.ts","sourceRoot":"","sources":["../../src/report/CpuAndMemoryOverview.tsx"],"names":[],"mappings":"AAcA,OAAO,KAAK,EAAE,EAAE,EAAO,MAAM,OAAO,CAAC;AAErC,OAAO,EAGL,KAAK,gCAAgC,EACtC,MAAM,kCAAkC,CAAC;AAQ1C,MAAM,WAAW,yBAAyB;IACxC,mBAAmB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC7C,sBAAsB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChD,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;;OAGG;IACH,YAAY,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAC;IAC3C,4EAA4E;IAC5E,YAAY,CAAC,EAAE,MAAM,IAAI,CAAC;IAC1B,qDAAqD;IACrD,aAAa,CAAC,EAAE,gCAAgC,CAAC;CAClD;AAuFD,eAAO,MAAM,oBAAoB,EAAE,EAAE,CAAC,yBAAyB,CAiJ9D,CAAC"}
@@ -0,0 +1,112 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { css } from "@emotion/css";
3
+ import { Card, CardBody, CardTitle, Dropdown, DropdownItem, DropdownList, Flex, FlexItem, MenuToggle, } from "@patternfly/react-core";
4
+ import { DataProcessorIcon } from "@patternfly/react-icons";
5
+ import { useMemo, useState } from "react";
6
+ import { MigrationDonutChart, } from "../charts/MigrationDonutChart.js";
7
+ import { CardEmptyState } from "./CardEmptyState.js";
8
+ import { ChartHeaderActions } from "./ChartDownloadButton.js";
9
+ import { ChartExportSurface } from "./ChartExportSurface.js";
10
+ import { chartExportViewsFromLabels } from "./chartExport.js";
11
+ import { REPORT_CARD_EMPTY_STATE_TITLES } from "./constants.js";
12
+ import { dashboardStyles } from "./dashboardStyles.js";
13
+ const VIEW_MODE_LABELS = {
14
+ memoryTiers: "VM distribution by memory size tier",
15
+ vcpuTiers: "VM distribution by vCPU count tier",
16
+ };
17
+ const VIEW_SUBTITLES = {
18
+ memoryTiers: "Memory size tiers",
19
+ vcpuTiers: "vCPU count tiers",
20
+ };
21
+ /**
22
+ * Categorical order shared by the agent-ui and ui-app CPU/memory donuts.
23
+ */
24
+ const TIER_COLORS = [
25
+ "#0066cc",
26
+ "#5e40be",
27
+ "#b6a6e9",
28
+ "#73c5c5",
29
+ "#b98412",
30
+ "#28a745",
31
+ "#f0ad4e",
32
+ "#d9534f",
33
+ "#009596",
34
+ "#6a6e73",
35
+ ];
36
+ const CHART_ID = "cpu-memory-overview";
37
+ const CHART_TITLE = "CPU & memory";
38
+ const cardSubtitle = css `
39
+ color: var(--pf-t--global--text--color--subtle);
40
+ font-size: 0.85rem;
41
+ `;
42
+ const menuToggleMinWidth = css `
43
+ min-width: 290px;
44
+ `;
45
+ const titleRow = css `
46
+ width: 100%;
47
+ `;
48
+ const isViewMode = (value) => value === "memoryTiers" || value === "vcpuTiers";
49
+ const tierSortKey = (label) => {
50
+ const value = Number.parseInt(label.trim(), 10);
51
+ return Number.isFinite(value) ? value : Number.MAX_SAFE_INTEGER;
52
+ };
53
+ const hasUnit = (label, unit) => unit === "GB" ? /gb$/i.test(label) : /cores?$/i.test(label);
54
+ const slicesFromDistribution = (distribution, unit) => Object.entries(distribution ?? {})
55
+ .filter(([, count]) => Number(count) > 0)
56
+ .sort(([left], [right]) => tierSortKey(left) - tierSortKey(right))
57
+ .map(([tier, count]) => {
58
+ const trimmed = tier.trim();
59
+ const name = hasUnit(trimmed, unit) ? trimmed : `${trimmed} ${unit}`;
60
+ const vmCount = Number(count);
61
+ return {
62
+ name,
63
+ count: vmCount,
64
+ countDisplay: `${vmCount} VMs`,
65
+ legendCategory: tier,
66
+ };
67
+ });
68
+ const legendFor = (categories) => {
69
+ const legend = {};
70
+ categories.forEach((category, index) => {
71
+ const color = TIER_COLORS[index % TIER_COLORS.length];
72
+ if (color) {
73
+ legend[category] = color;
74
+ }
75
+ });
76
+ return legend;
77
+ };
78
+ export const CpuAndMemoryOverview = ({ cpuTierDistribution, memoryTierDistribution, memoryTotalGB, cpuTotalCores, onTierSelect, onTitleClick, legendVariant = "html", }) => {
79
+ const [viewMode, setViewMode] = useState("memoryTiers");
80
+ const [isDropdownOpen, setIsDropdownOpen] = useState(false);
81
+ const memorySlices = useMemo(() => slicesFromDistribution(memoryTierDistribution, "GB"), [memoryTierDistribution]);
82
+ const vcpuSlices = useMemo(() => slicesFromDistribution(cpuTierDistribution, "cores"), [cpuTierDistribution]);
83
+ const activeSlices = viewMode === "memoryTiers" ? memorySlices : vcpuSlices;
84
+ const legend = useMemo(() => legendFor(activeSlices.map((slice) => slice.legendCategory)), [activeSlices]);
85
+ const totalVMs = useMemo(() => activeSlices.reduce((sum, slice) => sum + slice.count, 0), [activeSlices]);
86
+ const chartTitle = `${CHART_TITLE} — ${VIEW_MODE_LABELS[viewMode]}`;
87
+ const subTitle = viewMode === "memoryTiers"
88
+ ? typeof memoryTotalGB === "number"
89
+ ? `${memoryTotalGB.toLocaleString()} GB`
90
+ : undefined
91
+ : typeof cpuTotalCores === "number"
92
+ ? `${cpuTotalCores.toLocaleString()} Cores`
93
+ : undefined;
94
+ const emptyTitle = viewMode === "memoryTiers"
95
+ ? REPORT_CARD_EMPTY_STATE_TITLES.memory
96
+ : REPORT_CARD_EMPTY_STATE_TITLES.cpu;
97
+ return (_jsx(ChartExportSurface, { id: CHART_ID, title: chartTitle, exportViews: chartExportViewsFromLabels(CHART_TITLE, VIEW_MODE_LABELS), activeExportViewId: viewMode, onExportViewChange: (viewId) => {
98
+ if (isViewMode(viewId)) {
99
+ setViewMode(viewId);
100
+ }
101
+ }, children: _jsxs(Card, { className: dashboardStyles.card, children: [_jsx(CardTitle, { children: _jsxs(Flex, { className: titleRow, justifyContent: { default: "justifyContentSpaceBetween" }, alignItems: { default: "alignItemsCenter" }, children: [_jsx(FlexItem, { children: _jsxs("div", { children: [_jsxs("div", { children: [_jsx(DataProcessorIcon, {}), " ", CHART_TITLE] }), _jsx("div", { className: cardSubtitle, children: VIEW_SUBTITLES[viewMode] })] }) }), _jsx(ChartHeaderActions, { chartId: CHART_ID, title: chartTitle, children: _jsx(Dropdown, { isOpen: isDropdownOpen, onSelect: (_event, value) => {
102
+ if (isViewMode(value)) {
103
+ setViewMode(value);
104
+ }
105
+ setIsDropdownOpen(false);
106
+ }, onOpenChange: setIsDropdownOpen, toggle: (toggleRef) => (_jsx(MenuToggle, { ref: toggleRef, onClick: () => setIsDropdownOpen((open) => !open), isExpanded: isDropdownOpen, className: menuToggleMinWidth, children: VIEW_MODE_LABELS[viewMode] })), children: _jsxs(DropdownList, { children: [_jsx(DropdownItem, { value: "memoryTiers", children: VIEW_MODE_LABELS.memoryTiers }, "memoryTiers"), _jsx(DropdownItem, { value: "vcpuTiers", children: VIEW_MODE_LABELS.vcpuTiers }, "vcpuTiers")] }) }) })] }) }), _jsx(CardBody, { className: dashboardStyles.cardBodyScrollable, children: activeSlices.length === 0 ? (_jsx(CardEmptyState, { title: emptyTitle })) : (_jsx(MigrationDonutChart, { legendVariant: legendVariant, data: activeSlices, legend: legend, height: 300, width: 420, donutThickness: 18, titleFontSize: 34, title: `${totalVMs} VMs`, subTitle: subTitle, subTitleColor: "var(--pf-t--global--text--color--subtle)", legendLabelFormatter: ({ x, countDisplay }) => `${x} (${countDisplay})`, tooltipLabelFormatter: ({ datum, percent }) => `${datum.countDisplay}\n${percent.toFixed(1)}%`, itemsPerRow: legendVariant === "chart"
107
+ ? Math.ceil(activeSlices.length / 2)
108
+ : undefined, labelFontSize: legendVariant === "chart" ? 18 : undefined, marginLeft: legendVariant === "chart" ? "52%" : undefined, onItemClick: viewMode === "memoryTiers" && onTierSelect
109
+ ? (item) => onTierSelect(item.legendCategory)
110
+ : undefined, onTitleClick: onTitleClick })) })] }) }));
111
+ };
112
+ CpuAndMemoryOverview.displayName = "CpuAndMemoryOverview";
@@ -1 +1 @@
1
- {"version":3,"file":"captureChartElement.d.ts","sourceRoot":"","sources":["../../src/report/captureChartElement.ts"],"names":[],"mappings":"AAgCA;;;;GAIG;AACH,wBAAgB,0BAA0B,CAAC,OAAO,EAAE,WAAW,GAAG,MAAM,IAAI,CAqD3E;AAiPD,wBAAsB,mBAAmB,CACvC,OAAO,EAAE,WAAW,GACnB,OAAO,CAAC,iBAAiB,CAAC,CAkC5B"}
1
+ {"version":3,"file":"captureChartElement.d.ts","sourceRoot":"","sources":["../../src/report/captureChartElement.ts"],"names":[],"mappings":"AAsCA;;;;GAIG;AACH,wBAAgB,0BAA0B,CAAC,OAAO,EAAE,WAAW,GAAG,MAAM,IAAI,CAqD3E;AAuQD,wBAAsB,mBAAmB,CACvC,OAAO,EAAE,WAAW,GACnB,OAAO,CAAC,iBAAiB,CAAC,CAoC5B"}
@@ -1,5 +1,6 @@
1
+ import { t_color_gray_20, t_color_white, t_global_text_color_regular, } from "@patternfly/react-tokens";
1
2
  import html2canvas from "html2canvas-pro";
2
- import { CHART_EXPORT_CAPTURING_ATTR, CHART_EXPORT_SCROLL_ATTR, shouldIgnoreChartExportElement, } from "./chartExport.js";
3
+ import { applyLightThemeToDocument, CHART_EXPORT_CAPTURING_ATTR, CHART_EXPORT_SCROLL_ATTR, shouldIgnoreChartExportElement, } from "./chartExport.js";
3
4
  /** CSS-pixel scale; 2x made zip-all of ~15 live cards too slow. */
4
5
  const CHART_CAPTURE_SCALE = 1;
5
6
  const HIDDEN_LAYOUT_STYLE_KEYS = [
@@ -71,6 +72,10 @@ export function revealHiddenChartAncestors(element) {
71
72
  const PADDED_CAPTURE_SELECTOR = ".pf-v6-c-card__title, .pf-v6-c-card__header, .pf-v6-c-card__body";
72
73
  const CARD_CAPTURE_SELECTOR = ".pf-v6-c-card";
73
74
  const TRANSPARENT_FILLS = new Set(["", "transparent", "rgba(0, 0, 0, 0)"]);
75
+ /** PatternFly light-theme chrome. html2canvas inlines live computed SVG styles. */
76
+ const LIGHT_EXPORT_BACKGROUND = t_color_white.value;
77
+ const LIGHT_EXPORT_BORDER = t_color_gray_20.value;
78
+ const LIGHT_EXPORT_TEXT = t_global_text_color_regular.value;
74
79
  function queryCards(root) {
75
80
  const cards = Array.from(root.querySelectorAll(CARD_CAPTURE_SELECTOR));
76
81
  if (root.matches(CARD_CAPTURE_SELECTOR)) {
@@ -112,16 +117,29 @@ function readCardBorder(source) {
112
117
  }
113
118
  return { width, style, color };
114
119
  }
115
- function applyCardChrome(source, dest) {
120
+ function applyCardChrome(source, dest, colors) {
116
121
  const computed = window.getComputedStyle(source);
117
122
  dest.style.borderRadius = computed.borderRadius;
118
- dest.style.backgroundColor = resolvedFill(computed.backgroundColor);
119
123
  dest.style.overflow = "hidden";
124
+ dest.style.backgroundColor =
125
+ colors === "light"
126
+ ? LIGHT_EXPORT_BACKGROUND
127
+ : resolvedFill(computed.backgroundColor);
120
128
  const border = readCardBorder(source);
121
129
  if (border) {
122
130
  dest.style.borderWidth = border.width;
123
131
  dest.style.borderStyle = border.style;
124
- dest.style.borderColor = border.color;
132
+ dest.style.borderColor =
133
+ colors === "light" ? LIGHT_EXPORT_BORDER : border.color;
134
+ }
135
+ }
136
+ function applyLightThemeSvgText(root) {
137
+ for (const node of root.querySelectorAll("text, tspan")) {
138
+ if (!(node instanceof SVGElement)) {
139
+ continue;
140
+ }
141
+ node.style.fill = LIGHT_EXPORT_TEXT;
142
+ node.style.color = LIGHT_EXPORT_TEXT;
125
143
  }
126
144
  }
127
145
  function hideCardBeforePseudo() {
@@ -162,7 +180,7 @@ function copyCardChrome(node) {
162
180
  height: node.style.height,
163
181
  maxHeight: node.style.maxHeight,
164
182
  };
165
- applyCardChrome(node, node);
183
+ applyCardChrome(node, node, "computed");
166
184
  node.style.minHeight = "0px";
167
185
  node.style.height = "auto";
168
186
  node.style.maxHeight = "none";
@@ -174,7 +192,7 @@ function applyComputedStylesToClone(sourceRoot, clonedRoot) {
174
192
  sourceCards.forEach((source, index) => {
175
193
  const dest = cloneCards[index];
176
194
  if (dest) {
177
- applyCardChrome(source, dest);
195
+ applyCardChrome(source, dest, "light");
178
196
  }
179
197
  });
180
198
  const sources = sourceRoot.querySelectorAll(PADDED_CAPTURE_SELECTOR);
@@ -264,14 +282,16 @@ export async function captureChartElement(element) {
264
282
  }
265
283
  return html2canvas(element, {
266
284
  useCORS: true,
267
- backgroundColor: null,
285
+ backgroundColor: LIGHT_EXPORT_BACKGROUND,
268
286
  logging: false,
269
287
  scale: CHART_CAPTURE_SCALE,
270
288
  imageTimeout: 0,
271
289
  ignoreElements: (node) => node instanceof Element &&
272
290
  shouldIgnoreChartExportElement(node, element),
273
- onclone: (_clonedDoc, clonedElement) => {
291
+ onclone: (clonedDoc, clonedElement) => {
292
+ applyLightThemeToDocument(clonedDoc);
274
293
  applyComputedStylesToClone(element, clonedElement);
294
+ applyLightThemeSvgText(clonedElement);
275
295
  },
276
296
  });
277
297
  }