@openshift-migration-advisor/shared-components 0.28.0 → 0.29.0-24760762468f

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/dist/charts/MigrationDonutChart.d.ts.map +1 -1
  2. package/dist/charts/MigrationDonutChart.js +15 -4
  3. package/dist/report/ChartExportProvider.d.ts +1 -1
  4. package/dist/report/ChartExportProvider.d.ts.map +1 -1
  5. package/dist/report/ChartExportProvider.js +4 -1
  6. package/dist/report/FeatureStatusBadge.d.ts.map +1 -1
  7. package/dist/report/FeatureStatusBadge.js +2 -2
  8. package/dist/report/OsUpgradeRecommendationPopover.d.ts.map +1 -1
  9. package/dist/report/OsUpgradeRecommendationPopover.js +1 -7
  10. package/dist/report/ReportExportMenu.d.ts +1 -0
  11. package/dist/report/ReportExportMenu.d.ts.map +1 -1
  12. package/dist/report/ReportExportMenu.js +38 -4
  13. package/dist/report/StorageOverview.d.ts +27 -0
  14. package/dist/report/StorageOverview.d.ts.map +1 -0
  15. package/dist/report/StorageOverview.js +331 -0
  16. package/dist/report/VMMigrationStatus.d.ts +33 -0
  17. package/dist/report/VMMigrationStatus.d.ts.map +1 -0
  18. package/dist/report/VMMigrationStatus.js +129 -0
  19. package/dist/report/captureChartElement.d.ts +6 -1
  20. package/dist/report/captureChartElement.d.ts.map +1 -1
  21. package/dist/report/captureChartElement.js +13 -4
  22. package/dist/report/chartExport.js +1 -1
  23. package/dist/report/dashboardStyles.d.ts +6 -0
  24. package/dist/report/dashboardStyles.d.ts.map +1 -1
  25. package/dist/report/dashboardStyles.js +7 -4
  26. package/dist/report/index.d.ts +3 -1
  27. package/dist/report/index.d.ts.map +1 -1
  28. package/dist/report/index.js +2 -0
  29. package/dist/report/reportExportOptions.d.ts +13 -2
  30. package/dist/report/reportExportOptions.d.ts.map +1 -1
  31. package/dist/report/reportExportOptions.js +13 -5
  32. package/package.json +2 -2
  33. package/src/charts/MigrationDonutChart.tsx +27 -6
  34. package/src/charts/__tests__/MigrationDonutChart.test.tsx +53 -0
  35. package/src/report/ChartExportProvider.tsx +5 -1
  36. package/src/report/FeatureStatusBadge.tsx +10 -2
  37. package/src/report/OsUpgradeRecommendationPopover.tsx +0 -8
  38. package/src/report/ReportExportMenu.tsx +69 -13
  39. package/src/report/StorageOverview.tsx +646 -0
  40. package/src/report/VMMigrationStatus.tsx +358 -0
  41. package/src/report/__tests__/StorageOverview.test.tsx +267 -0
  42. package/src/report/__tests__/VMMigrationStatus.test.tsx +178 -0
  43. package/src/report/__tests__/captureChartElement.test.ts +47 -0
  44. package/src/report/captureChartElement.ts +18 -3
  45. package/src/report/chartExport.ts +1 -1
  46. package/src/report/dashboardStyles.ts +7 -4
  47. package/src/report/index.ts +12 -0
  48. package/src/report/reportExportOptions.ts +28 -5
