@openshift-migration-advisor/shared-components 0.25.0-0ad1e70d1fa5 → 0.25.0-1c27267d2ea6

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,18 @@
1
+ import type React from "react";
2
+ /**
3
+ * Minimal shape of a migration issue consumed by {@link ErrorTable}. Declared
4
+ * locally so this package does not depend on a specific SDK (`agent-sdk` or
5
+ * `planner-sdk`); callers pass the subset of `MigrationIssue` fields read here.
6
+ */
7
+ export interface MigrationIssueLike {
8
+ label: string;
9
+ assessment: string;
10
+ count: number;
11
+ }
12
+ export interface ErrorTableProps {
13
+ errors: MigrationIssueLike[];
14
+ /** Optional drill-down callback invoked with an issue's `label`. */
15
+ onConcernClick?: (concernLabel: string) => void;
16
+ }
17
+ export declare const ErrorTable: React.FC<ErrorTableProps>;
18
+ //# sourceMappingURL=ErrorTable.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ErrorTable.d.ts","sourceRoot":"","sources":["../../src/report/ErrorTable.tsx"],"names":[],"mappings":"AASA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAQ/B;;;;GAIG;AACH,MAAM,WAAW,kBAAkB;IACjC,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,kBAAkB,EAAE,CAAC;IAC7B,oEAAoE;IACpE,cAAc,CAAC,EAAE,CAAC,YAAY,EAAE,MAAM,KAAK,IAAI,CAAC;CACjD;AAED,eAAO,MAAM,UAAU,EAAE,KAAK,CAAC,EAAE,CAAC,eAAe,CAgDhD,CAAC"}
@@ -0,0 +1,20 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Card, CardBody, CardTitle, Flex, FlexItem, Icon, } from "@patternfly/react-core";
3
+ import { ExclamationCircleIcon } from "@patternfly/react-icons";
4
+ import { CardEmptyState } from "./CardEmptyState.js";
5
+ import { ChartHeaderActions } from "./ChartDownloadButton.js";
6
+ import { ChartExportSurface } from "./ChartExportSurface.js";
7
+ import { REPORT_CARD_EMPTY_STATE_TITLES } from "./constants.js";
8
+ import { dashboardStyles } from "./dashboardStyles.js";
9
+ import { ReportTable } from "./ReportTable.js";
10
+ export const ErrorTable = ({ errors, onConcernClick, }) => {
11
+ const handleRowClick = (issue) => {
12
+ if (issue.label && onConcernClick) {
13
+ onConcernClick(issue.label);
14
+ }
15
+ };
16
+ const chartId = "errors-table";
17
+ const chartTitle = "Errors";
18
+ return (_jsx(ChartExportSurface, { id: chartId, title: chartTitle, children: _jsxs(Card, { className: dashboardStyles.card, children: [_jsx(CardTitle, { children: _jsxs(Flex, { justifyContent: { default: "justifyContentSpaceBetween" }, alignItems: { default: "alignItemsCenter" }, children: [_jsxs(FlexItem, { children: [_jsx(Icon, { status: "danger", children: _jsx(ExclamationCircleIcon, {}) }), " ", "Errors"] }), _jsx(ChartHeaderActions, { chartId: chartId, title: chartTitle })] }) }), _jsx(CardBody, { className: dashboardStyles.cardBodyScrollable, children: errors.length === 0 ? (_jsx(CardEmptyState, { title: REPORT_CARD_EMPTY_STATE_TITLES.errors })) : (_jsx("div", { children: _jsx(ReportTable, { data: errors, columns: ["Description", "Total VMs"], fields: ["assessment", "count"], onRowClick: onConcernClick ? handleRowClick : undefined, clickableFields: onConcernClick ? ["assessment"] : [] }) })) })] }) }));
19
+ };
20
+ ErrorTable.displayName = "ErrorTable";
@@ -0,0 +1,31 @@
1
+ import type { FC } from "react";
2
+ import { type MigrationDonutChartLegendVariant } from "../charts/MigrationDonutChart.js";
3
+ /** Network fields this card reads. Apps pass SDK networks structurally. */
4
+ export interface NetworkLike {
5
+ vmsCount?: number;
6
+ vlanId?: string | number;
7
+ }
8
+ /** Infra fields this card reads. */
9
+ export interface NetworkInfraLike {
10
+ networks?: NetworkLike[];
11
+ }
12
+ /** Deprecated NIC histogram used when `distributionByNicCount` is absent. */
13
+ export interface NicCountHistogram {
14
+ data?: number[];
15
+ minValue?: number;
16
+ step?: number;
17
+ }
18
+ /** NIC summary fields this card reads from the VM resource breakdown. */
19
+ export interface NicCountSummary {
20
+ total?: number;
21
+ histogram?: NicCountHistogram;
22
+ }
23
+ export interface NetworkOverviewProps {
24
+ infra?: NetworkInfraLike;
25
+ nicCount?: NicCountSummary;
26
+ distributionByNicCount?: Record<string, number>;
27
+ /** `"html"` in agent-ui; ui-app passes `"chart"`. */
28
+ legendVariant?: MigrationDonutChartLegendVariant;
29
+ }
30
+ export declare const NetworkOverview: FC<NetworkOverviewProps>;
31
+ //# sourceMappingURL=NetworkOverview.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"NetworkOverview.d.ts","sourceRoot":"","sources":["../../src/report/NetworkOverview.tsx"],"names":[],"mappings":"AA0BA,OAAO,KAAK,EAAE,EAAE,EAAO,MAAM,OAAO,CAAC;AAErC,OAAO,EAGL,KAAK,gCAAgC,EACtC,MAAM,kCAAkC,CAAC;AAQ1C,2EAA2E;AAC3E,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;CAC1B;AAED,oCAAoC;AACpC,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,EAAE,WAAW,EAAE,CAAC;CAC1B;AAED,6EAA6E;AAC7E,MAAM,WAAW,iBAAiB;IAChC,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,yEAAyE;AACzE,MAAM,WAAW,eAAe;IAC9B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,iBAAiB,CAAC;CAC/B;AAED,MAAM,WAAW,oBAAoB;IACnC,KAAK,CAAC,EAAE,gBAAgB,CAAC;IACzB,QAAQ,CAAC,EAAE,eAAe,CAAC;IAC3B,sBAAsB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChD,qDAAqD;IACrD,aAAa,CAAC,EAAE,gCAAgC,CAAC;CAClD;AAiND,eAAO,MAAM,eAAe,EAAE,EAAE,CAAC,oBAAoB,CA2HpD,CAAC"}
@@ -0,0 +1,201 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { css } from "@emotion/css";
3
+ import { Card, CardBody, CardTitle, Dropdown, DropdownItem, DropdownList, Flex, FlexItem, MenuToggle, } from "@patternfly/react-core";
4
+ import { TopologyIcon } from "@patternfly/react-icons";
5
+ import { chart_color_black_500, chart_color_blue_300, chart_color_green_300, chart_color_orange_100, chart_color_purple_100, chart_color_purple_300, chart_color_red_orange_200, chart_color_teal_200, chart_color_teal_400, chart_color_yellow_400, } from "@patternfly/react-tokens";
6
+ import { useMemo, useState } from "react";
7
+ import { MigrationDonutChart, } from "../charts/MigrationDonutChart.js";
8
+ import { CardEmptyState } from "./CardEmptyState.js";
9
+ import { ChartHeaderActions } from "./ChartDownloadButton.js";
10
+ import { ChartExportSurface } from "./ChartExportSurface.js";
11
+ import { chartExportViewsFromLabels } from "./chartExport.js";
12
+ import { REPORT_CARD_EMPTY_STATE_TITLES } from "./constants.js";
13
+ import { dashboardStyles } from "./dashboardStyles.js";
14
+ const VIEW_MODE_LABELS = {
15
+ networkDistribution: "VM distribution by network",
16
+ nicCount: "VM distribution by NIC count",
17
+ };
18
+ const TOP_NETWORKS = 4;
19
+ const REST_OF_NETWORKS_LABEL = "Rest of networks";
20
+ const CHART_ID = "network-overview";
21
+ const CHART_TITLE = "Networks";
22
+ /**
23
+ * Categorical order from the ui-app network donut, mapped onto PatternFly
24
+ * `chart_color_*` tokens (nearest token where the old hex was not a token).
25
+ */
26
+ const NETWORK_COLORS = [
27
+ chart_color_blue_300.value,
28
+ chart_color_purple_300.value,
29
+ chart_color_purple_100.value,
30
+ chart_color_teal_200.value,
31
+ chart_color_yellow_400.value,
32
+ chart_color_green_300.value,
33
+ chart_color_orange_100.value,
34
+ chart_color_red_orange_200.value,
35
+ chart_color_teal_400.value,
36
+ chart_color_black_500.value,
37
+ ];
38
+ const cardSubtitle = css `
39
+ color: var(--pf-t--global--text--color--subtle);
40
+ font-size: 0.85rem;
41
+ `;
42
+ const menuToggleMinWidth = css `
43
+ min-width: 250px;
44
+ `;
45
+ const titleRow = css `
46
+ width: 100%;
47
+ `;
48
+ const isViewMode = (value) => value === "networkDistribution" || value === "nicCount";
49
+ const legendFor = (categories) => {
50
+ const legend = {};
51
+ categories.forEach((category, index) => {
52
+ const color = NETWORK_COLORS[index % NETWORK_COLORS.length];
53
+ if (color) {
54
+ legend[category] = color;
55
+ }
56
+ });
57
+ return legend;
58
+ };
59
+ const vlanLabel = (vlanId) => {
60
+ if (typeof vlanId !== "string" && typeof vlanId !== "number") {
61
+ return "-";
62
+ }
63
+ const value = String(vlanId).trim();
64
+ return value !== "" ? value : "-";
65
+ };
66
+ const buildNetworkDonut = (networks) => {
67
+ const items = (Array.isArray(networks) ? networks : [])
68
+ .map((network) => ({
69
+ vmsCount: typeof network?.vmsCount === "number" ? network.vmsCount : Number.NaN,
70
+ vlanId: vlanLabel(network?.vlanId),
71
+ }))
72
+ .filter((network) => Number.isFinite(network.vmsCount) && network.vmsCount >= 0)
73
+ .sort((a, b) => b.vmsCount - a.vmsCount);
74
+ const totalVMs = items.reduce((sum, network) => sum + network.vmsCount, 0);
75
+ const top = items.slice(0, TOP_NETWORKS);
76
+ const restSum = items
77
+ .slice(TOP_NETWORKS)
78
+ .reduce((sum, network) => sum + network.vmsCount, 0);
79
+ const vlanBySlice = {};
80
+ const slices = top.map((network, index) => {
81
+ const name = `Network ${index + 1}`;
82
+ vlanBySlice[name] = network.vlanId;
83
+ return {
84
+ name,
85
+ count: network.vmsCount,
86
+ countDisplay: `${network.vmsCount} VMs`,
87
+ legendCategory: name,
88
+ };
89
+ });
90
+ if (restSum > 0) {
91
+ slices.push({
92
+ name: REST_OF_NETWORKS_LABEL,
93
+ count: restSum,
94
+ countDisplay: `${restSum} VMs`,
95
+ legendCategory: REST_OF_NETWORKS_LABEL,
96
+ });
97
+ vlanBySlice[REST_OF_NETWORKS_LABEL] = "-";
98
+ }
99
+ return {
100
+ slices,
101
+ legend: legendFor(slices.map((slice) => slice.legendCategory)),
102
+ title: `${totalVMs}`,
103
+ vlanBySlice,
104
+ };
105
+ };
106
+ const nicBucketLabel = (bucket) => {
107
+ const count = Number.parseInt(bucket, 10);
108
+ if (bucket.endsWith("+")) {
109
+ return `${count}+ NIC`;
110
+ }
111
+ return count === 1 ? "1 NIC" : `${count} NIC`;
112
+ };
113
+ const compareNicBuckets = (a, b) => {
114
+ const aPlus = a.bucket.endsWith("+");
115
+ const bPlus = b.bucket.endsWith("+");
116
+ if (aPlus !== bPlus) {
117
+ return aPlus ? 1 : -1;
118
+ }
119
+ const aNum = Number.parseInt(a.bucket, 10);
120
+ const bNum = Number.parseInt(b.bucket, 10);
121
+ if (Number.isFinite(aNum) && Number.isFinite(bNum)) {
122
+ return aNum - bNum;
123
+ }
124
+ return a.bucket.localeCompare(b.bucket);
125
+ };
126
+ const slicesFromBuckets = (entries) => entries.map((entry) => {
127
+ const label = nicBucketLabel(entry.bucket);
128
+ return {
129
+ name: label,
130
+ count: entry.count,
131
+ countDisplay: `${entry.count} VMs`,
132
+ legendCategory: label,
133
+ };
134
+ });
135
+ const buildNicDonut = (distributionByNicCount, nicCount) => {
136
+ if (distributionByNicCount != null) {
137
+ const entries = Object.entries(distributionByNicCount)
138
+ .map(([bucket, value]) => ({
139
+ bucket,
140
+ count: Number.isFinite(Number(value)) ? Number(value) : 0,
141
+ }))
142
+ .filter((entry) => entry.count > 0)
143
+ .sort(compareNicBuckets);
144
+ const slices = slicesFromBuckets(entries);
145
+ const total = entries.reduce((sum, entry) => sum + entry.count, 0);
146
+ return {
147
+ slices,
148
+ legend: legendFor(slices.map((slice) => slice.legendCategory)),
149
+ title: `${total}`,
150
+ };
151
+ }
152
+ const histogram = nicCount?.histogram;
153
+ const data = Array.isArray(histogram?.data) ? histogram.data : [];
154
+ const minValue = typeof histogram?.minValue === "number" ? histogram.minValue : 0;
155
+ const step = typeof histogram?.step === "number" ? histogram.step : 1;
156
+ const buckets = data
157
+ .map((count, index) => ({
158
+ bucket: String(minValue + index * step),
159
+ count: Number(count) || 0,
160
+ }))
161
+ .filter((entry) => entry.count > 0)
162
+ .sort((a, b) => Number(a.bucket) - Number(b.bucket));
163
+ const slices = slicesFromBuckets(buckets);
164
+ const total = typeof nicCount?.total === "number"
165
+ ? nicCount.total
166
+ : buckets.reduce((sum, entry) => sum + entry.count, 0);
167
+ return {
168
+ slices,
169
+ legend: legendFor(slices.map((slice) => slice.legendCategory)),
170
+ title: `${total}`,
171
+ };
172
+ };
173
+ export const NetworkOverview = ({ infra, nicCount, distributionByNicCount, legendVariant = "html", }) => {
174
+ const [viewMode, setViewMode] = useState("networkDistribution");
175
+ const [isDropdownOpen, setIsDropdownOpen] = useState(false);
176
+ const networkChart = useMemo(() => buildNetworkDonut(infra?.networks), [infra?.networks]);
177
+ const nicChart = useMemo(() => buildNicDonut(distributionByNicCount, nicCount), [distributionByNicCount, nicCount]);
178
+ const chartTitle = `${CHART_TITLE} — ${VIEW_MODE_LABELS[viewMode]}`;
179
+ const activeChart = viewMode === "networkDistribution" ? networkChart : nicChart;
180
+ const emptyTitle = viewMode === "networkDistribution"
181
+ ? REPORT_CARD_EMPTY_STATE_TITLES.networks
182
+ : REPORT_CARD_EMPTY_STATE_TITLES.nicCount;
183
+ return (_jsx(ChartExportSurface, { id: CHART_ID, title: chartTitle, exportViews: chartExportViewsFromLabels(CHART_TITLE, VIEW_MODE_LABELS), activeExportViewId: viewMode, onExportViewChange: (viewId) => {
184
+ if (isViewMode(viewId)) {
185
+ setViewMode(viewId);
186
+ }
187
+ }, children: _jsxs(Card, { className: dashboardStyles.card, children: [_jsx(CardTitle, { children: _jsxs(Flex, { className: titleRow, justifyContent: { default: "justifyContentSpaceBetween" }, alignItems: { default: "alignItemsCenter" }, children: [_jsx(FlexItem, { children: _jsxs("div", { children: [_jsxs("div", { children: [_jsx(TopologyIcon, {}), " ", CHART_TITLE] }), viewMode === "networkDistribution" && (_jsx("div", { className: cardSubtitle, children: "Top 5 networks" }))] }) }), _jsx(ChartHeaderActions, { chartId: CHART_ID, title: chartTitle, children: _jsx(Dropdown, { isOpen: isDropdownOpen, onSelect: (_event, value) => {
188
+ if (isViewMode(value)) {
189
+ setViewMode(value);
190
+ }
191
+ setIsDropdownOpen(false);
192
+ }, onOpenChange: setIsDropdownOpen, toggle: (toggleRef) => (_jsx(MenuToggle, { ref: toggleRef, onClick: () => setIsDropdownOpen((open) => !open), isExpanded: isDropdownOpen, className: menuToggleMinWidth, children: VIEW_MODE_LABELS[viewMode] })), children: _jsxs(DropdownList, { children: [_jsx(DropdownItem, { value: "networkDistribution", children: VIEW_MODE_LABELS.networkDistribution }, "networkDistribution"), _jsx(DropdownItem, { value: "nicCount", children: VIEW_MODE_LABELS.nicCount }, "nicCount")] }) }) })] }) }), _jsx(CardBody, { className: dashboardStyles.cardBodyScrollable, children: activeChart.slices.length === 0 ? (_jsx(CardEmptyState, { title: emptyTitle })) : (_jsx(MigrationDonutChart, { legendVariant: legendVariant, data: activeChart.slices, legend: activeChart.legend, height: 300, width: 420, donutThickness: 18, titleFontSize: 34, title: activeChart.title, subTitle: "VMs", subTitleColor: "var(--pf-t--global--text--color--subtle)", itemsPerRow: Math.ceil(activeChart.slices.length / 2), labelFontSize: 18, marginLeft: "0%", tooltipLabelFormatter: ({ datum, percent }) => {
193
+ const summary = `${datum.countDisplay}\n${percent.toFixed(1)}%`;
194
+ if (viewMode !== "networkDistribution") {
195
+ return summary;
196
+ }
197
+ const vlan = networkChart.vlanBySlice[datum.legendCategory];
198
+ return `${summary}\nVLAN: ${vlan ?? "-"}`;
199
+ } })) })] }) }));
200
+ };
201
+ NetworkOverview.displayName = "NetworkOverview";
@@ -0,0 +1,14 @@
1
+ import type React from "react";
2
+ export interface ReportTableProps<DataItem> {
3
+ columns: string[];
4
+ data: DataItem[];
5
+ fields: Array<keyof DataItem>;
6
+ caption?: string;
7
+ onRowClick?: (item: DataItem) => void;
8
+ clickableFields?: Array<keyof DataItem>;
9
+ }
10
+ export declare function ReportTable<DataItem>(props: ReportTableProps<DataItem>): React.ReactNode;
11
+ export declare namespace ReportTable {
12
+ var displayName: string;
13
+ }
14
+ //# sourceMappingURL=ReportTable.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ReportTable.d.ts","sourceRoot":"","sources":["../../src/report/ReportTable.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAG/B,MAAM,WAAW,gBAAgB,CAAC,QAAQ;IACxC,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,IAAI,EAAE,QAAQ,EAAE,CAAC;IACjB,MAAM,EAAE,KAAK,CAAC,MAAM,QAAQ,CAAC,CAAC;IAC9B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,QAAQ,KAAK,IAAI,CAAC;IACtC,eAAe,CAAC,EAAE,KAAK,CAAC,MAAM,QAAQ,CAAC,CAAC;CACzC;AAED,wBAAgB,WAAW,CAAC,QAAQ,EAClC,KAAK,EAAE,gBAAgB,CAAC,QAAQ,CAAC,GAChC,KAAK,CAAC,SAAS,CA8DjB;yBAhEe,WAAW"}
@@ -0,0 +1,23 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Button } from "@patternfly/react-core";
3
+ import { Table, Tbody, Td, Th, Thead, Tr } from "@patternfly/react-table";
4
+ import { tableFullWidthStyle } from "./dashboardStyles.js";
5
+ export function ReportTable(props) {
6
+ const { columns, data, fields, caption, onRowClick, clickableFields = [], } = props;
7
+ const renderCell = (item, field) => {
8
+ const value = item[field];
9
+ if (value === "" || value == null) {
10
+ return "-";
11
+ }
12
+ if (typeof value === "boolean") {
13
+ return value ? "True" : "False";
14
+ }
15
+ // Make field clickable if specified
16
+ if (clickableFields.includes(field) && onRowClick) {
17
+ return (_jsx(Button, { variant: "link", isInline: true, onClick: () => onRowClick(item), style: { padding: 0, fontSize: "inherit" }, children: value }));
18
+ }
19
+ return value;
20
+ };
21
+ return (_jsxs(Table, { variant: "compact", borders: false, className: tableFullWidthStyle, children: [caption && _jsx("caption", { children: caption }), _jsx(Thead, { children: _jsx(Tr, { children: columns.map((name) => (_jsx(Th, { children: name }, name))) }) }), _jsx(Tbody, { children: data.map((item) => (_jsx(Tr, { children: fields.map((f) => (_jsx(Td, { children: renderCell(item, f) }, String(f)))) }, `row-${String(item[fields[0]])}`))) })] }));
22
+ }
23
+ ReportTable.displayName = "ReportTable";
@@ -19,6 +19,7 @@ export declare const REPORT_CARD_EMPTY_STATE_TITLES: {
19
19
  readonly operatingSystems: "Operating system data not collected";
20
20
  readonly migrationStatus: "Migration status data not collected";
21
21
  readonly issuesBreakdown: "Issues breakdown data not collected";
22
+ readonly errors: "Error data not collected";
22
23
  readonly hostPowerStates: "Host power state data not collected";
23
24
  readonly vmPowerStates: "VM power state data not collected";
24
25
  };
