@openshift-migration-advisor/shared-components 0.25.0 → 0.27.0-3f8ae10b086b

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 (138) hide show
  1. package/README.md +25 -0
  2. package/dist/report/ChartDownloadButton.d.ts +11 -0
  3. package/dist/report/ChartDownloadButton.d.ts.map +1 -0
  4. package/dist/report/ChartDownloadButton.js +24 -0
  5. package/dist/report/ChartExportProvider.d.ts +23 -0
  6. package/dist/report/ChartExportProvider.d.ts.map +1 -0
  7. package/dist/report/ChartExportProvider.js +222 -0
  8. package/dist/report/ChartExportSurface.d.ts +12 -0
  9. package/dist/report/ChartExportSurface.d.ts.map +1 -0
  10. package/dist/report/ChartExportSurface.js +32 -0
  11. package/dist/report/ClustersOverview.d.ts +24 -0
  12. package/dist/report/ClustersOverview.d.ts.map +1 -0
  13. package/dist/report/ClustersOverview.js +228 -0
  14. package/dist/report/ClustersOverviewStyles.d.ts +12 -0
  15. package/dist/report/ClustersOverviewStyles.d.ts.map +1 -0
  16. package/dist/report/ClustersOverviewStyles.js +101 -0
  17. package/dist/report/CpuAndMemoryOverview.d.ts +19 -0
  18. package/dist/report/CpuAndMemoryOverview.d.ts.map +1 -0
  19. package/dist/report/CpuAndMemoryOverview.js +112 -0
  20. package/dist/report/ErrorTable.d.ts +18 -0
  21. package/dist/report/ErrorTable.d.ts.map +1 -0
  22. package/dist/report/ErrorTable.js +20 -0
  23. package/dist/report/HostPowerStates.d.ts +0 -1
  24. package/dist/report/HostPowerStates.d.ts.map +1 -1
  25. package/dist/report/HostPowerStates.js +2 -2
  26. package/dist/report/HostsOverview.d.ts +13 -0
  27. package/dist/report/HostsOverview.d.ts.map +1 -0
  28. package/dist/report/HostsOverview.js +84 -0
  29. package/dist/report/InfrastructureSummary.d.ts.map +1 -1
  30. package/dist/report/InfrastructureSummary.js +6 -2
  31. package/dist/report/NetworkOverview.d.ts +31 -0
  32. package/dist/report/NetworkOverview.d.ts.map +1 -0
  33. package/dist/report/NetworkOverview.js +201 -0
  34. package/dist/report/OSDistribution.d.ts +0 -2
  35. package/dist/report/OSDistribution.d.ts.map +1 -1
  36. package/dist/report/OSDistribution.js +8 -5
  37. package/dist/report/OsUpgradeRecommendationPopover.d.ts +0 -1
  38. package/dist/report/OsUpgradeRecommendationPopover.d.ts.map +1 -1
  39. package/dist/report/OsUpgradeRecommendationPopover.js +4 -3
  40. package/dist/report/PowerStateCard.d.ts +0 -1
  41. package/dist/report/PowerStateCard.d.ts.map +1 -1
  42. package/dist/report/PowerStateCard.js +4 -2
  43. package/dist/report/ReportExportMenu.d.ts +19 -0
  44. package/dist/report/ReportExportMenu.d.ts.map +1 -0
  45. package/dist/report/ReportExportMenu.js +31 -0
  46. package/dist/report/ReportTable.d.ts +14 -0
  47. package/dist/report/ReportTable.d.ts.map +1 -0
  48. package/dist/report/ReportTable.js +23 -0
  49. package/dist/report/SupportTierBadge.d.ts +0 -1
  50. package/dist/report/SupportTierBadge.d.ts.map +1 -1
  51. package/dist/report/SupportTierBadge.js +1 -21
  52. package/dist/report/VCenterClusterDetails.d.ts +0 -1
  53. package/dist/report/VCenterClusterDetails.d.ts.map +1 -1
  54. package/dist/report/VCenterClusterDetails.js +9 -4
  55. package/dist/report/VmPowerStates.d.ts +0 -1
  56. package/dist/report/VmPowerStates.d.ts.map +1 -1
  57. package/dist/report/VmPowerStates.js +2 -2
  58. package/dist/report/WarningsTable.d.ts +9 -0
  59. package/dist/report/WarningsTable.d.ts.map +1 -0
  60. package/dist/report/WarningsTable.js +20 -0
  61. package/dist/report/captureChartElement.d.ts +8 -0
  62. package/dist/report/captureChartElement.d.ts.map +1 -0
  63. package/dist/report/captureChartElement.js +304 -0
  64. package/dist/report/chartExport.d.ts +80 -0
  65. package/dist/report/chartExport.d.ts.map +1 -0
  66. package/dist/report/chartExport.js +182 -0
  67. package/dist/report/chartExportContext.d.ts +32 -0
  68. package/dist/report/chartExportContext.d.ts.map +1 -0
  69. package/dist/report/chartExportContext.js +36 -0
  70. package/dist/report/chartExportFilenames.d.ts +4 -0
  71. package/dist/report/chartExportFilenames.d.ts.map +1 -0
  72. package/dist/report/chartExportFilenames.js +16 -0
  73. package/dist/report/constants.d.ts +2 -0
  74. package/dist/report/constants.d.ts.map +1 -1
  75. package/dist/report/constants.js +2 -0
  76. package/dist/report/dashboardStyles.d.ts +0 -4
  77. package/dist/report/dashboardStyles.d.ts.map +1 -1
  78. package/dist/report/dashboardStyles.js +0 -19
  79. package/dist/report/htmlExport.d.ts +9 -0
  80. package/dist/report/htmlExport.d.ts.map +1 -0
  81. package/dist/report/htmlExport.js +65 -0
  82. package/dist/report/index.d.ts +20 -0
  83. package/dist/report/index.d.ts.map +1 -1
  84. package/dist/report/index.js +20 -0
  85. package/dist/report/pdfExport.d.ts +3 -0
  86. package/dist/report/pdfExport.d.ts.map +1 -0
  87. package/dist/report/pdfExport.js +131 -0
  88. package/dist/report/pdfPage.d.ts +15 -0
  89. package/dist/report/pdfPage.d.ts.map +1 -0
  90. package/dist/report/pdfPage.js +44 -0
  91. package/dist/report/reportExportOptions.d.ts +17 -0
  92. package/dist/report/reportExportOptions.d.ts.map +1 -0
  93. package/dist/report/reportExportOptions.js +31 -0
  94. package/dist/report/zipChartPngs.d.ts +3 -0
  95. package/dist/report/zipChartPngs.d.ts.map +1 -0
  96. package/dist/report/zipChartPngs.js +8 -0
  97. package/package.json +7 -2
  98. package/src/report/ChartDownloadButton.tsx +59 -0
  99. package/src/report/ChartExportProvider.tsx +392 -0
  100. package/src/report/ChartExportSurface.tsx +60 -0
  101. package/src/report/ClustersOverview.tsx +451 -0
  102. package/src/report/ClustersOverviewStyles.ts +110 -0
  103. package/src/report/CpuAndMemoryOverview.tsx +277 -0
  104. package/src/report/ErrorTable.tsx +85 -0
  105. package/src/report/HostPowerStates.tsx +0 -3
  106. package/src/report/HostsOverview.tsx +185 -0
  107. package/src/report/InfrastructureSummary.tsx +48 -31
  108. package/src/report/NetworkOverview.tsx +405 -0
  109. package/src/report/OSDistribution.tsx +46 -42
  110. package/src/report/OsUpgradeRecommendationPopover.tsx +3 -4
  111. package/src/report/PowerStateCard.tsx +49 -36
  112. package/src/report/ReportExportMenu.tsx +119 -0
  113. package/src/report/ReportTable.tsx +81 -0
  114. package/src/report/SupportTierBadge.tsx +12 -40
  115. package/src/report/VCenterClusterDetails.tsx +38 -16
  116. package/src/report/VmPowerStates.tsx +0 -3
  117. package/src/report/WarningsTable.tsx +75 -0
  118. package/src/report/__tests__/ChartExportProvider.test.tsx +582 -0
  119. package/src/report/__tests__/ClustersOverview.test.tsx +241 -0
  120. package/src/report/__tests__/CpuAndMemoryOverview.test.tsx +200 -0
  121. package/src/report/__tests__/HostsOverview.test.tsx +145 -0
  122. package/src/report/__tests__/InfrastructureSummary.test.tsx +0 -22
  123. package/src/report/__tests__/NetworkOverview.test.tsx +269 -0
  124. package/src/report/__tests__/OSDistribution.test.tsx +10 -11
  125. package/src/report/__tests__/captureChartElement.test.ts +205 -0
  126. package/src/report/__tests__/chartExportPackagers.test.ts +299 -0
  127. package/src/report/captureChartElement.ts +398 -0
  128. package/src/report/chartExport.ts +277 -0
  129. package/src/report/chartExportContext.tsx +92 -0
  130. package/src/report/chartExportFilenames.ts +21 -0
  131. package/src/report/constants.ts +2 -0
  132. package/src/report/dashboardStyles.ts +0 -23
  133. package/src/report/htmlExport.ts +83 -0
  134. package/src/report/index.ts +107 -0
  135. package/src/report/pdfExport.ts +233 -0
  136. package/src/report/pdfPage.ts +83 -0
  137. package/src/report/reportExportOptions.ts +53 -0
  138. package/src/report/zipChartPngs.ts +10 -0
