@openshift-migration-advisor/shared-components 0.25.0-0ad1e70d1fa5 → 0.25.0-2ba17b54a3c7

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.
@@ -0,0 +1,145 @@
1
+ import "@testing-library/jest-dom";
2
+ import {
3
+ chart_color_blue_300,
4
+ chart_color_green_300,
5
+ chart_color_purple_100,
6
+ chart_color_purple_300,
7
+ chart_color_teal_200,
8
+ chart_color_yellow_400,
9
+ } from "@patternfly/react-tokens";
10
+ import { cleanup, render, screen } from "@testing-library/react";
11
+ import { afterEach, describe, expect, it, vi } from "vitest";
12
+ import type { MigrationDonutChartProps } from "../../charts/MigrationDonutChart.js";
13
+ import { HostsOverview } from "../HostsOverview.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="hosts-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 the host model donut to render");
39
+ }
40
+ return props;
41
+ };
42
+
43
+ describe("HostsOverview", () => {
44
+ it("shows the shared empty state when no hosts are available", () => {
45
+ render(<HostsOverview />);
46
+
47
+ expect(screen.getByText("Host distribution by model")).toBeInTheDocument();
48
+ expect(screen.getByText("Top 5 models")).toBeInTheDocument();
49
+ expect(screen.getByText("Host data not collected")).toBeInTheDocument();
50
+ expect(screen.queryByTestId("hosts-donut")).not.toBeInTheDocument();
51
+ });
52
+
53
+ it("buckets host counts into the top 5 models plus Other models", () => {
54
+ render(
55
+ <HostsOverview
56
+ hosts={[
57
+ { model: "M1" },
58
+ { model: "M1" },
59
+ { model: "M1" },
60
+ { model: "M1" },
61
+ { model: "M1" },
62
+ { model: "M1" },
63
+ { model: "M2" },
64
+ { model: "M2" },
65
+ { model: "M2" },
66
+ { model: "M2" },
67
+ { model: "M2" },
68
+ { model: "M3" },
69
+ { model: "M3" },
70
+ { model: "M3" },
71
+ { model: "M3" },
72
+ { model: "M4" },
73
+ { model: "M4" },
74
+ { model: "M4" },
75
+ { model: "M5" },
76
+ { model: "M5" },
77
+ { model: "M6" },
78
+ { model: "M7" },
79
+ ]}
80
+ />,
81
+ );
82
+
83
+ expect(screen.getByTestId("hosts-donut")).toHaveTextContent("22 Hosts");
84
+
85
+ const props = renderedDonut();
86
+ expect(props.data.map((slice) => [slice.name, slice.count])).toEqual([
87
+ ["M1", 6],
88
+ ["M2", 5],
89
+ ["M3", 4],
90
+ ["M4", 3],
91
+ ["M5", 2],
92
+ ["Other models", 2],
93
+ ]);
94
+ expect(props.data[5]?.countDisplay).toBe("2 hosts");
95
+ expect(props.legend).toEqual({
96
+ M1: chart_color_blue_300.value,
97
+ M2: chart_color_purple_300.value,
98
+ M3: chart_color_purple_100.value,
99
+ M4: chart_color_teal_200.value,
100
+ M5: chart_color_yellow_400.value,
101
+ "Other models": chart_color_green_300.value,
102
+ });
103
+ expect(props.legendVariant).toBe("html");
104
+ expect(props.height).toBe(300);
105
+ expect(props.width).toBe(420);
106
+ expect(props.donutThickness).toBe(18);
107
+ expect(props.titleFontSize).toBe(34);
108
+ expect(props.legendWidth).toBe(680);
109
+ expect(props.itemsPerRow).toBe(2);
110
+ expect(props.labelFontSize).toBe(16);
111
+ expect(props.marginLeft).toBe("0%");
112
+ expect(props.subTitleColor).toBe(
113
+ "var(--pf-t--global--text--color--subtle)",
114
+ );
115
+ expect(
116
+ props.tooltipLabelFormatter?.({
117
+ datum: {
118
+ x: "M1",
119
+ y: 6,
120
+ countDisplay: "6 hosts",
121
+ legendCategory: "M1",
122
+ },
123
+ percent: 27.2727,
124
+ total: 22,
125
+ }),
126
+ ).toBe("6 hosts\n27.3%");
127
+ });
128
+
129
+ it("treats numeric models as labels and blank models as unknown", () => {
130
+ render(
131
+ <HostsOverview
132
+ legendVariant="chart"
133
+ hosts={[{ model: 42 }, { model: " " }, { model: " R640 " }]}
134
+ />,
135
+ );
136
+
137
+ expect(renderedDonut().data.map((slice) => slice.name)).toEqual([
138
+ "42",
139
+ "Unknown model",
140
+ "R640",
141
+ ]);
142
+ expect(renderedDonut().legendVariant).toBe("chart");
143
+ expect(renderedDonut().title).toBe("3");
144
+ });
145
+ });
@@ -0,0 +1,269 @@
1
+ import "@testing-library/jest-dom";
2
+ import {
3
+ chart_color_black_500,
4
+ chart_color_blue_300,
5
+ chart_color_green_300,
6
+ chart_color_orange_100,
7
+ chart_color_purple_100,
8
+ chart_color_purple_300,
9
+ chart_color_red_orange_200,
10
+ chart_color_teal_200,
11
+ chart_color_teal_400,
12
+ chart_color_yellow_400,
13
+ } from "@patternfly/react-tokens";
14
+ import { cleanup, render, screen } from "@testing-library/react";
15
+ import userEvent from "@testing-library/user-event";
16
+ import { afterEach, describe, expect, it, vi } from "vitest";
17
+ import type { MigrationDonutChartProps } from "../../charts/MigrationDonutChart.js";
18
+ import { NetworkOverview } from "../NetworkOverview.js";
19
+
20
+ const donutProps = vi.hoisted(() => ({
21
+ current: null as MigrationDonutChartProps | null,
22
+ }));
23
+
24
+ vi.mock("../../charts/MigrationDonutChart.js", () => ({
25
+ MigrationDonutChart: (props: MigrationDonutChartProps): JSX.Element => {
26
+ donutProps.current = props;
27
+ return (
28
+ <div data-testid="network-donut">
29
+ {props.title} {props.subTitle}
30
+ </div>
31
+ );
32
+ },
33
+ }));
34
+
35
+ afterEach(() => {
36
+ donutProps.current = null;
37
+ cleanup();
38
+ });
39
+
40
+ const renderedDonut = (): MigrationDonutChartProps => {
41
+ const props = donutProps.current;
42
+ if (!props) {
43
+ throw new Error("expected a network donut to render");
44
+ }
45
+ return props;
46
+ };
47
+
48
+ describe("NetworkOverview", () => {
49
+ it("shows the network empty state when no networks were collected", () => {
50
+ render(<NetworkOverview />);
51
+
52
+ expect(screen.getByText("Networks")).toBeInTheDocument();
53
+ expect(screen.getByText("Top 5 networks")).toBeInTheDocument();
54
+ expect(screen.getByText("Network data not collected")).toBeInTheDocument();
55
+ expect(screen.queryByTestId("network-donut")).not.toBeInTheDocument();
56
+ });
57
+
58
+ it("keeps the top 4 networks and rolls the rest into one slice", () => {
59
+ render(
60
+ <NetworkOverview
61
+ infra={{
62
+ networks: [
63
+ { vmsCount: 10, vlanId: "100" },
64
+ { vmsCount: 8, vlanId: 200 },
65
+ { vmsCount: 6, vlanId: " " },
66
+ { vmsCount: 4 },
67
+ { vmsCount: 3, vlanId: "9" },
68
+ { vmsCount: 1, vlanId: "8" },
69
+ { vmsCount: Number.NaN },
70
+ ],
71
+ }}
72
+ />,
73
+ );
74
+
75
+ const props = renderedDonut();
76
+ expect(props.title).toBe("32");
77
+ expect(props.subTitle).toBe("VMs");
78
+ expect(props.data.map((slice) => [slice.name, slice.count])).toEqual([
79
+ ["Network 1", 10],
80
+ ["Network 2", 8],
81
+ ["Network 3", 6],
82
+ ["Network 4", 4],
83
+ ["Rest of networks", 4],
84
+ ]);
85
+ expect(props.legend).toEqual({
86
+ "Network 1": chart_color_blue_300.value,
87
+ "Network 2": chart_color_purple_300.value,
88
+ "Network 3": chart_color_purple_100.value,
89
+ "Network 4": chart_color_teal_200.value,
90
+ "Rest of networks": chart_color_yellow_400.value,
91
+ });
92
+ expect(props.legendVariant).toBe("html");
93
+ expect(props.height).toBe(300);
94
+ expect(props.width).toBe(420);
95
+ expect(props.donutThickness).toBe(18);
96
+ expect(props.titleFontSize).toBe(34);
97
+ expect(props.labelFontSize).toBe(18);
98
+ expect(props.itemsPerRow).toBe(3);
99
+ expect(props.marginLeft).toBe("0%");
100
+ expect(
101
+ props.tooltipLabelFormatter?.({
102
+ datum: {
103
+ x: "Network 1",
104
+ y: 10,
105
+ countDisplay: "10 VMs",
106
+ legendCategory: "Network 1",
107
+ },
108
+ percent: 31.25,
109
+ total: 32,
110
+ }),
111
+ ).toBe("10 VMs\n31.3%\nVLAN: 100");
112
+ expect(
113
+ props.tooltipLabelFormatter?.({
114
+ datum: {
115
+ x: "Network 2",
116
+ y: 8,
117
+ countDisplay: "8 VMs",
118
+ legendCategory: "Network 2",
119
+ },
120
+ percent: 25,
121
+ total: 32,
122
+ }),
123
+ ).toBe("8 VMs\n25.0%\nVLAN: 200");
124
+ expect(
125
+ props.tooltipLabelFormatter?.({
126
+ datum: {
127
+ x: "Rest of networks",
128
+ y: 4,
129
+ countDisplay: "4 VMs",
130
+ legendCategory: "Rest of networks",
131
+ },
132
+ percent: 12.5,
133
+ total: 32,
134
+ }),
135
+ ).toBe("4 VMs\n12.5%\nVLAN: -");
136
+ });
137
+
138
+ it("switches to NIC count and falls back to the histogram", async () => {
139
+ const user = userEvent.setup();
140
+ render(
141
+ <NetworkOverview
142
+ legendVariant="chart"
143
+ nicCount={{
144
+ total: 9,
145
+ histogram: { minValue: 1, step: 1, data: [4, 0, 5] },
146
+ }}
147
+ />,
148
+ );
149
+
150
+ await user.click(
151
+ screen.getByRole("button", { name: "VM distribution by network" }),
152
+ );
153
+ await user.click(
154
+ screen.getByRole("menuitem", { name: "VM distribution by NIC count" }),
155
+ );
156
+
157
+ expect(screen.queryByText("Top 5 networks")).not.toBeInTheDocument();
158
+ expect(
159
+ screen.getByRole("button", { name: "VM distribution by NIC count" }),
160
+ ).toBeInTheDocument();
161
+
162
+ const props = renderedDonut();
163
+ expect(props.legendVariant).toBe("chart");
164
+ expect(props.title).toBe("9");
165
+ expect(props.data.map((slice) => [slice.name, slice.count])).toEqual([
166
+ ["1 NIC", 4],
167
+ ["3 NIC", 5],
168
+ ]);
169
+ expect(
170
+ props.tooltipLabelFormatter?.({
171
+ datum: {
172
+ x: "1 NIC",
173
+ y: 4,
174
+ countDisplay: "4 VMs",
175
+ legendCategory: "1 NIC",
176
+ },
177
+ percent: 44.444,
178
+ total: 9,
179
+ }),
180
+ ).toBe("4 VMs\n44.4%");
181
+ });
182
+
183
+ it("plots distributionByNicCount ahead of the deprecated histogram", async () => {
184
+ const user = userEvent.setup();
185
+ render(
186
+ <NetworkOverview
187
+ distributionByNicCount={{ "2": 5, "4+": 2, "1": 3, "0": 1 }}
188
+ nicCount={{
189
+ total: 99,
190
+ histogram: { minValue: 1, step: 1, data: [9] },
191
+ }}
192
+ />,
193
+ );
194
+
195
+ await user.click(
196
+ screen.getByRole("button", { name: "VM distribution by network" }),
197
+ );
198
+ await user.click(
199
+ screen.getByRole("menuitem", { name: "VM distribution by NIC count" }),
200
+ );
201
+
202
+ const props = renderedDonut();
203
+ expect(props.title).toBe("11");
204
+ expect(props.data.map((slice) => slice.name)).toEqual([
205
+ "0 NIC",
206
+ "1 NIC",
207
+ "2 NIC",
208
+ "4+ NIC",
209
+ ]);
210
+ expect(props.legend).toEqual({
211
+ "0 NIC": chart_color_blue_300.value,
212
+ "1 NIC": chart_color_purple_300.value,
213
+ "2 NIC": chart_color_purple_100.value,
214
+ "4+ NIC": chart_color_teal_200.value,
215
+ });
216
+ });
217
+
218
+ it("shows the NIC empty state when the selected distribution is empty", async () => {
219
+ const user = userEvent.setup();
220
+ render(<NetworkOverview distributionByNicCount={{}} />);
221
+
222
+ await user.click(
223
+ screen.getByRole("button", { name: "VM distribution by network" }),
224
+ );
225
+ await user.click(
226
+ screen.getByRole("menuitem", { name: "VM distribution by NIC count" }),
227
+ );
228
+
229
+ expect(
230
+ screen.getByText("NIC count data not collected"),
231
+ ).toBeInTheDocument();
232
+ expect(screen.queryByTestId("network-donut")).not.toBeInTheDocument();
233
+ });
234
+
235
+ it("uses the extended palette when there are more NIC buckets than base colors", async () => {
236
+ const user = userEvent.setup();
237
+ render(
238
+ <NetworkOverview
239
+ distributionByNicCount={{
240
+ "0": 1,
241
+ "1": 1,
242
+ "2": 1,
243
+ "3": 1,
244
+ "4": 1,
245
+ "5": 1,
246
+ "6": 1,
247
+ "7": 1,
248
+ "8": 1,
249
+ "9": 1,
250
+ }}
251
+ />,
252
+ );
253
+
254
+ await user.click(
255
+ screen.getByRole("button", { name: "VM distribution by network" }),
256
+ );
257
+ await user.click(
258
+ screen.getByRole("menuitem", { name: "VM distribution by NIC count" }),
259
+ );
260
+
261
+ expect(renderedDonut().legend).toMatchObject({
262
+ "5 NIC": chart_color_green_300.value,
263
+ "6 NIC": chart_color_orange_100.value,
264
+ "7 NIC": chart_color_red_orange_200.value,
265
+ "8 NIC": chart_color_teal_400.value,
266
+ "9 NIC": chart_color_black_500.value,
267
+ });
268
+ });
269
+ });
@@ -25,6 +25,7 @@ export const REPORT_CARD_EMPTY_STATE_TITLES = {
25
25
  operatingSystems: "Operating system data not collected",
26
26
  migrationStatus: "Migration status data not collected",
27
27
  issuesBreakdown: "Issues breakdown data not collected",
28
+ errors: "Error data not collected",
28
29
  hostPowerStates: "Host power state data not collected",
29
30
  vmPowerStates: "VM power state data not collected",
30
31
  } as const;