@@ -1 +1 @@
1
- {"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../../src/report/constants.ts"],"names":[],"mappings":"AAKA;;;;GAIG;AACH,eAAO,MAAM,iBAAiB,WAA6B,CAAC;AAC5D,eAAO,MAAM,iBAAiB,WAA+B,CAAC;AAE9D,eAAO,MAAM,8BAA8B;;;;;;;;;;;;;;;;CAgBjC,CAAC"}
1
+ {"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../../src/report/constants.ts"],"names":[],"mappings":"AAKA;;;;GAIG;AACH,eAAO,MAAM,iBAAiB,WAA6B,CAAC;AAC5D,eAAO,MAAM,iBAAiB,WAA+B,CAAC;AAE9D,eAAO,MAAM,8BAA8B;;;;;;;;;;;;;;;;;CAiBjC,CAAC"}
@@ -20,6 +20,7 @@ export const REPORT_CARD_EMPTY_STATE_TITLES = {
20
20
  operatingSystems: "Operating system data not collected",
21
21
  migrationStatus: "Migration status data not collected",
22
22
  issuesBreakdown: "Issues breakdown data not collected",
23
+ errors: "Error data not collected",
23
24
  hostPowerStates: "Host power state data not collected",
24
25
  vmPowerStates: "VM power state data not collected",
25
26
  };