package/README.md CHANGED
@@ -7,6 +7,7 @@ Includes:
7
7
  - PatternFly form field wrappers (`react-hook-form`)
8
8
  - Guest OS support-tier helpers and badges
9
9
  - Report chart primitives (`MigrationDonutChart`)
10
+ - Chart export (`ChartExportProvider`, `ChartExportSurface`, PDF / PNG / HTML)
10
11
  - Operating Systems distribution card
11
12
  - Infrastructure summary, vCenter cluster details, and host/VM power-state cards
12
13
 
@@ -25,17 +26,41 @@ npm install @openshift-migration-advisor/shared-components
25
26
  ```tsx
26
27
  import {
27
28
  buildInfrastructureSummary,
29
+ ChartExportProvider,
28
30
  HostPowerStates,
29
31
  InfrastructureSummary,
30
32
  MigrationDonutChart,
31
33
  OSDistribution,
34
+ ReportExportMenu,
32
35
  SupportTierBadge,
33
36
  TextInputFormGroup,
37
+ useChartExport,
34
38
  VCenterClusterDetails,
35
39
  VmPowerStates,
36
40
  } from "@openshift-migration-advisor/shared-components";
41
+
42
+ <ChartExportProvider>
43
+ <Dashboard />
44
+ </ChartExportProvider>
45
+ ```
46
+
47
+ `ChartExportProvider` lazy-loads capture, ZIP, PDF, and HTML. Wrap the chart visual (not the Card chrome) in `ChartExportSurface`. All formats snapshot those same nodes, encode one chart at a time, and drop the canvas. There is no second print tree and no `isExportMode` render:
48
+
49
+ ```tsx
50
+ import { ChartExportSurface } from "@openshift-migration-advisor/shared-components";
51
+
52
+ <Card>
53
+ <CardTitle>CPU and memory</CardTitle>
54
+ <CardBody>
55
+ <ChartExportSurface id="cpu-memory" title="CPU and memory">
56
+ <MigrationDonutChart {...chartProps} />
57
+ </ChartExportSurface>
58
+ </CardBody>
59
+ </Card>
37
60
  ```
