@openshift-migration-advisor/shared-components 0.25.0-1c27267d2ea6 → 0.25.0-2ba17b54a3c7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/report/HostsOverview.d.ts +13 -0
- package/dist/report/HostsOverview.d.ts.map +1 -0
- package/dist/report/HostsOverview.js +84 -0
- package/dist/report/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/HostsOverview.tsx +185 -0
- package/src/report/__tests__/HostsOverview.test.tsx +145 -0
- package/src/report/index.ts +5 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { FC } from "react";
|
|
2
|
+
import { type MigrationDonutChartLegendVariant } from "../charts/MigrationDonutChart.js";
|
|
3
|
+
/** Smallest host shape this card reads. Apps pass SDK hosts structurally. */
|
|
4
|
+
export interface HostLike {
|
|
5
|
+
model?: string | number;
|
|
6
|
+
}
|
|
7
|
+
export interface HostsOverviewProps {
|
|
8
|
+
hosts?: HostLike[];
|
|
9
|
+
/** `"html"` in agent-ui; ui-app passes `"chart"`. */
|
|
10
|
+
legendVariant?: MigrationDonutChartLegendVariant;
|
|
11
|
+
}
|
|
12
|
+
export declare const HostsOverview: FC<HostsOverviewProps>;
|
|
13
|
+
//# sourceMappingURL=HostsOverview.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"HostsOverview.d.ts","sourceRoot":"","sources":["../../src/report/HostsOverview.tsx"],"names":[],"mappings":"AAiBA,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC;AAEhC,OAAO,EAGL,KAAK,gCAAgC,EACtC,MAAM,kCAAkC,CAAC;AAO1C,6EAA6E;AAC7E,MAAM,WAAW,QAAQ;IACvB,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,kBAAkB;IACjC,KAAK,CAAC,EAAE,QAAQ,EAAE,CAAC;IACnB,qDAAqD;IACrD,aAAa,CAAC,EAAE,gCAAgC,CAAC;CAClD;AAuFD,eAAO,MAAM,aAAa,EAAE,EAAE,CAAC,kBAAkB,CAwDhD,CAAC"}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { css } from "@emotion/css";
|
|
3
|
+
import { Card, CardBody, CardTitle, Flex, FlexItem, } from "@patternfly/react-core";
|
|
4
|
+
import { ServerIcon } from "@patternfly/react-icons";
|
|
5
|
+
import { chart_color_blue_300, chart_color_green_300, chart_color_purple_100, chart_color_purple_300, chart_color_teal_200, chart_color_yellow_400, } from "@patternfly/react-tokens";
|
|
6
|
+
import { useMemo } 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 { REPORT_CARD_EMPTY_STATE_TITLES } from "./constants.js";
|
|
12
|
+
import { dashboardStyles } from "./dashboardStyles.js";
|
|
13
|
+
const TOP_HOST_MODELS = 5;
|
|
14
|
+
const OTHER_MODELS_LABEL = "Other models";
|
|
15
|
+
const UNKNOWN_MODEL_LABEL = "Unknown model";
|
|
16
|
+
const CHART_ID = "hosts-overview";
|
|
17
|
+
const CHART_TITLE = "Host distribution by model";
|
|
18
|
+
/**
|
|
19
|
+
* Same categorical order as the ui-app host-model donut, mapped onto
|
|
20
|
+
* PatternFly `chart_color_*` tokens (nearest token where the old hex
|
|
21
|
+
* was not itself a token).
|
|
22
|
+
*/
|
|
23
|
+
const HOST_MODEL_COLORS = [
|
|
24
|
+
chart_color_blue_300.value,
|
|
25
|
+
chart_color_purple_300.value,
|
|
26
|
+
chart_color_purple_100.value,
|
|
27
|
+
chart_color_teal_200.value,
|
|
28
|
+
chart_color_yellow_400.value,
|
|
29
|
+
chart_color_green_300.value,
|
|
30
|
+
];
|
|
31
|
+
const cardSubtitle = css `
|
|
32
|
+
color: var(--pf-t--global--text--color--subtle);
|
|
33
|
+
font-size: 0.85rem;
|
|
34
|
+
`;
|
|
35
|
+
const hostModelLabel = (model) => {
|
|
36
|
+
const raw = typeof model === "string"
|
|
37
|
+
? model
|
|
38
|
+
: typeof model === "number"
|
|
39
|
+
? String(model)
|
|
40
|
+
: "";
|
|
41
|
+
const trimmed = raw.trim();
|
|
42
|
+
return trimmed !== "" ? trimmed : UNKNOWN_MODEL_LABEL;
|
|
43
|
+
};
|
|
44
|
+
const buildHostModelDonut = (hosts) => {
|
|
45
|
+
const counts = new Map();
|
|
46
|
+
for (const host of hosts) {
|
|
47
|
+
const label = hostModelLabel(host?.model);
|
|
48
|
+
counts.set(label, (counts.get(label) ?? 0) + 1);
|
|
49
|
+
}
|
|
50
|
+
const entries = [...counts.entries()]
|
|
51
|
+
.map(([model, count]) => ({ model, count }))
|
|
52
|
+
.sort((a, b) => b.count - a.count);
|
|
53
|
+
const top = entries.slice(0, TOP_HOST_MODELS);
|
|
54
|
+
const restSum = entries
|
|
55
|
+
.slice(TOP_HOST_MODELS)
|
|
56
|
+
.reduce((sum, entry) => sum + entry.count, 0);
|
|
57
|
+
const slices = top.map((entry) => ({
|
|
58
|
+
name: entry.model,
|
|
59
|
+
count: entry.count,
|
|
60
|
+
countDisplay: `${entry.count} hosts`,
|
|
61
|
+
legendCategory: entry.model,
|
|
62
|
+
}));
|
|
63
|
+
if (restSum > 0) {
|
|
64
|
+
slices.push({
|
|
65
|
+
name: OTHER_MODELS_LABEL,
|
|
66
|
+
count: restSum,
|
|
67
|
+
countDisplay: `${restSum} hosts`,
|
|
68
|
+
legendCategory: OTHER_MODELS_LABEL,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
const legend = {};
|
|
72
|
+
slices.forEach((slice, index) => {
|
|
73
|
+
const color = HOST_MODEL_COLORS[index % HOST_MODEL_COLORS.length];
|
|
74
|
+
if (color) {
|
|
75
|
+
legend[slice.legendCategory] = color;
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
return { slices, legend, totalHosts: hosts.length };
|
|
79
|
+
};
|
|
80
|
+
export const HostsOverview = ({ hosts, legendVariant = "html", }) => {
|
|
81
|
+
const { slices, legend, totalHosts } = useMemo(() => buildHostModelDonut(Array.isArray(hosts) ? hosts : []), [hosts]);
|
|
82
|
+
return (_jsx(ChartExportSurface, { id: CHART_ID, title: CHART_TITLE, children: _jsxs(Card, { className: dashboardStyles.card, children: [_jsx(CardTitle, { children: _jsxs(Flex, { justifyContent: { default: "justifyContentSpaceBetween" }, alignItems: { default: "alignItemsCenter" }, children: [_jsx(FlexItem, { children: _jsxs("div", { children: [_jsxs("div", { children: [_jsx(ServerIcon, {}), " ", CHART_TITLE] }), _jsx("div", { className: cardSubtitle, children: "Top 5 models" })] }) }), _jsx(ChartHeaderActions, { chartId: CHART_ID, title: CHART_TITLE })] }) }), _jsx(CardBody, { className: dashboardStyles.cardBodyScrollable, children: slices.length === 0 ? (_jsx(CardEmptyState, { title: REPORT_CARD_EMPTY_STATE_TITLES.hosts })) : (_jsx(MigrationDonutChart, { legendVariant: legendVariant, data: slices, height: 300, width: 420, donutThickness: 18, titleFontSize: 34, legend: legend, legendWidth: 680, title: `${totalHosts}`, subTitle: "Hosts", subTitleColor: "var(--pf-t--global--text--color--subtle)", itemsPerRow: 2, labelFontSize: 16, marginLeft: "0%", tooltipLabelFormatter: ({ datum, percent }) => `${datum.countDisplay}\n${percent.toFixed(1)}%` })) })] }) }));
|
|
83
|
+
};
|
|
84
|
+
HostsOverview.displayName = "HostsOverview";
|
package/dist/report/index.d.ts
CHANGED
|
@@ -13,6 +13,7 @@ export { EmptySearchResults, type EmptySearchResultsProps, } from "./EmptySearch
|
|
|
13
13
|
export { ErrorTable, type ErrorTableProps, type MigrationIssueLike, } from "./ErrorTable.js";
|
|
14
14
|
export { FeatureStatusBadge, type FeatureStatusBadgeProps, } from "./FeatureStatusBadge.js";
|
|
15
15
|
export { HostPowerStates, type HostPowerStatesProps, } from "./HostPowerStates.js";
|
|
16
|
+
export { type HostLike, HostsOverview, type HostsOverviewProps, } from "./HostsOverview.js";
|
|
16
17
|
export { buildHtmlFromCharts, buildHtmlReport, escapeHtml, type HtmlReportImage, } from "./htmlExport.js";
|
|
17
18
|
export { InfrastructureSummary, type InfrastructureSummaryProps, } from "./InfrastructureSummary.js";
|
|
18
19
|
export type { ClusterDetailRow, ClusterDetailsModel, FeatureStatus, HostCapability, InfrastructureSummaryModel, InventoryCluster, InventoryDatastore, InventoryHost, InventoryInfra, InventoryNetwork, InventoryVms, NetworkLabel, } from "./infrastructureSummaryModel.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
|
@@ -13,6 +13,7 @@ export { EmptySearchResults, } from "./EmptySearchResults.js";
|
|
|
13
13
|
export { ErrorTable, } from "./ErrorTable.js";
|
|
14
14
|
export { FeatureStatusBadge, } from "./FeatureStatusBadge.js";
|
|
15
15
|
export { HostPowerStates, } from "./HostPowerStates.js";
|
|
16
|
+
export { HostsOverview, } from "./HostsOverview.js";
|
|
16
17
|
export { buildHtmlFromCharts, buildHtmlReport, escapeHtml, } from "./htmlExport.js";
|
|
17
18
|
export { InfrastructureSummary, } from "./InfrastructureSummary.js";
|
|
18
19
|
export { booleanToFeatureStatus, buildClusterDetailRows, buildClusterDetails, buildInfrastructureSummary, countVCenters, formatVSphereVersion, hostCapabilityStatus, visibleNetworks, vsanStatus, } from "./infrastructureSummaryModel.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-2ba17b54a3c7",
|
|
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,185 @@
|
|
|
1
|
+
import { css } from "@emotion/css";
|
|
2
|
+
import {
|
|
3
|
+
Card,
|
|
4
|
+
CardBody,
|
|
5
|
+
CardTitle,
|
|
6
|
+
Flex,
|
|
7
|
+
FlexItem,
|
|
8
|
+
} from "@patternfly/react-core";
|
|
9
|
+
import { ServerIcon } from "@patternfly/react-icons";
|
|
10
|
+
import {
|
|
11
|
+
chart_color_blue_300,
|
|
12
|
+
chart_color_green_300,
|
|
13
|
+
chart_color_purple_100,
|
|
14
|
+
chart_color_purple_300,
|
|
15
|
+
chart_color_teal_200,
|
|
16
|
+
chart_color_yellow_400,
|
|
17
|
+
} from "@patternfly/react-tokens";
|
|
18
|
+
import type { FC } from "react";
|
|
19
|
+
import { useMemo } from "react";
|
|
20
|
+
import {
|
|
21
|
+
MigrationDonutChart,
|
|
22
|
+
type MigrationDonutChartDatum,
|
|
23
|
+
type MigrationDonutChartLegendVariant,
|
|
24
|
+
} from "../charts/MigrationDonutChart.js";
|
|
25
|
+
import { CardEmptyState } from "./CardEmptyState.js";
|
|
26
|
+
import { ChartHeaderActions } from "./ChartDownloadButton.js";
|
|
27
|
+
import { ChartExportSurface } from "./ChartExportSurface.js";
|
|
28
|
+
import { REPORT_CARD_EMPTY_STATE_TITLES } from "./constants.js";
|
|
29
|
+
import { dashboardStyles } from "./dashboardStyles.js";
|
|
30
|
+
|
|
31
|
+
/** Smallest host shape this card reads. Apps pass SDK hosts structurally. */
|
|
32
|
+
export interface HostLike {
|
|
33
|
+
model?: string | number;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface HostsOverviewProps {
|
|
37
|
+
hosts?: HostLike[];
|
|
38
|
+
/** `"html"` in agent-ui; ui-app passes `"chart"`. */
|
|
39
|
+
legendVariant?: MigrationDonutChartLegendVariant;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const TOP_HOST_MODELS = 5;
|
|
43
|
+
const OTHER_MODELS_LABEL = "Other models";
|
|
44
|
+
const UNKNOWN_MODEL_LABEL = "Unknown model";
|
|
45
|
+
const CHART_ID = "hosts-overview";
|
|
46
|
+
const CHART_TITLE = "Host distribution by model";
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Same categorical order as the ui-app host-model donut, mapped onto
|
|
50
|
+
* PatternFly `chart_color_*` tokens (nearest token where the old hex
|
|
51
|
+
* was not itself a token).
|
|
52
|
+
*/
|
|
53
|
+
const HOST_MODEL_COLORS = [
|
|
54
|
+
chart_color_blue_300.value,
|
|
55
|
+
chart_color_purple_300.value,
|
|
56
|
+
chart_color_purple_100.value,
|
|
57
|
+
chart_color_teal_200.value,
|
|
58
|
+
chart_color_yellow_400.value,
|
|
59
|
+
chart_color_green_300.value,
|
|
60
|
+
];
|
|
61
|
+
|
|
62
|
+
const cardSubtitle = css`
|
|
63
|
+
color: var(--pf-t--global--text--color--subtle);
|
|
64
|
+
font-size: 0.85rem;
|
|
65
|
+
`;
|
|
66
|
+
|
|
67
|
+
const hostModelLabel = (model: HostLike["model"]): string => {
|
|
68
|
+
const raw =
|
|
69
|
+
typeof model === "string"
|
|
70
|
+
? model
|
|
71
|
+
: typeof model === "number"
|
|
72
|
+
? String(model)
|
|
73
|
+
: "";
|
|
74
|
+
const trimmed = raw.trim();
|
|
75
|
+
return trimmed !== "" ? trimmed : UNKNOWN_MODEL_LABEL;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const buildHostModelDonut = (
|
|
79
|
+
hosts: HostLike[],
|
|
80
|
+
): {
|
|
81
|
+
slices: MigrationDonutChartDatum[];
|
|
82
|
+
legend: Record<string, string>;
|
|
83
|
+
totalHosts: number;
|
|
84
|
+
} => {
|
|
85
|
+
const counts = new Map<string, number>();
|
|
86
|
+
for (const host of hosts) {
|
|
87
|
+
const label = hostModelLabel(host?.model);
|
|
88
|
+
counts.set(label, (counts.get(label) ?? 0) + 1);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const entries = [...counts.entries()]
|
|
92
|
+
.map(([model, count]) => ({ model, count }))
|
|
93
|
+
.sort((a, b) => b.count - a.count);
|
|
94
|
+
|
|
95
|
+
const top = entries.slice(0, TOP_HOST_MODELS);
|
|
96
|
+
const restSum = entries
|
|
97
|
+
.slice(TOP_HOST_MODELS)
|
|
98
|
+
.reduce((sum, entry) => sum + entry.count, 0);
|
|
99
|
+
|
|
100
|
+
const slices: MigrationDonutChartDatum[] = top.map((entry) => ({
|
|
101
|
+
name: entry.model,
|
|
102
|
+
count: entry.count,
|
|
103
|
+
countDisplay: `${entry.count} hosts`,
|
|
104
|
+
legendCategory: entry.model,
|
|
105
|
+
}));
|
|
106
|
+
|
|
107
|
+
if (restSum > 0) {
|
|
108
|
+
slices.push({
|
|
109
|
+
name: OTHER_MODELS_LABEL,
|
|
110
|
+
count: restSum,
|
|
111
|
+
countDisplay: `${restSum} hosts`,
|
|
112
|
+
legendCategory: OTHER_MODELS_LABEL,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const legend: Record<string, string> = {};
|
|
117
|
+
slices.forEach((slice, index) => {
|
|
118
|
+
const color = HOST_MODEL_COLORS[index % HOST_MODEL_COLORS.length];
|
|
119
|
+
if (color) {
|
|
120
|
+
legend[slice.legendCategory] = color;
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
return { slices, legend, totalHosts: hosts.length };
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
export const HostsOverview: FC<HostsOverviewProps> = ({
|
|
128
|
+
hosts,
|
|
129
|
+
legendVariant = "html",
|
|
130
|
+
}) => {
|
|
131
|
+
const { slices, legend, totalHosts } = useMemo(
|
|
132
|
+
() => buildHostModelDonut(Array.isArray(hosts) ? hosts : []),
|
|
133
|
+
[hosts],
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
return (
|
|
137
|
+
<ChartExportSurface id={CHART_ID} title={CHART_TITLE}>
|
|
138
|
+
<Card className={dashboardStyles.card}>
|
|
139
|
+
<CardTitle>
|
|
140
|
+
<Flex
|
|
141
|
+
justifyContent={{ default: "justifyContentSpaceBetween" }}
|
|
142
|
+
alignItems={{ default: "alignItemsCenter" }}
|
|
143
|
+
>
|
|
144
|
+
<FlexItem>
|
|
145
|
+
<div>
|
|
146
|
+
<div>
|
|
147
|
+
<ServerIcon /> {CHART_TITLE}
|
|
148
|
+
</div>
|
|
149
|
+
<div className={cardSubtitle}>Top 5 models</div>
|
|
150
|
+
</div>
|
|
151
|
+
</FlexItem>
|
|
152
|
+
<ChartHeaderActions chartId={CHART_ID} title={CHART_TITLE} />
|
|
153
|
+
</Flex>
|
|
154
|
+
</CardTitle>
|
|
155
|
+
<CardBody className={dashboardStyles.cardBodyScrollable}>
|
|
156
|
+
{slices.length === 0 ? (
|
|
157
|
+
<CardEmptyState title={REPORT_CARD_EMPTY_STATE_TITLES.hosts} />
|
|
158
|
+
) : (
|
|
159
|
+
<MigrationDonutChart
|
|
160
|
+
legendVariant={legendVariant}
|
|
161
|
+
data={slices}
|
|
162
|
+
height={300}
|
|
163
|
+
width={420}
|
|
164
|
+
donutThickness={18}
|
|
165
|
+
titleFontSize={34}
|
|
166
|
+
legend={legend}
|
|
167
|
+
legendWidth={680}
|
|
168
|
+
title={`${totalHosts}`}
|
|
169
|
+
subTitle="Hosts"
|
|
170
|
+
subTitleColor="var(--pf-t--global--text--color--subtle)"
|
|
171
|
+
itemsPerRow={2}
|
|
172
|
+
labelFontSize={16}
|
|
173
|
+
marginLeft="0%"
|
|
174
|
+
tooltipLabelFormatter={({ datum, percent }) =>
|
|
175
|
+
`${datum.countDisplay}\n${percent.toFixed(1)}%`
|
|
176
|
+
}
|
|
177
|
+
/>
|
|
178
|
+
)}
|
|
179
|
+
</CardBody>
|
|
180
|
+
</Card>
|
|
181
|
+
</ChartExportSurface>
|
|
182
|
+
);
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
HostsOverview.displayName = "HostsOverview";
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import "@testing-library/jest-dom";
|
|
2
|
+
import {
|
|
3
|
+
chart_color_blue_300,
|
|
4
|
+
chart_color_green_300,
|
|
5
|
+
chart_color_purple_100,
|
|
6
|
+
chart_color_purple_300,
|
|
7
|
+
chart_color_teal_200,
|
|
8
|
+
chart_color_yellow_400,
|
|
9
|
+
} from "@patternfly/react-tokens";
|
|
10
|
+
import { cleanup, render, screen } from "@testing-library/react";
|
|
11
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
12
|
+
import type { MigrationDonutChartProps } from "../../charts/MigrationDonutChart.js";
|
|
13
|
+
import { HostsOverview } from "../HostsOverview.js";
|
|
14
|
+
|
|
15
|
+
const donutProps = vi.hoisted(() => ({
|
|
16
|
+
current: null as MigrationDonutChartProps | null,
|
|
17
|
+
}));
|
|
18
|
+
|
|
19
|
+
vi.mock("../../charts/MigrationDonutChart.js", () => ({
|
|
20
|
+
MigrationDonutChart: (props: MigrationDonutChartProps): JSX.Element => {
|
|
21
|
+
donutProps.current = props;
|
|
22
|
+
return (
|
|
23
|
+
<div data-testid="hosts-donut">
|
|
24
|
+
{props.title} {props.subTitle}
|
|
25
|
+
</div>
|
|
26
|
+
);
|
|
27
|
+
},
|
|
28
|
+
}));
|
|
29
|
+
|
|
30
|
+
afterEach(() => {
|
|
31
|
+
donutProps.current = null;
|
|
32
|
+
cleanup();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const renderedDonut = (): MigrationDonutChartProps => {
|
|
36
|
+
const props = donutProps.current;
|
|
37
|
+
if (!props) {
|
|
38
|
+
throw new Error("expected the host model donut to render");
|
|
39
|
+
}
|
|
40
|
+
return props;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
describe("HostsOverview", () => {
|
|
44
|
+
it("shows the shared empty state when no hosts are available", () => {
|
|
45
|
+
render(<HostsOverview />);
|
|
46
|
+
|
|
47
|
+
expect(screen.getByText("Host distribution by model")).toBeInTheDocument();
|
|
48
|
+
expect(screen.getByText("Top 5 models")).toBeInTheDocument();
|
|
49
|
+
expect(screen.getByText("Host data not collected")).toBeInTheDocument();
|
|
50
|
+
expect(screen.queryByTestId("hosts-donut")).not.toBeInTheDocument();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("buckets host counts into the top 5 models plus Other models", () => {
|
|
54
|
+
render(
|
|
55
|
+
<HostsOverview
|
|
56
|
+
hosts={[
|
|
57
|
+
{ model: "M1" },
|
|
58
|
+
{ model: "M1" },
|
|
59
|
+
{ model: "M1" },
|
|
60
|
+
{ model: "M1" },
|
|
61
|
+
{ model: "M1" },
|
|
62
|
+
{ model: "M1" },
|
|
63
|
+
{ model: "M2" },
|
|
64
|
+
{ model: "M2" },
|
|
65
|
+
{ model: "M2" },
|
|
66
|
+
{ model: "M2" },
|
|
67
|
+
{ model: "M2" },
|
|
68
|
+
{ model: "M3" },
|
|
69
|
+
{ model: "M3" },
|
|
70
|
+
{ model: "M3" },
|
|
71
|
+
{ model: "M3" },
|
|
72
|
+
{ model: "M4" },
|
|
73
|
+
{ model: "M4" },
|
|
74
|
+
{ model: "M4" },
|
|
75
|
+
{ model: "M5" },
|
|
76
|
+
{ model: "M5" },
|
|
77
|
+
{ model: "M6" },
|
|
78
|
+
{ model: "M7" },
|
|
79
|
+
]}
|
|
80
|
+
/>,
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
expect(screen.getByTestId("hosts-donut")).toHaveTextContent("22 Hosts");
|
|
84
|
+
|
|
85
|
+
const props = renderedDonut();
|
|
86
|
+
expect(props.data.map((slice) => [slice.name, slice.count])).toEqual([
|
|
87
|
+
["M1", 6],
|
|
88
|
+
["M2", 5],
|
|
89
|
+
["M3", 4],
|
|
90
|
+
["M4", 3],
|
|
91
|
+
["M5", 2],
|
|
92
|
+
["Other models", 2],
|
|
93
|
+
]);
|
|
94
|
+
expect(props.data[5]?.countDisplay).toBe("2 hosts");
|
|
95
|
+
expect(props.legend).toEqual({
|
|
96
|
+
M1: chart_color_blue_300.value,
|
|
97
|
+
M2: chart_color_purple_300.value,
|
|
98
|
+
M3: chart_color_purple_100.value,
|
|
99
|
+
M4: chart_color_teal_200.value,
|
|
100
|
+
M5: chart_color_yellow_400.value,
|
|
101
|
+
"Other models": chart_color_green_300.value,
|
|
102
|
+
});
|
|
103
|
+
expect(props.legendVariant).toBe("html");
|
|
104
|
+
expect(props.height).toBe(300);
|
|
105
|
+
expect(props.width).toBe(420);
|
|
106
|
+
expect(props.donutThickness).toBe(18);
|
|
107
|
+
expect(props.titleFontSize).toBe(34);
|
|
108
|
+
expect(props.legendWidth).toBe(680);
|
|
109
|
+
expect(props.itemsPerRow).toBe(2);
|
|
110
|
+
expect(props.labelFontSize).toBe(16);
|
|
111
|
+
expect(props.marginLeft).toBe("0%");
|
|
112
|
+
expect(props.subTitleColor).toBe(
|
|
113
|
+
"var(--pf-t--global--text--color--subtle)",
|
|
114
|
+
);
|
|
115
|
+
expect(
|
|
116
|
+
props.tooltipLabelFormatter?.({
|
|
117
|
+
datum: {
|
|
118
|
+
x: "M1",
|
|
119
|
+
y: 6,
|
|
120
|
+
countDisplay: "6 hosts",
|
|
121
|
+
legendCategory: "M1",
|
|
122
|
+
},
|
|
123
|
+
percent: 27.2727,
|
|
124
|
+
total: 22,
|
|
125
|
+
}),
|
|
126
|
+
).toBe("6 hosts\n27.3%");
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("treats numeric models as labels and blank models as unknown", () => {
|
|
130
|
+
render(
|
|
131
|
+
<HostsOverview
|
|
132
|
+
legendVariant="chart"
|
|
133
|
+
hosts={[{ model: 42 }, { model: " " }, { model: " R640 " }]}
|
|
134
|
+
/>,
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
expect(renderedDonut().data.map((slice) => slice.name)).toEqual([
|
|
138
|
+
"42",
|
|
139
|
+
"Unknown model",
|
|
140
|
+
"R640",
|
|
141
|
+
]);
|
|
142
|
+
expect(renderedDonut().legendVariant).toBe("chart");
|
|
143
|
+
expect(renderedDonut().title).toBe("3");
|
|
144
|
+
});
|
|
145
|
+
});
|
package/src/report/index.ts
CHANGED
|
@@ -78,6 +78,11 @@ export {
|
|
|
78
78
|
HostPowerStates,
|
|
79
79
|
type HostPowerStatesProps,
|
|
80
80
|
} from "./HostPowerStates.js";
|
|
81
|
+
export {
|
|
82
|
+
type HostLike,
|
|
83
|
+
HostsOverview,
|
|
84
|
+
type HostsOverviewProps,
|
|
85
|
+
} from "./HostsOverview.js";
|
|
81
86
|
export {
|
|
82
87
|
buildHtmlFromCharts,
|
|
83
88
|
buildHtmlReport,
|