@@ -10,12 +10,14 @@ export { chartColorFailure, chartColorSuccess, REPORT_CARD_EMPTY_STATE_TITLES, }
10
10
  export { DashboardEmptyStateBase, type DashboardEmptyStateBaseProps, } from "./DashboardEmptyStateBase.js";
11
11
  export { dashboardStyles, tableFullWidthStyle } from "./dashboardStyles.js";
12
12
  export { EmptySearchResults, type EmptySearchResultsProps, } from "./EmptySearchResults.js";
13
+ export { ErrorTable, type ErrorTableProps, type MigrationIssueLike, } from "./ErrorTable.js";
13
14
  export { FeatureStatusBadge, type FeatureStatusBadgeProps, } from "./FeatureStatusBadge.js";
14
15
  export { HostPowerStates, type HostPowerStatesProps, } from "./HostPowerStates.js";
15
16
  export { buildHtmlFromCharts, buildHtmlReport, escapeHtml, type HtmlReportImage, } from "./htmlExport.js";
16
17
  export { InfrastructureSummary, type InfrastructureSummaryProps, } from "./InfrastructureSummary.js";
17
18
  export type { ClusterDetailRow, ClusterDetailsModel, FeatureStatus, HostCapability, InfrastructureSummaryModel, InventoryCluster, InventoryDatastore, InventoryHost, InventoryInfra, InventoryNetwork, InventoryVms, NetworkLabel, } from "./infrastructureSummaryModel.js";
