@openshift-migration-advisor/shared-components 0.28.0 → 0.29.0-24760762468f
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 +15 -4
- package/dist/report/ChartExportProvider.d.ts +1 -1
- package/dist/report/ChartExportProvider.d.ts.map +1 -1
- package/dist/report/ChartExportProvider.js +4 -1
- package/dist/report/FeatureStatusBadge.d.ts.map +1 -1
- package/dist/report/FeatureStatusBadge.js +2 -2
- package/dist/report/OsUpgradeRecommendationPopover.d.ts.map +1 -1
- package/dist/report/OsUpgradeRecommendationPopover.js +1 -7
- package/dist/report/ReportExportMenu.d.ts +1 -0
- package/dist/report/ReportExportMenu.d.ts.map +1 -1
- package/dist/report/ReportExportMenu.js +38 -4
- package/dist/report/StorageOverview.d.ts +27 -0
- package/dist/report/StorageOverview.d.ts.map +1 -0
- package/dist/report/StorageOverview.js +331 -0
- package/dist/report/VMMigrationStatus.d.ts +33 -0
- package/dist/report/VMMigrationStatus.d.ts.map +1 -0
- package/dist/report/VMMigrationStatus.js +129 -0
- package/dist/report/captureChartElement.d.ts +6 -1
- package/dist/report/captureChartElement.d.ts.map +1 -1
- package/dist/report/captureChartElement.js +13 -4
- package/dist/report/chartExport.js +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 +3 -1
- package/dist/report/index.d.ts.map +1 -1
- package/dist/report/index.js +2 -0
- package/dist/report/reportExportOptions.d.ts +13 -2
- package/dist/report/reportExportOptions.d.ts.map +1 -1
- package/dist/report/reportExportOptions.js +13 -5
- package/package.json +2 -2
- package/src/charts/MigrationDonutChart.tsx +27 -6
- package/src/charts/__tests__/MigrationDonutChart.test.tsx +53 -0
- package/src/report/ChartExportProvider.tsx +5 -1
- package/src/report/FeatureStatusBadge.tsx +10 -2
- package/src/report/OsUpgradeRecommendationPopover.tsx +0 -8
- package/src/report/ReportExportMenu.tsx +69 -13
- package/src/report/StorageOverview.tsx +646 -0
- package/src/report/VMMigrationStatus.tsx +358 -0
- package/src/report/__tests__/StorageOverview.test.tsx +267 -0
- package/src/report/__tests__/VMMigrationStatus.test.tsx +178 -0
- package/src/report/__tests__/captureChartElement.test.ts +47 -0
- package/src/report/captureChartElement.ts +18 -3
- package/src/report/chartExport.ts +1 -1
- package/src/report/dashboardStyles.ts +7 -4
- package/src/report/index.ts +12 -0
- package/src/report/reportExportOptions.ts +28 -5
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MigrationDonutChart.d.ts","sourceRoot":"","sources":["../../src/charts/MigrationDonutChart.tsx"],"names":[],"mappings":"AAcA,OAAO,KAAK,EAAE,EAAE,EAAgB,MAAM,OAAO,CAAC;AAG9C,MAAM,WAAW,wBAAwB;IACvC,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,cAAc,EAAE,MAAM,CAAC;IACvB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,SAAS,CAAC,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,GAAG,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAC1C,WAAW,CAAC,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,GAAG,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAC5C,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;CACzB;AAED,MAAM,MAAM,gCAAgC,GAAG,MAAM,GAAG,OAAO,CAAC;AAEhE,MAAM,WAAW,wBAAwB;IACvC,IAAI,EAAE,wBAAwB,EAAE,CAAC;IACjC,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACtC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,qBAAqB,CAAC,EAAE,CAAC,IAAI,EAAE;QAC7B,KAAK,EAAE;YACL,CAAC,EAAE,MAAM,CAAC;YACV,CAAC,EAAE,MAAM,CAAC;YACV,YAAY,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;YAC/B,cAAc,EAAE,MAAM,CAAC;SACxB,CAAC;QACF,OAAO,EAAE,MAAM,CAAC;QAChB,KAAK,EAAE,MAAM,CAAC;KACf,KAAK,MAAM,CAAC;IACb,iDAAiD;IACjD,WAAW,CAAC,EAAE,CAAC,IAAI,EAAE,wBAAwB,KAAK,IAAI,CAAC;IACvD,gDAAgD;IAChD,YAAY,CAAC,EAAE,MAAM,IAAI,CAAC;IAC1B,mFAAmF;IACnF,oBAAoB,CAAC,EAAE,CAAC,IAAI,EAAE;QAC5B,CAAC,EAAE,MAAM,CAAC;QACV,YAAY,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;KAChC,KAAK,MAAM,CAAC;IACb;;;;OAIG;IACH,aAAa,CAAC,EAAE,gCAAgC,CAAC;IACjD,8CAA8C;IAC9C,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,2CAA2C;IAC3C,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,2CAA2C;IAC3C,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,0EAA0E;IAC1E,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC5B;
|
|
1
|
+
{"version":3,"file":"MigrationDonutChart.d.ts","sourceRoot":"","sources":["../../src/charts/MigrationDonutChart.tsx"],"names":[],"mappings":"AAcA,OAAO,KAAK,EAAE,EAAE,EAAgB,MAAM,OAAO,CAAC;AAG9C,MAAM,WAAW,wBAAwB;IACvC,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,cAAc,EAAE,MAAM,CAAC;IACvB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,SAAS,CAAC,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,GAAG,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAC1C,WAAW,CAAC,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,GAAG,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAC5C,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;CACzB;AAED,MAAM,MAAM,gCAAgC,GAAG,MAAM,GAAG,OAAO,CAAC;AAEhE,MAAM,WAAW,wBAAwB;IACvC,IAAI,EAAE,wBAAwB,EAAE,CAAC;IACjC,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACtC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,qBAAqB,CAAC,EAAE,CAAC,IAAI,EAAE;QAC7B,KAAK,EAAE;YACL,CAAC,EAAE,MAAM,CAAC;YACV,CAAC,EAAE,MAAM,CAAC;YACV,YAAY,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;YAC/B,cAAc,EAAE,MAAM,CAAC;SACxB,CAAC;QACF,OAAO,EAAE,MAAM,CAAC;QAChB,KAAK,EAAE,MAAM,CAAC;KACf,KAAK,MAAM,CAAC;IACb,iDAAiD;IACjD,WAAW,CAAC,EAAE,CAAC,IAAI,EAAE,wBAAwB,KAAK,IAAI,CAAC;IACvD,gDAAgD;IAChD,YAAY,CAAC,EAAE,MAAM,IAAI,CAAC;IAC1B,mFAAmF;IACnF,oBAAoB,CAAC,EAAE,CAAC,IAAI,EAAE;QAC5B,CAAC,EAAE,MAAM,CAAC;QACV,YAAY,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;KAChC,KAAK,MAAM,CAAC;IACb;;;;OAIG;IACH,aAAa,CAAC,EAAE,gCAAgC,CAAC;IACjD,8CAA8C;IAC9C,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,2CAA2C;IAC3C,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,2CAA2C;IAC3C,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,0EAA0E;IAC1E,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC5B;AAoFD,eAAO,MAAM,mBAAmB,EAAE,EAAE,CAAC,wBAAwB,CAgY5D,CAAC;AAEF,eAAe,mBAAmB,CAAC"}
|
|
@@ -35,6 +35,8 @@ const themedChartTooltipFlyoutPadding = {
|
|
|
35
35
|
left: 10,
|
|
36
36
|
right: 10,
|
|
37
37
|
};
|
|
38
|
+
/** Victory SVG text defaults to a serif face; match PatternFly / html2canvas export. */
|
|
39
|
+
const chartTextFontFamily = "RedHatText, Overpass, Helvetica, Arial, sans-serif";
|
|
38
40
|
const styles = {
|
|
39
41
|
legendIcon: css `
|
|
40
42
|
margin-right: 4px;
|
|
@@ -63,6 +65,7 @@ const styles = {
|
|
|
63
65
|
background: none;
|
|
64
66
|
padding: var(--pf-t--global--spacer--xs) var(--pf-t--global--spacer--ml);
|
|
65
67
|
margin: 0;
|
|
68
|
+
font-family: ${chartTextFontFamily};
|
|
66
69
|
transition: opacity var(--pf-t--global--motion--duration--short);
|
|
67
70
|
white-space: nowrap;
|
|
68
71
|
|
|
@@ -73,6 +76,7 @@ const styles = {
|
|
|
73
76
|
legendLabel: css `
|
|
74
77
|
gap: var(--pf-t--global--spacer--lg);
|
|
75
78
|
padding: var(--pf-t--global--spacer--xs) var(--pf-t--global--spacer--ml);
|
|
79
|
+
font-family: ${chartTextFontFamily};
|
|
76
80
|
white-space: nowrap;
|
|
77
81
|
`,
|
|
78
82
|
legendInner: css `
|
|
@@ -107,9 +111,13 @@ export const MigrationDonutChart = ({ data, legend, customColors, height = 260,
|
|
|
107
111
|
countDisplay: item.countDisplay ?? item.count,
|
|
108
112
|
}));
|
|
109
113
|
}, [data]);
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
114
|
+
// Victory renders zero-value slices as degenerate zero-angle wedges but still
|
|
115
|
+
// keeps a `padAngle` gap around each one.
|
|
116
|
+
const donutChartData = useMemo(() => chartData.filter((item) => Number(item.y) > 0), [chartData]);
|
|
117
|
+
const donutColorScale = useMemo(() => donutChartData.map((item) => getColor(item.legendCategory)), [donutChartData, getColor]);
|
|
118
|
+
// A lone slice has no neighbour to pad against; forcing padAngle to 0 keeps it
|
|
119
|
+
// from leaving a gap against itself.
|
|
120
|
+
const effectivePadAngle = donutChartData.length <= 1 ? 0 : padAngle;
|
|
113
121
|
const legendData = useMemo(() => {
|
|
114
122
|
return chartData.map((item) => ({
|
|
115
123
|
name: `${item.x} (${item.countDisplay})`,
|
|
@@ -214,9 +222,10 @@ export const MigrationDonutChart = ({ data, legend, customColors, height = 260,
|
|
|
214
222
|
labels: {
|
|
215
223
|
fontSize: labelFontSize,
|
|
216
224
|
fill: "var(--pf-t--global--text--color--regular)",
|
|
225
|
+
fontFamily: chartTextFontFamily,
|
|
217
226
|
},
|
|
218
227
|
} }) }) }) }));
|
|
219
|
-
return (_jsxs(Flex, { direction: { default: "column" }, alignItems: { default: "alignItemsCenter" }, className: `${onItemClick ? styles.cursorPointer : styles.cursorDefault} ${styles.chartContainer}`, children: [_jsxs("div", { className: styles.donutWrapper, children: [_jsx(ChartDonut, { ariaDesc: "Migration data donut chart", data:
|
|
228
|
+
return (_jsxs(Flex, { direction: { default: "column" }, alignItems: { default: "alignItemsCenter" }, className: `${onItemClick ? styles.cursorPointer : styles.cursorDefault} ${styles.chartContainer}`, children: [_jsxs("div", { className: styles.donutWrapper, children: [_jsx(ChartDonut, { ariaDesc: "Migration data donut chart", data: donutChartData, events: chartEvents, labelComponent: donutTooltip, labels: ({ datum, }) => formatLabel(datum), colorScale: donutColorScale, constrainToVisibleArea: true, innerRadius: innerRadius, padAngle: effectivePadAngle, title: title, subTitle: subTitle, height: height, width: width, padding: {
|
|
220
229
|
bottom: 5,
|
|
221
230
|
left: 20,
|
|
222
231
|
right: 20,
|
|
@@ -226,11 +235,13 @@ export const MigrationDonutChart = ({ data, legend, customColors, height = 260,
|
|
|
226
235
|
fill: titleColor,
|
|
227
236
|
fontSize: titleFontSize,
|
|
228
237
|
fontWeight: "bold",
|
|
238
|
+
fontFamily: chartTextFontFamily,
|
|
229
239
|
},
|
|
230
240
|
] })) : undefined, subTitleComponent: subTitle ? (_jsx(ChartLabel, { dy: showChartLegend ? 7 : undefined, style: [
|
|
231
241
|
{
|
|
232
242
|
fill: subTitleColor,
|
|
233
243
|
fontSize: subTitleFontSize,
|
|
244
|
+
fontFamily: chartTextFontFamily,
|
|
234
245
|
},
|
|
235
246
|
] })) : undefined }), onTitleClick && title && (_jsx("div", { onClick: onTitleClick, className: css `
|
|
236
247
|
position: absolute;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type FC, type ReactNode } from "react";
|
|
2
2
|
import { type ChartCaptureSource, type ChartExportFile, type PdfTextPage } from "./chartExport.js";
|
|
3
|
-
export type CaptureChartElement = (element: HTMLElement) => Promise<HTMLCanvasElement>;
|
|
3
|
+
export type CaptureChartElement = (element: HTMLElement, options?: import("./captureChartElement.js").CaptureChartElementOptions) => 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>;
|
|
@@ -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;
|
|
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;AAEf,OAAO,EACL,KAAK,kBAAkB,EACvB,KAAK,eAAe,EAGpB,KAAK,WAAW,EAMjB,MAAM,kBAAkB,CAAC;AAU1B,MAAM,MAAM,mBAAmB,GAAG,CAChC,OAAO,EAAE,WAAW,EACpB,OAAO,CAAC,EAAE,OAAO,0BAA0B,EAAE,0BAA0B,KACpE,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,CAmQ5D,CAAC"}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { useCallback, useMemo, useRef, useState, } from "react";
|
|
3
|
+
import { CHART_CAPTURE_SCALE_SINGLE } from "./captureChartElement.js";
|
|
3
4
|
import { canvasToBlob, downloadBlob, releaseCanvas, restoreChartExportViews, sortRegisteredChartsByDocumentOrder, toBulkChartCaptureSources, } from "./chartExport.js";
|
|
4
5
|
import { ChartExportApiContext, ChartExportRegistryContext, } from "./chartExportContext.js";
|
|
5
6
|
import { getChartExportFilename } from "./chartExportFilenames.js";
|
|
@@ -108,7 +109,9 @@ export const ChartExportProvider = ({ children, capture, zipFiles, encodePng = c
|
|
|
108
109
|
throw new Error("The chart is not ready to export yet.");
|
|
109
110
|
}
|
|
110
111
|
const captureFn = await resolveCapture(capture);
|
|
111
|
-
const canvas = await captureFn(chart.element
|
|
112
|
+
const canvas = await captureFn(chart.element, {
|
|
113
|
+
scale: CHART_CAPTURE_SCALE_SINGLE,
|
|
114
|
+
});
|
|
112
115
|
try {
|
|
113
116
|
downloadFile(await encodePng(canvas), chart.filename);
|
|
114
117
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"FeatureStatusBadge.d.ts","sourceRoot":"","sources":["../../src/report/FeatureStatusBadge.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC;AAChC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iCAAiC,CAAC;AAErE,MAAM,WAAW,uBAAuB;IACtC,MAAM,EAAE,aAAa,CAAC;CACvB;AAED,eAAO,MAAM,kBAAkB,EAAE,EAAE,CAAC,uBAAuB,
|
|
1
|
+
{"version":3,"file":"FeatureStatusBadge.d.ts","sourceRoot":"","sources":["../../src/report/FeatureStatusBadge.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC;AAChC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iCAAiC,CAAC;AAErE,MAAM,WAAW,uBAAuB;IACtC,MAAM,EAAE,aAAa,CAAC;CACvB;AAED,eAAO,MAAM,kBAAkB,EAAE,EAAE,CAAC,uBAAuB,CA0B1D,CAAC"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import { Label } from "@patternfly/react-core";
|
|
2
|
+
import { Icon, Label } from "@patternfly/react-core";
|
|
3
3
|
import { CheckCircleIcon, TimesCircleIcon } from "@patternfly/react-icons";
|
|
4
4
|
export const FeatureStatusBadge = ({ status }) => {
|
|
5
5
|
if (status === "unknown") {
|
|
@@ -8,6 +8,6 @@ export const FeatureStatusBadge = ({ status }) => {
|
|
|
8
8
|
if (status === "enabled") {
|
|
9
9
|
return (_jsx(Label, { color: "green", isCompact: true, icon: _jsx(CheckCircleIcon, {}), children: "Enabled" }));
|
|
10
10
|
}
|
|
11
|
-
return (_jsx(Label, {
|
|
11
|
+
return (_jsx(Label, { variant: "outline", isCompact: true, icon: _jsx(Icon, { status: "danger", children: _jsx(TimesCircleIcon, {}) }), children: "Disabled" }));
|
|
12
12
|
};
|
|
13
13
|
FeatureStatusBadge.displayName = "FeatureStatusBadge";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"OsUpgradeRecommendationPopover.d.ts","sourceRoot":"","sources":["../../src/report/OsUpgradeRecommendationPopover.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"OsUpgradeRecommendationPopover.d.ts","sourceRoot":"","sources":["../../src/report/OsUpgradeRecommendationPopover.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC;AAQhC,UAAU,mCAAmC;IAC3C,qBAAqB,EAAE,MAAM,CAAC;CAC/B;AAED,eAAO,MAAM,8BAA8B,EAAE,EAAE,CAC7C,mCAAmC,CAgBpC,CAAC;AAIF,UAAU,eAAe;IACvB,MAAM,EAAE,MAAM,CAAC;IACf,qBAAqB,CAAC,EAAE,MAAM,CAAC;CAChC;AAED,eAAO,MAAM,UAAU,EAAE,EAAE,CAAC,eAAe,CAyB1C,CAAC"}
|
|
@@ -3,17 +3,11 @@ import { css } from "@emotion/css";
|
|
|
3
3
|
import { Button, Flex, FlexItem, Popover } from "@patternfly/react-core";
|
|
4
4
|
import { InfoCircleIcon } from "@patternfly/react-icons";
|
|
5
5
|
import { chartExportHideProps } from "./chartExport.js";
|
|
6
|
-
const upgradeRecommendationPopoverCloseButton = css `
|
|
7
|
-
.pf-v6-c-popover__close .pf-v6-c-button.pf-m-plain,
|
|
8
|
-
.pf-v6-c-popover__close .pf-v6-c-button.pf-m-plain:hover {
|
|
9
|
-
color: var(--pf-t--global--text--color--regular);
|
|
10
|
-
}
|
|
11
|
-
`;
|
|
12
6
|
const infoButtonStyle = css `
|
|
13
7
|
padding: 0;
|
|
14
8
|
vertical-align: middle;
|
|
15
9
|
`;
|
|
16
|
-
export const OsUpgradeRecommendationPopover = ({ upgradeRecommendation }) => (_jsx(Popover, {
|
|
10
|
+
export const OsUpgradeRecommendationPopover = ({ upgradeRecommendation }) => (_jsx(Popover, { position: "bottom", headerContent: "Upgrade to get support", bodyContent: _jsx("div", { children: upgradeRecommendation }), children: _jsx(Button, { type: "button", "aria-label": "Open operating system upgrade information", variant: "plain", className: infoButtonStyle, children: _jsx(InfoCircleIcon, { color: "var(--pf-t--global--icon--color--status--info--default)" }) }) }));
|
|
17
11
|
OsUpgradeRecommendationPopover.displayName = "OsUpgradeRecommendationPopover";
|
|
18
12
|
export const OsNameCell = ({ osName, upgradeRecommendation, }) => {
|
|
19
13
|
const hasUpgradeRecommendation = upgradeRecommendation && upgradeRecommendation.trim() !== "";
|
|
@@ -14,6 +14,7 @@ export interface ExportReportButtonProps extends StandardReportExportHandlers {
|
|
|
14
14
|
loadingLabel?: string | null;
|
|
15
15
|
isDisabled?: boolean;
|
|
16
16
|
toggleLabel?: string;
|
|
17
|
+
assessmentSectionLabel?: string;
|
|
17
18
|
}
|
|
18
19
|
export declare const ExportReportButton: FC<ExportReportButtonProps>;
|
|
19
20
|
//# sourceMappingURL=ReportExportMenu.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ReportExportMenu.d.ts","sourceRoot":"","sources":["../../src/report/ReportExportMenu.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"ReportExportMenu.d.ts","sourceRoot":"","sources":["../../src/report/ReportExportMenu.tsx"],"names":[],"mappings":"AAWA,OAAO,EAAE,KAAK,EAAE,EAA+C,MAAM,OAAO,CAAC;AAC7E,OAAO,EACL,KAAK,kBAAkB,EAEvB,KAAK,4BAA4B,EAElC,MAAM,0BAA0B,CAAC;AA6BlC,MAAM,WAAW,qBAAqB;IACpC,OAAO,EAAE,kBAAkB,EAAE,CAAC;IAC9B,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,eAAO,MAAM,gBAAgB,EAAE,EAAE,CAAC,qBAAqB,CAqFtD,CAAC;AAIF,MAAM,WAAW,uBAAwB,SAAQ,4BAA4B;IAC3E,YAAY,CAAC,EAAE,kBAAkB,EAAE,CAAC;IACpC,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,sBAAsB,CAAC,EAAE,MAAM,CAAC;CACjC;AAED,eAAO,MAAM,kBAAkB,EAAE,EAAE,CAAC,uBAAuB,CAoB1D,CAAC"}
|
|
@@ -1,13 +1,46 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import { Dropdown, DropdownItem, DropdownList, MenuToggle, Spinner, } from "@patternfly/react-core";
|
|
2
|
+
import { Divider, Dropdown, DropdownGroup, DropdownItem, DropdownList, MenuToggle, Spinner, } from "@patternfly/react-core";
|
|
3
3
|
import { DownloadIcon } from "@patternfly/react-icons";
|
|
4
|
-
import { useState } from "react";
|
|
4
|
+
import { useMemo, useState } from "react";
|
|
5
5
|
import { standardReportExportOptions, } from "./reportExportOptions.js";
|
|
6
|
+
function sectionGroupKey(section) {
|
|
7
|
+
return section?.id;
|
|
8
|
+
}
|
|
9
|
+
function groupExportOptions(options) {
|
|
10
|
+
const groups = [];
|
|
11
|
+
for (const option of options) {
|
|
12
|
+
const sectionKey = sectionGroupKey(option.section);
|
|
13
|
+
const last = groups.length > 0 ? groups[groups.length - 1] : undefined;
|
|
14
|
+
const lastKey = last ? sectionGroupKey(last.section) : undefined;
|
|
15
|
+
if (last && lastKey === sectionKey) {
|
|
16
|
+
last.options.push(option);
|
|
17
|
+
}
|
|
18
|
+
else {
|
|
19
|
+
groups.push({ section: option.section, options: [option] });
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return groups;
|
|
23
|
+
}
|
|
6
24
|
export const ReportExportMenu = ({ options, isLoading = false, loadingLabel = null, isDisabled = false, toggleLabel = "Export", }) => {
|
|
7
25
|
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
|
|
26
|
+
const optionGroups = useMemo(() => groupExportOptions(options), [options]);
|
|
8
27
|
if (options.length === 0) {
|
|
9
28
|
return null;
|
|
10
29
|
}
|
|
30
|
+
const menuItems = [];
|
|
31
|
+
optionGroups.forEach((group, index) => {
|
|
32
|
+
if (index > 0) {
|
|
33
|
+
const dividerKey = group.section?.id ?? group.options[0]?.key ?? "ungrouped";
|
|
34
|
+
menuItems.push(_jsx(Divider, { component: "li" }, `divider-before-${dividerKey}`));
|
|
35
|
+
}
|
|
36
|
+
const items = group.options.map((option) => (_jsx(DropdownItem, { isDisabled: option.isDisabled, description: option.description, onClick: option.onSelect, children: option.label }, option.key)));
|
|
37
|
+
if (group.section) {
|
|
38
|
+
menuItems.push(_jsx(DropdownGroup, { label: group.section.label, children: items }, group.section.id));
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
menuItems.push(...items);
|
|
42
|
+
}
|
|
43
|
+
});
|
|
11
44
|
return (_jsx(Dropdown, { isOpen: isDropdownOpen, popperProps: {
|
|
12
45
|
placement: "bottom-end",
|
|
13
46
|
preventOverflow: true,
|
|
@@ -17,14 +50,15 @@ export const ReportExportMenu = ({ options, isLoading = false, loadingLabel = nu
|
|
|
17
50
|
setIsDropdownOpen(isOpen);
|
|
18
51
|
}, toggle: (toggleRef) => (_jsx(MenuToggle, { ref: toggleRef, onClick: () => {
|
|
19
52
|
setIsDropdownOpen((open) => !open);
|
|
20
|
-
}, isExpanded: isDropdownOpen, variant: "secondary", isDisabled: isLoading || isDisabled, "aria-label": "Export report options", icon: isLoading ? (_jsx(Spinner, { size: "sm", "aria-hidden": "true" })) : (_jsx(DownloadIcon, { "aria-hidden": "true" })), children: isLoading ? (loadingLabel ?? "Generating...") : toggleLabel })), children: _jsx(DropdownList, { children:
|
|
53
|
+
}, isExpanded: isDropdownOpen, variant: "secondary", isDisabled: isLoading || isDisabled, "aria-label": "Export report options", icon: isLoading ? (_jsx(Spinner, { size: "sm", "aria-hidden": "true" })) : (_jsx(DownloadIcon, { "aria-hidden": "true" })), children: isLoading ? (loadingLabel ?? "Generating...") : toggleLabel })), children: _jsx(DropdownList, { children: menuItems }) }));
|
|
21
54
|
};
|
|
22
55
|
ReportExportMenu.displayName = "ReportExportMenu";
|
|
23
|
-
export const ExportReportButton = ({ onExportPdf, onExportPng, onExportHtml, extraOptions = [], ...menuProps }) => (_jsx(ReportExportMenu, { options: [
|
|
56
|
+
export const ExportReportButton = ({ onExportPdf, onExportPng, onExportHtml, extraOptions = [], assessmentSectionLabel, ...menuProps }) => (_jsx(ReportExportMenu, { options: [
|
|
24
57
|
...standardReportExportOptions({
|
|
25
58
|
onExportPdf,
|
|
26
59
|
onExportPng,
|
|
27
60
|
onExportHtml,
|
|
61
|
+
assessmentSectionLabel,
|
|
28
62
|
}),
|
|
29
63
|
...extraOptions,
|
|
30
64
|
], ...menuProps }));
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { FC } from "react";
|
|
2
|
+
import { type MigrationDonutChartLegendVariant } from "../charts/MigrationDonutChart.js";
|
|
3
|
+
export interface StorageDiskTierData {
|
|
4
|
+
vmCount?: number;
|
|
5
|
+
totalSizeTB?: number;
|
|
6
|
+
}
|
|
7
|
+
export interface StorageOverviewProps {
|
|
8
|
+
diskSizeTier?: Record<string, StorageDiskTierData>;
|
|
9
|
+
diskTypes?: Record<string, StorageDiskTierData>;
|
|
10
|
+
totalVMs?: number;
|
|
11
|
+
totalWithSharedDisks?: number;
|
|
12
|
+
/**
|
|
13
|
+
* Disk-tier slice / center-title click. The argument is the tier's parsed
|
|
14
|
+
* disk-size range (in MB). Agent-ui maps it to a VM disk filter; ui-app omits
|
|
15
|
+
* it. Called with `undefined` for the center-title click (no tier filter).
|
|
16
|
+
*/
|
|
17
|
+
onDiskTierSelect?: (diskRange: {
|
|
18
|
+
min: number;
|
|
19
|
+
max?: number;
|
|
20
|
+
}) => void;
|
|
21
|
+
/** Center-title click. Agent-ui opens the VM list with no extra filters. */
|
|
22
|
+
onTitleClick?: () => void;
|
|
23
|
+
/** `"html"` in agent-ui; ui-app passes `"chart"`. */
|
|
24
|
+
legendVariant?: MigrationDonutChartLegendVariant;
|
|
25
|
+
}
|
|
26
|
+
export declare const StorageOverview: FC<StorageOverviewProps>;
|
|
27
|
+
//# sourceMappingURL=StorageOverview.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"StorageOverview.d.ts","sourceRoot":"","sources":["../../src/report/StorageOverview.tsx"],"names":[],"mappings":"AAyBA,OAAO,KAAK,EAAE,EAAE,EAAO,MAAM,OAAO,CAAC;AAErC,OAAO,EAGL,KAAK,gCAAgC,EACtC,MAAM,kCAAkC,CAAC;AAQ1C,MAAM,WAAW,mBAAmB;IAClC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,oBAAoB;IACnC,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,mBAAmB,CAAC,CAAC;IACnD,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,mBAAmB,CAAC,CAAC;IAChD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B;;;;OAIG;IACH,gBAAgB,CAAC,EAAE,CAAC,SAAS,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,GAAG,CAAC,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;IACtE,4EAA4E;IAC5E,YAAY,CAAC,EAAE,MAAM,IAAI,CAAC;IAC1B,qDAAqD;IACrD,aAAa,CAAC,EAAE,gCAAgC,CAAC;CAClD;AA0TD,eAAO,MAAM,eAAe,EAAE,EAAE,CAAC,oBAAoB,CA8QpD,CAAC"}
|
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { Card, CardBody, CardTitle, Content, Dropdown, DropdownItem, DropdownList, Flex, FlexItem, MenuToggle, } from "@patternfly/react-core";
|
|
3
|
+
import { DatabaseIcon } from "@patternfly/react-icons";
|
|
4
|
+
import { chart_color_blue_300, chart_color_green_300, chart_color_orange_200, chart_color_purple_100, chart_color_purple_300, chart_color_red_orange_300, chart_color_teal_200, chart_color_teal_300, 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 { 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
|
+
totalSize: "Total disk size by tier",
|
|
15
|
+
vmCount: "VM count by disk size tier",
|
|
16
|
+
vmCountByDiskType: "VM count by disk type",
|
|
17
|
+
sharedDisks: "Shared disks VS. No shared disks",
|
|
18
|
+
};
|
|
19
|
+
const CHART_ID = "storage-overview";
|
|
20
|
+
const CHART_TITLE = "Storage";
|
|
21
|
+
const TIER_CONFIG = {
|
|
22
|
+
Easy: { order: 0, label: "0-10 TB", legendCategory: "Easy" },
|
|
23
|
+
Medium: { order: 1, label: "11-20 TB", legendCategory: "Medium" },
|
|
24
|
+
Hard: { order: 2, label: "21-50 TB", legendCategory: "Hard" },
|
|
25
|
+
White: { order: 3, label: "> 50 TB", legendCategory: "White glove" },
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* Categorical order shared by the agent-ui and ui-app storage donuts, mapped
|
|
29
|
+
* onto PatternFly `chart_color_*` tokens.
|
|
30
|
+
*/
|
|
31
|
+
const COLOR_PALETTE = [
|
|
32
|
+
chart_color_blue_300.value,
|
|
33
|
+
chart_color_purple_300.value,
|
|
34
|
+
chart_color_purple_100.value,
|
|
35
|
+
chart_color_teal_300.value,
|
|
36
|
+
chart_color_yellow_400.value,
|
|
37
|
+
chart_color_green_300.value,
|
|
38
|
+
chart_color_orange_200.value,
|
|
39
|
+
chart_color_red_orange_300.value,
|
|
40
|
+
];
|
|
41
|
+
const DISK_TYPE_BAR_COLORS = [
|
|
42
|
+
chart_color_blue_300.value,
|
|
43
|
+
chart_color_purple_300.value,
|
|
44
|
+
chart_color_purple_100.value,
|
|
45
|
+
chart_color_yellow_400.value,
|
|
46
|
+
chart_color_teal_200.value,
|
|
47
|
+
];
|
|
48
|
+
const SHARED_DISKS_COLORS = {
|
|
49
|
+
"With shared disks": chart_color_yellow_400.value,
|
|
50
|
+
"No shared disks": chart_color_blue_300.value,
|
|
51
|
+
};
|
|
52
|
+
const DISK_TYPE_PREFERRED_ORDER = ["VMFS", "NFS", "vSAN", "RDM", "vVols"];
|
|
53
|
+
// --- Disk-tier label parsing (self-contained copy so the shared card does not
|
|
54
|
+
// depend on agent-ui internals). Used for both tier ordering and drill-down. ---
|
|
55
|
+
const MB_IN_GIB = 1024;
|
|
56
|
+
const MB_IN_TIB = 1024 * 1024;
|
|
57
|
+
const diskSizeRanges = [
|
|
58
|
+
{ label: "0-100GiB", min: 0, max: 100 * MB_IN_GIB },
|
|
59
|
+
{ label: "100-500GiB", min: 100 * MB_IN_GIB, max: 500 * MB_IN_GIB },
|
|
60
|
+
{ label: "500GiB-1TiB", min: 500 * MB_IN_GIB, max: MB_IN_TIB },
|
|
61
|
+
{ label: "1-2TiB", min: MB_IN_TIB, max: 2 * MB_IN_TIB },
|
|
62
|
+
{ label: "2-5TiB", min: 2 * MB_IN_TIB, max: 5 * MB_IN_TIB },
|
|
63
|
+
];
|
|
64
|
+
const legacyDiskSizeRanges = [
|
|
65
|
+
{ label: "0-10 TB", min: 0, max: 10 * MB_IN_TIB },
|
|
66
|
+
{ label: "11-20 TB", min: 10 * MB_IN_TIB + 1, max: 20 * MB_IN_TIB },
|
|
67
|
+
{ label: "21-50 TB", min: 20 * MB_IN_TIB + 1, max: 50 * MB_IN_TIB },
|
|
68
|
+
{ label: "50+ TB", min: 50 * MB_IN_TIB + 1, max: undefined },
|
|
69
|
+
];
|
|
70
|
+
const sizeValueToMB = (value, unit) => {
|
|
71
|
+
const amount = Number.parseFloat(value);
|
|
72
|
+
if (!Number.isFinite(amount)) {
|
|
73
|
+
return Number.NaN;
|
|
74
|
+
}
|
|
75
|
+
switch (unit.toLowerCase()) {
|
|
76
|
+
case "gib":
|
|
77
|
+
case "gb":
|
|
78
|
+
return amount * MB_IN_GIB;
|
|
79
|
+
case "tib":
|
|
80
|
+
case "tb":
|
|
81
|
+
return amount * MB_IN_TIB;
|
|
82
|
+
case "mib":
|
|
83
|
+
case "mb":
|
|
84
|
+
return amount;
|
|
85
|
+
default:
|
|
86
|
+
return Number.NaN;
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
/** Parses inventory disk tier labels (GiB/TiB or legacy TB buckets) into MB ranges. */
|
|
90
|
+
const parseDiskTierLabelToRange = (label) => {
|
|
91
|
+
const normalized = label.trim();
|
|
92
|
+
if (!normalized) {
|
|
93
|
+
return undefined;
|
|
94
|
+
}
|
|
95
|
+
const predefined = diskSizeRanges.find((range) => range.label === normalized) ??
|
|
96
|
+
legacyDiskSizeRanges.find((range) => range.label === normalized ||
|
|
97
|
+
(normalized === "> 50 TB" && range.label === "50+ TB"));
|
|
98
|
+
if (predefined) {
|
|
99
|
+
return { min: predefined.min, max: predefined.max };
|
|
100
|
+
}
|
|
101
|
+
const mixedUnitRange = /^(\d+(?:\.\d+)?)\s*(GiB|TiB|GB|TB|MIB|MB)\s*-\s*(\d+(?:\.\d+)?)\s*(GiB|TiB|GB|TB|MIB|MB)$/i.exec(normalized);
|
|
102
|
+
if (mixedUnitRange) {
|
|
103
|
+
const min = sizeValueToMB(mixedUnitRange[1], mixedUnitRange[2]);
|
|
104
|
+
const max = sizeValueToMB(mixedUnitRange[3], mixedUnitRange[4]);
|
|
105
|
+
if (Number.isFinite(min) && Number.isFinite(max)) {
|
|
106
|
+
return { min: Math.floor(min), max: Math.floor(max) };
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
const singleUnitRange = /^(\d+(?:\.\d+)?)\s*-\s*(\d+(?:\.\d+)?)\s*(GiB|TiB|GB|TB|MIB|MB)$/i.exec(normalized);
|
|
110
|
+
if (singleUnitRange) {
|
|
111
|
+
const min = sizeValueToMB(singleUnitRange[1], singleUnitRange[3]);
|
|
112
|
+
const max = sizeValueToMB(singleUnitRange[2], singleUnitRange[3]);
|
|
113
|
+
if (Number.isFinite(min) && Number.isFinite(max)) {
|
|
114
|
+
return { min: Math.floor(min), max: Math.floor(max) };
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
const openEndedRange = /^(\d+(?:\.\d+)?)\s*(GiB|TiB|GB|TB|MIB|MB)?\s*\+$/i.exec(normalized);
|
|
118
|
+
if (openEndedRange) {
|
|
119
|
+
const unit = openEndedRange[2] || "TB";
|
|
120
|
+
const min = sizeValueToMB(openEndedRange[1], unit);
|
|
121
|
+
if (Number.isFinite(min)) {
|
|
122
|
+
const minMB = Math.floor(min);
|
|
123
|
+
return { min: minMB > 0 ? minMB + 1 : minMB, max: undefined };
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return undefined;
|
|
127
|
+
};
|
|
128
|
+
const buildTierChartData = (summary, selector) => {
|
|
129
|
+
if (!summary) {
|
|
130
|
+
return [];
|
|
131
|
+
}
|
|
132
|
+
const getTierPrefix = (key) => {
|
|
133
|
+
const normalizedKey = key.trim().toLowerCase();
|
|
134
|
+
for (const prefix of Object.keys(TIER_CONFIG)) {
|
|
135
|
+
if (normalizedKey.startsWith(prefix.toLowerCase())) {
|
|
136
|
+
return prefix;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return null;
|
|
140
|
+
};
|
|
141
|
+
return Object.entries(summary)
|
|
142
|
+
.map(([key, tier]) => {
|
|
143
|
+
const prefix = getTierPrefix(key);
|
|
144
|
+
const display = prefix
|
|
145
|
+
? TIER_CONFIG[prefix]
|
|
146
|
+
: { label: key, legendCategory: key };
|
|
147
|
+
const { count, countDisplay } = selector(tier);
|
|
148
|
+
const label = display.label;
|
|
149
|
+
return {
|
|
150
|
+
name: label,
|
|
151
|
+
count,
|
|
152
|
+
countDisplay,
|
|
153
|
+
legendCategory: display.legendCategory,
|
|
154
|
+
diskRange: parseDiskTierLabelToRange(key) ?? parseDiskTierLabelToRange(label),
|
|
155
|
+
sortOrder: parseDiskTierLabelToRange(key)?.min ??
|
|
156
|
+
parseDiskTierLabelToRange(label)?.min ??
|
|
157
|
+
(prefix ? TIER_CONFIG[prefix].order : 999),
|
|
158
|
+
};
|
|
159
|
+
})
|
|
160
|
+
.sort((a, b) => a.sortOrder - b.sortOrder)
|
|
161
|
+
.map(({ sortOrder: _sortOrder, ...datum }) => datum);
|
|
162
|
+
};
|
|
163
|
+
const getContrastColor = (hexColor) => {
|
|
164
|
+
const hex = hexColor.replace("#", "");
|
|
165
|
+
const r = Number.parseInt(hex.substring(0, 2), 16);
|
|
166
|
+
const g = Number.parseInt(hex.substring(2, 4), 16);
|
|
167
|
+
const b = Number.parseInt(hex.substring(4, 6), 16);
|
|
168
|
+
const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
|
|
169
|
+
return luminance > 0.5 ? "#000" : "#fff";
|
|
170
|
+
};
|
|
171
|
+
const DiskTypeBarChart = ({ data, colors }) => {
|
|
172
|
+
const maxCount = useMemo(() => (data.length > 0 ? Math.max(...data.map((d) => d.count)) : 0), [data]);
|
|
173
|
+
if (data.length === 0) {
|
|
174
|
+
return _jsx(CardEmptyState, { title: REPORT_CARD_EMPTY_STATE_TITLES.diskTypes });
|
|
175
|
+
}
|
|
176
|
+
return (_jsxs(_Fragment, { children: [_jsx("div", { className: dashboardStyles.storageChartWrapper, children: _jsx(Flex, { direction: { default: "row" }, alignItems: { default: "alignItemsFlexEnd" }, justifyContent: { default: "justifyContentCenter" }, spaceItems: { default: "spaceItemsMd" }, style: { height: "250px", width: "100%" }, children: data.map((item, index) => {
|
|
177
|
+
const heightPercentage = maxCount > 0 ? (item.count / maxCount) * 100 : 0;
|
|
178
|
+
const minHeight = item.count > 0 ? 10 : 0;
|
|
179
|
+
const barColor = colors[index % colors.length];
|
|
180
|
+
const textColor = getContrastColor(barColor);
|
|
181
|
+
return (_jsxs(Flex, { direction: { default: "column" }, alignItems: { default: "alignItemsCenter" }, spaceItems: { default: "spaceItemsSm" }, style: { flex: "1", maxWidth: "120px" }, children: [_jsx(FlexItem, { style: {
|
|
182
|
+
height: "200px",
|
|
183
|
+
display: "flex",
|
|
184
|
+
alignItems: "flex-end",
|
|
185
|
+
width: "100%",
|
|
186
|
+
justifyContent: "center",
|
|
187
|
+
}, children: _jsx("div", { style: {
|
|
188
|
+
width: "40px",
|
|
189
|
+
height: `${Math.max(heightPercentage, minHeight)}%`,
|
|
190
|
+
backgroundColor: barColor,
|
|
191
|
+
transition: "height 0.3s ease",
|
|
192
|
+
borderRadius: "4px 4px 0 0",
|
|
193
|
+
display: "flex",
|
|
194
|
+
alignItems: "flex-start",
|
|
195
|
+
justifyContent: "center",
|
|
196
|
+
paddingTop: "4px",
|
|
197
|
+
}, title: `${item.name}: ${item.count} VMs`, children: _jsx(Content, { component: "small", style: {
|
|
198
|
+
color: textColor,
|
|
199
|
+
fontWeight: "bold",
|
|
200
|
+
fontSize: "10px",
|
|
201
|
+
}, children: item.count }) }) }), _jsx(FlexItem, { children: _jsx(Content, { component: "small", style: {
|
|
202
|
+
fontSize: "12px",
|
|
203
|
+
textAlign: "center",
|
|
204
|
+
wordBreak: "break-word",
|
|
205
|
+
}, children: item.name }) })] }, item.name));
|
|
206
|
+
}) }) }), _jsx(Content, { component: "small", className: dashboardStyles.storageTotalsNote, children: "Totals may exceed the unique VM count because individual VMs can have multiple disk types" })] }));
|
|
207
|
+
};
|
|
208
|
+
const isViewMode = (value) => value === "totalSize" ||
|
|
209
|
+
value === "vmCount" ||
|
|
210
|
+
value === "vmCountByDiskType" ||
|
|
211
|
+
value === "sharedDisks";
|
|
212
|
+
export const StorageOverview = ({ diskSizeTier = {}, diskTypes = {}, totalVMs, totalWithSharedDisks, onDiskTierSelect, onTitleClick, legendVariant = "html", }) => {
|
|
213
|
+
const [viewMode, setViewMode] = useState("vmCount");
|
|
214
|
+
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
|
|
215
|
+
const totals = useMemo(() => Object.values(diskSizeTier).reduce((acc, tier) => ({
|
|
216
|
+
totalSize: acc.totalSize + (tier?.totalSizeTB || 0),
|
|
217
|
+
totalVMs: acc.totalVMs + (tier?.vmCount || 0),
|
|
218
|
+
}), { totalSize: 0, totalVMs: 0 }), [diskSizeTier]);
|
|
219
|
+
const chartData = useMemo(() => buildTierChartData(diskSizeTier, (tier) => {
|
|
220
|
+
const count = viewMode === "totalSize"
|
|
221
|
+
? tier?.totalSizeTB || 0
|
|
222
|
+
: tier?.vmCount || 0;
|
|
223
|
+
return {
|
|
224
|
+
count,
|
|
225
|
+
countDisplay: viewMode === "totalSize" ? `${count.toFixed(2)}` : `${count}`,
|
|
226
|
+
};
|
|
227
|
+
}), [diskSizeTier, viewMode]);
|
|
228
|
+
const diskTypeChartData = useMemo(() => {
|
|
229
|
+
if (Object.keys(diskTypes).length === 0) {
|
|
230
|
+
return [];
|
|
231
|
+
}
|
|
232
|
+
const orderIndex = (name) => {
|
|
233
|
+
const idx = DISK_TYPE_PREFERRED_ORDER.indexOf(name);
|
|
234
|
+
return idx === -1 ? Number.MAX_SAFE_INTEGER : idx;
|
|
235
|
+
};
|
|
236
|
+
return Object.entries(diskTypes)
|
|
237
|
+
.map(([diskTypeName, summary]) => ({
|
|
238
|
+
name: diskTypeName,
|
|
239
|
+
count: summary?.vmCount || 0,
|
|
240
|
+
}))
|
|
241
|
+
.sort((a, b) => orderIndex(a.name) - orderIndex(b.name));
|
|
242
|
+
}, [diskTypes]);
|
|
243
|
+
const isSharedDisksViewAvailable = totalWithSharedDisks !== 0;
|
|
244
|
+
// Exclude the "Shared disks" view from the chart export cycle (used to
|
|
245
|
+
// build the PDF/PNG/HTML exports) when that view is disabled in the
|
|
246
|
+
// dropdown, so the exported report stays consistent with what's
|
|
247
|
+
// selectable on screen.
|
|
248
|
+
const availableExportViewLabels = useMemo(() => {
|
|
249
|
+
if (isSharedDisksViewAvailable) {
|
|
250
|
+
return VIEW_MODE_LABELS;
|
|
251
|
+
}
|
|
252
|
+
const { sharedDisks: _sharedDisks, ...rest } = VIEW_MODE_LABELS;
|
|
253
|
+
return rest;
|
|
254
|
+
}, [isSharedDisksViewAvailable]);
|
|
255
|
+
const sharedDisksChartData = useMemo(() => {
|
|
256
|
+
if (totalWithSharedDisks === undefined ||
|
|
257
|
+
totalVMs === undefined ||
|
|
258
|
+
totalWithSharedDisks === 0) {
|
|
259
|
+
return null;
|
|
260
|
+
}
|
|
261
|
+
const withSharedDisks = Math.min(totalWithSharedDisks, totalVMs);
|
|
262
|
+
const withoutSharedDisks = Math.max(0, totalVMs - withSharedDisks);
|
|
263
|
+
return [
|
|
264
|
+
{
|
|
265
|
+
name: "With shared disks",
|
|
266
|
+
count: withSharedDisks,
|
|
267
|
+
countDisplay: `${withSharedDisks}`,
|
|
268
|
+
legendCategory: "With shared disks",
|
|
269
|
+
},
|
|
270
|
+
{
|
|
271
|
+
name: "No shared disks",
|
|
272
|
+
count: withoutSharedDisks,
|
|
273
|
+
countDisplay: `${withoutSharedDisks}`,
|
|
274
|
+
legendCategory: "No shared disks",
|
|
275
|
+
},
|
|
276
|
+
];
|
|
277
|
+
}, [totalVMs, totalWithSharedDisks]);
|
|
278
|
+
const tierLegend = useMemo(() => {
|
|
279
|
+
const legendMap = {};
|
|
280
|
+
chartData.forEach((slice, idx) => {
|
|
281
|
+
const color = COLOR_PALETTE[idx % COLOR_PALETTE.length];
|
|
282
|
+
if (color) {
|
|
283
|
+
legendMap[slice.legendCategory] = color;
|
|
284
|
+
}
|
|
285
|
+
});
|
|
286
|
+
return legendMap;
|
|
287
|
+
}, [chartData]);
|
|
288
|
+
const commonDonutProps = {
|
|
289
|
+
height: 300,
|
|
290
|
+
width: 420,
|
|
291
|
+
donutThickness: 18,
|
|
292
|
+
titleFontSize: 34,
|
|
293
|
+
subTitleColor: "var(--pf-t--global--text--color--subtle)",
|
|
294
|
+
legendVariant,
|
|
295
|
+
};
|
|
296
|
+
const handleDiskTierClick = (item) => {
|
|
297
|
+
const diskRange = item.diskRange ??
|
|
298
|
+
parseDiskTierLabelToRange(item.name) ??
|
|
299
|
+
parseDiskTierLabelToRange(item.legendCategory);
|
|
300
|
+
if (diskRange) {
|
|
301
|
+
onDiskTierSelect?.(diskRange);
|
|
302
|
+
}
|
|
303
|
+
};
|
|
304
|
+
const chartTitle = `${CHART_TITLE} — ${VIEW_MODE_LABELS[viewMode]}`;
|
|
305
|
+
return (_jsx(ChartExportSurface, { id: CHART_ID, title: chartTitle, exportViews: chartExportViewsFromLabels(CHART_TITLE, availableExportViewLabels), activeExportViewId: viewMode, onExportViewChange: (viewId) => {
|
|
306
|
+
if (isViewMode(viewId)) {
|
|
307
|
+
setViewMode(viewId);
|
|
308
|
+
}
|
|
309
|
+
}, children: _jsxs(Card, { className: `${dashboardStyles.card} ${dashboardStyles.storageCardOverflowHidden}`, children: [_jsx(CardTitle, { children: _jsxs(Flex, { justifyContent: { default: "justifyContentSpaceBetween" }, alignItems: { default: "alignItemsCenter" }, className: dashboardStyles.storageFlexFullWidth, children: [_jsxs(FlexItem, { children: [_jsx(DatabaseIcon, {}), " Disks"] }), _jsx(ChartHeaderActions, { chartId: CHART_ID, title: chartTitle, children: _jsx(Dropdown, { isOpen: isDropdownOpen, onSelect: (_event, value) => {
|
|
310
|
+
if (isViewMode(value)) {
|
|
311
|
+
setViewMode(value);
|
|
312
|
+
}
|
|
313
|
+
setIsDropdownOpen(false);
|
|
314
|
+
}, onOpenChange: setIsDropdownOpen, toggle: (toggleRef) => (_jsx(MenuToggle, { ref: toggleRef, onClick: () => setIsDropdownOpen((open) => !open), isExpanded: isDropdownOpen, className: dashboardStyles.storageMenuToggleMinWidth, children: VIEW_MODE_LABELS[viewMode] })), children: _jsxs(DropdownList, { children: [_jsx(DropdownItem, { value: "vmCount", children: VIEW_MODE_LABELS.vmCount }, "vmCount"), _jsx(DropdownItem, { value: "vmCountByDiskType", children: VIEW_MODE_LABELS.vmCountByDiskType }, "vmCountByDiskType"), _jsx(DropdownItem, { value: "totalSize", children: VIEW_MODE_LABELS.totalSize }, "totalSize"), _jsx(DropdownItem, { value: "sharedDisks", isDisabled: totalWithSharedDisks !== undefined &&
|
|
315
|
+
totalWithSharedDisks === 0, children: VIEW_MODE_LABELS.sharedDisks }, "sharedDisks")] }) }) })] }) }), _jsx(CardBody, { className: dashboardStyles.cardBodyScrollable, children: viewMode === "vmCountByDiskType" ? (_jsx(DiskTypeBarChart, { data: diskTypeChartData, colors: DISK_TYPE_BAR_COLORS })) : viewMode === "sharedDisks" ? (sharedDisksChartData && totalVMs !== undefined ? (_jsx(MigrationDonutChart, { ...commonDonutProps, data: sharedDisksChartData.map((item) => ({
|
|
316
|
+
...item,
|
|
317
|
+
countDisplay: `${item.countDisplay} VMs`,
|
|
318
|
+
})), customColors: SHARED_DISKS_COLORS, title: `${totalVMs} VMs`, subTitle: `${totalWithSharedDisks ?? 0} with shared disks`, marginLeft: "25%", tooltipLabelFormatter: ({ datum, percent }) => `${datum.x}: ${datum.countDisplay}\n${percent.toFixed(1)}%` })) : (_jsx(CardEmptyState, { title: REPORT_CARD_EMPTY_STATE_TITLES.storage }))) : chartData.length === 0 ? (_jsx(CardEmptyState, { title: REPORT_CARD_EMPTY_STATE_TITLES.storage })) : (_jsx(MigrationDonutChart, { ...commonDonutProps, data: chartData.map((item) => ({
|
|
319
|
+
...item,
|
|
320
|
+
countDisplay: viewMode === "totalSize"
|
|
321
|
+
? `${item.countDisplay} TB`
|
|
322
|
+
: `${item.countDisplay} VMs`,
|
|
323
|
+
})), legend: tierLegend, title: viewMode === "totalSize"
|
|
324
|
+
? `${totals.totalSize.toFixed(2)} TB`
|
|
325
|
+
: `${totals.totalVMs} VMs`, subTitle: viewMode === "totalSize"
|
|
326
|
+
? `${totals.totalVMs} VMs`
|
|
327
|
+
: `${totals.totalSize.toFixed(2)} TB`, legendLabelFormatter: ({ x, countDisplay }) => `${x} (${countDisplay})`, tooltipLabelFormatter: ({ datum, percent }) => `${datum.x}: ${datum.countDisplay}\n${percent.toFixed(1)}%`, onItemClick: viewMode === "vmCount" || viewMode === "totalSize"
|
|
328
|
+
? handleDiskTierClick
|
|
329
|
+
: undefined, onTitleClick: onTitleClick })) })] }) }));
|
|
330
|
+
};
|
|
331
|
+
StorageOverview.displayName = "StorageOverview";
|