@openshift-migration-advisor/shared-components 0.28.0-16fc442157c1 → 0.28.0-9da6a7613f16
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/report/StorageOverview.d.ts +27 -0
- package/dist/report/StorageOverview.d.ts.map +1 -0
- package/dist/report/StorageOverview.js +319 -0
- package/dist/report/index.d.ts +1 -0
- package/dist/report/index.d.ts.map +1 -1
- package/dist/report/index.js +1 -0
- package/package.json +1 -1
- package/src/report/StorageOverview.tsx +629 -0
- package/src/report/__tests__/StorageOverview.test.tsx +223 -0
- package/src/report/index.ts +5 -0
|
@@ -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,CA6PpD,CAAC"}
|
|
@@ -0,0 +1,319 @@
|
|
|
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 sharedDisksChartData = useMemo(() => {
|
|
244
|
+
if (totalWithSharedDisks === undefined ||
|
|
245
|
+
totalVMs === undefined ||
|
|
246
|
+
totalWithSharedDisks === 0) {
|
|
247
|
+
return null;
|
|
248
|
+
}
|
|
249
|
+
const withSharedDisks = Math.min(totalWithSharedDisks, totalVMs);
|
|
250
|
+
const withoutSharedDisks = Math.max(0, totalVMs - withSharedDisks);
|
|
251
|
+
return [
|
|
252
|
+
{
|
|
253
|
+
name: "With shared disks",
|
|
254
|
+
count: withSharedDisks,
|
|
255
|
+
countDisplay: `${withSharedDisks}`,
|
|
256
|
+
legendCategory: "With shared disks",
|
|
257
|
+
},
|
|
258
|
+
{
|
|
259
|
+
name: "No shared disks",
|
|
260
|
+
count: withoutSharedDisks,
|
|
261
|
+
countDisplay: `${withoutSharedDisks}`,
|
|
262
|
+
legendCategory: "No shared disks",
|
|
263
|
+
},
|
|
264
|
+
];
|
|
265
|
+
}, [totalVMs, totalWithSharedDisks]);
|
|
266
|
+
const tierLegend = useMemo(() => {
|
|
267
|
+
const legendMap = {};
|
|
268
|
+
chartData.forEach((slice, idx) => {
|
|
269
|
+
const color = COLOR_PALETTE[idx % COLOR_PALETTE.length];
|
|
270
|
+
if (color) {
|
|
271
|
+
legendMap[slice.legendCategory] = color;
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
return legendMap;
|
|
275
|
+
}, [chartData]);
|
|
276
|
+
const commonDonutProps = {
|
|
277
|
+
height: 300,
|
|
278
|
+
width: 420,
|
|
279
|
+
donutThickness: 18,
|
|
280
|
+
titleFontSize: 34,
|
|
281
|
+
subTitleColor: "var(--pf-t--global--text--color--subtle)",
|
|
282
|
+
legendVariant,
|
|
283
|
+
};
|
|
284
|
+
const handleDiskTierClick = (item) => {
|
|
285
|
+
const diskRange = item.diskRange ??
|
|
286
|
+
parseDiskTierLabelToRange(item.name) ??
|
|
287
|
+
parseDiskTierLabelToRange(item.legendCategory);
|
|
288
|
+
if (diskRange) {
|
|
289
|
+
onDiskTierSelect?.(diskRange);
|
|
290
|
+
}
|
|
291
|
+
};
|
|
292
|
+
const chartTitle = `${CHART_TITLE} — ${VIEW_MODE_LABELS[viewMode]}`;
|
|
293
|
+
return (_jsx(ChartExportSurface, { id: CHART_ID, title: chartTitle, exportViews: chartExportViewsFromLabels(CHART_TITLE, VIEW_MODE_LABELS), activeExportViewId: viewMode, onExportViewChange: (viewId) => {
|
|
294
|
+
if (isViewMode(viewId)) {
|
|
295
|
+
setViewMode(viewId);
|
|
296
|
+
}
|
|
297
|
+
}, 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) => {
|
|
298
|
+
if (isViewMode(value)) {
|
|
299
|
+
setViewMode(value);
|
|
300
|
+
}
|
|
301
|
+
setIsDropdownOpen(false);
|
|
302
|
+
}, 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 &&
|
|
303
|
+
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) => ({
|
|
304
|
+
...item,
|
|
305
|
+
countDisplay: `${item.countDisplay} VMs`,
|
|
306
|
+
})), 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) => ({
|
|
307
|
+
...item,
|
|
308
|
+
countDisplay: viewMode === "totalSize"
|
|
309
|
+
? `${item.countDisplay} TB`
|
|
310
|
+
: `${item.countDisplay} VMs`,
|
|
311
|
+
})), legend: tierLegend, title: viewMode === "totalSize"
|
|
312
|
+
? `${totals.totalSize.toFixed(2)} TB`
|
|
313
|
+
: `${totals.totalVMs} VMs`, subTitle: viewMode === "totalSize"
|
|
314
|
+
? `${totals.totalVMs} VMs`
|
|
315
|
+
: `${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"
|
|
316
|
+
? handleDiskTierClick
|
|
317
|
+
: undefined, onTitleClick: onTitleClick })) })] }) }));
|
|
318
|
+
};
|
|
319
|
+
StorageOverview.displayName = "StorageOverview";
|
package/dist/report/index.d.ts
CHANGED
|
@@ -36,6 +36,7 @@ export { buildHostPowerStateChart, buildVmPowerStateChart, HOST_POWER_COLORS, VM
|
|
|
36
36
|
export { ExportReportButton, type ExportReportButtonProps, ReportExportMenu, type ReportExportMenuProps, } from "./ReportExportMenu.js";
|
|
37
37
|
export { ReportTable, type ReportTableProps } from "./ReportTable.js";
|
|
38
38
|
export { type ReportExportOption, type StandardReportExportHandlers, standardReportExportOptions, } from "./reportExportOptions.js";
|
|
39
|
+
export { type StorageDiskTierData, StorageOverview, type StorageOverviewProps, } from "./StorageOverview.js";
|
|
39
40
|
export { SupportTierBadge } from "./SupportTierBadge.js";
|
|
40
41
|
export { ALL_TIERS_FILTER, type OsBarChartViewModel, type OsTableRow, useOsBarChartViewModel, } from "./useOsBarChartViewModel.js";
|
|
41
42
|
export { VCenterClusterDetails, type VCenterClusterDetailsProps, } from "./VCenterClusterDetails.js";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/report/index.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/report/index.ts"],"names":[],"mappings":"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"}
|
package/dist/report/index.js
CHANGED
|
@@ -33,6 +33,7 @@ export { buildHostPowerStateChart, buildVmPowerStateChart, HOST_POWER_COLORS, VM
|
|
|
33
33
|
export { ExportReportButton, ReportExportMenu, } from "./ReportExportMenu.js";
|
|
34
34
|
export { ReportTable } from "./ReportTable.js";
|
|
35
35
|
export { standardReportExportOptions, } from "./reportExportOptions.js";
|
|
36
|
+
export { StorageOverview, } from "./StorageOverview.js";
|
|
36
37
|
export { SupportTierBadge } from "./SupportTierBadge.js";
|
|
37
38
|
export { ALL_TIERS_FILTER, useOsBarChartViewModel, } from "./useOsBarChartViewModel.js";
|
|
38
39
|
export { VCenterClusterDetails, } from "./VCenterClusterDetails.js";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openshift-migration-advisor/shared-components",
|
|
3
|
-
"version": "0.28.0-
|
|
3
|
+
"version": "0.28.0-9da6a7613f16",
|
|
4
4
|
"description": "Shared React UI components for Migration Advisor apps (report charts, forms, OS support tiers)",
|
|
5
5
|
"author": "Montse Ortega <mortegag@redhat.com>",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -0,0 +1,629 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Card,
|
|
3
|
+
CardBody,
|
|
4
|
+
CardTitle,
|
|
5
|
+
Content,
|
|
6
|
+
Dropdown,
|
|
7
|
+
DropdownItem,
|
|
8
|
+
DropdownList,
|
|
9
|
+
Flex,
|
|
10
|
+
FlexItem,
|
|
11
|
+
MenuToggle,
|
|
12
|
+
type MenuToggleElement,
|
|
13
|
+
} from "@patternfly/react-core";
|
|
14
|
+
import { DatabaseIcon } from "@patternfly/react-icons";
|
|
15
|
+
import {
|
|
16
|
+
chart_color_blue_300,
|
|
17
|
+
chart_color_green_300,
|
|
18
|
+
chart_color_orange_200,
|
|
19
|
+
chart_color_purple_100,
|
|
20
|
+
chart_color_purple_300,
|
|
21
|
+
chart_color_red_orange_300,
|
|
22
|
+
chart_color_teal_200,
|
|
23
|
+
chart_color_teal_300,
|
|
24
|
+
chart_color_yellow_400,
|
|
25
|
+
} from "@patternfly/react-tokens";
|
|
26
|
+
import type { FC, Ref } from "react";
|
|
27
|
+
import { useMemo, useState } from "react";
|
|
28
|
+
import {
|
|
29
|
+
MigrationDonutChart,
|
|
30
|
+
type MigrationDonutChartDatum,
|
|
31
|
+
type MigrationDonutChartLegendVariant,
|
|
32
|
+
} from "../charts/MigrationDonutChart.js";
|
|
33
|
+
import { CardEmptyState } from "./CardEmptyState.js";
|
|
34
|
+
import { ChartHeaderActions } from "./ChartDownloadButton.js";
|
|
35
|
+
import { ChartExportSurface } from "./ChartExportSurface.js";
|
|
36
|
+
import { chartExportViewsFromLabels } from "./chartExport.js";
|
|
37
|
+
import { REPORT_CARD_EMPTY_STATE_TITLES } from "./constants.js";
|
|
38
|
+
import { dashboardStyles } from "./dashboardStyles.js";
|
|
39
|
+
|
|
40
|
+
export interface StorageDiskTierData {
|
|
41
|
+
vmCount?: number;
|
|
42
|
+
totalSizeTB?: number;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface StorageOverviewProps {
|
|
46
|
+
diskSizeTier?: Record<string, StorageDiskTierData>;
|
|
47
|
+
diskTypes?: Record<string, StorageDiskTierData>;
|
|
48
|
+
totalVMs?: number;
|
|
49
|
+
totalWithSharedDisks?: number;
|
|
50
|
+
/**
|
|
51
|
+
* Disk-tier slice / center-title click. The argument is the tier's parsed
|
|
52
|
+
* disk-size range (in MB). Agent-ui maps it to a VM disk filter; ui-app omits
|
|
53
|
+
* it. Called with `undefined` for the center-title click (no tier filter).
|
|
54
|
+
*/
|
|
55
|
+
onDiskTierSelect?: (diskRange: { min: number; max?: number }) => void;
|
|
56
|
+
/** Center-title click. Agent-ui opens the VM list with no extra filters. */
|
|
57
|
+
onTitleClick?: () => void;
|
|
58
|
+
/** `"html"` in agent-ui; ui-app passes `"chart"`. */
|
|
59
|
+
legendVariant?: MigrationDonutChartLegendVariant;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
type ViewMode = "totalSize" | "vmCount" | "vmCountByDiskType" | "sharedDisks";
|
|
63
|
+
|
|
64
|
+
const VIEW_MODE_LABELS: Record<ViewMode, string> = {
|
|
65
|
+
totalSize: "Total disk size by tier",
|
|
66
|
+
vmCount: "VM count by disk size tier",
|
|
67
|
+
vmCountByDiskType: "VM count by disk type",
|
|
68
|
+
sharedDisks: "Shared disks VS. No shared disks",
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const CHART_ID = "storage-overview";
|
|
72
|
+
const CHART_TITLE = "Storage";
|
|
73
|
+
|
|
74
|
+
const TIER_CONFIG: Record<
|
|
75
|
+
string,
|
|
76
|
+
{ order: number; label: string; legendCategory: string }
|
|
77
|
+
> = {
|
|
78
|
+
Easy: { order: 0, label: "0-10 TB", legendCategory: "Easy" },
|
|
79
|
+
Medium: { order: 1, label: "11-20 TB", legendCategory: "Medium" },
|
|
80
|
+
Hard: { order: 2, label: "21-50 TB", legendCategory: "Hard" },
|
|
81
|
+
White: { order: 3, label: "> 50 TB", legendCategory: "White glove" },
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Categorical order shared by the agent-ui and ui-app storage donuts, mapped
|
|
86
|
+
* onto PatternFly `chart_color_*` tokens.
|
|
87
|
+
*/
|
|
88
|
+
const COLOR_PALETTE = [
|
|
89
|
+
chart_color_blue_300.value,
|
|
90
|
+
chart_color_purple_300.value,
|
|
91
|
+
chart_color_purple_100.value,
|
|
92
|
+
chart_color_teal_300.value,
|
|
93
|
+
chart_color_yellow_400.value,
|
|
94
|
+
chart_color_green_300.value,
|
|
95
|
+
chart_color_orange_200.value,
|
|
96
|
+
chart_color_red_orange_300.value,
|
|
97
|
+
];
|
|
98
|
+
|
|
99
|
+
const DISK_TYPE_BAR_COLORS = [
|
|
100
|
+
chart_color_blue_300.value,
|
|
101
|
+
chart_color_purple_300.value,
|
|
102
|
+
chart_color_purple_100.value,
|
|
103
|
+
chart_color_yellow_400.value,
|
|
104
|
+
chart_color_teal_200.value,
|
|
105
|
+
];
|
|
106
|
+
|
|
107
|
+
const SHARED_DISKS_COLORS: Record<string, string> = {
|
|
108
|
+
"With shared disks": chart_color_yellow_400.value,
|
|
109
|
+
"No shared disks": chart_color_blue_300.value,
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const DISK_TYPE_PREFERRED_ORDER = ["VMFS", "NFS", "vSAN", "RDM", "vVols"];
|
|
113
|
+
|
|
114
|
+
// --- Disk-tier label parsing (self-contained copy so the shared card does not
|
|
115
|
+
// depend on agent-ui internals). Used for both tier ordering and drill-down. ---
|
|
116
|
+
|
|
117
|
+
const MB_IN_GIB = 1024;
|
|
118
|
+
const MB_IN_TIB = 1024 * 1024;
|
|
119
|
+
|
|
120
|
+
const diskSizeRanges = [
|
|
121
|
+
{ label: "0-100GiB", min: 0, max: 100 * MB_IN_GIB },
|
|
122
|
+
{ label: "100-500GiB", min: 100 * MB_IN_GIB, max: 500 * MB_IN_GIB },
|
|
123
|
+
{ label: "500GiB-1TiB", min: 500 * MB_IN_GIB, max: MB_IN_TIB },
|
|
124
|
+
{ label: "1-2TiB", min: MB_IN_TIB, max: 2 * MB_IN_TIB },
|
|
125
|
+
{ label: "2-5TiB", min: 2 * MB_IN_TIB, max: 5 * MB_IN_TIB },
|
|
126
|
+
];
|
|
127
|
+
|
|
128
|
+
const legacyDiskSizeRanges = [
|
|
129
|
+
{ label: "0-10 TB", min: 0, max: 10 * MB_IN_TIB },
|
|
130
|
+
{ label: "11-20 TB", min: 10 * MB_IN_TIB + 1, max: 20 * MB_IN_TIB },
|
|
131
|
+
{ label: "21-50 TB", min: 20 * MB_IN_TIB + 1, max: 50 * MB_IN_TIB },
|
|
132
|
+
{ label: "50+ TB", min: 50 * MB_IN_TIB + 1, max: undefined },
|
|
133
|
+
];
|
|
134
|
+
|
|
135
|
+
const sizeValueToMB = (value: string, unit: string): number => {
|
|
136
|
+
const amount = Number.parseFloat(value);
|
|
137
|
+
if (!Number.isFinite(amount)) {
|
|
138
|
+
return Number.NaN;
|
|
139
|
+
}
|
|
140
|
+
switch (unit.toLowerCase()) {
|
|
141
|
+
case "gib":
|
|
142
|
+
case "gb":
|
|
143
|
+
return amount * MB_IN_GIB;
|
|
144
|
+
case "tib":
|
|
145
|
+
case "tb":
|
|
146
|
+
return amount * MB_IN_TIB;
|
|
147
|
+
case "mib":
|
|
148
|
+
case "mb":
|
|
149
|
+
return amount;
|
|
150
|
+
default:
|
|
151
|
+
return Number.NaN;
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
/** Parses inventory disk tier labels (GiB/TiB or legacy TB buckets) into MB ranges. */
|
|
156
|
+
const parseDiskTierLabelToRange = (
|
|
157
|
+
label: string,
|
|
158
|
+
): { min: number; max?: number } | undefined => {
|
|
159
|
+
const normalized = label.trim();
|
|
160
|
+
if (!normalized) {
|
|
161
|
+
return undefined;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const predefined =
|
|
165
|
+
diskSizeRanges.find((range) => range.label === normalized) ??
|
|
166
|
+
legacyDiskSizeRanges.find(
|
|
167
|
+
(range) =>
|
|
168
|
+
range.label === normalized ||
|
|
169
|
+
(normalized === "> 50 TB" && range.label === "50+ TB"),
|
|
170
|
+
);
|
|
171
|
+
if (predefined) {
|
|
172
|
+
return { min: predefined.min, max: predefined.max };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const mixedUnitRange =
|
|
176
|
+
/^(\d+(?:\.\d+)?)\s*(GiB|TiB|GB|TB|MIB|MB)\s*-\s*(\d+(?:\.\d+)?)\s*(GiB|TiB|GB|TB|MIB|MB)$/i.exec(
|
|
177
|
+
normalized,
|
|
178
|
+
);
|
|
179
|
+
if (mixedUnitRange) {
|
|
180
|
+
const min = sizeValueToMB(mixedUnitRange[1], mixedUnitRange[2]);
|
|
181
|
+
const max = sizeValueToMB(mixedUnitRange[3], mixedUnitRange[4]);
|
|
182
|
+
if (Number.isFinite(min) && Number.isFinite(max)) {
|
|
183
|
+
return { min: Math.floor(min), max: Math.floor(max) };
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const singleUnitRange =
|
|
188
|
+
/^(\d+(?:\.\d+)?)\s*-\s*(\d+(?:\.\d+)?)\s*(GiB|TiB|GB|TB|MIB|MB)$/i.exec(
|
|
189
|
+
normalized,
|
|
190
|
+
);
|
|
191
|
+
if (singleUnitRange) {
|
|
192
|
+
const min = sizeValueToMB(singleUnitRange[1], singleUnitRange[3]);
|
|
193
|
+
const max = sizeValueToMB(singleUnitRange[2], singleUnitRange[3]);
|
|
194
|
+
if (Number.isFinite(min) && Number.isFinite(max)) {
|
|
195
|
+
return { min: Math.floor(min), max: Math.floor(max) };
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const openEndedRange =
|
|
200
|
+
/^(\d+(?:\.\d+)?)\s*(GiB|TiB|GB|TB|MIB|MB)?\s*\+$/i.exec(normalized);
|
|
201
|
+
if (openEndedRange) {
|
|
202
|
+
const unit = openEndedRange[2] || "TB";
|
|
203
|
+
const min = sizeValueToMB(openEndedRange[1], unit);
|
|
204
|
+
if (Number.isFinite(min)) {
|
|
205
|
+
const minMB = Math.floor(min);
|
|
206
|
+
return { min: minMB > 0 ? minMB + 1 : minMB, max: undefined };
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return undefined;
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
const buildTierChartData = (
|
|
214
|
+
summary: Record<string, StorageDiskTierData> | undefined,
|
|
215
|
+
selector: (tier: StorageDiskTierData) => {
|
|
216
|
+
count: number;
|
|
217
|
+
countDisplay: string;
|
|
218
|
+
},
|
|
219
|
+
): MigrationDonutChartDatum[] => {
|
|
220
|
+
if (!summary) {
|
|
221
|
+
return [];
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const getTierPrefix = (key: string): string | null => {
|
|
225
|
+
const normalizedKey = key.trim().toLowerCase();
|
|
226
|
+
for (const prefix of Object.keys(TIER_CONFIG)) {
|
|
227
|
+
if (normalizedKey.startsWith(prefix.toLowerCase())) {
|
|
228
|
+
return prefix;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return null;
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
return Object.entries(summary)
|
|
235
|
+
.map(([key, tier]) => {
|
|
236
|
+
const prefix = getTierPrefix(key);
|
|
237
|
+
const display = prefix
|
|
238
|
+
? TIER_CONFIG[prefix]
|
|
239
|
+
: { label: key, legendCategory: key };
|
|
240
|
+
const { count, countDisplay } = selector(tier);
|
|
241
|
+
const label = display.label;
|
|
242
|
+
return {
|
|
243
|
+
name: label,
|
|
244
|
+
count,
|
|
245
|
+
countDisplay,
|
|
246
|
+
legendCategory: display.legendCategory,
|
|
247
|
+
diskRange:
|
|
248
|
+
parseDiskTierLabelToRange(key) ?? parseDiskTierLabelToRange(label),
|
|
249
|
+
sortOrder:
|
|
250
|
+
parseDiskTierLabelToRange(key)?.min ??
|
|
251
|
+
parseDiskTierLabelToRange(label)?.min ??
|
|
252
|
+
(prefix ? TIER_CONFIG[prefix].order : 999),
|
|
253
|
+
};
|
|
254
|
+
})
|
|
255
|
+
.sort((a, b) => a.sortOrder - b.sortOrder)
|
|
256
|
+
.map(({ sortOrder: _sortOrder, ...datum }) => datum);
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
const getContrastColor = (hexColor: string): string => {
|
|
260
|
+
const hex = hexColor.replace("#", "");
|
|
261
|
+
const r = Number.parseInt(hex.substring(0, 2), 16);
|
|
262
|
+
const g = Number.parseInt(hex.substring(2, 4), 16);
|
|
263
|
+
const b = Number.parseInt(hex.substring(4, 6), 16);
|
|
264
|
+
const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
|
|
265
|
+
return luminance > 0.5 ? "#000" : "#fff";
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
interface DiskTypeBarChartProps {
|
|
269
|
+
data: Array<{ name: string; count: number }>;
|
|
270
|
+
colors: string[];
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const DiskTypeBarChart: FC<DiskTypeBarChartProps> = ({ data, colors }) => {
|
|
274
|
+
const maxCount = useMemo(
|
|
275
|
+
() => (data.length > 0 ? Math.max(...data.map((d) => d.count)) : 0),
|
|
276
|
+
[data],
|
|
277
|
+
);
|
|
278
|
+
|
|
279
|
+
if (data.length === 0) {
|
|
280
|
+
return <CardEmptyState title={REPORT_CARD_EMPTY_STATE_TITLES.diskTypes} />;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return (
|
|
284
|
+
<>
|
|
285
|
+
<div className={dashboardStyles.storageChartWrapper}>
|
|
286
|
+
<Flex
|
|
287
|
+
direction={{ default: "row" }}
|
|
288
|
+
alignItems={{ default: "alignItemsFlexEnd" }}
|
|
289
|
+
justifyContent={{ default: "justifyContentCenter" }}
|
|
290
|
+
spaceItems={{ default: "spaceItemsMd" }}
|
|
291
|
+
style={{ height: "250px", width: "100%" }}
|
|
292
|
+
>
|
|
293
|
+
{data.map((item, index) => {
|
|
294
|
+
const heightPercentage =
|
|
295
|
+
maxCount > 0 ? (item.count / maxCount) * 100 : 0;
|
|
296
|
+
const minHeight = item.count > 0 ? 10 : 0;
|
|
297
|
+
const barColor = colors[index % colors.length];
|
|
298
|
+
const textColor = getContrastColor(barColor);
|
|
299
|
+
|
|
300
|
+
return (
|
|
301
|
+
<Flex
|
|
302
|
+
key={item.name}
|
|
303
|
+
direction={{ default: "column" }}
|
|
304
|
+
alignItems={{ default: "alignItemsCenter" }}
|
|
305
|
+
spaceItems={{ default: "spaceItemsSm" }}
|
|
306
|
+
style={{ flex: "1", maxWidth: "120px" }}
|
|
307
|
+
>
|
|
308
|
+
<FlexItem
|
|
309
|
+
style={{
|
|
310
|
+
height: "200px",
|
|
311
|
+
display: "flex",
|
|
312
|
+
alignItems: "flex-end",
|
|
313
|
+
width: "100%",
|
|
314
|
+
justifyContent: "center",
|
|
315
|
+
}}
|
|
316
|
+
>
|
|
317
|
+
<div
|
|
318
|
+
style={{
|
|
319
|
+
width: "40px",
|
|
320
|
+
height: `${Math.max(heightPercentage, minHeight)}%`,
|
|
321
|
+
backgroundColor: barColor,
|
|
322
|
+
transition: "height 0.3s ease",
|
|
323
|
+
borderRadius: "4px 4px 0 0",
|
|
324
|
+
display: "flex",
|
|
325
|
+
alignItems: "flex-start",
|
|
326
|
+
justifyContent: "center",
|
|
327
|
+
paddingTop: "4px",
|
|
328
|
+
}}
|
|
329
|
+
title={`${item.name}: ${item.count} VMs`}
|
|
330
|
+
>
|
|
331
|
+
<Content
|
|
332
|
+
component="small"
|
|
333
|
+
style={{
|
|
334
|
+
color: textColor,
|
|
335
|
+
fontWeight: "bold",
|
|
336
|
+
fontSize: "10px",
|
|
337
|
+
}}
|
|
338
|
+
>
|
|
339
|
+
{item.count}
|
|
340
|
+
</Content>
|
|
341
|
+
</div>
|
|
342
|
+
</FlexItem>
|
|
343
|
+
<FlexItem>
|
|
344
|
+
<Content
|
|
345
|
+
component="small"
|
|
346
|
+
style={{
|
|
347
|
+
fontSize: "12px",
|
|
348
|
+
textAlign: "center",
|
|
349
|
+
wordBreak: "break-word",
|
|
350
|
+
}}
|
|
351
|
+
>
|
|
352
|
+
{item.name}
|
|
353
|
+
</Content>
|
|
354
|
+
</FlexItem>
|
|
355
|
+
</Flex>
|
|
356
|
+
);
|
|
357
|
+
})}
|
|
358
|
+
</Flex>
|
|
359
|
+
</div>
|
|
360
|
+
<Content component="small" className={dashboardStyles.storageTotalsNote}>
|
|
361
|
+
Totals may exceed the unique VM count because individual VMs can have
|
|
362
|
+
multiple disk types
|
|
363
|
+
</Content>
|
|
364
|
+
</>
|
|
365
|
+
);
|
|
366
|
+
};
|
|
367
|
+
|
|
368
|
+
const isViewMode = (value: string | number | undefined): value is ViewMode =>
|
|
369
|
+
value === "totalSize" ||
|
|
370
|
+
value === "vmCount" ||
|
|
371
|
+
value === "vmCountByDiskType" ||
|
|
372
|
+
value === "sharedDisks";
|
|
373
|
+
|
|
374
|
+
export const StorageOverview: FC<StorageOverviewProps> = ({
|
|
375
|
+
diskSizeTier = {},
|
|
376
|
+
diskTypes = {},
|
|
377
|
+
totalVMs,
|
|
378
|
+
totalWithSharedDisks,
|
|
379
|
+
onDiskTierSelect,
|
|
380
|
+
onTitleClick,
|
|
381
|
+
legendVariant = "html",
|
|
382
|
+
}) => {
|
|
383
|
+
const [viewMode, setViewMode] = useState<ViewMode>("vmCount");
|
|
384
|
+
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
|
|
385
|
+
|
|
386
|
+
const totals = useMemo(
|
|
387
|
+
() =>
|
|
388
|
+
Object.values(diskSizeTier).reduce(
|
|
389
|
+
(acc, tier) => ({
|
|
390
|
+
totalSize: acc.totalSize + (tier?.totalSizeTB || 0),
|
|
391
|
+
totalVMs: acc.totalVMs + (tier?.vmCount || 0),
|
|
392
|
+
}),
|
|
393
|
+
{ totalSize: 0, totalVMs: 0 },
|
|
394
|
+
),
|
|
395
|
+
[diskSizeTier],
|
|
396
|
+
);
|
|
397
|
+
|
|
398
|
+
const chartData = useMemo(
|
|
399
|
+
() =>
|
|
400
|
+
buildTierChartData(diskSizeTier, (tier) => {
|
|
401
|
+
const count =
|
|
402
|
+
viewMode === "totalSize"
|
|
403
|
+
? tier?.totalSizeTB || 0
|
|
404
|
+
: tier?.vmCount || 0;
|
|
405
|
+
return {
|
|
406
|
+
count,
|
|
407
|
+
countDisplay:
|
|
408
|
+
viewMode === "totalSize" ? `${count.toFixed(2)}` : `${count}`,
|
|
409
|
+
};
|
|
410
|
+
}),
|
|
411
|
+
[diskSizeTier, viewMode],
|
|
412
|
+
);
|
|
413
|
+
|
|
414
|
+
const diskTypeChartData = useMemo(() => {
|
|
415
|
+
if (Object.keys(diskTypes).length === 0) {
|
|
416
|
+
return [];
|
|
417
|
+
}
|
|
418
|
+
const orderIndex = (name: string): number => {
|
|
419
|
+
const idx = DISK_TYPE_PREFERRED_ORDER.indexOf(name);
|
|
420
|
+
return idx === -1 ? Number.MAX_SAFE_INTEGER : idx;
|
|
421
|
+
};
|
|
422
|
+
return Object.entries(diskTypes)
|
|
423
|
+
.map(([diskTypeName, summary]) => ({
|
|
424
|
+
name: diskTypeName,
|
|
425
|
+
count: summary?.vmCount || 0,
|
|
426
|
+
}))
|
|
427
|
+
.sort((a, b) => orderIndex(a.name) - orderIndex(b.name));
|
|
428
|
+
}, [diskTypes]);
|
|
429
|
+
|
|
430
|
+
const sharedDisksChartData = useMemo(() => {
|
|
431
|
+
if (
|
|
432
|
+
totalWithSharedDisks === undefined ||
|
|
433
|
+
totalVMs === undefined ||
|
|
434
|
+
totalWithSharedDisks === 0
|
|
435
|
+
) {
|
|
436
|
+
return null;
|
|
437
|
+
}
|
|
438
|
+
const withSharedDisks = Math.min(totalWithSharedDisks, totalVMs);
|
|
439
|
+
const withoutSharedDisks = Math.max(0, totalVMs - withSharedDisks);
|
|
440
|
+
return [
|
|
441
|
+
{
|
|
442
|
+
name: "With shared disks",
|
|
443
|
+
count: withSharedDisks,
|
|
444
|
+
countDisplay: `${withSharedDisks}`,
|
|
445
|
+
legendCategory: "With shared disks",
|
|
446
|
+
},
|
|
447
|
+
{
|
|
448
|
+
name: "No shared disks",
|
|
449
|
+
count: withoutSharedDisks,
|
|
450
|
+
countDisplay: `${withoutSharedDisks}`,
|
|
451
|
+
legendCategory: "No shared disks",
|
|
452
|
+
},
|
|
453
|
+
];
|
|
454
|
+
}, [totalVMs, totalWithSharedDisks]);
|
|
455
|
+
|
|
456
|
+
const tierLegend = useMemo(() => {
|
|
457
|
+
const legendMap: Record<string, string> = {};
|
|
458
|
+
chartData.forEach((slice, idx) => {
|
|
459
|
+
const color = COLOR_PALETTE[idx % COLOR_PALETTE.length];
|
|
460
|
+
if (color) {
|
|
461
|
+
legendMap[slice.legendCategory] = color;
|
|
462
|
+
}
|
|
463
|
+
});
|
|
464
|
+
return legendMap;
|
|
465
|
+
}, [chartData]);
|
|
466
|
+
|
|
467
|
+
const commonDonutProps = {
|
|
468
|
+
height: 300,
|
|
469
|
+
width: 420,
|
|
470
|
+
donutThickness: 18,
|
|
471
|
+
titleFontSize: 34,
|
|
472
|
+
subTitleColor: "var(--pf-t--global--text--color--subtle)",
|
|
473
|
+
legendVariant,
|
|
474
|
+
};
|
|
475
|
+
|
|
476
|
+
const handleDiskTierClick = (item: MigrationDonutChartDatum): void => {
|
|
477
|
+
const diskRange =
|
|
478
|
+
item.diskRange ??
|
|
479
|
+
parseDiskTierLabelToRange(item.name) ??
|
|
480
|
+
parseDiskTierLabelToRange(item.legendCategory);
|
|
481
|
+
if (diskRange) {
|
|
482
|
+
onDiskTierSelect?.(diskRange);
|
|
483
|
+
}
|
|
484
|
+
};
|
|
485
|
+
|
|
486
|
+
const chartTitle = `${CHART_TITLE} — ${VIEW_MODE_LABELS[viewMode]}`;
|
|
487
|
+
|
|
488
|
+
return (
|
|
489
|
+
<ChartExportSurface
|
|
490
|
+
id={CHART_ID}
|
|
491
|
+
title={chartTitle}
|
|
492
|
+
exportViews={chartExportViewsFromLabels(CHART_TITLE, VIEW_MODE_LABELS)}
|
|
493
|
+
activeExportViewId={viewMode}
|
|
494
|
+
onExportViewChange={(viewId) => {
|
|
495
|
+
if (isViewMode(viewId)) {
|
|
496
|
+
setViewMode(viewId);
|
|
497
|
+
}
|
|
498
|
+
}}
|
|
499
|
+
>
|
|
500
|
+
<Card
|
|
501
|
+
className={`${dashboardStyles.card} ${dashboardStyles.storageCardOverflowHidden}`}
|
|
502
|
+
>
|
|
503
|
+
<CardTitle>
|
|
504
|
+
<Flex
|
|
505
|
+
justifyContent={{ default: "justifyContentSpaceBetween" }}
|
|
506
|
+
alignItems={{ default: "alignItemsCenter" }}
|
|
507
|
+
className={dashboardStyles.storageFlexFullWidth}
|
|
508
|
+
>
|
|
509
|
+
<FlexItem>
|
|
510
|
+
<DatabaseIcon /> Disks
|
|
511
|
+
</FlexItem>
|
|
512
|
+
<ChartHeaderActions chartId={CHART_ID} title={chartTitle}>
|
|
513
|
+
<Dropdown
|
|
514
|
+
isOpen={isDropdownOpen}
|
|
515
|
+
onSelect={(_event, value) => {
|
|
516
|
+
if (isViewMode(value)) {
|
|
517
|
+
setViewMode(value);
|
|
518
|
+
}
|
|
519
|
+
setIsDropdownOpen(false);
|
|
520
|
+
}}
|
|
521
|
+
onOpenChange={setIsDropdownOpen}
|
|
522
|
+
toggle={(toggleRef: Ref<MenuToggleElement>) => (
|
|
523
|
+
<MenuToggle
|
|
524
|
+
ref={toggleRef}
|
|
525
|
+
onClick={() => setIsDropdownOpen((open) => !open)}
|
|
526
|
+
isExpanded={isDropdownOpen}
|
|
527
|
+
className={dashboardStyles.storageMenuToggleMinWidth}
|
|
528
|
+
>
|
|
529
|
+
{VIEW_MODE_LABELS[viewMode]}
|
|
530
|
+
</MenuToggle>
|
|
531
|
+
)}
|
|
532
|
+
>
|
|
533
|
+
<DropdownList>
|
|
534
|
+
<DropdownItem key="vmCount" value="vmCount">
|
|
535
|
+
{VIEW_MODE_LABELS.vmCount}
|
|
536
|
+
</DropdownItem>
|
|
537
|
+
<DropdownItem
|
|
538
|
+
key="vmCountByDiskType"
|
|
539
|
+
value="vmCountByDiskType"
|
|
540
|
+
>
|
|
541
|
+
{VIEW_MODE_LABELS.vmCountByDiskType}
|
|
542
|
+
</DropdownItem>
|
|
543
|
+
<DropdownItem key="totalSize" value="totalSize">
|
|
544
|
+
{VIEW_MODE_LABELS.totalSize}
|
|
545
|
+
</DropdownItem>
|
|
546
|
+
<DropdownItem
|
|
547
|
+
key="sharedDisks"
|
|
548
|
+
value="sharedDisks"
|
|
549
|
+
isDisabled={
|
|
550
|
+
totalWithSharedDisks !== undefined &&
|
|
551
|
+
totalWithSharedDisks === 0
|
|
552
|
+
}
|
|
553
|
+
>
|
|
554
|
+
{VIEW_MODE_LABELS.sharedDisks}
|
|
555
|
+
</DropdownItem>
|
|
556
|
+
</DropdownList>
|
|
557
|
+
</Dropdown>
|
|
558
|
+
</ChartHeaderActions>
|
|
559
|
+
</Flex>
|
|
560
|
+
</CardTitle>
|
|
561
|
+
<CardBody className={dashboardStyles.cardBodyScrollable}>
|
|
562
|
+
{viewMode === "vmCountByDiskType" ? (
|
|
563
|
+
<DiskTypeBarChart
|
|
564
|
+
data={diskTypeChartData}
|
|
565
|
+
colors={DISK_TYPE_BAR_COLORS}
|
|
566
|
+
/>
|
|
567
|
+
) : viewMode === "sharedDisks" ? (
|
|
568
|
+
sharedDisksChartData && totalVMs !== undefined ? (
|
|
569
|
+
<MigrationDonutChart
|
|
570
|
+
{...commonDonutProps}
|
|
571
|
+
data={sharedDisksChartData.map((item) => ({
|
|
572
|
+
...item,
|
|
573
|
+
countDisplay: `${item.countDisplay} VMs`,
|
|
574
|
+
}))}
|
|
575
|
+
customColors={SHARED_DISKS_COLORS}
|
|
576
|
+
title={`${totalVMs} VMs`}
|
|
577
|
+
subTitle={`${totalWithSharedDisks ?? 0} with shared disks`}
|
|
578
|
+
marginLeft="25%"
|
|
579
|
+
tooltipLabelFormatter={({ datum, percent }) =>
|
|
580
|
+
`${datum.x}: ${datum.countDisplay}\n${percent.toFixed(1)}%`
|
|
581
|
+
}
|
|
582
|
+
/>
|
|
583
|
+
) : (
|
|
584
|
+
<CardEmptyState title={REPORT_CARD_EMPTY_STATE_TITLES.storage} />
|
|
585
|
+
)
|
|
586
|
+
) : chartData.length === 0 ? (
|
|
587
|
+
<CardEmptyState title={REPORT_CARD_EMPTY_STATE_TITLES.storage} />
|
|
588
|
+
) : (
|
|
589
|
+
<MigrationDonutChart
|
|
590
|
+
{...commonDonutProps}
|
|
591
|
+
data={chartData.map((item) => ({
|
|
592
|
+
...item,
|
|
593
|
+
countDisplay:
|
|
594
|
+
viewMode === "totalSize"
|
|
595
|
+
? `${item.countDisplay} TB`
|
|
596
|
+
: `${item.countDisplay} VMs`,
|
|
597
|
+
}))}
|
|
598
|
+
legend={tierLegend}
|
|
599
|
+
title={
|
|
600
|
+
viewMode === "totalSize"
|
|
601
|
+
? `${totals.totalSize.toFixed(2)} TB`
|
|
602
|
+
: `${totals.totalVMs} VMs`
|
|
603
|
+
}
|
|
604
|
+
subTitle={
|
|
605
|
+
viewMode === "totalSize"
|
|
606
|
+
? `${totals.totalVMs} VMs`
|
|
607
|
+
: `${totals.totalSize.toFixed(2)} TB`
|
|
608
|
+
}
|
|
609
|
+
legendLabelFormatter={({ x, countDisplay }) =>
|
|
610
|
+
`${x} (${countDisplay})`
|
|
611
|
+
}
|
|
612
|
+
tooltipLabelFormatter={({ datum, percent }) =>
|
|
613
|
+
`${datum.x}: ${datum.countDisplay}\n${percent.toFixed(1)}%`
|
|
614
|
+
}
|
|
615
|
+
onItemClick={
|
|
616
|
+
viewMode === "vmCount" || viewMode === "totalSize"
|
|
617
|
+
? handleDiskTierClick
|
|
618
|
+
: undefined
|
|
619
|
+
}
|
|
620
|
+
onTitleClick={onTitleClick}
|
|
621
|
+
/>
|
|
622
|
+
)}
|
|
623
|
+
</CardBody>
|
|
624
|
+
</Card>
|
|
625
|
+
</ChartExportSurface>
|
|
626
|
+
);
|
|
627
|
+
};
|
|
628
|
+
|
|
629
|
+
StorageOverview.displayName = "StorageOverview";
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import "@testing-library/jest-dom";
|
|
2
|
+
import {
|
|
3
|
+
chart_color_blue_300,
|
|
4
|
+
chart_color_purple_100,
|
|
5
|
+
chart_color_purple_300,
|
|
6
|
+
chart_color_teal_300,
|
|
7
|
+
chart_color_yellow_400,
|
|
8
|
+
} from "@patternfly/react-tokens";
|
|
9
|
+
import { cleanup, render, screen } from "@testing-library/react";
|
|
10
|
+
import userEvent from "@testing-library/user-event";
|
|
11
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
12
|
+
import type { MigrationDonutChartProps } from "../../charts/MigrationDonutChart.js";
|
|
13
|
+
import { StorageOverview } from "../StorageOverview.js";
|
|
14
|
+
|
|
15
|
+
const donutProps = vi.hoisted(() => ({
|
|
16
|
+
current: null as MigrationDonutChartProps | null,
|
|
17
|
+
}));
|
|
18
|
+
|
|
19
|
+
vi.mock("../../charts/MigrationDonutChart.js", () => ({
|
|
20
|
+
MigrationDonutChart: (props: MigrationDonutChartProps): JSX.Element => {
|
|
21
|
+
donutProps.current = props;
|
|
22
|
+
return (
|
|
23
|
+
<div data-testid="storage-donut">
|
|
24
|
+
{props.title} {props.subTitle}
|
|
25
|
+
</div>
|
|
26
|
+
);
|
|
27
|
+
},
|
|
28
|
+
}));
|
|
29
|
+
|
|
30
|
+
afterEach(() => {
|
|
31
|
+
donutProps.current = null;
|
|
32
|
+
cleanup();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const renderedDonut = (): MigrationDonutChartProps => {
|
|
36
|
+
const props = donutProps.current;
|
|
37
|
+
if (!props) {
|
|
38
|
+
throw new Error("expected a storage donut to render");
|
|
39
|
+
}
|
|
40
|
+
return props;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const selectView = async (name: string): Promise<void> => {
|
|
44
|
+
const user = userEvent.setup();
|
|
45
|
+
await user.click(
|
|
46
|
+
screen.getByRole("button", { name: "VM count by disk size tier" }),
|
|
47
|
+
);
|
|
48
|
+
await user.click(screen.getByRole("menuitem", { name }));
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
describe("StorageOverview", () => {
|
|
52
|
+
it("shows the storage empty state when no disk tiers were collected", () => {
|
|
53
|
+
render(<StorageOverview />);
|
|
54
|
+
|
|
55
|
+
expect(screen.getByText("Disks")).toBeInTheDocument();
|
|
56
|
+
expect(screen.getByText("Storage data not collected")).toBeInTheDocument();
|
|
57
|
+
expect(screen.queryByTestId("storage-donut")).not.toBeInTheDocument();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("orders VM-count tiers and totals the VM count", () => {
|
|
61
|
+
render(
|
|
62
|
+
<StorageOverview
|
|
63
|
+
diskSizeTier={{
|
|
64
|
+
Hard: { vmCount: 3, totalSizeTB: 30 },
|
|
65
|
+
Easy: { vmCount: 10, totalSizeTB: 5 },
|
|
66
|
+
Medium: { vmCount: 6, totalSizeTB: 15 },
|
|
67
|
+
}}
|
|
68
|
+
/>,
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
expect(screen.getByTestId("storage-donut")).toHaveTextContent("19 VMs");
|
|
72
|
+
|
|
73
|
+
const props = renderedDonut();
|
|
74
|
+
expect(props.data.map((slice) => [slice.name, slice.count])).toEqual([
|
|
75
|
+
["0-10 TB", 10],
|
|
76
|
+
["11-20 TB", 6],
|
|
77
|
+
["21-50 TB", 3],
|
|
78
|
+
]);
|
|
79
|
+
expect(props.data[0]?.countDisplay).toBe("10 VMs");
|
|
80
|
+
expect(props.legend).toEqual({
|
|
81
|
+
Easy: chart_color_blue_300.value,
|
|
82
|
+
Medium: chart_color_purple_300.value,
|
|
83
|
+
Hard: chart_color_purple_100.value,
|
|
84
|
+
});
|
|
85
|
+
expect(props.legendVariant).toBe("html");
|
|
86
|
+
expect(props.subTitleColor).toBe(
|
|
87
|
+
"var(--pf-t--global--text--color--subtle)",
|
|
88
|
+
);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("switches to total disk size and formats TB totals", async () => {
|
|
92
|
+
render(
|
|
93
|
+
<StorageOverview
|
|
94
|
+
diskSizeTier={{
|
|
95
|
+
Easy: { vmCount: 10, totalSizeTB: 5.5 },
|
|
96
|
+
Medium: { vmCount: 6, totalSizeTB: 15 },
|
|
97
|
+
}}
|
|
98
|
+
/>,
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
await selectView("Total disk size by tier");
|
|
102
|
+
|
|
103
|
+
const props = renderedDonut();
|
|
104
|
+
expect(props.title).toBe("20.50 TB");
|
|
105
|
+
expect(props.subTitle).toBe("16 VMs");
|
|
106
|
+
expect(props.data[0]?.countDisplay).toBe("5.50 TB");
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("drills down into a disk-size range when a tier slice is clicked", () => {
|
|
110
|
+
const onDiskTierSelect = vi.fn();
|
|
111
|
+
render(
|
|
112
|
+
<StorageOverview
|
|
113
|
+
diskSizeTier={{ Easy: { vmCount: 10, totalSizeTB: 5 } }}
|
|
114
|
+
onDiskTierSelect={onDiskTierSelect}
|
|
115
|
+
/>,
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
const props = renderedDonut();
|
|
119
|
+
const slice = props.data[0];
|
|
120
|
+
if (!slice) {
|
|
121
|
+
throw new Error("expected a tier slice");
|
|
122
|
+
}
|
|
123
|
+
props.onItemClick?.(slice);
|
|
124
|
+
|
|
125
|
+
expect(onDiskTierSelect).toHaveBeenCalledWith({
|
|
126
|
+
min: 0,
|
|
127
|
+
max: 10 * 1024 * 1024,
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("renders the disk-type bar chart and its empty state", async () => {
|
|
132
|
+
const { rerender } = render(
|
|
133
|
+
<StorageOverview
|
|
134
|
+
diskTypes={{ NFS: { vmCount: 2 }, VMFS: { vmCount: 5 } }}
|
|
135
|
+
/>,
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
await selectView("VM count by disk type");
|
|
139
|
+
|
|
140
|
+
// Preferred order places VMFS before NFS.
|
|
141
|
+
const vmfs = screen.getByText("VMFS");
|
|
142
|
+
const nfs = screen.getByText("NFS");
|
|
143
|
+
expect(vmfs.compareDocumentPosition(nfs)).toBe(
|
|
144
|
+
Node.DOCUMENT_POSITION_FOLLOWING,
|
|
145
|
+
);
|
|
146
|
+
expect(
|
|
147
|
+
screen.getByText(/Totals may exceed the unique VM count/),
|
|
148
|
+
).toBeInTheDocument();
|
|
149
|
+
|
|
150
|
+
rerender(<StorageOverview diskTypes={{}} />);
|
|
151
|
+
expect(
|
|
152
|
+
screen.getByText("Disk type data not collected"),
|
|
153
|
+
).toBeInTheDocument();
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it("renders the shared-disks donut", async () => {
|
|
157
|
+
render(<StorageOverview totalVMs={10} totalWithSharedDisks={4} />);
|
|
158
|
+
|
|
159
|
+
await selectView("Shared disks VS. No shared disks");
|
|
160
|
+
|
|
161
|
+
const props = renderedDonut();
|
|
162
|
+
expect(props.title).toBe("10 VMs");
|
|
163
|
+
expect(props.subTitle).toBe("4 with shared disks");
|
|
164
|
+
expect(props.data.map((slice) => [slice.name, slice.count])).toEqual([
|
|
165
|
+
["With shared disks", 4],
|
|
166
|
+
["No shared disks", 6],
|
|
167
|
+
]);
|
|
168
|
+
expect(props.customColors).toEqual({
|
|
169
|
+
"With shared disks": chart_color_yellow_400.value,
|
|
170
|
+
"No shared disks": chart_color_blue_300.value,
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it("disables the shared-disks view when no VMs have shared disks", async () => {
|
|
175
|
+
const user = userEvent.setup();
|
|
176
|
+
render(<StorageOverview totalVMs={10} totalWithSharedDisks={0} />);
|
|
177
|
+
|
|
178
|
+
await user.click(
|
|
179
|
+
screen.getByRole("button", { name: "VM count by disk size tier" }),
|
|
180
|
+
);
|
|
181
|
+
const sharedItem = screen.getByRole("menuitem", {
|
|
182
|
+
name: "Shared disks VS. No shared disks",
|
|
183
|
+
});
|
|
184
|
+
expect(sharedItem).toBeDisabled();
|
|
185
|
+
|
|
186
|
+
await user.click(sharedItem);
|
|
187
|
+
// The disabled item does not switch the active view.
|
|
188
|
+
expect(
|
|
189
|
+
screen.getByRole("button", { name: "VM count by disk size tier" }),
|
|
190
|
+
).toBeInTheDocument();
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it("passes the chart legend variant through", () => {
|
|
194
|
+
render(
|
|
195
|
+
<StorageOverview
|
|
196
|
+
legendVariant="chart"
|
|
197
|
+
diskSizeTier={{ Easy: { vmCount: 2, totalSizeTB: 1 } }}
|
|
198
|
+
/>,
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
expect(renderedDonut().legendVariant).toBe("chart");
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it("keeps the configured tier palette aligned with the categorical order", () => {
|
|
205
|
+
render(
|
|
206
|
+
<StorageOverview
|
|
207
|
+
diskSizeTier={{
|
|
208
|
+
Easy: { vmCount: 1 },
|
|
209
|
+
Medium: { vmCount: 1 },
|
|
210
|
+
Hard: { vmCount: 1 },
|
|
211
|
+
White: { vmCount: 1 },
|
|
212
|
+
}}
|
|
213
|
+
/>,
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
expect(renderedDonut().legend).toEqual({
|
|
217
|
+
Easy: chart_color_blue_300.value,
|
|
218
|
+
Medium: chart_color_purple_300.value,
|
|
219
|
+
Hard: chart_color_purple_100.value,
|
|
220
|
+
"White glove": chart_color_teal_300.value,
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
});
|
package/src/report/index.ts
CHANGED
|
@@ -195,6 +195,11 @@ export {
|
|
|
195
195
|
type StandardReportExportHandlers,
|
|
196
196
|
standardReportExportOptions,
|
|
197
197
|
} from "./reportExportOptions.js";
|
|
198
|
+
export {
|
|
199
|
+
type StorageDiskTierData,
|
|
200
|
+
StorageOverview,
|
|
201
|
+
type StorageOverviewProps,
|
|
202
|
+
} from "./StorageOverview.js";
|
|
198
203
|
export { SupportTierBadge } from "./SupportTierBadge.js";
|
|
199
204
|
export {
|
|
200
205
|
ALL_TIERS_FILTER,
|