18
19
  export { booleanToFeatureStatus, buildClusterDetailRows, buildClusterDetails, buildInfrastructureSummary, countVCenters, formatVSphereVersion, hostCapabilityStatus, visibleNetworks, vsanStatus, } from "./infrastructureSummaryModel.js";
20
+ export { type NetworkInfraLike, type NetworkLike, NetworkOverview, type NetworkOverviewProps, type NicCountHistogram, type NicCountSummary, } from "./NetworkOverview.js";
19
21
  export { OSBarChart, OSDistribution } from "./OSDistribution.js";
20
22
  export { OsSupportTiersHelpPopover } from "./OsSupportTiersHelpPopover.js";
21
23
  export { OsUpgradeNotice } from "./OsUpgradeNotice.js";
@@ -29,6 +31,7 @@ export { type PdfExportSegment, sliceCanvas, splitSegmentForPageHeight, } from "
29
31
  export type { PowerStateChartModel } from "./powerStates.js";
30
32
  export { buildHostPowerStateChart, buildVmPowerStateChart, HOST_POWER_COLORS, VM_POWER_COLORS, } from "./powerStates.js";
31
33
  export { ExportReportButton, type ExportReportButtonProps, ReportExportMenu, type ReportExportMenuProps, } from "./ReportExportMenu.js";