@@ -65,6 +65,11 @@ export {
65
65
  EmptySearchResults,
66
66
  type EmptySearchResultsProps,
67
67
  } from "./EmptySearchResults.js";
68
+ export {
69
+ ErrorTable,
70
+ type ErrorTableProps,
71
+ type MigrationIssueLike,
72
+ } from "./ErrorTable.js";
68
73
  export {
69
74
  FeatureStatusBadge,
70
75
  type FeatureStatusBadgeProps,
@@ -73,6 +78,11 @@ export {
73
78
  HostPowerStates,
74
79
  type HostPowerStatesProps,
75
80
  } from "./HostPowerStates.js";
81
+ export {
82
+ type HostLike,
83
+ HostsOverview,
84
+ type HostsOverviewProps,
85
+ } from "./HostsOverview.js";
76
86
  export {
77
87
  buildHtmlFromCharts,
78
88
  buildHtmlReport,
@@ -108,6 +118,14 @@ export {
108
118
  visibleNetworks,
109
119
  vsanStatus,
110
120
  } from "./infrastructureSummaryModel.js";
121
+ export {
122
+ type NetworkInfraLike,
123
+ type NetworkLike,
124
+ NetworkOverview,
125
+ type NetworkOverviewProps,
126
+ type NicCountHistogram,
127
+ type NicCountSummary,
128
+ } from "./NetworkOverview.js";
111
129
  export { OSBarChart, OSDistribution } from "./OSDistribution.js";
112
130
  export { OsSupportTiersHelpPopover } from "./OsSupportTiersHelpPopover.js";
113
131
  export { OsUpgradeNotice } from "./OsUpgradeNotice.js";
@@ -159,6 +177,7 @@ export {
159
177
  ReportExportMenu,
160
178
  type ReportExportMenuProps,
161
179
  } from "./ReportExportMenu.js";
180
+ export { ReportTable, type ReportTableProps } from "./ReportTable.js";
162
181
  export {
163
182
  type ReportExportOption,
164
183
  type StandardReportExportHandlers,