@openshift-migration-advisor/shared-components 0.22.0-dd51551836b7 → 0.23.0
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/charts/MigrationDonutChart.d.ts +18 -1
- package/dist/charts/MigrationDonutChart.d.ts.map +1 -1
- package/dist/charts/MigrationDonutChart.js +103 -43
- package/dist/report/OsSupportTiersHelpPopover.d.ts.map +1 -1
- package/dist/report/OsSupportTiersHelpPopover.js +1 -1
- package/package.json +1 -1
- package/src/charts/MigrationDonutChart.tsx +232 -111
- package/src/report/OsSupportTiersHelpPopover.tsx +1 -0
|
@@ -15,6 +15,7 @@ export interface MigrationDonutChartDatum {
|
|
|
15
15
|
clusterNames?: string[];
|
|
16
16
|
networkNames?: string[];
|
|
17
17
|
}
|
|
18
|
+
export type MigrationDonutChartLegendVariant = "html" | "chart";
|
|
18
19
|
export interface MigrationDonutChartProps {
|
|
19
20
|
data: MigrationDonutChartDatum[];
|
|
20
21
|
legend?: Record<string, string>;
|
|
@@ -40,13 +41,29 @@ export interface MigrationDonutChartProps {
|
|
|
40
41
|
percent: number;
|
|
41
42
|
total: number;
|
|
42
43
|
}) => string;
|
|
44
|
+
/** Agent-ui: slice / legend click drill-down. */
|
|
43
45
|
onItemClick?: (item: MigrationDonutChartDatum) => void;
|
|
46
|
+
/** Agent-ui: clickable center title overlay. */
|
|
44
47
|
onTitleClick?: () => void;
|
|
45
|
-
/**
|
|
48
|
+
/** Agent-ui: custom HTML legend labels (ignored when legendVariant is "chart"). */
|
|
46
49
|
legendLabelFormatter?: (item: {
|
|
47
50
|
x: string;
|
|
48
51
|
countDisplay?: string | number;
|
|
49
52
|
}) => string;
|
|
53
|
+
/**
|
|
54
|
+
* Legend rendering mode.
|
|
55
|
+
* - `"html"` (default): custom clickable legend used by agent-ui
|
|
56
|
+
* - `"chart"`: PatternFly ChartLegend used by ui-app
|
|
57
|
+
*/
|
|
58
|
+
legendVariant?: MigrationDonutChartLegendVariant;
|
|
59
|
+
/** ui-app ChartLegend: total legend width. */
|
|
60
|
+
legendWidth?: number;
|
|
61
|
+
/** ui-app ChartLegend: columns per row. */
|
|
62
|
+
itemsPerRow?: number;
|
|
63
|
+
/** ui-app ChartLegend: label font size. */
|
|
64
|
+
labelFontSize?: number;
|
|
65
|
+
/** Prefer themed Victory ChartTooltip (default true for chart legend). */
|
|
66
|
+
useThemedTooltip?: boolean;
|
|
50
67
|
}
|
|
51
68
|
export declare const MigrationDonutChart: FC<MigrationDonutChartProps>;
|
|
52
69
|
export default MigrationDonutChart;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MigrationDonutChart.d.ts","sourceRoot":"","sources":["../../src/charts/MigrationDonutChart.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"MigrationDonutChart.d.ts","sourceRoot":"","sources":["../../src/charts/MigrationDonutChart.tsx"],"names":[],"mappings":"AAcA,OAAO,KAAK,EAAE,EAAE,EAAgB,MAAM,OAAO,CAAC;AAG9C,MAAM,WAAW,wBAAwB;IACvC,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,cAAc,EAAE,MAAM,CAAC;IACvB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,SAAS,CAAC,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,GAAG,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAC1C,WAAW,CAAC,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,GAAG,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAC5C,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;CACzB;AAED,MAAM,MAAM,gCAAgC,GAAG,MAAM,GAAG,OAAO,CAAC;AAEhE,MAAM,WAAW,wBAAwB;IACvC,IAAI,EAAE,wBAAwB,EAAE,CAAC;IACjC,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACtC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,qBAAqB,CAAC,EAAE,CAAC,IAAI,EAAE;QAC7B,KAAK,EAAE;YACL,CAAC,EAAE,MAAM,CAAC;YACV,CAAC,EAAE,MAAM,CAAC;YACV,YAAY,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;YAC/B,cAAc,EAAE,MAAM,CAAC;SACxB,CAAC;QACF,OAAO,EAAE,MAAM,CAAC;QAChB,KAAK,EAAE,MAAM,CAAC;KACf,KAAK,MAAM,CAAC;IACb,iDAAiD;IACjD,WAAW,CAAC,EAAE,CAAC,IAAI,EAAE,wBAAwB,KAAK,IAAI,CAAC;IACvD,gDAAgD;IAChD,YAAY,CAAC,EAAE,MAAM,IAAI,CAAC;IAC1B,mFAAmF;IACnF,oBAAoB,CAAC,EAAE,CAAC,IAAI,EAAE;QAC5B,CAAC,EAAE,MAAM,CAAC;QACV,YAAY,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;KAChC,KAAK,MAAM,CAAC;IACb;;;;OAIG;IACH,aAAa,CAAC,EAAE,gCAAgC,CAAC;IACjD,8CAA8C;IAC9C,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,2CAA2C;IAC3C,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,2CAA2C;IAC3C,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,0EAA0E;IAC1E,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC5B;AA8ED,eAAO,MAAM,mBAAmB,EAAE,EAAE,CAAC,wBAAwB,CAiX5D,CAAC;AAEF,eAAe,mBAAmB,CAAC"}
|
|
@@ -6,11 +6,35 @@ import { css } from "@emotion/css";
|
|
|
6
6
|
// "@patternfly/react-charts/victory" subpath has no exports map entry and
|
|
7
7
|
// resolves to a directory, which webpack refuses under fully-specified —
|
|
8
8
|
// breaking the ui-app federated build.
|
|
9
|
-
import { ChartDonut, ChartLabel, } from "@patternfly/react-charts/dist/esm/victory/index.js";
|
|
10
|
-
import {
|
|
11
|
-
import { InboxIcon } from "@patternfly/react-icons";
|
|
9
|
+
import { ChartDonut, ChartLabel, ChartLegend, ChartTooltip, } from "@patternfly/react-charts/dist/esm/victory/index.js";
|
|
10
|
+
import { Flex, FlexItem } from "@patternfly/react-core";
|
|
12
11
|
import { useCallback, useMemo } from "react";
|
|
13
|
-
const legendColors = [
|
|
12
|
+
const legendColors = [
|
|
13
|
+
"#0066cc",
|
|
14
|
+
"#5e40be",
|
|
15
|
+
"#009596",
|
|
16
|
+
"#4cb140",
|
|
17
|
+
"#f0ab00",
|
|
18
|
+
"#b98412",
|
|
19
|
+
"#a30000",
|
|
20
|
+
"#b6a6e9",
|
|
21
|
+
];
|
|
22
|
+
/** Victory chart tooltips that respect PatternFly light/dark themes. */
|
|
23
|
+
const themedChartTooltipStyle = {
|
|
24
|
+
fontSize: 9,
|
|
25
|
+
fill: "var(--pf-t--global--text--color--regular)",
|
|
26
|
+
};
|
|
27
|
+
const themedChartTooltipFlyoutStyle = {
|
|
28
|
+
stroke: "var(--pf-t--global--border--color--default)",
|
|
29
|
+
strokeWidth: 1,
|
|
30
|
+
fill: "var(--pf-t--global--background--color--floating--default)",
|
|
31
|
+
};
|
|
32
|
+
const themedChartTooltipFlyoutPadding = {
|
|
33
|
+
top: 6,
|
|
34
|
+
bottom: 6,
|
|
35
|
+
left: 10,
|
|
36
|
+
right: 10,
|
|
37
|
+
};
|
|
14
38
|
const styles = {
|
|
15
39
|
legendIcon: css `
|
|
16
40
|
margin-right: 4px;
|
|
@@ -56,21 +80,25 @@ const styles = {
|
|
|
56
80
|
padding: var(--pf-t--global--spacer--ml);
|
|
57
81
|
`,
|
|
58
82
|
};
|
|
59
|
-
export const MigrationDonutChart = ({ data, legend, customColors, height = 260, width = 420, title, subTitle, titleColor = "
|
|
83
|
+
export const MigrationDonutChart = ({ data, legend, customColors, height = 260, width = 420, title, subTitle, titleColor = "var(--pf-t--global--text--color--regular)", subTitleColor = "var(--pf-t--global--text--color--subtle)", marginLeft = "0%", titleFontSize = 28, subTitleFontSize = 14, donutThickness = 45, padAngle = 1, tooltipLabelFormatter, onItemClick, onTitleClick, legendLabelFormatter, legendVariant = "html", legendWidth, itemsPerRow = 1, labelFontSize = 25, useThemedTooltip, }) => {
|
|
84
|
+
const showChartLegend = legendVariant === "chart";
|
|
85
|
+
const themedTooltipEnabled = useThemedTooltip ?? showChartLegend;
|
|
60
86
|
const dynamicLegend = useMemo(() => {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
87
|
+
const legendMap = {};
|
|
88
|
+
const seen = new Set();
|
|
89
|
+
for (const item of data) {
|
|
90
|
+
const key = item.legendCategory;
|
|
91
|
+
if (!seen.has(key)) {
|
|
92
|
+
seen.add(key);
|
|
93
|
+
legendMap[key] =
|
|
94
|
+
customColors?.[key] ??
|
|
95
|
+
legendColors[(seen.size - 1) % legendColors.length];
|
|
68
96
|
}
|
|
69
|
-
|
|
70
|
-
|
|
97
|
+
}
|
|
98
|
+
return legendMap;
|
|
71
99
|
}, [data, customColors]);
|
|
72
|
-
const chartLegend = legend
|
|
73
|
-
const getColor = useCallback((name) => chartLegend[name], [chartLegend]);
|
|
100
|
+
const chartLegend = legend ?? dynamicLegend;
|
|
101
|
+
const getColor = useCallback((name) => chartLegend[name] ?? legendColors[0], [chartLegend]);
|
|
74
102
|
const chartData = useMemo(() => {
|
|
75
103
|
return data.map((item) => ({
|
|
76
104
|
x: item.name,
|
|
@@ -82,6 +110,29 @@ export const MigrationDonutChart = ({ data, legend, customColors, height = 260,
|
|
|
82
110
|
const colorScale = useMemo(() => {
|
|
83
111
|
return chartData.map((item) => getColor(item.legendCategory));
|
|
84
112
|
}, [chartData, getColor]);
|
|
113
|
+
const legendData = useMemo(() => {
|
|
114
|
+
return chartData.map((item) => ({
|
|
115
|
+
name: `${item.x} (${item.countDisplay})`,
|
|
116
|
+
symbol: { fill: getColor(item.legendCategory) },
|
|
117
|
+
}));
|
|
118
|
+
}, [chartData, getColor]);
|
|
119
|
+
const legendWidthValue = legendWidth ?? 800;
|
|
120
|
+
const legendX = useMemo(() => {
|
|
121
|
+
const symbolAndGap = 34;
|
|
122
|
+
const charWidth = labelFontSize * 0.55;
|
|
123
|
+
const itemWidths = legendData.map((d) => symbolAndGap + d.name.length * charWidth);
|
|
124
|
+
const numCols = Math.min(legendData.length, itemsPerRow);
|
|
125
|
+
const gutter = 16;
|
|
126
|
+
let contentWidth = (numCols - 1) * gutter;
|
|
127
|
+
for (let c = 0; c < numCols; c++) {
|
|
128
|
+
let maxW = 0;
|
|
129
|
+
for (let i = c; i < itemWidths.length; i += itemsPerRow) {
|
|
130
|
+
maxW = Math.max(maxW, itemWidths[i] ?? 0);
|
|
131
|
+
}
|
|
132
|
+
contentWidth += maxW;
|
|
133
|
+
}
|
|
134
|
+
return Math.max(0, (legendWidthValue - contentWidth) / 2);
|
|
135
|
+
}, [legendData, itemsPerRow, legendWidthValue, labelFontSize]);
|
|
85
136
|
const innerRadius = useMemo(() => {
|
|
86
137
|
const outerApprox = Math.min(width, height) / 2;
|
|
87
138
|
const computed = outerApprox - donutThickness;
|
|
@@ -130,24 +181,42 @@ export const MigrationDonutChart = ({ data, legend, customColors, height = 260,
|
|
|
130
181
|
},
|
|
131
182
|
];
|
|
132
183
|
}, [onItemClick, handleClick]);
|
|
184
|
+
const donutTooltip = useMemo(() => themedTooltipEnabled ? (_jsx(ChartTooltip, { style: themedChartTooltipStyle, flyoutStyle: themedChartTooltipFlyoutStyle, flyoutPadding: themedChartTooltipFlyoutPadding })) : undefined, [themedTooltipEnabled]);
|
|
185
|
+
const formatLabel = useCallback((datum) => {
|
|
186
|
+
const percent = totalY > 0 ? (Number(datum.y) / totalY) * 100 : 0;
|
|
187
|
+
return tooltipLabelFormatter
|
|
188
|
+
? tooltipLabelFormatter({
|
|
189
|
+
datum: {
|
|
190
|
+
x: datum.x,
|
|
191
|
+
y: Number(datum.y),
|
|
192
|
+
countDisplay: datum.countDisplay,
|
|
193
|
+
legendCategory: datum.legendCategory,
|
|
194
|
+
},
|
|
195
|
+
percent,
|
|
196
|
+
total: totalY,
|
|
197
|
+
})
|
|
198
|
+
: `${datum.x}: ${datum.countDisplay ?? datum.y}`;
|
|
199
|
+
}, [tooltipLabelFormatter, totalY]);
|
|
133
200
|
if (!data || data.length === 0) {
|
|
134
|
-
return
|
|
201
|
+
return null;
|
|
135
202
|
}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
},
|
|
203
|
+
const htmlLegend = (_jsx(Flex, { className: `${styles.legendContainer} ${css `margin-left: ${marginLeft};`}`, justifyContent: { default: "justifyContentCenter" }, alignItems: { default: "alignItemsFlexStart" }, children: _jsx(Flex, { className: styles.legendInner, spaceItems: { default: "spaceItemsMd" }, justifyContent: { default: "justifyContentCenter" }, alignItems: { default: "alignItemsCenter" }, flexWrap: { default: "wrap" }, children: data.map((item) => {
|
|
204
|
+
const label = legendLabelFormatter
|
|
205
|
+
? legendLabelFormatter({
|
|
206
|
+
x: item.name,
|
|
207
|
+
countDisplay: item.countDisplay,
|
|
208
|
+
})
|
|
209
|
+
: item.name;
|
|
210
|
+
const content = (_jsxs(_Fragment, { children: [_jsxs("svg", { width: "10", height: "10", "aria-hidden": "true", className: styles.legendIcon, children: [_jsx("title", { children: "Legend color indicator" }), _jsx("rect", { width: "10", height: "10", fill: getColor(item.legendCategory) })] }), _jsx("span", { children: label })] }));
|
|
211
|
+
return (_jsx(FlexItem, { children: onItemClick ? (_jsx("button", { type: "button", onClick: () => onItemClick(item), className: styles.legendButton, children: content })) : (_jsx("span", { className: styles.legendLabel, children: content })) }, `${item.legendCategory}-${item.name}`));
|
|
212
|
+
}) }) }));
|
|
213
|
+
const chartLegendElement = (_jsx(FlexItem, { flex: { default: "flex_1" }, children: _jsx(Flex, { justifyContent: { default: "justifyContentCenter" }, alignItems: { default: "alignItemsCenter" }, children: _jsx(FlexItem, { children: _jsx(ChartLegend, { data: legendData, orientation: "horizontal", height: 200, width: legendWidthValue, x: legendX, itemsPerRow: itemsPerRow, style: {
|
|
214
|
+
labels: {
|
|
215
|
+
fontSize: labelFontSize,
|
|
216
|
+
fill: "var(--pf-t--global--text--color--regular)",
|
|
217
|
+
},
|
|
218
|
+
} }) }) }) }));
|
|
219
|
+
return (_jsxs(Flex, { direction: { default: "column" }, alignItems: { default: "alignItemsCenter" }, className: `${onItemClick ? styles.cursorPointer : styles.cursorDefault} ${styles.chartContainer}`, children: [_jsxs("div", { className: styles.donutWrapper, children: [_jsx(ChartDonut, { ariaDesc: "Migration data donut chart", data: chartData, events: chartEvents, labelComponent: donutTooltip, labels: ({ datum, }) => formatLabel(datum), colorScale: colorScale, constrainToVisibleArea: true, innerRadius: innerRadius, padAngle: padAngle, title: title, subTitle: subTitle, height: height, width: width, padding: {
|
|
151
220
|
bottom: 5,
|
|
152
221
|
left: 20,
|
|
153
222
|
right: 20,
|
|
@@ -158,7 +227,7 @@ export const MigrationDonutChart = ({ data, legend, customColors, height = 260,
|
|
|
158
227
|
fontSize: titleFontSize,
|
|
159
228
|
fontWeight: "bold",
|
|
160
229
|
},
|
|
161
|
-
] })) : undefined, subTitleComponent: subTitle ? (_jsx(ChartLabel, { style: [
|
|
230
|
+
] })) : undefined, subTitleComponent: subTitle ? (_jsx(ChartLabel, { dy: showChartLegend ? 7 : undefined, style: [
|
|
162
231
|
{
|
|
163
232
|
fill: subTitleColor,
|
|
164
233
|
fontSize: subTitleFontSize,
|
|
@@ -178,15 +247,6 @@ export const MigrationDonutChart = ({ data, legend, customColors, height = 260,
|
|
|
178
247
|
e.preventDefault();
|
|
179
248
|
onTitleClick();
|
|
180
249
|
}
|
|
181
|
-
} }))] }),
|
|
182
|
-
const label = legendLabelFormatter
|
|
183
|
-
? legendLabelFormatter({
|
|
184
|
-
x: item.name,
|
|
185
|
-
countDisplay: item.countDisplay,
|
|
186
|
-
})
|
|
187
|
-
: item.name;
|
|
188
|
-
const content = (_jsxs(_Fragment, { children: [_jsxs("svg", { width: "10", height: "10", "aria-hidden": "true", className: styles.legendIcon, children: [_jsx("title", { children: "Legend color indicator" }), _jsx("rect", { width: "10", height: "10", fill: getColor(item.legendCategory) })] }), _jsx("span", { children: label })] }));
|
|
189
|
-
return (_jsx(FlexItem, { children: onItemClick ? (_jsx("button", { type: "button", onClick: () => onItemClick(item), className: styles.legendButton, children: content })) : (_jsx("span", { className: styles.legendLabel, children: content })) }, `${item.legendCategory}-${item.name}`));
|
|
190
|
-
}) }) })] }));
|
|
250
|
+
} }))] }), showChartLegend ? chartLegendElement : htmlLegend] }));
|
|
191
251
|
};
|
|
192
252
|
export default MigrationDonutChart;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"OsSupportTiersHelpPopover.d.ts","sourceRoot":"","sources":["../../src/report/OsSupportTiersHelpPopover.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"OsSupportTiersHelpPopover.d.ts","sourceRoot":"","sources":["../../src/report/OsSupportTiersHelpPopover.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC;AAyEhC,eAAO,MAAM,yBAAyB,EAAE,EASvC,CAAC"}
|
|
@@ -32,6 +32,6 @@ const learnMoreLinkStyle = css `
|
|
|
32
32
|
gap: var(--pf-t--global--spacer--xs);
|
|
33
33
|
margin-top: var(--pf-t--global--spacer--400);
|
|
34
34
|
`;
|
|
35
|
-
const OsSupportTiersHelpBody = () => (_jsxs("div", { children: [_jsx("ul", { className: tierListStyle, children: ORDERED_SUPPORT_TIERS.map((tier) => (_jsxs("li", { className: tierItemStyle, children: [_jsx(Label, { className: tierBadgeStyle, color: getSupportTierBadgeColor(tier), isCompact: true, children: getSupportTierLegendLabel(tier) }), _jsx("p", { className: tierDescriptionStyle, children: getSupportTierDefinition(tier) })] }, tier))) }), _jsxs("a", { className: learnMoreLinkStyle, href: SUPPORT_TIER_LEARN_MORE_URL, target: "_blank", rel: "noopener noreferrer", children: ["Learn more ", _jsx(ExternalLinkAltIcon, {})] })] }));
|
|
35
|
+
const OsSupportTiersHelpBody = () => (_jsxs("div", { children: [_jsx("ul", { className: tierListStyle, children: ORDERED_SUPPORT_TIERS.map((tier) => (_jsxs("li", { className: tierItemStyle, children: [_jsx(Label, { className: tierBadgeStyle, color: getSupportTierBadgeColor(tier), isCompact: true, children: getSupportTierLegendLabel(tier) }), _jsx("p", { className: tierDescriptionStyle, children: getSupportTierDefinition(tier) })] }, tier))) }), _jsxs("a", { "aria-label": "Learn more about operating system support tiers", className: learnMoreLinkStyle, href: SUPPORT_TIER_LEARN_MORE_URL, target: "_blank", rel: "noopener noreferrer", children: ["Learn more ", _jsx(ExternalLinkAltIcon, {})] })] }));
|
|
36
36
|
export const OsSupportTiersHelpPopover = () => (_jsx(PopoverIcon, { noVerticalAlign: true, maxWidth: "40rem", headerContent: "Guest OS support tiers", bodyContent: _jsx(OsSupportTiersHelpBody, {}), buttonOuiaId: "guest-os-support-tiers-help", "aria-label": "Guest OS support tiers help" }));
|
|
37
37
|
OsSupportTiersHelpPopover.displayName = "OsSupportTiersHelpPopover";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openshift-migration-advisor/shared-components",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.23.0",
|
|
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",
|
|
@@ -8,15 +8,11 @@ import { css } from "@emotion/css";
|
|
|
8
8
|
import {
|
|
9
9
|
ChartDonut,
|
|
10
10
|
ChartLabel,
|
|
11
|
+
ChartLegend,
|
|
12
|
+
ChartTooltip,
|
|
11
13
|
} from "@patternfly/react-charts/dist/esm/victory/index.js";
|
|
12
|
-
import {
|
|
13
|
-
|
|
14
|
-
EmptyStateVariant,
|
|
15
|
-
Flex,
|
|
16
|
-
FlexItem,
|
|
17
|
-
} from "@patternfly/react-core";
|
|
18
|
-
import { InboxIcon } from "@patternfly/react-icons";
|
|
19
|
-
import type { FC } from "react";
|
|
14
|
+
import { Flex, FlexItem } from "@patternfly/react-core";
|
|
15
|
+
import type { FC, ReactElement } from "react";
|
|
20
16
|
import { useCallback, useMemo } from "react";
|
|
21
17
|
|
|
22
18
|
export interface MigrationDonutChartDatum {
|
|
@@ -30,6 +26,8 @@ export interface MigrationDonutChartDatum {
|
|
|
30
26
|
networkNames?: string[];
|
|
31
27
|
}
|
|
32
28
|
|
|
29
|
+
export type MigrationDonutChartLegendVariant = "html" | "chart";
|
|
30
|
+
|
|
33
31
|
export interface MigrationDonutChartProps {
|
|
34
32
|
data: MigrationDonutChartDatum[];
|
|
35
33
|
legend?: Record<string, string>;
|
|
@@ -55,16 +53,60 @@ export interface MigrationDonutChartProps {
|
|
|
55
53
|
percent: number;
|
|
56
54
|
total: number;
|
|
57
55
|
}) => string;
|
|
56
|
+
/** Agent-ui: slice / legend click drill-down. */
|
|
58
57
|
onItemClick?: (item: MigrationDonutChartDatum) => void;
|
|
58
|
+
/** Agent-ui: clickable center title overlay. */
|
|
59
59
|
onTitleClick?: () => void;
|
|
60
|
-
/**
|
|
60
|
+
/** Agent-ui: custom HTML legend labels (ignored when legendVariant is "chart"). */
|
|
61
61
|
legendLabelFormatter?: (item: {
|
|
62
62
|
x: string;
|
|
63
63
|
countDisplay?: string | number;
|
|
64
64
|
}) => string;
|
|
65
|
+
/**
|
|
66
|
+
* Legend rendering mode.
|
|
67
|
+
* - `"html"` (default): custom clickable legend used by agent-ui
|
|
68
|
+
* - `"chart"`: PatternFly ChartLegend used by ui-app
|
|
69
|
+
*/
|
|
70
|
+
legendVariant?: MigrationDonutChartLegendVariant;
|
|
71
|
+
/** ui-app ChartLegend: total legend width. */
|
|
72
|
+
legendWidth?: number;
|
|
73
|
+
/** ui-app ChartLegend: columns per row. */
|
|
74
|
+
itemsPerRow?: number;
|
|
75
|
+
/** ui-app ChartLegend: label font size. */
|
|
76
|
+
labelFontSize?: number;
|
|
77
|
+
/** Prefer themed Victory ChartTooltip (default true for chart legend). */
|
|
78
|
+
useThemedTooltip?: boolean;
|
|
65
79
|
}
|
|
66
80
|
|
|
67
|
-
const legendColors = [
|
|
81
|
+
const legendColors = [
|
|
82
|
+
"#0066cc",
|
|
83
|
+
"#5e40be",
|
|
84
|
+
"#009596",
|
|
85
|
+
"#4cb140",
|
|
86
|
+
"#f0ab00",
|
|
87
|
+
"#b98412",
|
|
88
|
+
"#a30000",
|
|
89
|
+
"#b6a6e9",
|
|
90
|
+
];
|
|
91
|
+
|
|
92
|
+
/** Victory chart tooltips that respect PatternFly light/dark themes. */
|
|
93
|
+
const themedChartTooltipStyle = {
|
|
94
|
+
fontSize: 9,
|
|
95
|
+
fill: "var(--pf-t--global--text--color--regular)",
|
|
96
|
+
} as const;
|
|
97
|
+
|
|
98
|
+
const themedChartTooltipFlyoutStyle = {
|
|
99
|
+
stroke: "var(--pf-t--global--border--color--default)",
|
|
100
|
+
strokeWidth: 1,
|
|
101
|
+
fill: "var(--pf-t--global--background--color--floating--default)",
|
|
102
|
+
} as const;
|
|
103
|
+
|
|
104
|
+
const themedChartTooltipFlyoutPadding = {
|
|
105
|
+
top: 6,
|
|
106
|
+
bottom: 6,
|
|
107
|
+
left: 10,
|
|
108
|
+
right: 10,
|
|
109
|
+
} as const;
|
|
68
110
|
|
|
69
111
|
const styles = {
|
|
70
112
|
legendIcon: css`
|
|
@@ -120,8 +162,8 @@ export const MigrationDonutChart: FC<MigrationDonutChartProps> = ({
|
|
|
120
162
|
width = 420,
|
|
121
163
|
title,
|
|
122
164
|
subTitle,
|
|
123
|
-
titleColor = "
|
|
124
|
-
subTitleColor = "
|
|
165
|
+
titleColor = "var(--pf-t--global--text--color--regular)",
|
|
166
|
+
subTitleColor = "var(--pf-t--global--text--color--subtle)",
|
|
125
167
|
marginLeft = "0%",
|
|
126
168
|
titleFontSize = 28,
|
|
127
169
|
subTitleFontSize = 14,
|
|
@@ -131,30 +173,35 @@ export const MigrationDonutChart: FC<MigrationDonutChartProps> = ({
|
|
|
131
173
|
onItemClick,
|
|
132
174
|
onTitleClick,
|
|
133
175
|
legendLabelFormatter,
|
|
176
|
+
legendVariant = "html",
|
|
177
|
+
legendWidth,
|
|
178
|
+
itemsPerRow = 1,
|
|
179
|
+
labelFontSize = 25,
|
|
180
|
+
useThemedTooltip,
|
|
134
181
|
}) => {
|
|
182
|
+
const showChartLegend = legendVariant === "chart";
|
|
183
|
+
const themedTooltipEnabled = useThemedTooltip ?? showChartLegend;
|
|
184
|
+
|
|
135
185
|
const dynamicLegend = useMemo(() => {
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
result: Record<string, string>[];
|
|
151
|
-
},
|
|
152
|
-
).result;
|
|
186
|
+
const legendMap: Record<string, string> = {};
|
|
187
|
+
const seen = new Set<string>();
|
|
188
|
+
|
|
189
|
+
for (const item of data) {
|
|
190
|
+
const key = item.legendCategory;
|
|
191
|
+
if (!seen.has(key)) {
|
|
192
|
+
seen.add(key);
|
|
193
|
+
legendMap[key] =
|
|
194
|
+
customColors?.[key] ??
|
|
195
|
+
legendColors[(seen.size - 1) % legendColors.length];
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return legendMap;
|
|
153
200
|
}, [data, customColors]);
|
|
154
201
|
|
|
155
|
-
const chartLegend = legend
|
|
202
|
+
const chartLegend = legend ?? dynamicLegend;
|
|
156
203
|
const getColor = useCallback(
|
|
157
|
-
(name: string): string => chartLegend[name],
|
|
204
|
+
(name: string): string => chartLegend[name] ?? legendColors[0],
|
|
158
205
|
[chartLegend],
|
|
159
206
|
);
|
|
160
207
|
|
|
@@ -171,6 +218,33 @@ export const MigrationDonutChart: FC<MigrationDonutChartProps> = ({
|
|
|
171
218
|
return chartData.map((item) => getColor(item.legendCategory));
|
|
172
219
|
}, [chartData, getColor]);
|
|
173
220
|
|
|
221
|
+
const legendData = useMemo(() => {
|
|
222
|
+
return chartData.map((item) => ({
|
|
223
|
+
name: `${item.x} (${item.countDisplay})`,
|
|
224
|
+
symbol: { fill: getColor(item.legendCategory) },
|
|
225
|
+
}));
|
|
226
|
+
}, [chartData, getColor]);
|
|
227
|
+
|
|
228
|
+
const legendWidthValue = legendWidth ?? 800;
|
|
229
|
+
const legendX = useMemo(() => {
|
|
230
|
+
const symbolAndGap = 34;
|
|
231
|
+
const charWidth = labelFontSize * 0.55;
|
|
232
|
+
const itemWidths = legendData.map(
|
|
233
|
+
(d) => symbolAndGap + d.name.length * charWidth,
|
|
234
|
+
);
|
|
235
|
+
const numCols = Math.min(legendData.length, itemsPerRow);
|
|
236
|
+
const gutter = 16;
|
|
237
|
+
let contentWidth = (numCols - 1) * gutter;
|
|
238
|
+
for (let c = 0; c < numCols; c++) {
|
|
239
|
+
let maxW = 0;
|
|
240
|
+
for (let i = c; i < itemWidths.length; i += itemsPerRow) {
|
|
241
|
+
maxW = Math.max(maxW, itemWidths[i] ?? 0);
|
|
242
|
+
}
|
|
243
|
+
contentWidth += maxW;
|
|
244
|
+
}
|
|
245
|
+
return Math.max(0, (legendWidthValue - contentWidth) / 2);
|
|
246
|
+
}, [legendData, itemsPerRow, legendWidthValue, labelFontSize]);
|
|
247
|
+
|
|
174
248
|
const innerRadius = useMemo(() => {
|
|
175
249
|
const outerApprox = Math.min(width, height) / 2;
|
|
176
250
|
const computed = outerApprox - donutThickness;
|
|
@@ -230,17 +304,132 @@ export const MigrationDonutChart: FC<MigrationDonutChartProps> = ({
|
|
|
230
304
|
];
|
|
231
305
|
}, [onItemClick, handleClick]);
|
|
232
306
|
|
|
307
|
+
const donutTooltip = useMemo(
|
|
308
|
+
() =>
|
|
309
|
+
themedTooltipEnabled ? (
|
|
310
|
+
<ChartTooltip
|
|
311
|
+
style={themedChartTooltipStyle}
|
|
312
|
+
flyoutStyle={themedChartTooltipFlyoutStyle}
|
|
313
|
+
flyoutPadding={themedChartTooltipFlyoutPadding}
|
|
314
|
+
/>
|
|
315
|
+
) : undefined,
|
|
316
|
+
[themedTooltipEnabled],
|
|
317
|
+
);
|
|
318
|
+
|
|
319
|
+
const formatLabel = useCallback(
|
|
320
|
+
(datum: {
|
|
321
|
+
x: string;
|
|
322
|
+
y: number;
|
|
323
|
+
legendCategory: string;
|
|
324
|
+
countDisplay?: string | number;
|
|
325
|
+
}) => {
|
|
326
|
+
const percent = totalY > 0 ? (Number(datum.y) / totalY) * 100 : 0;
|
|
327
|
+
return tooltipLabelFormatter
|
|
328
|
+
? tooltipLabelFormatter({
|
|
329
|
+
datum: {
|
|
330
|
+
x: datum.x,
|
|
331
|
+
y: Number(datum.y),
|
|
332
|
+
countDisplay: datum.countDisplay,
|
|
333
|
+
legendCategory: datum.legendCategory,
|
|
334
|
+
},
|
|
335
|
+
percent,
|
|
336
|
+
total: totalY,
|
|
337
|
+
})
|
|
338
|
+
: `${datum.x}: ${datum.countDisplay ?? datum.y}`;
|
|
339
|
+
},
|
|
340
|
+
[tooltipLabelFormatter, totalY],
|
|
341
|
+
);
|
|
342
|
+
|
|
233
343
|
if (!data || data.length === 0) {
|
|
234
|
-
return
|
|
235
|
-
<EmptyState
|
|
236
|
-
headingLevel="h3"
|
|
237
|
-
titleText="No data available"
|
|
238
|
-
icon={InboxIcon}
|
|
239
|
-
variant={EmptyStateVariant.xs}
|
|
240
|
-
/>
|
|
241
|
-
);
|
|
344
|
+
return null;
|
|
242
345
|
}
|
|
243
346
|
|
|
347
|
+
const htmlLegend: ReactElement = (
|
|
348
|
+
<Flex
|
|
349
|
+
className={`${styles.legendContainer} ${css`margin-left: ${marginLeft};`}`}
|
|
350
|
+
justifyContent={{ default: "justifyContentCenter" }}
|
|
351
|
+
alignItems={{ default: "alignItemsFlexStart" }}
|
|
352
|
+
>
|
|
353
|
+
<Flex
|
|
354
|
+
className={styles.legendInner}
|
|
355
|
+
spaceItems={{ default: "spaceItemsMd" }}
|
|
356
|
+
justifyContent={{ default: "justifyContentCenter" }}
|
|
357
|
+
alignItems={{ default: "alignItemsCenter" }}
|
|
358
|
+
flexWrap={{ default: "wrap" }}
|
|
359
|
+
>
|
|
360
|
+
{data.map((item) => {
|
|
361
|
+
const label = legendLabelFormatter
|
|
362
|
+
? legendLabelFormatter({
|
|
363
|
+
x: item.name,
|
|
364
|
+
countDisplay: item.countDisplay,
|
|
365
|
+
})
|
|
366
|
+
: item.name;
|
|
367
|
+
|
|
368
|
+
const content = (
|
|
369
|
+
<>
|
|
370
|
+
<svg
|
|
371
|
+
width="10"
|
|
372
|
+
height="10"
|
|
373
|
+
aria-hidden="true"
|
|
374
|
+
className={styles.legendIcon}
|
|
375
|
+
>
|
|
376
|
+
<title>Legend color indicator</title>
|
|
377
|
+
<rect
|
|
378
|
+
width="10"
|
|
379
|
+
height="10"
|
|
380
|
+
fill={getColor(item.legendCategory)}
|
|
381
|
+
/>
|
|
382
|
+
</svg>
|
|
383
|
+
<span>{label}</span>
|
|
384
|
+
</>
|
|
385
|
+
);
|
|
386
|
+
|
|
387
|
+
return (
|
|
388
|
+
<FlexItem key={`${item.legendCategory}-${item.name}`}>
|
|
389
|
+
{onItemClick ? (
|
|
390
|
+
<button
|
|
391
|
+
type="button"
|
|
392
|
+
onClick={() => onItemClick(item)}
|
|
393
|
+
className={styles.legendButton}
|
|
394
|
+
>
|
|
395
|
+
{content}
|
|
396
|
+
</button>
|
|
397
|
+
) : (
|
|
398
|
+
<span className={styles.legendLabel}>{content}</span>
|
|
399
|
+
)}
|
|
400
|
+
</FlexItem>
|
|
401
|
+
);
|
|
402
|
+
})}
|
|
403
|
+
</Flex>
|
|
404
|
+
</Flex>
|
|
405
|
+
);
|
|
406
|
+
|
|
407
|
+
const chartLegendElement: ReactElement = (
|
|
408
|
+
<FlexItem flex={{ default: "flex_1" }}>
|
|
409
|
+
<Flex
|
|
410
|
+
justifyContent={{ default: "justifyContentCenter" }}
|
|
411
|
+
alignItems={{ default: "alignItemsCenter" }}
|
|
412
|
+
>
|
|
413
|
+
<FlexItem>
|
|
414
|
+
<ChartLegend
|
|
415
|
+
data={legendData}
|
|
416
|
+
orientation="horizontal"
|
|
417
|
+
height={200}
|
|
418
|
+
width={legendWidthValue}
|
|
419
|
+
x={legendX}
|
|
420
|
+
itemsPerRow={itemsPerRow}
|
|
421
|
+
style={{
|
|
422
|
+
labels: {
|
|
423
|
+
fontSize: labelFontSize,
|
|
424
|
+
fill: "var(--pf-t--global--text--color--regular)",
|
|
425
|
+
},
|
|
426
|
+
}}
|
|
427
|
+
/>
|
|
428
|
+
</FlexItem>
|
|
429
|
+
</Flex>
|
|
430
|
+
</FlexItem>
|
|
431
|
+
);
|
|
432
|
+
|
|
244
433
|
return (
|
|
245
434
|
<Flex
|
|
246
435
|
direction={{ default: "column" }}
|
|
@@ -252,6 +441,7 @@ export const MigrationDonutChart: FC<MigrationDonutChartProps> = ({
|
|
|
252
441
|
ariaDesc="Migration data donut chart"
|
|
253
442
|
data={chartData}
|
|
254
443
|
events={chartEvents}
|
|
444
|
+
labelComponent={donutTooltip}
|
|
255
445
|
labels={({
|
|
256
446
|
datum,
|
|
257
447
|
}: {
|
|
@@ -261,21 +451,7 @@ export const MigrationDonutChart: FC<MigrationDonutChartProps> = ({
|
|
|
261
451
|
legendCategory: string;
|
|
262
452
|
countDisplay?: string | number;
|
|
263
453
|
};
|
|
264
|
-
}) =>
|
|
265
|
-
const percent = totalY > 0 ? (Number(datum.y) / totalY) * 100 : 0;
|
|
266
|
-
return tooltipLabelFormatter
|
|
267
|
-
? tooltipLabelFormatter({
|
|
268
|
-
datum: {
|
|
269
|
-
x: datum.x,
|
|
270
|
-
y: Number(datum.y),
|
|
271
|
-
countDisplay: datum.countDisplay,
|
|
272
|
-
legendCategory: datum.legendCategory,
|
|
273
|
-
},
|
|
274
|
-
percent,
|
|
275
|
-
total: totalY,
|
|
276
|
-
})
|
|
277
|
-
: `${datum.x}: ${datum.countDisplay ?? datum.y}`;
|
|
278
|
-
}}
|
|
454
|
+
}) => formatLabel(datum)}
|
|
279
455
|
colorScale={colorScale}
|
|
280
456
|
constrainToVisibleArea
|
|
281
457
|
innerRadius={innerRadius}
|
|
@@ -306,6 +482,7 @@ export const MigrationDonutChart: FC<MigrationDonutChartProps> = ({
|
|
|
306
482
|
subTitleComponent={
|
|
307
483
|
subTitle ? (
|
|
308
484
|
<ChartLabel
|
|
485
|
+
dy={showChartLegend ? 7 : undefined}
|
|
309
486
|
style={[
|
|
310
487
|
{
|
|
311
488
|
fill: subTitleColor,
|
|
@@ -343,63 +520,7 @@ export const MigrationDonutChart: FC<MigrationDonutChartProps> = ({
|
|
|
343
520
|
/>
|
|
344
521
|
)}
|
|
345
522
|
</div>
|
|
346
|
-
|
|
347
|
-
className={`${styles.legendContainer} ${css`margin-left: ${marginLeft};`}`}
|
|
348
|
-
justifyContent={{ default: "justifyContentCenter" }}
|
|
349
|
-
alignItems={{ default: "alignItemsFlexStart" }}
|
|
350
|
-
>
|
|
351
|
-
<Flex
|
|
352
|
-
className={styles.legendInner}
|
|
353
|
-
spaceItems={{ default: "spaceItemsMd" }}
|
|
354
|
-
justifyContent={{ default: "justifyContentCenter" }}
|
|
355
|
-
alignItems={{ default: "alignItemsCenter" }}
|
|
356
|
-
flexWrap={{ default: "wrap" }}
|
|
357
|
-
>
|
|
358
|
-
{data.map((item) => {
|
|
359
|
-
const label = legendLabelFormatter
|
|
360
|
-
? legendLabelFormatter({
|
|
361
|
-
x: item.name,
|
|
362
|
-
countDisplay: item.countDisplay,
|
|
363
|
-
})
|
|
364
|
-
: item.name;
|
|
365
|
-
|
|
366
|
-
const content = (
|
|
367
|
-
<>
|
|
368
|
-
<svg
|
|
369
|
-
width="10"
|
|
370
|
-
height="10"
|
|
371
|
-
aria-hidden="true"
|
|
372
|
-
className={styles.legendIcon}
|
|
373
|
-
>
|
|
374
|
-
<title>Legend color indicator</title>
|
|
375
|
-
<rect
|
|
376
|
-
width="10"
|
|
377
|
-
height="10"
|
|
378
|
-
fill={getColor(item.legendCategory)}
|
|
379
|
-
/>
|
|
380
|
-
</svg>
|
|
381
|
-
<span>{label}</span>
|
|
382
|
-
</>
|
|
383
|
-
);
|
|
384
|
-
|
|
385
|
-
return (
|
|
386
|
-
<FlexItem key={`${item.legendCategory}-${item.name}`}>
|
|
387
|
-
{onItemClick ? (
|
|
388
|
-
<button
|
|
389
|
-
type="button"
|
|
390
|
-
onClick={() => onItemClick(item)}
|
|
391
|
-
className={styles.legendButton}
|
|
392
|
-
>
|
|
393
|
-
{content}
|
|
394
|
-
</button>
|
|
395
|
-
) : (
|
|
396
|
-
<span className={styles.legendLabel}>{content}</span>
|
|
397
|
-
)}
|
|
398
|
-
</FlexItem>
|
|
399
|
-
);
|
|
400
|
-
})}
|
|
401
|
-
</Flex>
|
|
402
|
-
</Flex>
|
|
523
|
+
{showChartLegend ? chartLegendElement : htmlLegend}
|
|
403
524
|
</Flex>
|
|
404
525
|
);
|
|
405
526
|
};
|