@@ -0,0 +1,178 @@
1
+ import "@testing-library/jest-dom";
2
+ import { cleanup, render, screen } from "@testing-library/react";
3
+ import userEvent from "@testing-library/user-event";
4
+ import { afterEach, describe, expect, it, vi } from "vitest";
5
+ import type { MigrationDonutChartProps } from "../../charts/MigrationDonutChart.js";
6
+ import { chartColorFailure, chartColorSuccess } from "../constants.js";
7
+ import { VMMigrationStatus } from "../VMMigrationStatus.js";
8
+
9
+ const donutProps = vi.hoisted(() => ({
10
+ current: null as MigrationDonutChartProps | null,
11
+ }));
12
+
13
+ vi.mock("../../charts/MigrationDonutChart.js", () => ({
14
+ MigrationDonutChart: (props: MigrationDonutChartProps): JSX.Element => {
15
+ donutProps.current = props;
16
+ return (
17
+ <div data-testid="migration-donut">
18
+ {props.title} {props.subTitle}
19
+ </div>
20
+ );
21
+ },
22
+ }));
23
+
24
+ afterEach(() => {
25
+ donutProps.current = null;
26
+ cleanup();
27
+ });
28
+
29
+ const renderedDonut = (): MigrationDonutChartProps => {
30
+ const props = donutProps.current;
31
+ if (!props) {
32
+ throw new Error("expected a migration-status donut to render");
33
+ }
34
+ return props;
35
+ };
36
+
37
+ const selectView = async (name: string): Promise<void> => {
38
+ const user = userEvent.setup();
39
+ await user.click(
40
+ screen.getByRole("button", { name: "No issues vs with issues" }),
41
+ );
42
+ await user.click(screen.getByRole("menuitem", { name }));
43
+ };
44
+
45
+ describe("VMMigrationStatus", () => {
46
+ it("shows the empty state when there are no VMs", () => {
47
+ render(<VMMigrationStatus data={{ migratable: 0, nonMigratable: 0 }} />);
48
+
49
+ expect(screen.getByText("VM Migration Status")).toBeInTheDocument();
50
+ expect(
51
+ screen.getByText("Migration status data not collected"),
52
+ ).toBeInTheDocument();
53
+ expect(screen.queryByTestId("migration-donut")).not.toBeInTheDocument();
54
+ });
55
+
56
+ it("renders the migratable vs not-ready donut", () => {
57
+ render(<VMMigrationStatus data={{ migratable: 7, nonMigratable: 3 }} />);
58
+
59
+ expect(screen.getByTestId("migration-donut")).toHaveTextContent("10 VMs");
60
+
61
+ const props = renderedDonut();
62
+ expect(props.data.map((slice) => [slice.name, slice.count])).toEqual([
63
+ ["Migratable", 7],
64
+ ["Not ready for migration", 3],
65
+ ]);
66
+ expect(props.legend).toEqual({
67
+ Migratable: chartColorSuccess,
68
+ "Not ready for migration": chartColorFailure,
69
+ });
70
+ expect(props.legendVariant).toBe("html");
71
+ expect(props.subTitleColor).toBe(
72
+ "var(--pf-t--global--text--color--subtle)",
73
+ );
74
+ });
75
+
76
+ it("drills down into readiness when a donut slice is clicked", () => {
77
+ const onReadinessSelect = vi.fn();
78
+ render(
79
+ <VMMigrationStatus
80
+ data={{ migratable: 7, nonMigratable: 3 }}
81
+ onReadinessSelect={onReadinessSelect}
82
+ />,
83
+ );
84
+
85
+ const props = renderedDonut();
86
+ const [migratable, notReady] = props.data;
87
+ if (!migratable || !notReady) {
88
+ throw new Error("expected both readiness slices");
89
+ }
90
+
91
+ props.onItemClick?.(migratable);
92
+ expect(onReadinessSelect).toHaveBeenLastCalledWith(true);
93
+
94
+ props.onItemClick?.(notReady);
95
+ expect(onReadinessSelect).toHaveBeenLastCalledWith(false);
96
+ });
97
+
98
+ it("forwards the center-title click", () => {
99
+ const onTitleClick = vi.fn();
100
+ render(
101
+ <VMMigrationStatus
102
+ data={{ migratable: 1, nonMigratable: 0 }}
103
+ onTitleClick={onTitleClick}
104
+ />,
105
+ );
106
+
107
+ renderedDonut().onTitleClick?.();
108
+ expect(onTitleClick).toHaveBeenCalledTimes(1);
109
+ });
110
+
111
+ it("renders the issue breakdown bars in category order", async () => {
112
+ render(
113
+ <VMMigrationStatus
114
+ data={{ migratable: 1, nonMigratable: 9 }}
115
+ issuesBreakdown={{
116
+ critical: 3,
117
+ error: 2,
118
+ warning: 5,
119
+ information: 1,
120
+ advisory: 0,
121
+ }}
122
+ />,
123
+ );
124
+
125
+ await selectView("With issues breakdown");
126
+
127
+ expect(screen.queryByTestId("migration-donut")).not.toBeInTheDocument();
128
+ expect(screen.getByTitle("Critical: 3 VMs")).toBeInTheDocument();
129
+ expect(screen.getByTitle("Warning: 5 VMs")).toBeInTheDocument();
130
+ expect(screen.getByTitle("Advisory: 0 VMs")).toBeInTheDocument();
131
+ expect(
132
+ screen.getByText(/Totals may exceed the unique VM count/),
133
+ ).toBeInTheDocument();
134
+ });
135
+
136
+ it("drills down into a concern category when a bar is clicked", async () => {
137
+ const onCategorySelect = vi.fn();
138
+ render(
139
+ <VMMigrationStatus
140
+ data={{ migratable: 1, nonMigratable: 9 }}
141
+ issuesBreakdown={{
142
+ critical: 3,
143
+ error: 2,
144
+ warning: 5,
145
+ information: 1,
146
+ advisory: 0,
147
+ }}
148
+ onCategorySelect={onCategorySelect}
149
+ />,
150
+ );
151
+
152
+ await selectView("With issues breakdown");
153
+ await userEvent.setup().click(screen.getByTitle("Warning: 5 VMs"));
154
+
155
+ expect(onCategorySelect).toHaveBeenCalledWith("Warning");
156
+ });
157
+
158
+ it("shows the breakdown empty state when no issue data was collected", async () => {
159
+ render(<VMMigrationStatus data={{ migratable: 1, nonMigratable: 9 }} />);
160
+
161
+ await selectView("With issues breakdown");
162
+
163
+ expect(
164
+ screen.getByText("Issues breakdown data not collected"),
165
+ ).toBeInTheDocument();
166
+ });
167
+
168
+ it("passes the chart legend variant through", () => {
169
+ render(
170
+ <VMMigrationStatus
171
+ legendVariant="chart"
172
+ data={{ migratable: 1, nonMigratable: 1 }}
173
+ />,
174
+ );
175
+
176
+ expect(renderedDonut().legendVariant).toBe("chart");
177
+ });
178
+ });
@@ -202,4 +202,51 @@ describe("captureChartElement", () => {
202
202
  ).toBe(true);
203
203
  expect(card.style.backgroundColor).toBe("rgb(30, 30, 30)");
204
204
  });
