@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.
@@ -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";
@@ -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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"}
@@ -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-1c27267d2ea6",
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
+ });
@@ -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,