34
+ export { ReportTable, type ReportTableProps } from "./ReportTable.js";
32
35
  export { type ReportExportOption, type StandardReportExportHandlers, standardReportExportOptions, } from "./reportExportOptions.js";
33
36
  export { SupportTierBadge } from "./SupportTierBadge.js";
34
37
  export { ALL_TIERS_FILTER, type OsBarChartViewModel, type OsTableRow, useOsBarChartViewModel, } from "./useOsBarChartViewModel.js";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/report/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/report/index.ts"],"names":[],"mappings":"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"}
@@ -10,11 +10,13 @@ export { chartColorFailure, chartColorSuccess, REPORT_CARD_EMPTY_STATE_TITLES, }
10
10
  export { DashboardEmptyStateBase, } from "./DashboardEmptyStateBase.js";
11
11
  export { dashboardStyles, tableFullWidthStyle } from "./dashboardStyles.js";
12
12
  export { EmptySearchResults, } from "./EmptySearchResults.js";
13
+ export { ErrorTable, } from "./ErrorTable.js";
13
14
  export { FeatureStatusBadge, } from "./FeatureStatusBadge.js";
14
15
  export { HostPowerStates, } from "./HostPowerStates.js";
15
16
  export { buildHtmlFromCharts, buildHtmlReport, escapeHtml, } from "./htmlExport.js";
16
17
  export { InfrastructureSummary, } from "./InfrastructureSummary.js";
17
18
  export { booleanToFeatureStatus, buildClusterDetailRows, buildClusterDetails, buildInfrastructureSummary, countVCenters, formatVSphereVersion, hostCapabilityStatus, visibleNetworks, vsanStatus, } from "./infrastructureSummaryModel.js";
19
+ export { NetworkOverview, } from "./NetworkOverview.js";
18
20
  export { OSBarChart, OSDistribution } from "./OSDistribution.js";
19
21
  export { OsSupportTiersHelpPopover } from "./OsSupportTiersHelpPopover.js";
20
22
  export { OsUpgradeNotice } from "./OsUpgradeNotice.js";
@@ -26,6 +28,7 @@ export { buildPdfFromCharts } from "./pdfExport.js";
26
28
  export { sliceCanvas, splitSegmentForPageHeight, } from "./pdfPage.js";
27
29
  export { buildHostPowerStateChart, buildVmPowerStateChart, HOST_POWER_COLORS, VM_POWER_COLORS, } from "./powerStates.js";
28
30
  export { ExportReportButton, ReportExportMenu, } from "./ReportExportMenu.js";
31
+ export { ReportTable } from "./ReportTable.js";
29
32
  export { standardReportExportOptions, } from "./reportExportOptions.js";
30
33
  export { SupportTierBadge } from "./SupportTierBadge.js";
31
34
  export { ALL_TIERS_FILTER, useOsBarChartViewModel, } from "./useOsBarChartViewModel.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openshift-migration-advisor/shared-components",
3
- "version": "0.25.0-0ad1e70d1fa5",
3
+ "version": "0.25.0-1c27267d2ea6",
4
4
  "description": "Shared React UI components for Migration Advisor apps (report charts, forms, OS support tiers)",
5
5
  "author": "Montse Ortega <mortegag@redhat.com>",
6
6
  "license": "Apache-2.0",
@@ -46,7 +46,7 @@
46
46
  "README.md"
47
47
  ],
48
48
  "dependencies": {
49
- "html2canvas-pro": "^2.4.4",
49
+ "html2canvas-pro": "^2.4.5",
50
50
  "jspdf": "^4.2.1",
51
51
  "jszip": "^3.10.2"
52
52
  }