205
+
206
+ it("relights tables and labels via the clone cascade, not by hand-painting them", async () => {
207
+ document.documentElement.classList.add("pf-v6-theme-dark");
208
+ const root = document.createElement("div");
209
+ const label = document.createElement("span");
210
+ label.className = "pf-v6-c-label pf-m-compact pf-m-outline";
211
+ const labelContent = document.createElement("span");
212
+ labelContent.className = "pf-v6-c-label__content";
213
+ label.append(labelContent);
214
+ root.append(label);
215
+ document.body.append(root);
216
+
217
+ mockedHtml2Canvas.mockImplementation(async (_element, options) => {
218
+ const clonedDoc = document.implementation.createHTMLDocument("export");
219
+ clonedDoc.documentElement.classList.add("pf-v6-theme-dark");
220
+ const cloneRoot = clonedDoc.createElement("div");
221
+ const cloneTh = clonedDoc.createElement("th");
222
+ cloneTh.className = "pf-v6-c-table__th";
223
+ const cloneLabelContent = clonedDoc.createElement("span");
224
+ cloneLabelContent.className = "pf-v6-c-label__content";
225
+ cloneRoot.append(cloneTh, cloneLabelContent);
226
+ clonedDoc.body.append(cloneRoot);
227
+
228
+ await options?.onclone?.(clonedDoc, cloneRoot);
229
+
230
+ // Dropping the dark class off the clone root is what relights these
231
+ // surfaces — the cascade does the work. We must NOT hand-paint inline
232
+ // colors onto PatternFly table/label nodes, and the live page stays dark.
233
+ expect(
234
+ clonedDoc.documentElement.classList.contains("pf-v6-theme-dark"),
235
+ ).toBe(false);
236
+ expect(cloneTh.style.backgroundColor).toBe("");
237
+ expect(cloneLabelContent.style.backgroundColor).toBe("");
238
+
239
+ const canvas = document.createElement("canvas");
240
+ canvas.width = 1;
241
+ canvas.height = 1;
242
+ return canvas;
243
+ });
244
+
245
+ await captureChartElement(root);
246
+
247
+ expect(
248
+ document.documentElement.classList.contains("pf-v6-theme-dark"),
249
+ ).toBe(true);
250
+ expect(labelContent.style.backgroundColor).toBe("");
251
+ });
205
252
  });
@@ -11,8 +11,15 @@ import {
11
11
  shouldIgnoreChartExportElement,
12
12
  } from "./chartExport.js";
