@openshift-migration-advisor/shared-components 0.28.0-9da6a7613f16 → 0.28.0-cc35dd2cf135
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 +8 -4
- 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.map +1 -1
- package/dist/report/StorageOverview.js +13 -1
- 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/index.d.ts +2 -1
- package/dist/report/index.d.ts.map +1 -1
- package/dist/report/index.js +1 -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 +1 -1
- package/src/charts/MigrationDonutChart.tsx +18 -6
- package/src/charts/__tests__/MigrationDonutChart.test.tsx +53 -0
- package/src/report/OsUpgradeRecommendationPopover.tsx +0 -8
- package/src/report/ReportExportMenu.tsx +69 -13
- package/src/report/StorageOverview.tsx +18 -1
- package/src/report/VMMigrationStatus.tsx +358 -0
- package/src/report/__tests__/StorageOverview.test.tsx +44 -0
- package/src/report/__tests__/VMMigrationStatus.test.tsx +178 -0
- package/src/report/index.ts +7 -0
- package/src/report/reportExportOptions.ts +28 -5
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
|
+
Divider,
|
|
2
3
|
Dropdown,
|
|
4
|
+
DropdownGroup,
|
|
3
5
|
DropdownItem,
|
|
4
6
|
DropdownList,
|
|
5
7
|
MenuToggle,
|
|
@@ -7,13 +9,41 @@ import {
|
|
|
7
9
|
Spinner,
|
|
8
10
|
} from "@patternfly/react-core";
|
|
9
11
|
import { DownloadIcon } from "@patternfly/react-icons";
|
|
10
|
-
import { type FC, type Ref, useState } from "react";
|
|
12
|
+
import { type FC, type ReactNode, type Ref, useMemo, useState } from "react";
|
|
11
13
|
import {
|
|
12
14
|
type ReportExportOption,
|
|
15
|
+
type ReportExportSection,
|
|
13
16
|
type StandardReportExportHandlers,
|
|
14
17
|
standardReportExportOptions,
|
|
15
18
|
} from "./reportExportOptions.js";
|
|
16
19
|
|
|
20
|
+
function sectionGroupKey(section?: ReportExportSection): string | undefined {
|
|
21
|
+
return section?.id;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function groupExportOptions(options: ReportExportOption[]): {
|
|
25
|
+
section?: ReportExportSection;
|
|
26
|
+
options: ReportExportOption[];
|
|
27
|
+
}[] {
|
|
28
|
+
const groups: {
|
|
29
|
+
section?: ReportExportSection;
|
|
30
|
+
options: ReportExportOption[];
|
|
31
|
+
}[] = [];
|
|
32
|
+
|
|
33
|
+
for (const option of options) {
|
|
34
|
+
const sectionKey = sectionGroupKey(option.section);
|
|
35
|
+
const last = groups.length > 0 ? groups[groups.length - 1] : undefined;
|
|
36
|
+
const lastKey = last ? sectionGroupKey(last.section) : undefined;
|
|
37
|
+
if (last && lastKey === sectionKey) {
|
|
38
|
+
last.options.push(option);
|
|
39
|
+
} else {
|
|
40
|
+
groups.push({ section: option.section, options: [option] });
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return groups;
|
|
45
|
+
}
|
|
46
|
+
|
|
17
47
|
export interface ReportExportMenuProps {
|
|
18
48
|
options: ReportExportOption[];
|
|
19
49
|
isLoading?: boolean;
|
|
@@ -31,10 +61,44 @@ export const ReportExportMenu: FC<ReportExportMenuProps> = ({
|
|
|
31
61
|
}) => {
|
|
32
62
|
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
|
|
33
63
|
|
|
64
|
+
const optionGroups = useMemo(() => groupExportOptions(options), [options]);
|
|
65
|
+
|
|
34
66
|
if (options.length === 0) {
|
|
35
67
|
return null;
|
|
36
68
|
}
|
|
37
69
|
|
|
70
|
+
const menuItems: ReactNode[] = [];
|
|
71
|
+
optionGroups.forEach((group, index) => {
|
|
72
|
+
if (index > 0) {
|
|
73
|
+
const dividerKey =
|
|
74
|
+
group.section?.id ?? group.options[0]?.key ?? "ungrouped";
|
|
75
|
+
menuItems.push(
|
|
76
|
+
<Divider key={`divider-before-${dividerKey}`} component="li" />,
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const items = group.options.map((option) => (
|
|
81
|
+
<DropdownItem
|
|
82
|
+
key={option.key}
|
|
83
|
+
isDisabled={option.isDisabled}
|
|
84
|
+
description={option.description}
|
|
85
|
+
onClick={option.onSelect}
|
|
86
|
+
>
|
|
87
|
+
{option.label}
|
|
88
|
+
</DropdownItem>
|
|
89
|
+
));
|
|
90
|
+
|
|
91
|
+
if (group.section) {
|
|
92
|
+
menuItems.push(
|
|
93
|
+
<DropdownGroup key={group.section.id} label={group.section.label}>
|
|
94
|
+
{items}
|
|
95
|
+
</DropdownGroup>,
|
|
96
|
+
);
|
|
97
|
+
} else {
|
|
98
|
+
menuItems.push(...items);
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
|
|
38
102
|
return (
|
|
39
103
|
<Dropdown
|
|
40
104
|
isOpen={isDropdownOpen}
|
|
@@ -70,18 +134,7 @@ export const ReportExportMenu: FC<ReportExportMenuProps> = ({
|
|
|
70
134
|
</MenuToggle>
|
|
71
135
|
)}
|
|
72
136
|
>
|
|
73
|
-
<DropdownList>
|
|
74
|
-
{options.map((option) => (
|
|
75
|
-
<DropdownItem
|
|
76
|
-
key={option.key}
|
|
77
|
-
isDisabled={option.isDisabled}
|
|
78
|
-
description={option.description}
|
|
79
|
-
onClick={option.onSelect}
|
|
80
|
-
>
|
|
81
|
-
{option.label}
|
|
82
|
-
</DropdownItem>
|
|
83
|
-
))}
|
|
84
|
-
</DropdownList>
|
|
137
|
+
<DropdownList>{menuItems}</DropdownList>
|
|
85
138
|
</Dropdown>
|
|
86
139
|
);
|
|
87
140
|
};
|
|
@@ -94,6 +147,7 @@ export interface ExportReportButtonProps extends StandardReportExportHandlers {
|
|
|
94
147
|
loadingLabel?: string | null;
|
|
95
148
|
isDisabled?: boolean;
|
|
96
149
|
toggleLabel?: string;
|
|
150
|
+
assessmentSectionLabel?: string;
|
|
97
151
|
}
|
|
98
152
|
|
|
99
153
|
export const ExportReportButton: FC<ExportReportButtonProps> = ({
|
|
@@ -101,6 +155,7 @@ export const ExportReportButton: FC<ExportReportButtonProps> = ({
|
|
|
101
155
|
onExportPng,
|
|
102
156
|
onExportHtml,
|
|
103
157
|
extraOptions = [],
|
|
158
|
+
assessmentSectionLabel,
|
|
104
159
|
...menuProps
|
|
105
160
|
}) => (
|
|
106
161
|
<ReportExportMenu
|
|
@@ -109,6 +164,7 @@ export const ExportReportButton: FC<ExportReportButtonProps> = ({
|
|
|
109
164
|
onExportPdf,
|
|
110
165
|
onExportPng,
|
|
111
166
|
onExportHtml,
|
|
167
|
+
assessmentSectionLabel,
|
|
112
168
|
}),
|
|
113
169
|
...extraOptions,
|
|
114
170
|
]}
|
|
@@ -427,6 +427,20 @@ export const StorageOverview: FC<StorageOverviewProps> = ({
|
|
|
427
427
|
.sort((a, b) => orderIndex(a.name) - orderIndex(b.name));
|
|
428
428
|
}, [diskTypes]);
|
|
429
429
|
|
|
430
|
+
const isSharedDisksViewAvailable = totalWithSharedDisks !== 0;
|
|
431
|
+
|
|
432
|
+
// Exclude the "Shared disks" view from the chart export cycle (used to
|
|
433
|
+
// build the PDF/PNG/HTML exports) when that view is disabled in the
|
|
434
|
+
// dropdown, so the exported report stays consistent with what's
|
|
435
|
+
// selectable on screen.
|
|
436
|
+
const availableExportViewLabels = useMemo(() => {
|
|
437
|
+
if (isSharedDisksViewAvailable) {
|
|
438
|
+
return VIEW_MODE_LABELS;
|
|
439
|
+
}
|
|
440
|
+
const { sharedDisks: _sharedDisks, ...rest } = VIEW_MODE_LABELS;
|
|
441
|
+
return rest;
|
|
442
|
+
}, [isSharedDisksViewAvailable]);
|
|
443
|
+
|
|
430
444
|
const sharedDisksChartData = useMemo(() => {
|
|
431
445
|
if (
|
|
432
446
|
totalWithSharedDisks === undefined ||
|
|
@@ -489,7 +503,10 @@ export const StorageOverview: FC<StorageOverviewProps> = ({
|
|
|
489
503
|
<ChartExportSurface
|
|
490
504
|
id={CHART_ID}
|
|
491
505
|
title={chartTitle}
|
|
492
|
-
exportViews={chartExportViewsFromLabels(
|
|
506
|
+
exportViews={chartExportViewsFromLabels(
|
|
507
|
+
CHART_TITLE,
|
|
508
|
+
availableExportViewLabels,
|
|
509
|
+
)}
|
|
493
510
|
activeExportViewId={viewMode}
|
|
494
511
|
onExportViewChange={(viewId) => {
|
|
495
512
|
if (isViewMode(viewId)) {
|
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
import { css } from "@emotion/css";
|
|
2
|
+
import {
|
|
3
|
+
Card,
|
|
4
|
+
CardBody,
|
|
5
|
+
CardTitle,
|
|
6
|
+
Content,
|
|
7
|
+
Dropdown,
|
|
8
|
+
DropdownItem,
|
|
9
|
+
DropdownList,
|
|
10
|
+
Flex,
|
|
11
|
+
FlexItem,
|
|
12
|
+
MenuToggle,
|
|
13
|
+
type MenuToggleElement,
|
|
14
|
+
} from "@patternfly/react-core";
|
|
15
|
+
import { VirtualMachineIcon } from "@patternfly/react-icons";
|
|
16
|
+
import {
|
|
17
|
+
chart_color_blue_300,
|
|
18
|
+
chart_color_purple_100,
|
|
19
|
+
chart_color_purple_300,
|
|
20
|
+
chart_color_teal_200,
|
|
21
|
+
chart_color_yellow_400,
|
|
22
|
+
} from "@patternfly/react-tokens";
|
|
23
|
+
import type { FC, Ref } from "react";
|
|
24
|
+
import { useMemo, useState } from "react";
|
|
25
|
+
import {
|
|
26
|
+
MigrationDonutChart,
|
|
27
|
+
type MigrationDonutChartLegendVariant,
|
|
28
|
+
} from "../charts/MigrationDonutChart.js";
|
|
29
|
+
import { CardEmptyState } from "./CardEmptyState.js";
|
|
30
|
+
import { ChartHeaderActions } from "./ChartDownloadButton.js";
|
|
31
|
+
import { ChartExportSurface } from "./ChartExportSurface.js";
|
|
32
|
+
import { chartExportViewsFromLabels } from "./chartExport.js";
|
|
33
|
+
import {
|
|
34
|
+
chartColorFailure,
|
|
35
|
+
chartColorSuccess,
|
|
36
|
+
REPORT_CARD_EMPTY_STATE_TITLES,
|
|
37
|
+
} from "./constants.js";
|
|
38
|
+
import { dashboardStyles } from "./dashboardStyles.js";
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* VM-issue counts this card reads. agent-sdk's `IssuesBreakdown` satisfies it;
|
|
42
|
+
* each field stays optional so older or partial payloads do not throw.
|
|
43
|
+
*/
|
|
44
|
+
export interface IssuesBreakdownLike {
|
|
45
|
+
critical?: number;
|
|
46
|
+
error?: number;
|
|
47
|
+
warning?: number;
|
|
48
|
+
information?: number;
|
|
49
|
+
advisory?: number;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface VMMigrationStatusProps {
|
|
53
|
+
data: {
|
|
54
|
+
migratable: number;
|
|
55
|
+
nonMigratable: number;
|
|
56
|
+
};
|
|
57
|
+
issuesBreakdown?: IssuesBreakdownLike;
|
|
58
|
+
/**
|
|
59
|
+
* Donut-slice click. `true` for the migratable slice, `false` otherwise.
|
|
60
|
+
* Agent-ui maps it to a VM migration-readiness filter; ui-app omits it.
|
|
61
|
+
*/
|
|
62
|
+
onReadinessSelect?: (isMigratable: boolean) => void;
|
|
63
|
+
/** Breakdown-bar click. The argument is the concern category label. */
|
|
64
|
+
onCategorySelect?: (category: string) => void;
|
|
65
|
+
/** Center-title click. Agent-ui opens the VM list with no extra filters. */
|
|
66
|
+
onTitleClick?: () => void;
|
|
67
|
+
/** `"html"` in agent-ui; ui-app passes `"chart"`. */
|
|
68
|
+
legendVariant?: MigrationDonutChartLegendVariant;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
type ViewMode = "issuesVsNoIssues" | "issuesBreakdown";
|
|
72
|
+
|
|
73
|
+
const VIEW_MODE_LABELS: Record<ViewMode, string> = {
|
|
74
|
+
issuesVsNoIssues: "No issues vs with issues",
|
|
75
|
+
issuesBreakdown: "With issues breakdown",
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const CHART_ID = "vm-migration-status";
|
|
79
|
+
const CHART_TITLE = "VM migration status";
|
|
80
|
+
|
|
81
|
+
const MIGRATABLE_LABEL = "Migratable";
|
|
82
|
+
const NOT_MIGRATABLE_LABEL = "Not ready for migration";
|
|
83
|
+
|
|
84
|
+
const CATEGORY_ORDER: Array<{ label: string; key: keyof IssuesBreakdownLike }> =
|
|
85
|
+
[
|
|
86
|
+
{ label: "Critical", key: "critical" },
|
|
87
|
+
{ label: "Error", key: "error" },
|
|
88
|
+
{ label: "Warning", key: "warning" },
|
|
89
|
+
{ label: "Information", key: "information" },
|
|
90
|
+
{ label: "Advisory", key: "advisory" },
|
|
91
|
+
];
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Same categorical order as the previous breakdown hex palette, mapped onto
|
|
95
|
+
* PatternFly `chart_color_*` tokens (nearest token where the old hex was not
|
|
96
|
+
* itself a token).
|
|
97
|
+
*/
|
|
98
|
+
const CATEGORY_COLORS: Record<string, string> = {
|
|
99
|
+
Critical: chart_color_blue_300.value,
|
|
100
|
+
Error: chart_color_purple_300.value,
|
|
101
|
+
Warning: chart_color_purple_100.value,
|
|
102
|
+
Information: chart_color_teal_200.value,
|
|
103
|
+
Advisory: chart_color_yellow_400.value,
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const menuToggleMinWidth = css`
|
|
107
|
+
min-width: 250px;
|
|
108
|
+
`;
|
|
109
|
+
|
|
110
|
+
const isViewMode = (value: string | number | undefined): value is ViewMode =>
|
|
111
|
+
value === "issuesVsNoIssues" || value === "issuesBreakdown";
|
|
112
|
+
|
|
113
|
+
const toCount = (value: number | undefined): number =>
|
|
114
|
+
typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : 0;
|
|
115
|
+
|
|
116
|
+
export const VMMigrationStatus: FC<VMMigrationStatusProps> = ({
|
|
117
|
+
data,
|
|
118
|
+
issuesBreakdown,
|
|
119
|
+
onReadinessSelect,
|
|
120
|
+
onCategorySelect,
|
|
121
|
+
onTitleClick,
|
|
122
|
+
legendVariant = "html",
|
|
123
|
+
}) => {
|
|
124
|
+
const [viewMode, setViewMode] = useState<ViewMode>("issuesVsNoIssues");
|
|
125
|
+
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
|
|
126
|
+
|
|
127
|
+
const totalVMs = data.migratable + data.nonMigratable;
|
|
128
|
+
|
|
129
|
+
const donutData = [
|
|
130
|
+
{
|
|
131
|
+
name: MIGRATABLE_LABEL,
|
|
132
|
+
count: data.migratable,
|
|
133
|
+
countDisplay: `${data.migratable} VMs`,
|
|
134
|
+
legendCategory: MIGRATABLE_LABEL,
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
name: NOT_MIGRATABLE_LABEL,
|
|
138
|
+
count: data.nonMigratable,
|
|
139
|
+
countDisplay: `${data.nonMigratable} VMs`,
|
|
140
|
+
legendCategory: NOT_MIGRATABLE_LABEL,
|
|
141
|
+
},
|
|
142
|
+
];
|
|
143
|
+
|
|
144
|
+
const legend = {
|
|
145
|
+
[MIGRATABLE_LABEL]: chartColorSuccess,
|
|
146
|
+
[NOT_MIGRATABLE_LABEL]: chartColorFailure,
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
const breakdownData = useMemo(() => {
|
|
150
|
+
if (!issuesBreakdown) {
|
|
151
|
+
return [];
|
|
152
|
+
}
|
|
153
|
+
return CATEGORY_ORDER.map(({ label, key }) => ({
|
|
154
|
+
name: label,
|
|
155
|
+
count: toCount(issuesBreakdown[key]),
|
|
156
|
+
}));
|
|
157
|
+
}, [issuesBreakdown]);
|
|
158
|
+
|
|
159
|
+
const maxCount = useMemo(
|
|
160
|
+
() =>
|
|
161
|
+
breakdownData.length > 0
|
|
162
|
+
? Math.max(...breakdownData.map((d) => d.count))
|
|
163
|
+
: 0,
|
|
164
|
+
[breakdownData],
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
const chartTitle = `${CHART_TITLE} — ${VIEW_MODE_LABELS[viewMode]}`;
|
|
168
|
+
|
|
169
|
+
return (
|
|
170
|
+
<ChartExportSurface
|
|
171
|
+
id={CHART_ID}
|
|
172
|
+
title={chartTitle}
|
|
173
|
+
exportViews={chartExportViewsFromLabels(CHART_TITLE, VIEW_MODE_LABELS)}
|
|
174
|
+
activeExportViewId={viewMode}
|
|
175
|
+
onExportViewChange={(viewId) => {
|
|
176
|
+
if (isViewMode(viewId)) {
|
|
177
|
+
setViewMode(viewId);
|
|
178
|
+
}
|
|
179
|
+
}}
|
|
180
|
+
>
|
|
181
|
+
<Card className={dashboardStyles.card}>
|
|
182
|
+
<CardTitle>
|
|
183
|
+
<Flex
|
|
184
|
+
alignItems={{ default: "alignItemsCenter" }}
|
|
185
|
+
justifyContent={{ default: "justifyContentSpaceBetween" }}
|
|
186
|
+
>
|
|
187
|
+
<FlexItem>
|
|
188
|
+
<VirtualMachineIcon /> VM Migration Status
|
|
189
|
+
</FlexItem>
|
|
190
|
+
<ChartHeaderActions chartId={CHART_ID} title={chartTitle}>
|
|
191
|
+
<Dropdown
|
|
192
|
+
isOpen={isDropdownOpen}
|
|
193
|
+
onSelect={(_event, value) => {
|
|
194
|
+
if (isViewMode(value)) {
|
|
195
|
+
setViewMode(value);
|
|
196
|
+
}
|
|
197
|
+
setIsDropdownOpen(false);
|
|
198
|
+
}}
|
|
199
|
+
onOpenChange={setIsDropdownOpen}
|
|
200
|
+
toggle={(toggleRef: Ref<MenuToggleElement>) => (
|
|
201
|
+
<MenuToggle
|
|
202
|
+
ref={toggleRef}
|
|
203
|
+
onClick={() => setIsDropdownOpen((open) => !open)}
|
|
204
|
+
isExpanded={isDropdownOpen}
|
|
205
|
+
className={menuToggleMinWidth}
|
|
206
|
+
>
|
|
207
|
+
{VIEW_MODE_LABELS[viewMode]}
|
|
208
|
+
</MenuToggle>
|
|
209
|
+
)}
|
|
210
|
+
>
|
|
211
|
+
<DropdownList>
|
|
212
|
+
<DropdownItem value="issuesVsNoIssues" key="issuesVsNoIssues">
|
|
213
|
+
{VIEW_MODE_LABELS.issuesVsNoIssues}
|
|
214
|
+
</DropdownItem>
|
|
215
|
+
<DropdownItem value="issuesBreakdown" key="issuesBreakdown">
|
|
216
|
+
{VIEW_MODE_LABELS.issuesBreakdown}
|
|
217
|
+
</DropdownItem>
|
|
218
|
+
</DropdownList>
|
|
219
|
+
</Dropdown>
|
|
220
|
+
</ChartHeaderActions>
|
|
221
|
+
</Flex>
|
|
222
|
+
</CardTitle>
|
|
223
|
+
<CardBody className={dashboardStyles.cardBodyScrollable}>
|
|
224
|
+
{viewMode === "issuesVsNoIssues" ? (
|
|
225
|
+
totalVMs === 0 ? (
|
|
226
|
+
<CardEmptyState
|
|
227
|
+
title={REPORT_CARD_EMPTY_STATE_TITLES.migrationStatus}
|
|
228
|
+
/>
|
|
229
|
+
) : (
|
|
230
|
+
<MigrationDonutChart
|
|
231
|
+
legendVariant={legendVariant}
|
|
232
|
+
data={donutData}
|
|
233
|
+
legend={legend}
|
|
234
|
+
height={300}
|
|
235
|
+
width={420}
|
|
236
|
+
donutThickness={18}
|
|
237
|
+
padAngle={1}
|
|
238
|
+
title={`${totalVMs}`}
|
|
239
|
+
subTitle="VMs"
|
|
240
|
+
subTitleColor="var(--pf-t--global--text--color--subtle)"
|
|
241
|
+
titleFontSize={34}
|
|
242
|
+
legendLabelFormatter={({ x, countDisplay }) =>
|
|
243
|
+
`${x} (${countDisplay})`
|
|
244
|
+
}
|
|
245
|
+
onItemClick={
|
|
246
|
+
onReadinessSelect
|
|
247
|
+
? (item) =>
|
|
248
|
+
onReadinessSelect(item.name === MIGRATABLE_LABEL)
|
|
249
|
+
: undefined
|
|
250
|
+
}
|
|
251
|
+
onTitleClick={onTitleClick}
|
|
252
|
+
/>
|
|
253
|
+
)
|
|
254
|
+
) : breakdownData.length === 0 ? (
|
|
255
|
+
<CardEmptyState
|
|
256
|
+
title={REPORT_CARD_EMPTY_STATE_TITLES.issuesBreakdown}
|
|
257
|
+
/>
|
|
258
|
+
) : (
|
|
259
|
+
<div>
|
|
260
|
+
<div className={dashboardStyles.storageChartWrapper}>
|
|
261
|
+
<Flex
|
|
262
|
+
direction={{ default: "row" }}
|
|
263
|
+
alignItems={{ default: "alignItemsFlexEnd" }}
|
|
264
|
+
justifyContent={{ default: "justifyContentCenter" }}
|
|
265
|
+
spaceItems={{ default: "spaceItemsMd" }}
|
|
266
|
+
style={{
|
|
267
|
+
height: "250px",
|
|
268
|
+
width: "100%",
|
|
269
|
+
}}
|
|
270
|
+
>
|
|
271
|
+
{breakdownData.map((item) => {
|
|
272
|
+
const heightPercentage =
|
|
273
|
+
maxCount > 0 ? (item.count / maxCount) * 100 : 0;
|
|
274
|
+
const minHeightPercentage = item.count > 0 ? 20 : 0;
|
|
275
|
+
const finalHeightPercentage = Math.max(
|
|
276
|
+
heightPercentage,
|
|
277
|
+
minHeightPercentage,
|
|
278
|
+
);
|
|
279
|
+
const barColor =
|
|
280
|
+
CATEGORY_COLORS[item.name] || CATEGORY_COLORS.Critical;
|
|
281
|
+
|
|
282
|
+
return (
|
|
283
|
+
<Flex
|
|
284
|
+
key={item.name}
|
|
285
|
+
direction={{ default: "column" }}
|
|
286
|
+
alignItems={{ default: "alignItemsCenter" }}
|
|
287
|
+
spaceItems={{ default: "spaceItemsSm" }}
|
|
288
|
+
style={{ flex: "1", maxWidth: "120px" }}
|
|
289
|
+
>
|
|
290
|
+
<FlexItem
|
|
291
|
+
style={{
|
|
292
|
+
height: "200px",
|
|
293
|
+
display: "flex",
|
|
294
|
+
alignItems: "flex-end",
|
|
295
|
+
width: "100%",
|
|
296
|
+
justifyContent: "center",
|
|
297
|
+
}}
|
|
298
|
+
>
|
|
299
|
+
<button
|
|
300
|
+
type="button"
|
|
301
|
+
onClick={() => onCategorySelect?.(item.name)}
|
|
302
|
+
title={`${item.name}: ${item.count} VMs`}
|
|
303
|
+
style={{
|
|
304
|
+
width: "60px",
|
|
305
|
+
height: `${finalHeightPercentage}%`,
|
|
306
|
+
backgroundColor: barColor,
|
|
307
|
+
transition: "height 0.3s ease",
|
|
308
|
+
borderRadius: "4px 4px 0 0",
|
|
309
|
+
cursor: "pointer",
|
|
310
|
+
border: "none",
|
|
311
|
+
padding: 0,
|
|
312
|
+
}}
|
|
313
|
+
/>
|
|
314
|
+
</FlexItem>
|
|
315
|
+
<FlexItem>
|
|
316
|
+
<Content
|
|
317
|
+
component="small"
|
|
318
|
+
style={{
|
|
319
|
+
fontSize: "12px",
|
|
320
|
+
textAlign: "center",
|
|
321
|
+
wordBreak: "break-word",
|
|
322
|
+
color:
|
|
323
|
+
"var(--pf-t--global--text--color--regular)",
|
|
324
|
+
}}
|
|
325
|
+
>
|
|
326
|
+
{item.name}
|
|
327
|
+
<br />({item.count} VMs)
|
|
328
|
+
</Content>
|
|
329
|
+
</FlexItem>
|
|
330
|
+
</Flex>
|
|
331
|
+
);
|
|
332
|
+
})}
|
|
333
|
+
</Flex>
|
|
334
|
+
</div>
|
|
335
|
+
<div>
|
|
336
|
+
<Content
|
|
337
|
+
component="small"
|
|
338
|
+
style={{
|
|
339
|
+
fontSize: "12px",
|
|
340
|
+
color: "var(--pf-t--global--text--color--subtle)",
|
|
341
|
+
marginTop: "16px",
|
|
342
|
+
textAlign: "center",
|
|
343
|
+
display: "block",
|
|
344
|
+
}}
|
|
345
|
+
>
|
|
346
|
+
Totals may exceed the unique VM count because a VM can appear
|
|
347
|
+
in multiple categories
|
|
348
|
+
</Content>
|
|
349
|
+
</div>
|
|
350
|
+
</div>
|
|
351
|
+
)}
|
|
352
|
+
</CardBody>
|
|
353
|
+
</Card>
|
|
354
|
+
</ChartExportSurface>
|
|
355
|
+
);
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
VMMigrationStatus.displayName = "VMMigrationStatus";
|
|
@@ -27,6 +27,23 @@ vi.mock("../../charts/MigrationDonutChart.js", () => ({
|
|
|
27
27
|
},
|
|
28
28
|
}));
|
|
29
29
|
|
|
30
|
+
vi.mock("../ChartExportSurface.js", () => ({
|
|
31
|
+
ChartExportSurface: ({
|
|
32
|
+
exportViews,
|
|
33
|
+
children,
|
|
34
|
+
}: {
|
|
35
|
+
exportViews?: Array<{ id: string; title: string }>;
|
|
36
|
+
children?: React.ReactNode;
|
|
37
|
+
}): JSX.Element => (
|
|
38
|
+
<div
|
|
39
|
+
data-testid="chart-export-surface"
|
|
40
|
+
data-export-view-ids={JSON.stringify(exportViews?.map((view) => view.id))}
|
|
41
|
+
>
|
|
42
|
+
{children}
|
|
43
|
+
</div>
|
|
44
|
+
),
|
|
45
|
+
}));
|
|
46
|
+
|
|
30
47
|
afterEach(() => {
|
|
31
48
|
donutProps.current = null;
|
|
32
49
|
cleanup();
|
|
@@ -171,6 +188,33 @@ describe("StorageOverview", () => {
|
|
|
171
188
|
});
|
|
172
189
|
});
|
|
173
190
|
|
|
191
|
+
it("excludes the shared disks view from the chart export cycle when disabled, so PDF/PNG/HTML exports stay consistent with the dropdown", () => {
|
|
192
|
+
render(<StorageOverview totalVMs={10} totalWithSharedDisks={0} />);
|
|
193
|
+
|
|
194
|
+
const exportViewIds = JSON.parse(
|
|
195
|
+
screen
|
|
196
|
+
.getByTestId("chart-export-surface")
|
|
197
|
+
.getAttribute("data-export-view-ids") || "[]",
|
|
198
|
+
) as string[];
|
|
199
|
+
|
|
200
|
+
expect(exportViewIds).not.toContain("sharedDisks");
|
|
201
|
+
expect(exportViewIds).toEqual(
|
|
202
|
+
expect.arrayContaining(["totalSize", "vmCount", "vmCountByDiskType"]),
|
|
203
|
+
);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it("includes the shared disks view in the chart export cycle when available", () => {
|
|
207
|
+
render(<StorageOverview totalVMs={10} totalWithSharedDisks={4} />);
|
|
208
|
+
|
|
209
|
+
const exportViewIds = JSON.parse(
|
|
210
|
+
screen
|
|
211
|
+
.getByTestId("chart-export-surface")
|
|
212
|
+
.getAttribute("data-export-view-ids") || "[]",
|
|
213
|
+
) as string[];
|
|
214
|
+
|
|
215
|
+
expect(exportViewIds).toContain("sharedDisks");
|
|
216
|
+
});
|
|
217
|
+
|
|
174
218
|
it("disables the shared-disks view when no VMs have shared disks", async () => {
|
|
175
219
|
const user = userEvent.setup();
|
|
176
220
|
render(<StorageOverview totalVMs={10} totalWithSharedDisks={0} />);
|