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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/dist/charts/MigrationDonutChart.d.ts.map +1 -1
  2. package/dist/charts/MigrationDonutChart.js +7 -0
  3. package/dist/report/ChartExportProvider.d.ts +4 -3
  4. package/dist/report/ChartExportProvider.d.ts.map +1 -1
  5. package/dist/report/ChartExportProvider.js +7 -4
  6. package/dist/report/ClustersOverview.d.ts +24 -0
  7. package/dist/report/ClustersOverview.d.ts.map +1 -0
  8. package/dist/report/ClustersOverview.js +228 -0
  9. package/dist/report/ClustersOverviewStyles.d.ts +12 -0
  10. package/dist/report/ClustersOverviewStyles.d.ts.map +1 -0
  11. package/dist/report/ClustersOverviewStyles.js +101 -0
  12. package/dist/report/CpuAndMemoryOverview.d.ts +19 -0
  13. package/dist/report/CpuAndMemoryOverview.d.ts.map +1 -0
  14. package/dist/report/CpuAndMemoryOverview.js +112 -0
  15. package/dist/report/FeatureStatusBadge.js +1 -1
  16. package/dist/report/ReportExportMenu.d.ts +1 -0
  17. package/dist/report/ReportExportMenu.d.ts.map +1 -1
  18. package/dist/report/ReportExportMenu.js +38 -4
  19. package/dist/report/StorageOverview.d.ts +27 -0
  20. package/dist/report/StorageOverview.d.ts.map +1 -0
  21. package/dist/report/StorageOverview.js +319 -0
  22. package/dist/report/VMMigrationStatus.d.ts +33 -0
  23. package/dist/report/VMMigrationStatus.d.ts.map +1 -0
  24. package/dist/report/VMMigrationStatus.js +129 -0
  25. package/dist/report/captureChartElement.d.ts +6 -1
  26. package/dist/report/captureChartElement.d.ts.map +1 -1
  27. package/dist/report/captureChartElement.js +41 -12
  28. package/dist/report/chartExport.d.ts +23 -0
  29. package/dist/report/chartExport.d.ts.map +1 -1
  30. package/dist/report/chartExport.js +21 -1
  31. package/dist/report/chartExportContext.d.ts +6 -2
  32. package/dist/report/chartExportContext.d.ts.map +1 -1
  33. package/dist/report/dashboardStyles.d.ts +6 -0
  34. package/dist/report/dashboardStyles.d.ts.map +1 -1
  35. package/dist/report/dashboardStyles.js +7 -4
  36. package/dist/report/index.d.ts +7 -3
  37. package/dist/report/index.d.ts.map +1 -1
  38. package/dist/report/index.js +4 -0
  39. package/dist/report/pdfExport.d.ts +2 -2
  40. package/dist/report/pdfExport.d.ts.map +1 -1
  41. package/dist/report/pdfExport.js +52 -3
  42. package/dist/report/reportExportOptions.d.ts +13 -2
  43. package/dist/report/reportExportOptions.d.ts.map +1 -1
  44. package/dist/report/reportExportOptions.js +13 -5
  45. package/package.json +2 -2
  46. package/src/charts/MigrationDonutChart.tsx +9 -0
  47. package/src/report/ChartExportProvider.tsx +18 -6
  48. package/src/report/ClustersOverview.tsx +451 -0
  49. package/src/report/ClustersOverviewStyles.ts +110 -0
  50. package/src/report/CpuAndMemoryOverview.tsx +277 -0
  51. package/src/report/FeatureStatusBadge.tsx +1 -1
  52. package/src/report/ReportExportMenu.tsx +69 -13
  53. package/src/report/StorageOverview.tsx +629 -0
  54. package/src/report/VMMigrationStatus.tsx +358 -0
  55. package/src/report/__tests__/ChartExportProvider.test.tsx +110 -2
  56. package/src/report/__tests__/ClustersOverview.test.tsx +241 -0
  57. package/src/report/__tests__/CpuAndMemoryOverview.test.tsx +200 -0
  58. package/src/report/__tests__/StorageOverview.test.tsx +223 -0
  59. package/src/report/__tests__/VMMigrationStatus.test.tsx +178 -0
  60. package/src/report/__tests__/captureChartElement.test.ts +120 -0
  61. package/src/report/__tests__/chartExportPackagers.test.ts +75 -0
  62. package/src/report/captureChartElement.ts +55 -10
  63. package/src/report/chartExport.ts +41 -1
  64. package/src/report/chartExportContext.tsx +9 -1
  65. package/src/report/dashboardStyles.ts +7 -4
  66. package/src/report/index.ts +24 -0
  67. package/src/report/pdfExport.ts +81 -2
  68. package/src/report/reportExportOptions.ts +28 -5
@@ -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";