38
61
 
62
+ `ReportExportMenu` accepts a fully custom `options` list. `standardReportExportOptions()` builds the PDF / HTML / PNG entries; omit a handler to hide that format.
63
+
39
64
  Components are SDK-agnostic: pass already-shaped props (for example `OSDistributionEntry` maps). Map `agent-sdk` / `planner-sdk` models at the app boundary.
40
65
 
41
66
  ## Development
@@ -0,0 +1,11 @@
1
+ import type { FC, ReactNode } from "react";
2
+ export declare const ChartDownloadButton: FC<{
3
+ chartId: string;
4
+ title?: string;
5
+ }>;
6
+ export declare const ChartHeaderActions: FC<{
7
+ chartId: string;
8
+ title?: string;
9
+ children?: ReactNode;
10
+ }>;
11
+ //# sourceMappingURL=ChartDownloadButton.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ChartDownloadButton.d.ts","sourceRoot":"","sources":["../../src/report/ChartDownloadButton.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,EAAE,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAI3C,eAAO,MAAM,mBAAmB,EAAE,EAAE,CAAC;IACnC,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAoBA,CAAC;AAIF,eAAO,MAAM,kBAAkB,EAAE,EAAE,CAAC;IAClC,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,SAAS,CAAC;CACtB,CAmBA,CAAC"}
@@ -0,0 +1,24 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Button, Flex, FlexItem } from "@patternfly/react-core";
3
+ import { DownloadIcon } from "@patternfly/react-icons";
4
+ import { chartExportHideProps } from "./chartExport.js";
5
+ import { useChartExport } from "./chartExportContext.js";
6
+ export const ChartDownloadButton = ({ chartId, title }) => {
7
+ const exportApi = useChartExport();
8
+ if (!exportApi) {
9
+ return null;
10
+ }
11
+ const label = title ? `Download ${title} as PNG` : "Download chart as PNG";
12
+ return (_jsx(Button, { variant: "plain", icon: _jsx(DownloadIcon, {}), "aria-label": label, isLoading: exportApi.downloadingChartId === chartId, isDisabled: exportApi.isBusy, onClick: () => {
13
+ void exportApi.downloadChart(chartId);
14
+ } }));
15
+ };
16
+ ChartDownloadButton.displayName = "ChartDownloadButton";
17
+ export const ChartHeaderActions = ({ chartId, title, children }) => {
18
+ const exportApi = useChartExport();
19
+ if (!exportApi && !children) {
20
+ return null;
21
+ }
22
+ return (_jsx(FlexItem, { ...chartExportHideProps, children: _jsxs(Flex, { alignItems: { default: "alignItemsCenter" }, spaceItems: { default: "spaceItemsSm" }, children: [children, exportApi ? (_jsx(ChartDownloadButton, { chartId: chartId, title: title })) : null] }) }));
23
+ };
24
+ ChartHeaderActions.displayName = "ChartHeaderActions";
@@ -0,0 +1,23 @@
1
+ import { type FC, type ReactNode } from "react";
2
+ import { type ChartCaptureSource, type ChartExportFile, type PdfTextPage } from "./chartExport.js";
3
+ export type CaptureChartElement = (element: HTMLElement) => Promise<HTMLCanvasElement>;
4
+ export type ZipChartFiles = (files: ChartExportFile[]) => Promise<Blob>;
5
+ export type EncodeChartPng = (canvas: HTMLCanvasElement) => Promise<Blob>;
6
+ export type BuildChartDocument = (charts: ChartCaptureSource[], documentTitle: string) => Promise<Blob>;
7
+ export type BuildPdfDocument = (charts: ChartCaptureSource[], documentTitle: string, extraPages?: PdfTextPage[]) => Promise<Blob>;
8
+ export type { ChartExportApi, ChartExportFormat, } from "./chartExportContext.js";
9
+ export { useChartExport, useRegisterChart } from "./chartExportContext.js";
10
+ export interface ChartExportProviderProps {
11
+ children: ReactNode;
12
+ capture?: CaptureChartElement;
13
+ zipFiles?: ZipChartFiles;
14
+ encodePng?: EncodeChartPng;
15
+ buildPdf?: BuildPdfDocument;
16
+ buildHtml?: BuildChartDocument;
17
+ downloadFile?: (blob: Blob, filename: string) => void;
18
+ getZipFilename?: () => string;
19
+ getPdfFilename?: () => string;
20
+ getHtmlFilename?: () => string;
21
+ }
22
+ export declare const ChartExportProvider: FC<ChartExportProviderProps>;
23
+ //# sourceMappingURL=ChartExportProvider.d.ts.map
@@ -0,0 +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,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"}
@@ -0,0 +1,222 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useCallback, useMemo, useRef, useState, } from "react";
3
+ import { canvasToBlob, downloadBlob, releaseCanvas, restoreChartExportViews, sortRegisteredChartsByDocumentOrder, toBulkChartCaptureSources, } from "./chartExport.js";
4
+ import { ChartExportApiContext, ChartExportRegistryContext, } from "./chartExportContext.js";
5
+ import { getChartExportFilename } from "./chartExportFilenames.js";
6
+ export { useChartExport, useRegisterChart } from "./chartExportContext.js";
7
+ const EXPORT_LOADING_LABELS = {
8
+ pdf: "Generating PDF...",
9
+ png: "Generating PNG...",
10
+ html: "Generating HTML...",
11
+ };
12
+ const defaultZipFilename = () => getChartExportFilename("zip");
13
+ const defaultPdfFilename = () => getChartExportFilename("pdf");
14
+ const defaultHtmlFilename = () => getChartExportFilename("html");
15
+ async function resolveCapture(override) {
16
+ if (override) {
17
+ return override;
18
+ }
19
+ const { captureChartElement } = await import("./captureChartElement.js");
20
+ return captureChartElement;
21
+ }
22
+ async function resolvePrepareElement(overrideCapture) {
23
+ if (overrideCapture) {
24
+ return undefined;
25
+ }
26
+ const { revealHiddenChartAncestors } = await import("./captureChartElement.js");
27
+ return revealHiddenChartAncestors;
28
+ }
29
+ async function resolveZip(override) {
30
+ if (override) {
31
+ return override;
32
+ }
33
+ const { zipChartPngs } = await import("./zipChartPngs.js");
34
+ return zipChartPngs;
35
+ }
36
+ async function resolvePdf(override) {
37
+ if (override) {
38
+ return override;
39
+ }
40
+ const { buildPdfFromCharts } = await import("./pdfExport.js");
41
+ return buildPdfFromCharts;
42
+ }
43
+ async function resolveHtml(override) {
44
+ if (override) {
45
+ return override;
46
+ }
47
+ const { buildHtmlFromCharts } = await import("./htmlExport.js");
48
+ return buildHtmlFromCharts;
49
+ }
50
+ function toCaptureSources(charts, capture, prepareElement) {
51
+ return toBulkChartCaptureSources(charts, capture, prepareElement);
52
+ }
53
+ export const ChartExportProvider = ({ children, capture, zipFiles, encodePng = canvasToBlob, buildPdf, buildHtml, downloadFile = downloadBlob, getZipFilename = defaultZipFilename, getPdfFilename = defaultPdfFilename, getHtmlFilename = defaultHtmlFilename, }) => {
54
+ const chartsRef = useRef(new Map());
55
+ const isBusyRef = useRef(false);
56
+ const [downloadingChartId, setDownloadingChartId] = useState(null);
57
+ const [exportingFormat, setExportingFormat] = useState(null);
58
+ const [exportError, setExportError] = useState(null);
59
+ const registry = useMemo(() => ({
60
+ register(chart) {
61
+ const existing = chartsRef.current.get(chart.id);
62
+ if (existing &&
63
+ existing.element !== chart.element &&
64
+ existing.element.isConnected) {
65
+ console.warn(`Duplicate chart export id "${chart.id}". The previous chart will not be exported.`);
66
+ }
67
+ chartsRef.current.set(chart.id, chart);
68
+ return () => {
69
+ const current = chartsRef.current.get(chart.id);
70
+ if (current?.element === chart.element) {
71
+ chartsRef.current.delete(chart.id);
72
+ }
73
+ };
74
+ },
75
+ }), []);
76
+ const listCharts = useCallback(() => {
77
+ return sortRegisteredChartsByDocumentOrder(Array.from(chartsRef.current.values()).filter((chart) => chart.element.isConnected));
78
+ }, []);
79
+ const runCapture = useCallback(async (work, chartId, format) => {
80
+ if (isBusyRef.current) {
81
+ return;
82
+ }
83
+ isBusyRef.current = true;
84
+ setExportError(null);
85
+ if (chartId) {
86
+ setDownloadingChartId(chartId);
87
+ }
88
+ else {
89
+ setExportingFormat(format);
90
+ }
91
+ try {
92
+ await work();
93
+ }
94
+ catch (error) {
95
+ console.error(`Error exporting ${format}:`, error);
96
+ setExportError(`Failed to export ${format.toUpperCase()}. Please try again.`);
97
+ }
98
+ finally {
99
+ isBusyRef.current = false;
100
+ setDownloadingChartId(null);
101
+ setExportingFormat(null);
102
+ }
103
+ }, []);
104
+ const downloadChart = useCallback(async (id) => {
105
+ await runCapture(async () => {
106
+ const chart = chartsRef.current.get(id);
107
+ if (!chart?.element.isConnected) {
108
+ throw new Error("The chart is not ready to export yet.");
109
+ }
110
+ const captureFn = await resolveCapture(capture);
111
+ const canvas = await captureFn(chart.element);
112
+ try {
113
+ downloadFile(await encodePng(canvas), chart.filename);
114
+ }
115
+ finally {
116
+ releaseCanvas(canvas);
117
+ }
118
+ }, id, "png");
119
+ }, [capture, downloadFile, encodePng, runCapture]);
120
+ const downloadAll = useCallback(async () => {
121
+ await runCapture(async () => {
122
+ const charts = listCharts();
123
+ if (charts.length === 0) {
124
+ throw new Error("The report is not ready to export yet.");
125
+ }
126
+ const captureFn = await resolveCapture(capture);
127
+ const prepareElement = await resolvePrepareElement(capture);
128
+ const zipFn = await resolveZip(zipFiles);
129
+ const files = [];
130
+ const sources = toBulkChartCaptureSources(charts, captureFn, prepareElement);
131
+ try {
132
+ for (const [index, source] of sources.entries()) {
133
+ const canvas = await source.capture();
134
+ try {
135
+ files.push({
136
+ filename: `${String(index + 1).padStart(2, "0")}-${source.filename}`,
137
+ blob: await encodePng(canvas),
138
+ });
139
+ }
140
+ finally {
141
+ releaseCanvas(canvas);
142
+ }
143
+ }
144
+ }
145
+ finally {
146
+ await restoreChartExportViews(charts);
147
+ }
148
+ downloadFile(await zipFn(files), getZipFilename());
149
+ }, null, "png");
150
+ }, [
151
+ capture,
152
+ downloadFile,
153
+ encodePng,
154
+ getZipFilename,
155
+ listCharts,
156
+ runCapture,
157
+ zipFiles,
158
+ ]);
159
+ const downloadPdf = useCallback(async (documentTitle, extraPages) => {
160
+ await runCapture(async () => {
161
+ const charts = listCharts();
162
+ if (charts.length === 0 && (extraPages?.length ?? 0) === 0) {
163
+ throw new Error("The report is not ready to export yet.");
164
+ }
165
+ const captureFn = await resolveCapture(capture);
166
+ const prepareElement = await resolvePrepareElement(capture);
167
+ const build = await resolvePdf(buildPdf);
168
+ try {
169
+ downloadFile(await build(toCaptureSources(charts, captureFn, prepareElement), documentTitle, extraPages), getPdfFilename());
170
+ }
171
+ finally {
172
+ await restoreChartExportViews(charts);
173
+ }
174
+ }, null, "pdf");
175
+ }, [buildPdf, capture, downloadFile, getPdfFilename, listCharts, runCapture]);
176
+ const downloadHtml = useCallback(async (documentTitle) => {
177
+ await runCapture(async () => {
178
+ const charts = listCharts();
179
+ if (charts.length === 0) {
180
+ throw new Error("The report is not ready to export yet.");
181
+ }
182
+ const captureFn = await resolveCapture(capture);
183
+ const prepareElement = await resolvePrepareElement(capture);
184
+ const build = await resolveHtml(buildHtml);
185
+ try {
186
+ downloadFile(await build(toCaptureSources(charts, captureFn, prepareElement), documentTitle), getHtmlFilename());
187
+ }
188
+ finally {
189
+ await restoreChartExportViews(charts);
190
+ }
191
+ }, null, "html");
192
+ }, [buildHtml, capture, downloadFile, getHtmlFilename, listCharts, runCapture]);
193
+ const clearExportError = useCallback(() => {
194
+ setExportError(null);
195
+ }, []);
196
+ const api = useMemo(() => ({
197
+ downloadChart,
198
+ downloadAll,
199
+ downloadPdf,
200
+ downloadHtml,
201
+ downloadingChartId,
202
+ exportingFormat,
203
+ exportLoadingLabel: exportingFormat
204
+ ? EXPORT_LOADING_LABELS[exportingFormat]
205
+ : null,
206
+ isExportingAll: exportingFormat !== null,
207
+ isBusy: downloadingChartId !== null || exportingFormat !== null,
208
+ exportError,
209
+ clearExportError,
210
+ }), [
211
+ clearExportError,
212
+ downloadAll,
213
+ downloadChart,
214
+ downloadHtml,
215
+ downloadPdf,
216
+ downloadingChartId,
217
+ exportError,
218
+ exportingFormat,
219
+ ]);
220
+ return (_jsx(ChartExportRegistryContext.Provider, { value: registry, children: _jsx(ChartExportApiContext.Provider, { value: api, children: children }) }));
221
+ };
222
+ ChartExportProvider.displayName = "ChartExportProvider";
@@ -0,0 +1,12 @@
1
+ import { type FC, type ReactNode } from "react";
2
+ import { type ChartExportView } from "./chartExport.js";
3
+ export declare const ChartExportSurface: FC<{
4
+ id: string;
5
+ title: string;
6
+ filename?: string;
7
+ exportViews?: ChartExportView[];
8
+ activeExportViewId?: string;
9
+ onExportViewChange?: (viewId: string) => void;
10
+ children: ReactNode;
11
+ }>;
12
+ //# sourceMappingURL=ChartExportSurface.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ChartExportSurface.d.ts","sourceRoot":"","sources":["../../src/report/ChartExportSurface.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,EAAE,KAAK,SAAS,EAAuB,MAAM,OAAO,CAAC;AACrE,OAAO,EACL,KAAK,eAAe,EAGrB,MAAM,kBAAkB,CAAC;AAG1B,eAAO,MAAM,kBAAkB,EAAE,EAAE,CAAC;IAClC,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,eAAe,EAAE,CAAC;IAChC,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,kBAAkB,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IAC9C,QAAQ,EAAE,SAAS,CAAC;CACrB,CAyCA,CAAC"}
@@ -0,0 +1,32 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useCallback, useRef } from "react";
3
+ import { chartExportRootStyle, waitForChartExportPaint, } from "./chartExport.js";
4
+ import { useRegisterChart } from "./chartExportContext.js";
5
+ export const ChartExportSurface = ({ id, title, filename, exportViews, activeExportViewId, onExportViewChange, children, }) => {
6
+ const activeViewRef = useRef(activeExportViewId);
7
+ activeViewRef.current = activeExportViewId;
8
+ const onExportViewChangeRef = useRef(onExportViewChange);
9
+ onExportViewChangeRef.current = onExportViewChange;
10
+ const setExportView = useCallback(async (viewId) => {
11
+ const change = onExportViewChangeRef.current;
12
+ if (change && activeViewRef.current !== viewId) {
13
+ change(viewId);
14
+ await new Promise((resolve) => {
15
+ window.setTimeout(resolve, 0);
16
+ });
17
+ }
18
+ await waitForChartExportPaint();
19
+ }, []);
20
+ const ref = useRegisterChart({
21
+ id,
22
+ title,
23
+ filename,
24
+ exportViews,
25
+ activeExportViewId,
26
+ setExportView: exportViews && exportViews.length > 1 && onExportViewChange
27
+ ? setExportView
28
+ : undefined,
29
+ });
30
+ return (_jsx("div", { ref: ref, id: id, style: chartExportRootStyle, children: children }));
31
+ };
32
+ ChartExportSurface.displayName = "ChartExportSurface";
@@ -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"}