@openshift-migration-advisor/shared-components 0.25.0-6fd4e1ee8e11 → 0.25.0-db3dbade32ea
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/ErrorTable.d.ts +18 -0
- package/dist/report/ErrorTable.d.ts.map +1 -0
- package/dist/report/ErrorTable.js +20 -0
- package/dist/report/HostsOverview.d.ts +13 -0
- package/dist/report/HostsOverview.d.ts.map +1 -0
- package/dist/report/HostsOverview.js +84 -0
- package/dist/report/NetworkOverview.d.ts +31 -0
- package/dist/report/NetworkOverview.d.ts.map +1 -0
- package/dist/report/NetworkOverview.js +201 -0
- package/dist/report/ReportTable.d.ts +14 -0
- package/dist/report/ReportTable.d.ts.map +1 -0
- package/dist/report/ReportTable.js +23 -0
- package/dist/report/WarningsTable.d.ts +9 -0
- package/dist/report/WarningsTable.d.ts.map +1 -0
- package/dist/report/WarningsTable.js +20 -0
- package/dist/report/constants.d.ts +2 -0
- package/dist/report/constants.d.ts.map +1 -1
- package/dist/report/constants.js +2 -0
- package/dist/report/index.d.ts +5 -0
- package/dist/report/index.d.ts.map +1 -1
- package/dist/report/index.js +5 -0
- package/package.json +3 -3
- package/src/report/ErrorTable.tsx +85 -0
- package/src/report/HostsOverview.tsx +185 -0
- package/src/report/NetworkOverview.tsx +405 -0
- package/src/report/ReportTable.tsx +81 -0
- package/src/report/WarningsTable.tsx +75 -0
- package/src/report/__tests__/HostsOverview.test.tsx +145 -0
- package/src/report/__tests__/NetworkOverview.test.tsx +269 -0
- package/src/report/constants.ts +2 -0
- package/src/report/index.ts +23 -0
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { Button } from "@patternfly/react-core";
|
|
2
|
+
import { Table, Tbody, Td, Th, Thead, Tr } from "@patternfly/react-table";
|
|
3
|
+
import type React from "react";
|
|
4
|
+
import { tableFullWidthStyle } from "./dashboardStyles.js";
|
|
5
|
+
|
|
6
|
+
export interface ReportTableProps<DataItem> {
|
|
7
|
+
columns: string[];
|
|
8
|
+
data: DataItem[];
|
|
9
|
+
fields: Array<keyof DataItem>;
|
|
10
|
+
caption?: string;
|
|
11
|
+
onRowClick?: (item: DataItem) => void;
|
|
12
|
+
clickableFields?: Array<keyof DataItem>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function ReportTable<DataItem>(
|
|
16
|
+
props: ReportTableProps<DataItem>,
|
|
17
|
+
): React.ReactNode {
|
|
18
|
+
const {
|
|
19
|
+
columns,
|
|
20
|
+
data,
|
|
21
|
+
fields,
|
|
22
|
+
caption,
|
|
23
|
+
onRowClick,
|
|
24
|
+
clickableFields = [],
|
|
25
|
+
} = props;
|
|
26
|
+
|
|
27
|
+
const renderCell = (
|
|
28
|
+
item: DataItem,
|
|
29
|
+
field: keyof DataItem,
|
|
30
|
+
): React.ReactNode => {
|
|
31
|
+
const value = item[field];
|
|
32
|
+
|
|
33
|
+
if (value === "" || value == null) {
|
|
34
|
+
return "-";
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (typeof value === "boolean") {
|
|
38
|
+
return value ? "True" : "False";
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Make field clickable if specified
|
|
42
|
+
if (clickableFields.includes(field) && onRowClick) {
|
|
43
|
+
return (
|
|
44
|
+
<Button
|
|
45
|
+
variant="link"
|
|
46
|
+
isInline
|
|
47
|
+
onClick={() => onRowClick(item)}
|
|
48
|
+
style={{ padding: 0, fontSize: "inherit" }}
|
|
49
|
+
>
|
|
50
|
+
{value as React.ReactNode}
|
|
51
|
+
</Button>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return value as React.ReactNode;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<Table variant="compact" borders={false} className={tableFullWidthStyle}>
|
|
60
|
+
{caption && <caption>{caption}</caption>}
|
|
61
|
+
<Thead>
|
|
62
|
+
<Tr>
|
|
63
|
+
{columns.map((name) => (
|
|
64
|
+
<Th key={name}>{name}</Th>
|
|
65
|
+
))}
|
|
66
|
+
</Tr>
|
|
67
|
+
</Thead>
|
|
68
|
+
<Tbody>
|
|
69
|
+
{data.map((item) => (
|
|
70
|
+
<Tr key={`row-${String(item[fields[0]])}`}>
|
|
71
|
+
{fields.map((f) => (
|
|
72
|
+
<Td key={String(f)}>{renderCell(item, f)}</Td>
|
|
73
|
+
))}
|
|
74
|
+
</Tr>
|
|
75
|
+
))}
|
|
76
|
+
</Tbody>
|
|
77
|
+
</Table>
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
ReportTable.displayName = "ReportTable";
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Card,
|
|
3
|
+
CardBody,
|
|
4
|
+
CardTitle,
|
|
5
|
+
Flex,
|
|
6
|
+
FlexItem,
|
|
7
|
+
Icon,
|
|
8
|
+
} from "@patternfly/react-core";
|
|
9
|
+
import { ExclamationTriangleIcon } from "@patternfly/react-icons";
|
|
10
|
+
import type React from "react";
|
|
11
|
+
import { CardEmptyState } from "./CardEmptyState.js";
|
|
12
|
+
import { ChartHeaderActions } from "./ChartDownloadButton.js";
|
|
13
|
+
import { ChartExportSurface } from "./ChartExportSurface.js";
|
|
14
|
+
import { REPORT_CARD_EMPTY_STATE_TITLES } from "./constants.js";
|
|
15
|
+
import { dashboardStyles } from "./dashboardStyles.js";
|
|
16
|
+
import type { MigrationIssueLike } from "./ErrorTable.js";
|
|
17
|
+
import { ReportTable } from "./ReportTable.js";
|
|
18
|
+
|
|
19
|
+
export interface WarningsTableProps {
|
|
20
|
+
warnings: MigrationIssueLike[];
|
|
21
|
+
/** Optional drill-down callback invoked with an issue's `label`. */
|
|
22
|
+
onConcernClick?: (concernLabel: string) => void;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const WarningsTable: React.FC<WarningsTableProps> = ({
|
|
26
|
+
warnings,
|
|
27
|
+
onConcernClick,
|
|
28
|
+
}) => {
|
|
29
|
+
const handleRowClick = (issue: MigrationIssueLike) => {
|
|
30
|
+
if (issue.label && onConcernClick) {
|
|
31
|
+
onConcernClick(issue.label);
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const chartId = "warnings-table";
|
|
36
|
+
const chartTitle = "Warnings";
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<ChartExportSurface id={chartId} title={chartTitle}>
|
|
40
|
+
<Card className={dashboardStyles.card}>
|
|
41
|
+
<CardTitle>
|
|
42
|
+
<Flex
|
|
43
|
+
justifyContent={{ default: "justifyContentSpaceBetween" }}
|
|
44
|
+
alignItems={{ default: "alignItemsCenter" }}
|
|
45
|
+
>
|
|
46
|
+
<FlexItem>
|
|
47
|
+
<Icon status="warning">
|
|
48
|
+
<ExclamationTriangleIcon />
|
|
49
|
+
</Icon>{" "}
|
|
50
|
+
Warnings
|
|
51
|
+
</FlexItem>
|
|
52
|
+
<ChartHeaderActions chartId={chartId} title={chartTitle} />
|
|
53
|
+
</Flex>
|
|
54
|
+
</CardTitle>
|
|
55
|
+
<CardBody className={dashboardStyles.cardBodyScrollable}>
|
|
56
|
+
{warnings.length === 0 ? (
|
|
57
|
+
<CardEmptyState title={REPORT_CARD_EMPTY_STATE_TITLES.warnings} />
|
|
58
|
+
) : (
|
|
59
|
+
<div>
|
|
60
|
+
<ReportTable<MigrationIssueLike>
|
|
61
|
+
data={warnings}
|
|
62
|
+
columns={["Description", "Total VMs"]}
|
|
63
|
+
fields={["assessment", "count"]}
|
|
64
|
+
onRowClick={onConcernClick ? handleRowClick : undefined}
|
|
65
|
+
clickableFields={onConcernClick ? ["assessment"] : []}
|
|
66
|
+
/>
|
|
67
|
+
</div>
|
|
68
|
+
)}
|
|
69
|
+
</CardBody>
|
|
70
|
+
</Card>
|
|
71
|
+
</ChartExportSurface>
|
|
72
|
+
);
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
WarningsTable.displayName = "WarningsTable";
|
|
@@ -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
|
+
});
|
package/src/report/constants.ts
CHANGED
|
@@ -25,6 +25,8 @@ 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",
|
|
29
|
+
warnings: "Warning data not collected",
|
|
28
30
|
hostPowerStates: "Host power state data not collected",
|
|
29
31
|
vmPowerStates: "VM power state data not collected",
|
|
30
32
|
} as const;
|
package/src/report/index.ts
CHANGED
|
@@ -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,
|
|
@@ -179,4 +198,8 @@ export {
|
|
|
179
198
|
VmPowerStates,
|
|
180
199
|
type VmPowerStatesProps,
|
|
181
200
|
} from "./VmPowerStates.js";
|
|
201
|
+
export {
|
|
202
|
+
WarningsTable,
|
|
203
|
+
type WarningsTableProps,
|
|
204
|
+
} from "./WarningsTable.js";
|
|
182
205
|
export { zipChartPngs } from "./zipChartPngs.js";
|