@openshift-migration-advisor/shared-components 0.25.0-883f4c9ae996 → 0.25.0-fce5293f01c1
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/NetworkOverview.d.ts +31 -0
- package/dist/report/NetworkOverview.d.ts.map +1 -0
- package/dist/report/NetworkOverview.js +201 -0
- package/dist/report/index.d.ts +1 -0
- package/dist/report/index.d.ts.map +1 -1
- package/dist/report/index.js +1 -0
- package/package.json +1 -1
- package/src/report/NetworkOverview.tsx +405 -0
- package/src/report/__tests__/NetworkOverview.test.tsx +269 -0
- package/src/report/index.ts +8 -0
|
@@ -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";
|
package/dist/report/index.d.ts
CHANGED
|
@@ -16,6 +16,7 @@ export { buildHtmlFromCharts, buildHtmlReport, escapeHtml, type HtmlReportImage,
|
|
|
16
16
|
export { InfrastructureSummary, type InfrastructureSummaryProps, } from "./InfrastructureSummary.js";
|
|
17
17
|
export type { ClusterDetailRow, ClusterDetailsModel, FeatureStatus, HostCapability, InfrastructureSummaryModel, InventoryCluster, InventoryDatastore, InventoryHost, InventoryInfra, InventoryNetwork, InventoryVms, NetworkLabel, } from "./infrastructureSummaryModel.js";
|
|
18
18
|
export { booleanToFeatureStatus, buildClusterDetailRows, buildClusterDetails, buildInfrastructureSummary, countVCenters, formatVSphereVersion, hostCapabilityStatus, visibleNetworks, vsanStatus, } from "./infrastructureSummaryModel.js";
|
|
19
|
+
export { type NetworkInfraLike, type NetworkLike, NetworkOverview, type NetworkOverviewProps, type NicCountHistogram, type NicCountSummary, } from "./NetworkOverview.js";
|
|
19
20
|
export { OSBarChart, OSDistribution } from "./OSDistribution.js";
|
|
20
21
|
export { OsSupportTiersHelpPopover } from "./OsSupportTiersHelpPopover.js";
|
|
21
22
|
export { OsUpgradeNotice } from "./OsUpgradeNotice.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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"}
|
package/dist/report/index.js
CHANGED
|
@@ -15,6 +15,7 @@ export { HostPowerStates, } from "./HostPowerStates.js";
|
|
|
15
15
|
export { buildHtmlFromCharts, buildHtmlReport, escapeHtml, } from "./htmlExport.js";
|
|
16
16
|
export { InfrastructureSummary, } from "./InfrastructureSummary.js";
|
|
17
17
|
export { booleanToFeatureStatus, buildClusterDetailRows, buildClusterDetails, buildInfrastructureSummary, countVCenters, formatVSphereVersion, hostCapabilityStatus, visibleNetworks, vsanStatus, } from "./infrastructureSummaryModel.js";
|
|
18
|
+
export { NetworkOverview, } from "./NetworkOverview.js";
|
|
18
19
|
export { OSBarChart, OSDistribution } from "./OSDistribution.js";
|
|
19
20
|
export { OsSupportTiersHelpPopover } from "./OsSupportTiersHelpPopover.js";
|
|
20
21
|
export { OsUpgradeNotice } from "./OsUpgradeNotice.js";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openshift-migration-advisor/shared-components",
|
|
3
|
-
"version": "0.25.0-
|
|
3
|
+
"version": "0.25.0-fce5293f01c1",
|
|
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",
|
|
@@ -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,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/index.ts
CHANGED
|
@@ -108,6 +108,14 @@ export {
|
|
|
108
108
|
visibleNetworks,
|
|
109
109
|
vsanStatus,
|
|
110
110
|
} from "./infrastructureSummaryModel.js";
|
|
111
|
+
export {
|
|
112
|
+
type NetworkInfraLike,
|
|
113
|
+
type NetworkLike,
|
|
114
|
+
NetworkOverview,
|
|
115
|
+
type NetworkOverviewProps,
|
|
116
|
+
type NicCountHistogram,
|
|
117
|
+
type NicCountSummary,
|
|
118
|
+
} from "./NetworkOverview.js";
|
|
111
119
|
export { OSBarChart, OSDistribution } from "./OSDistribution.js";
|
|
112
120
|
export { OsSupportTiersHelpPopover } from "./OsSupportTiersHelpPopover.js";
|
|
113
121
|
export { OsUpgradeNotice } from "./OsUpgradeNotice.js";
|