@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.
@@ -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
- /** When set, used for legend label instead of "name (countDisplay)". */
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":"AAkBA,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC;AAGhC,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,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,WAAW,CAAC,EAAE,CAAC,IAAI,EAAE,wBAAwB,KAAK,IAAI,CAAC;IACvD,YAAY,CAAC,EAAE,MAAM,IAAI,CAAC;IAC1B,wEAAwE;IACxE,oBAAoB,CAAC,EAAE,CAAC,IAAI,EAAE;QAC5B,CAAC,EAAE,MAAM,CAAC;QACV,YAAY,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;KAChC,KAAK,MAAM,CAAC;CACd;AAkDD,eAAO,MAAM,mBAAmB,EAAE,EAAE,CAAC,wBAAwB,CAkS5D,CAAC;AAEF,eAAe,mBAAmB,CAAC"}
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 { EmptyState, EmptyStateVariant, Flex, FlexItem, } from "@patternfly/react-core";
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 = ["#0066cc", "#5e40be", "#b6a6e9", "#b98412"];
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 = "#000000", subTitleColor = "#000000", marginLeft = "0%", titleFontSize = 28, subTitleFontSize = 14, donutThickness = 45, padAngle = 1, tooltipLabelFormatter, onItemClick, onTitleClick, legendLabelFormatter, }) => {
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
- return data.reduce((acc, current) => {
62
- const key = `${current.legendCategory}`;
63
- if (!acc.seen.has(key)) {
64
- acc.seen.add(key);
65
- const color = customColors?.[key] ??
66
- legendColors[(acc.seen.size - 1) % legendColors.length];
67
- acc.result.push({ [key]: color });
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
- return acc;
70
- }, { seen: new Set(), result: [] }).result;
97
+ }
98
+ return legendMap;
71
99
  }, [data, customColors]);
72
- const chartLegend = legend ? legend : Object.assign({}, ...dynamicLegend);
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 (_jsx(EmptyState, { headingLevel: "h3", titleText: "No data available", icon: InboxIcon, variant: EmptyStateVariant.xs }));
201
+ return null;
135
202
  }
136
- 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, labels: ({ datum, }) => {
137
- const percent = totalY > 0 ? (Number(datum.y) / totalY) * 100 : 0;
138
- return tooltipLabelFormatter
139
- ? tooltipLabelFormatter({
140
- datum: {
141
- x: datum.x,
142
- y: Number(datum.y),
143
- countDisplay: datum.countDisplay,
144
- legendCategory: datum.legendCategory,
145
- },
146
- percent,
147
- total: totalY,
148
- })
149
- : `${datum.x}: ${datum.countDisplay ?? datum.y}`;
150
- }, colorScale: colorScale, constrainToVisibleArea: true, innerRadius: innerRadius, padAngle: padAngle, title: title, subTitle: subTitle, height: height, width: width, padding: {
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
- } }))] }), _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) => {
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;AAwEhC,eAAO,MAAM,yBAAyB,EAAE,EASvC,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.22.0-dd51551836b7",
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
- EmptyState,
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
- /** When set, used for legend label instead of "name (countDisplay)". */
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 = ["#0066cc", "#5e40be", "#b6a6e9", "#b98412"];
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 = "#000000",
124
- subTitleColor = "#000000",
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
- return data.reduce(
137
- (acc, current) => {
138
- const key = `${current.legendCategory}`;
139
- if (!acc.seen.has(key)) {
140
- acc.seen.add(key);
141
- const color =
142
- customColors?.[key] ??
143
- legendColors[(acc.seen.size - 1) % legendColors.length];
144
- acc.result.push({ [key]: color });
145
- }
146
- return acc;
147
- },
148
- { seen: new Set(), result: [] } as {
149
- seen: Set<string>;
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 ? legend : Object.assign({}, ...dynamicLegend);
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
- <Flex
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
  };
@@ -63,6 +63,7 @@ const OsSupportTiersHelpBody: FC = () => (
63
63
  ))}
64
64
  </ul>
65
65
  <a
66
+ aria-label="Learn more about operating system support tiers"
66
67
  className={learnMoreLinkStyle}
67
68
  href={SUPPORT_TIER_LEARN_MORE_URL}
68
69
  target="_blank"