13
13
 
14
- /** CSS-pixel scale; 2x made zip-all of ~15 live cards too slow. */
15
- const CHART_CAPTURE_SCALE = 1;
14
+ /** Default scale for bulk exports; single-chart PNG uses a higher scale. */
15
+ const CHART_CAPTURE_SCALE_DEFAULT = 1;
16
+ export const CHART_CAPTURE_SCALE_SINGLE = 2;
17
+ const EXPORT_FONT_FAMILY = "RedHatText, Overpass, Helvetica, Arial, sans-serif";
18
+
19
+ export type CaptureChartElementOptions = {
20
+ /** Higher scale sharpens text in PNG exports; keep at 1 for bulk PDF/zip. */
21
+ scale?: number;
22
+ };
16
23
  const HIDDEN_LAYOUT_STYLE_KEYS = [
17
24
  "display",
18
25
  "position",
@@ -187,6 +194,7 @@ function applyLightThemeSvgText(root: HTMLElement): void {
187
194
  }
188
195
  node.style.fill = LIGHT_EXPORT_TEXT;
189
196
  node.style.color = LIGHT_EXPORT_TEXT;
197
+ node.style.fontFamily = EXPORT_FONT_FAMILY;
190
198
  }
191
199
  }
192
200
 
@@ -359,7 +367,9 @@ function prepareCaptureLayout(element: HTMLElement): () => void {
359
367
 
360
368
  export async function captureChartElement(
361
369
  element: HTMLElement,
370
+ options: CaptureChartElementOptions = {},
362
371
  ): Promise<HTMLCanvasElement> {
372
+ const scale = options.scale ?? CHART_CAPTURE_SCALE_DEFAULT;
363
373
  const restoreHidden = revealHiddenChartAncestors(element);
364
374
  const restoreLayout = prepareCaptureLayout(element);
365
375
  element.setAttribute(CHART_EXPORT_CAPTURING_ATTR, "");
@@ -378,15 +388,20 @@ export async function captureChartElement(
378
388
  useCORS: true,
379
389
  backgroundColor: LIGHT_EXPORT_BACKGROUND,
380
390
  logging: false,
381
- scale: CHART_CAPTURE_SCALE,
391
+ scale,
382
392
  imageTimeout: 0,
383
393
  ignoreElements: (node) =>
384
394
  node instanceof Element &&
385
395
  shouldIgnoreChartExportElement(node, element),
386
396
  onclone: (clonedDoc, clonedElement) => {
397
+ // Dropping the dark-theme class off the clone root makes every
398
+ // PatternFly CSS-variable surface (cards, tables, labels) resolve to
399
+ // light automatically — html2canvas reads computed styles from this
400
+ // clone. Only Victory's inlined SVG text needs explicit recoloring.
387
401
  applyLightThemeToDocument(clonedDoc);
388
402
  applyComputedStylesToClone(element, clonedElement);
389
403
  applyLightThemeSvgText(clonedElement);
404
+ clonedElement.style.fontFamily = EXPORT_FONT_FAMILY;
390
405
  },
391
406
  });
392
407
  } finally {
@@ -14,7 +14,7 @@ export const chartExportScrollProps = {
14
14
 
15
15
  export const chartExportRootStyle: CSSProperties = {
16
16
  width: "100%",
17
- height: "100%",
17
+ height: "auto",
18
18
  };
19
19
 
20
20
  export type ChartExportMeta = {
@@ -30,6 +30,12 @@ export const dashboardStyles = {
30
30
  height: 100%;
31
31
  `,
32
32
 
33
+ /**
34
+ * Flex child of {@link dashboardStyles.card}. `min-height: 0` lets the body
35
+ * shrink inside the card max-height; `overflow: auto` scrolls tables and tall
36
+ * charts when needed (phantom scrollbars on donut cards are avoided by the
37
+ * card layout and ChartExportSurface sizing, not a second body class).
38
+ */
33
39
  cardBodyScrollable: css`
34
40
  overflow: auto;
35
41
  `,
@@ -38,12 +44,9 @@ export const dashboardStyles = {
38
44
  // at the same specificity, which wins in the production build due to stylesheet
39
45
  // injection order. `!important` forces `hidden` regardless of that order.
40
46
  card: css`
41
- min-height: 430px;
42
- max-height: 520px;
43
- height: 100%;
47
+ height: 520px;
44
48
  display: flex;
45
49
  flex-direction: column;
46
- justify-content: space-between;
47
50
  overflow: hidden !important;
48
51
  `,
49
52
 
@@ -192,9 +192,16 @@ export {
192
192
  export { ReportTable, type ReportTableProps } from "./ReportTable.js";
193
193
  export {
194
194
  type ReportExportOption,
195
+ type ReportExportSection,
195
196
  type StandardReportExportHandlers,
197
+ type StandardReportExportOptionsConfig,
196
198
  standardReportExportOptions,
197
199
  } from "./reportExportOptions.js";
200
+ export {
201
+ type StorageDiskTierData,
202
+ StorageOverview,
203
+ type StorageOverviewProps,
204
+ } from "./StorageOverview.js";
198
205
  export { SupportTierBadge } from "./SupportTierBadge.js";
199
206
  export {
200
207
  ALL_TIERS_FILTER,
@@ -206,6 +213,11 @@ export {
206
213
  VCenterClusterDetails,
207
214
  type VCenterClusterDetailsProps,
208
215
  } from "./VCenterClusterDetails.js";
216
+ export {
217
+ type IssuesBreakdownLike,
218
+ VMMigrationStatus,
219
+ type VMMigrationStatusProps,
220
+ } from "./VMMigrationStatus.js";
209
221
  export {
210
222
  VmPowerStates,
211
223
  type VmPowerStatesProps,
@@ -1,9 +1,16 @@
1
+ export interface ReportExportSection {
2
+ id: string;
3
+ label: string;
4
+ }
5
+
1
6
  export interface ReportExportOption {
2
7
  key: string;
3
8
  label: string;
4
9
  description?: string;
5
10
  onSelect: () => void;
6
11
  isDisabled?: boolean;
12
+ /** When set, the menu groups rows and shows `label` as the group heading. */
13
+ section?: ReportExportSection;
7
14
  }
8
15
 
9
16
  export interface StandardReportExportHandlers {
@@ -12,22 +19,38 @@ export interface StandardReportExportHandlers {
12
19
  onExportHtml?: () => void;
13
20
  }
14
21
 
22
+ export type StandardReportExportOptionsConfig = StandardReportExportHandlers & {
23
+ /** Dropdown group label for PDF / HTML / PNG. Default: `"Assessment report"`. */
24
+ assessmentSectionLabel?: string;
25
+ };
26
+
27
+ const DEFAULT_ASSESSMENT_SECTION: ReportExportSection = {
28
+ id: "assessment-report",
29
+ label: "Assessment report",
30
+ };
31
+
15
32
  /**
16
- * Standard report-download actions. Omit a handler to hide that format.
33
+ * Optional helper for chart-based assessment downloads. Omit a handler to hide
34
+ * that format. Override `assessmentSectionLabel` if product copy differs per app.
17
35
  */
18
36
  export const standardReportExportOptions = ({
19
37
  onExportPdf,
20
38
  onExportPng,
21
39
  onExportHtml,
22
- }: StandardReportExportHandlers): ReportExportOption[] => {
40
+ assessmentSectionLabel,
41
+ }: StandardReportExportOptionsConfig): ReportExportOption[] => {
42
+ const section: ReportExportSection = assessmentSectionLabel
43
+ ? { ...DEFAULT_ASSESSMENT_SECTION, label: assessmentSectionLabel }
44
+ : DEFAULT_ASSESSMENT_SECTION;
45
+
23
46
  const options: ReportExportOption[] = [];
24
47
 
25
48
  if (onExportPdf) {
26
49
  options.push({
27
50
  key: "pdf",
28
51
  label: "PDF",
29
- description: "Export the report as a PDF",
30
52
  onSelect: onExportPdf,
53
+ section,
31
54
  });
32
55
  }
33
56
 
@@ -35,8 +58,8 @@ export const standardReportExportOptions = ({
35
58
  options.push({
36
59
  key: "html",
37
60
  label: "HTML",
38
- description: "Export the report as HTML",
39
61
  onSelect: onExportHtml,
62
+ section,
40
63
  });
41
64
  }
42
65
 
@@ -44,8 +67,8 @@ export const standardReportExportOptions = ({
44
67
  options.push({
45
68
  key: "png",
46
69
  label: "PNG",
47
- description: "Download all charts as PNG files",
48
70
  onSelect: onExportPng,
71
+ section,
49
72
  });
50
73
  }
51
74