@@ -0,0 +1,85 @@
1
+ import {
2
+ Card,
3
+ CardBody,
4
+ CardTitle,
5
+ Flex,
6
+ FlexItem,
7
+ Icon,
8
+ } from "@patternfly/react-core";
9
+ import { ExclamationCircleIcon } 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 { ReportTable } from "./ReportTable.js";
17
+
18
+ /**
19
+ * Minimal shape of a migration issue consumed by {@link ErrorTable}. Declared
20
+ * locally so this package does not depend on a specific SDK (`agent-sdk` or
21
+ * `planner-sdk`); callers pass the subset of `MigrationIssue` fields read here.
22
+ */
23
+ export interface MigrationIssueLike {
24
+ label: string;
25
+ assessment: string;
26
+ count: number;
27
+ }
28
+
29
+ export interface ErrorTableProps {
30
+ errors: MigrationIssueLike[];
31
+ /** Optional drill-down callback invoked with an issue's `label`. */
32
+ onConcernClick?: (concernLabel: string) => void;
33
+ }
34
+
35
+ export const ErrorTable: React.FC<ErrorTableProps> = ({
36
+ errors,
37
+ onConcernClick,
38
+ }) => {
39
+ const handleRowClick = (issue: MigrationIssueLike) => {
40
+ if (issue.label && onConcernClick) {
41
+ onConcernClick(issue.label);
42
+ }
43
+ };
44
+
45
+ const chartId = "errors-table";
46
+ const chartTitle = "Errors";
47
+
48
+ return (
49
+ <ChartExportSurface id={chartId} title={chartTitle}>
50
+ <Card className={dashboardStyles.card}>
51
+ <CardTitle>
52
+ <Flex
53
+ justifyContent={{ default: "justifyContentSpaceBetween" }}
54
+ alignItems={{ default: "alignItemsCenter" }}
55
+ >
56
+ <FlexItem>
57
+ <Icon status="danger">
58
+ <ExclamationCircleIcon />
59
+ </Icon>{" "}
60
+ Errors
61
+ </FlexItem>
62
+ <ChartHeaderActions chartId={chartId} title={chartTitle} />
63
+ </Flex>
64
+ </CardTitle>
65
+ <CardBody className={dashboardStyles.cardBodyScrollable}>
66
+ {errors.length === 0 ? (
67
+ <CardEmptyState title={REPORT_CARD_EMPTY_STATE_TITLES.errors} />
68
+ ) : (
69
+ <div>
70
+ <ReportTable<MigrationIssueLike>
71
+ data={errors}
72
+ columns={["Description", "Total VMs"]}
73
+ fields={["assessment", "count"]}
74
+ onRowClick={onConcernClick ? handleRowClick : undefined}
75
+ clickableFields={onConcernClick ? ["assessment"] : []}
76
+ />
77
+ </div>
78
+ )}
79
+ </CardBody>
80
+ </Card>
81
+ </ChartExportSurface>
82
+ );
83
+ };
84
+
85
+ ErrorTable.displayName = "ErrorTable";
@@ -0,0 +1,405 @@
1
+ import { css } from "@emotion/css";
2
+ import {
3
+ Card,
4
+ CardBody,
5
+ CardTitle,
6
+ Dropdown,
7
+ DropdownItem,
8
+ DropdownList,
9
+ Flex,
10
+ FlexItem,
11
+ MenuToggle,
12
+ type MenuToggleElement,
13
+ } from "@patternfly/react-core";
14
+ import { TopologyIcon } from "@patternfly/react-icons";
15
+ import {
16
+ chart_color_black_500,
17
+ chart_color_blue_300,
18
+ chart_color_green_300,
19
+ chart_color_orange_100,
20
+ chart_color_purple_100,
21
+ chart_color_purple_300,
22
+ chart_color_red_orange_200,
23
+ chart_color_teal_200,
24
+ chart_color_teal_400,
25
+ chart_color_yellow_400,
26
+ } from "@patternfly/react-tokens";
27
+ import type { FC, Ref } from "react";
28
+ import { useMemo, useState } from "react";
29
+ import {
30
+ MigrationDonutChart,
31
+ type MigrationDonutChartDatum,
32
+ type MigrationDonutChartLegendVariant,
33
+ } from "../charts/MigrationDonutChart.js";
34
+ import { CardEmptyState } from "./CardEmptyState.js";
35
+ import { ChartHeaderActions } from "./ChartDownloadButton.js";
36
+ import { ChartExportSurface } from "./ChartExportSurface.js";
37
+ import { chartExportViewsFromLabels } from "./chartExport.js";
38
+ import { REPORT_CARD_EMPTY_STATE_TITLES } from "./constants.js";
39
+ import { dashboardStyles } from "./dashboardStyles.js";
40
+
41
+ /** Network fields this card reads. Apps pass SDK networks structurally. */
42
+ export interface NetworkLike {
43
+ vmsCount?: number;
44
+ vlanId?: string | number;
45
+ }
46
+
47
+ /** Infra fields this card reads. */
48
+ export interface NetworkInfraLike {
49
+ networks?: NetworkLike[];
50
+ }
51
+
52
+ /** Deprecated NIC histogram used when `distributionByNicCount` is absent. */
53
+ export interface NicCountHistogram {
54
+ data?: number[];
55
+ minValue?: number;
56
+ step?: number;
57
+ }
58
+
59
+ /** NIC summary fields this card reads from the VM resource breakdown. */
60
+ export interface NicCountSummary {
61
+ total?: number;
62
+ histogram?: NicCountHistogram;
63
+ }
64
+
65
+ export interface NetworkOverviewProps {
66
+ infra?: NetworkInfraLike;
67
+ nicCount?: NicCountSummary;
68
+ distributionByNicCount?: Record<string, number>;
69
+ /** `"html"` in agent-ui; ui-app passes `"chart"`. */
70
+ legendVariant?: MigrationDonutChartLegendVariant;
71
+ }
72
+
73
+ type ViewMode = "networkDistribution" | "nicCount";
74
+
75
+ const VIEW_MODE_LABELS: Record<ViewMode, string> = {
76
+ networkDistribution: "VM distribution by network",
77
+ nicCount: "VM distribution by NIC count",
78
+ };
79
+
80
+ const TOP_NETWORKS = 4;
81
+ const REST_OF_NETWORKS_LABEL = "Rest of networks";
82
+ const CHART_ID = "network-overview";
83
+ const CHART_TITLE = "Networks";
84
+
85
+ /**
86
+ * Categorical order from the ui-app network donut, mapped onto PatternFly
87
+ * `chart_color_*` tokens (nearest token where the old hex was not a token).
88
+ */
89
+ const NETWORK_COLORS = [
90
+ chart_color_blue_300.value,
91
+ chart_color_purple_300.value,
92
+ chart_color_purple_100.value,
93
+ chart_color_teal_200.value,
94
+ chart_color_yellow_400.value,
95
+ chart_color_green_300.value,
96
+ chart_color_orange_100.value,
97
+ chart_color_red_orange_200.value,
98
+ chart_color_teal_400.value,
99
+ chart_color_black_500.value,
100
+ ];
101
+
102
+ const cardSubtitle = css`
103
+ color: var(--pf-t--global--text--color--subtle);
104
+ font-size: 0.85rem;
105
+ `;
106
+
107
+ const menuToggleMinWidth = css`
108
+ min-width: 250px;
109
+ `;
110
+
111
+ const titleRow = css`
112
+ width: 100%;
113
+ `;
114
+
115
+ const isViewMode = (value: string | number | undefined): value is ViewMode =>
116
+ value === "networkDistribution" || value === "nicCount";
117
+
118
+ const legendFor = (categories: string[]): Record<string, string> => {
119
+ const legend: Record<string, string> = {};
120
+ categories.forEach((category, index) => {
121
+ const color = NETWORK_COLORS[index % NETWORK_COLORS.length];
122
+ if (color) {
123
+ legend[category] = color;
124
+ }
125
+ });
126
+ return legend;
127
+ };
128
+
129
+ const vlanLabel = (vlanId: NetworkLike["vlanId"]): string => {
130
+ if (typeof vlanId !== "string" && typeof vlanId !== "number") {
131
+ return "-";
132
+ }
133
+ const value = String(vlanId).trim();
134
+ return value !== "" ? value : "-";
135
+ };
136
+
137
+ const buildNetworkDonut = (
138
+ networks: NetworkLike[] | undefined,
139
+ ): {
140
+ slices: MigrationDonutChartDatum[];
141
+ legend: Record<string, string>;
142
+ title: string;
143
+ vlanBySlice: Record<string, string>;
144
+ } => {
145
+ const items = (Array.isArray(networks) ? networks : [])
146
+ .map((network) => ({
147
+ vmsCount:
148
+ typeof network?.vmsCount === "number" ? network.vmsCount : Number.NaN,
149
+ vlanId: vlanLabel(network?.vlanId),
150
+ }))
151
+ .filter(
152
+ (network) => Number.isFinite(network.vmsCount) && network.vmsCount >= 0,
153
+ )
154
+ .sort((a, b) => b.vmsCount - a.vmsCount);
155
+
156
+ const totalVMs = items.reduce((sum, network) => sum + network.vmsCount, 0);
157
+ const top = items.slice(0, TOP_NETWORKS);
158
+ const restSum = items
159
+ .slice(TOP_NETWORKS)
160
+ .reduce((sum, network) => sum + network.vmsCount, 0);
161
+
162
+ const vlanBySlice: Record<string, string> = {};
163
+ const slices: MigrationDonutChartDatum[] = top.map((network, index) => {
164
+ const name = `Network ${index + 1}`;
165
+ vlanBySlice[name] = network.vlanId;
166
+ return {
167
+ name,
168
+ count: network.vmsCount,
169
+ countDisplay: `${network.vmsCount} VMs`,
170
+ legendCategory: name,
171
+ };
172
+ });
173
+
174
+ if (restSum > 0) {
175
+ slices.push({
176
+ name: REST_OF_NETWORKS_LABEL,
177
+ count: restSum,
178
+ countDisplay: `${restSum} VMs`,
179
+ legendCategory: REST_OF_NETWORKS_LABEL,
180
+ });
181
+ vlanBySlice[REST_OF_NETWORKS_LABEL] = "-";
182
+ }
183
+
184
+ return {
185
+ slices,
186
+ legend: legendFor(slices.map((slice) => slice.legendCategory)),
187
+ title: `${totalVMs}`,
188
+ vlanBySlice,
189
+ };
190
+ };
191
+
192
+ const nicBucketLabel = (bucket: string): string => {
193
+ const count = Number.parseInt(bucket, 10);
194
+ if (bucket.endsWith("+")) {
195
+ return `${count}+ NIC`;
196
+ }
197
+ return count === 1 ? "1 NIC" : `${count} NIC`;
198
+ };
199
+
200
+ const compareNicBuckets = (
201
+ a: { bucket: string },
202
+ b: { bucket: string },
203
+ ): number => {
204
+ const aPlus = a.bucket.endsWith("+");
205
+ const bPlus = b.bucket.endsWith("+");
206
+ if (aPlus !== bPlus) {
207
+ return aPlus ? 1 : -1;
208
+ }
209
+ const aNum = Number.parseInt(a.bucket, 10);
210
+ const bNum = Number.parseInt(b.bucket, 10);
211
+ if (Number.isFinite(aNum) && Number.isFinite(bNum)) {
212
+ return aNum - bNum;
213
+ }
214
+ return a.bucket.localeCompare(b.bucket);
215
+ };
216
+
217
+ const slicesFromBuckets = (
218
+ entries: { bucket: string; count: number }[],
219
+ ): MigrationDonutChartDatum[] =>
220
+ entries.map((entry) => {
221
+ const label = nicBucketLabel(entry.bucket);
222
+ return {
223
+ name: label,
224
+ count: entry.count,
225
+ countDisplay: `${entry.count} VMs`,
226
+ legendCategory: label,
227
+ };
228
+ });
229
+
230
+ const buildNicDonut = (
231
+ distributionByNicCount: Record<string, number> | undefined,
232
+ nicCount: NicCountSummary | undefined,
233
+ ): {
234
+ slices: MigrationDonutChartDatum[];
235
+ legend: Record<string, string>;
236
+ title: string;
237
+ } => {
238
+ if (distributionByNicCount != null) {
239
+ const entries = Object.entries(distributionByNicCount)
240
+ .map(([bucket, value]) => ({
241
+ bucket,
242
+ count: Number.isFinite(Number(value)) ? Number(value) : 0,
243
+ }))
244
+ .filter((entry) => entry.count > 0)
245
+ .sort(compareNicBuckets);
246
+ const slices = slicesFromBuckets(entries);
247
+ const total = entries.reduce((sum, entry) => sum + entry.count, 0);
248
+ return {
249
+ slices,
250
+ legend: legendFor(slices.map((slice) => slice.legendCategory)),
251
+ title: `${total}`,
252
+ };
253
+ }
254
+
255
+ const histogram = nicCount?.histogram;
256
+ const data = Array.isArray(histogram?.data) ? histogram.data : [];
257
+ const minValue =
258
+ typeof histogram?.minValue === "number" ? histogram.minValue : 0;
259
+ const step = typeof histogram?.step === "number" ? histogram.step : 1;
260
+ const buckets = data
261
+ .map((count, index) => ({
262
+ bucket: String(minValue + index * step),
263
+ count: Number(count) || 0,
264
+ }))
265
+ .filter((entry) => entry.count > 0)
266
+ .sort((a, b) => Number(a.bucket) - Number(b.bucket));
267
+ const slices = slicesFromBuckets(buckets);
268
+ const total =
269
+ typeof nicCount?.total === "number"
270
+ ? nicCount.total
271
+ : buckets.reduce((sum, entry) => sum + entry.count, 0);
272
+
273
+ return {
274
+ slices,
275
+ legend: legendFor(slices.map((slice) => slice.legendCategory)),
276
+ title: `${total}`,
277
+ };
278
+ };
279
+
280
+ export const NetworkOverview: FC<NetworkOverviewProps> = ({
281
+ infra,
282
+ nicCount,
283
+ distributionByNicCount,
284
+ legendVariant = "html",
285
+ }) => {
286
+ const [viewMode, setViewMode] = useState<ViewMode>("networkDistribution");
287
+ const [isDropdownOpen, setIsDropdownOpen] = useState(false);
288
+
289
+ const networkChart = useMemo(
290
+ () => buildNetworkDonut(infra?.networks),
291
+ [infra?.networks],
292
+ );
293
+ const nicChart = useMemo(
294
+ () => buildNicDonut(distributionByNicCount, nicCount),
295
+ [distributionByNicCount, nicCount],
296
+ );
297
+
298
+ const chartTitle = `${CHART_TITLE} — ${VIEW_MODE_LABELS[viewMode]}`;
299
+ const activeChart =
300
+ viewMode === "networkDistribution" ? networkChart : nicChart;
301
+ const emptyTitle =
302
+ viewMode === "networkDistribution"
303
+ ? REPORT_CARD_EMPTY_STATE_TITLES.networks
304
+ : REPORT_CARD_EMPTY_STATE_TITLES.nicCount;
305
+
306
+ return (
307
+ <ChartExportSurface
308
+ id={CHART_ID}
309
+ title={chartTitle}
310
+ exportViews={chartExportViewsFromLabels(CHART_TITLE, VIEW_MODE_LABELS)}
311
+ activeExportViewId={viewMode}
312
+ onExportViewChange={(viewId) => {
313
+ if (isViewMode(viewId)) {
314
+ setViewMode(viewId);
315
+ }
316
+ }}
317
+ >
318
+ <Card className={dashboardStyles.card}>
319
+ <CardTitle>
320
+ <Flex
321
+ className={titleRow}
322
+ justifyContent={{ default: "justifyContentSpaceBetween" }}
323
+ alignItems={{ default: "alignItemsCenter" }}
324
+ >
325
+ <FlexItem>
326
+ <div>
327
+ <div>
328
+ <TopologyIcon /> {CHART_TITLE}
329
+ </div>
330
+ {viewMode === "networkDistribution" && (
331
+ <div className={cardSubtitle}>Top 5 networks</div>
332
+ )}
333
+ </div>
334
+ </FlexItem>
335
+ <ChartHeaderActions chartId={CHART_ID} title={chartTitle}>
336
+ <Dropdown
337
+ isOpen={isDropdownOpen}
338
+ onSelect={(_event, value) => {
339
+ if (isViewMode(value)) {
340
+ setViewMode(value);
341
+ }
342
+ setIsDropdownOpen(false);
343
+ }}
344
+ onOpenChange={setIsDropdownOpen}
345
+ toggle={(toggleRef: Ref<MenuToggleElement>) => (
346
+ <MenuToggle
347
+ ref={toggleRef}
348
+ onClick={() => setIsDropdownOpen((open) => !open)}
349
+ isExpanded={isDropdownOpen}
350
+ className={menuToggleMinWidth}
351
+ >
352
+ {VIEW_MODE_LABELS[viewMode]}
353
+ </MenuToggle>
354
+ )}
355
+ >
356
+ <DropdownList>
357
+ <DropdownItem
358
+ value="networkDistribution"
359
+ key="networkDistribution"
360
+ >
361
+ {VIEW_MODE_LABELS.networkDistribution}
362
+ </DropdownItem>
363
+ <DropdownItem value="nicCount" key="nicCount">
364
+ {VIEW_MODE_LABELS.nicCount}
365
+ </DropdownItem>
366
+ </DropdownList>
367
+ </Dropdown>
368
+ </ChartHeaderActions>
369
+ </Flex>
370
+ </CardTitle>
371
+ <CardBody className={dashboardStyles.cardBodyScrollable}>
372
+ {activeChart.slices.length === 0 ? (
373
+ <CardEmptyState title={emptyTitle} />
374
+ ) : (
375
+ <MigrationDonutChart
376
+ legendVariant={legendVariant}
377
+ data={activeChart.slices}
378
+ legend={activeChart.legend}
379
+ height={300}
380
+ width={420}
381
+ donutThickness={18}
382
+ titleFontSize={34}
383
+ title={activeChart.title}
384
+ subTitle="VMs"
385
+ subTitleColor="var(--pf-t--global--text--color--subtle)"
386
+ itemsPerRow={Math.ceil(activeChart.slices.length / 2)}
387
+ labelFontSize={18}
388
+ marginLeft="0%"
389
+ tooltipLabelFormatter={({ datum, percent }) => {
390
+ const summary = `${datum.countDisplay}\n${percent.toFixed(1)}%`;
391
+ if (viewMode !== "networkDistribution") {
392
+ return summary;
393
+ }
394
+ const vlan = networkChart.vlanBySlice[datum.legendCategory];
395
+ return `${summary}\nVLAN: ${vlan ?? "-"}`;
396
+ }}
397
+ />
398
+ )}
399
+ </CardBody>
400
+ </Card>
401
+ </ChartExportSurface>
402
+ );
403
+ };
404
+
405
+ NetworkOverview.displayName = "NetworkOverview";
@@ -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,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,
@@ -108,6 +113,14 @@ export {
108
113
  visibleNetworks,
109
114
  vsanStatus,
110
115
  } from "./infrastructureSummaryModel.js";
116
+ export {
117
+ type NetworkInfraLike,
118
+ type NetworkLike,
119
+ NetworkOverview,
120
+ type NetworkOverviewProps,
121
+ type NicCountHistogram,
122
+ type NicCountSummary,
123
+ } from "./NetworkOverview.js";
111
124
  export { OSBarChart, OSDistribution } from "./OSDistribution.js";
112
125
  export { OsSupportTiersHelpPopover } from "./OsSupportTiersHelpPopover.js";
113
126
  export { OsUpgradeNotice } from "./OsUpgradeNotice.js";
@@ -159,6 +172,7 @@ export {
159
172
  ReportExportMenu,
160
173
  type ReportExportMenuProps,
161
174
  } from "./ReportExportMenu.js";
175
+ export { ReportTable, type ReportTableProps } from "./ReportTable.js";
162
176
  export {
163
177
  type ReportExportOption,
164
178
  type StandardReportExportHandlers,