@openshift-migration-advisor/shared-components 0.0.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/LICENSE +13 -0
- package/README.md +76 -0
- package/dist/charts/MigrationDonutChart.d.ts +53 -0
- package/dist/charts/MigrationDonutChart.d.ts.map +1 -0
- package/dist/charts/MigrationDonutChart.js +186 -0
- package/dist/charts/index.d.ts +2 -0
- package/dist/charts/index.d.ts.map +1 -0
- package/dist/charts/index.js +1 -0
- package/dist/forms/CheckboxFormGroup.d.ts +5 -0
- package/dist/forms/CheckboxFormGroup.d.ts.map +1 -0
- package/dist/forms/CheckboxFormGroup.js +14 -0
- package/dist/forms/FormFieldHelperText.d.ts +7 -0
- package/dist/forms/FormFieldHelperText.d.ts.map +1 -0
- package/dist/forms/FormFieldHelperText.js +8 -0
- package/dist/forms/RadioButtonFormGroup.d.ts +12 -0
- package/dist/forms/RadioButtonFormGroup.d.ts.map +1 -0
- package/dist/forms/RadioButtonFormGroup.js +12 -0
- package/dist/forms/SelectFormGroup.d.ts +8 -0
- package/dist/forms/SelectFormGroup.d.ts.map +1 -0
- package/dist/forms/SelectFormGroup.js +12 -0
- package/dist/forms/TextAreaFormGroup.d.ts +3 -0
- package/dist/forms/TextAreaFormGroup.d.ts.map +1 -0
- package/dist/forms/TextAreaFormGroup.js +12 -0
- package/dist/forms/TextInputFormGroup.d.ts +11 -0
- package/dist/forms/TextInputFormGroup.d.ts.map +1 -0
- package/dist/forms/TextInputFormGroup.js +16 -0
- package/dist/forms/index.d.ts +8 -0
- package/dist/forms/index.d.ts.map +1 -0
- package/dist/forms/index.js +6 -0
- package/dist/forms/types.d.ts +11 -0
- package/dist/forms/types.d.ts.map +1 -0
- package/dist/forms/types.js +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/report/CardEmptyState.d.ts +8 -0
- package/dist/report/CardEmptyState.d.ts.map +1 -0
- package/dist/report/CardEmptyState.js +13 -0
- package/dist/report/DashboardEmptyStateBase.d.ts +8 -0
- package/dist/report/DashboardEmptyStateBase.d.ts.map +1 -0
- package/dist/report/DashboardEmptyStateBase.js +5 -0
- package/dist/report/EmptySearchResults.d.ts +7 -0
- package/dist/report/EmptySearchResults.d.ts.map +1 -0
- package/dist/report/EmptySearchResults.js +4 -0
- package/dist/report/OSDistribution.d.ts +14 -0
- package/dist/report/OSDistribution.d.ts.map +1 -0
- package/dist/report/OSDistribution.js +37 -0
- package/dist/report/OsSupportTiersHelpPopover.d.ts +3 -0
- package/dist/report/OsSupportTiersHelpPopover.d.ts.map +1 -0
- package/dist/report/OsSupportTiersHelpPopover.js +37 -0
- package/dist/report/OsUpgradeNotice.d.ts +3 -0
- package/dist/report/OsUpgradeNotice.d.ts.map +1 -0
- package/dist/report/OsUpgradeNotice.js +9 -0
- package/dist/report/OsUpgradeRecommendationPopover.d.ts +13 -0
- package/dist/report/OsUpgradeRecommendationPopover.d.ts.map +1 -0
- package/dist/report/OsUpgradeRecommendationPopover.js +24 -0
- package/dist/report/PopoverIcon.d.ts +11 -0
- package/dist/report/PopoverIcon.d.ts.map +1 -0
- package/dist/report/PopoverIcon.js +8 -0
- package/dist/report/SupportTierBadge.d.ts +9 -0
- package/dist/report/SupportTierBadge.d.ts.map +1 -0
- package/dist/report/SupportTierBadge.js +23 -0
- package/dist/report/constants.d.ts +11 -0
- package/dist/report/constants.d.ts.map +1 -0
- package/dist/report/constants.js +12 -0
- package/dist/report/dashboardStyles.d.ts +20 -0
- package/dist/report/dashboardStyles.d.ts.map +1 -0
- package/dist/report/dashboardStyles.js +99 -0
- package/dist/report/index.d.ts +15 -0
- package/dist/report/index.d.ts.map +1 -0
- package/dist/report/index.js +13 -0
- package/dist/report/osSupportTier.d.ts +42 -0
- package/dist/report/osSupportTier.d.ts.map +1 -0
- package/dist/report/osSupportTier.js +106 -0
- package/dist/report/useOsBarChartViewModel.d.ts +26 -0
- package/dist/report/useOsBarChartViewModel.d.ts.map +1 -0
- package/dist/report/useOsBarChartViewModel.js +59 -0
- package/package.json +63 -0
- package/src/charts/MigrationDonutChart.tsx +398 -0
- package/src/charts/index.ts +5 -0
- package/src/forms/CheckboxFormGroup.tsx +53 -0
- package/src/forms/FormFieldHelperText.tsx +32 -0
- package/src/forms/RadioButtonFormGroup.tsx +62 -0
- package/src/forms/SelectFormGroup.tsx +57 -0
- package/src/forms/TextAreaFormGroup.tsx +43 -0
- package/src/forms/TextInputFormGroup.tsx +63 -0
- package/src/forms/index.ts +7 -0
- package/src/forms/types.ts +11 -0
- package/src/index.ts +3 -0
- package/src/report/CardEmptyState.tsx +30 -0
- package/src/report/DashboardEmptyStateBase.tsx +30 -0
- package/src/report/EmptySearchResults.tsx +14 -0
- package/src/report/OSDistribution.tsx +206 -0
- package/src/report/OsSupportTiersHelpPopover.tsx +87 -0
- package/src/report/OsUpgradeNotice.tsx +29 -0
- package/src/report/OsUpgradeRecommendationPopover.tsx +78 -0
- package/src/report/PopoverIcon.tsx +46 -0
- package/src/report/SupportTierBadge.tsx +55 -0
- package/src/report/__tests__/OSDistribution.test.tsx +236 -0
- package/src/report/__tests__/osSupportTier.test.ts +101 -0
- package/src/report/constants.ts +14 -0
- package/src/report/dashboardStyles.ts +116 -0
- package/src/report/index.ts +54 -0
- package/src/report/osSupportTier.ts +161 -0
- package/src/report/useOsBarChartViewModel.ts +115 -0
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { useMemo, useState } from "react";
|
|
2
|
+
import { getSupportTierLegendLabel, getSupportTierSortOrder, hasOsUpgradeNotice, resolveSupportTier, } from "./osSupportTier.js";
|
|
3
|
+
export const ALL_TIERS_FILTER = "all";
|
|
4
|
+
function buildOsTableRows(osData) {
|
|
5
|
+
return Object.entries(osData)
|
|
6
|
+
.filter(([osName]) => osName.trim() !== "")
|
|
7
|
+
.map(([osName, entry]) => ({
|
|
8
|
+
osName,
|
|
9
|
+
tier: resolveSupportTier(entry.supportTier, entry.supported),
|
|
10
|
+
count: entry.count,
|
|
11
|
+
upgradeRecommendation: entry.upgradeRecommendation || undefined,
|
|
12
|
+
}))
|
|
13
|
+
.sort((a, b) => {
|
|
14
|
+
const tierOrder = getSupportTierSortOrder(a.tier) - getSupportTierSortOrder(b.tier);
|
|
15
|
+
if (tierOrder !== 0) {
|
|
16
|
+
return tierOrder;
|
|
17
|
+
}
|
|
18
|
+
return b.count - a.count;
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
export function useOsBarChartViewModel(osData) {
|
|
22
|
+
const [osFilter, setOsFilter] = useState("");
|
|
23
|
+
const [tierFilter, setTierFilter] = useState(ALL_TIERS_FILTER);
|
|
24
|
+
const [isTierSelectOpen, setIsTierSelectOpen] = useState(false);
|
|
25
|
+
const tableRows = useMemo(() => buildOsTableRows(osData), [osData]);
|
|
26
|
+
const filteredRows = useMemo(() => {
|
|
27
|
+
const normalizedFilter = osFilter.trim().toLowerCase();
|
|
28
|
+
return tableRows.filter((row) => {
|
|
29
|
+
const matchesOs = normalizedFilter === "" ||
|
|
30
|
+
row.osName.toLowerCase().includes(normalizedFilter);
|
|
31
|
+
const matchesTier = tierFilter === ALL_TIERS_FILTER || row.tier === tierFilter;
|
|
32
|
+
return matchesOs && matchesTier;
|
|
33
|
+
});
|
|
34
|
+
}, [osFilter, tableRows, tierFilter]);
|
|
35
|
+
const tierFilterLabel = tierFilter === ALL_TIERS_FILTER
|
|
36
|
+
? "Filter by tier"
|
|
37
|
+
: getSupportTierLegendLabel(tierFilter);
|
|
38
|
+
const handleTierSelect = (_event, value) => {
|
|
39
|
+
if (typeof value === "string") {
|
|
40
|
+
setTierFilter(value);
|
|
41
|
+
}
|
|
42
|
+
setIsTierSelectOpen(false);
|
|
43
|
+
};
|
|
44
|
+
return {
|
|
45
|
+
osFilter,
|
|
46
|
+
tierFilter,
|
|
47
|
+
isTierSelectOpen,
|
|
48
|
+
tableRows,
|
|
49
|
+
filteredRows,
|
|
50
|
+
tierFilterLabel,
|
|
51
|
+
showNoResults: filteredRows.length === 0,
|
|
52
|
+
showUpgradeNotice: hasOsUpgradeNotice(osData),
|
|
53
|
+
setOsFilter,
|
|
54
|
+
clearOsFilter: () => setOsFilter(""),
|
|
55
|
+
setIsTierSelectOpen,
|
|
56
|
+
toggleTierSelectOpen: () => setIsTierSelectOpen((open) => !open),
|
|
57
|
+
handleTierSelect,
|
|
58
|
+
};
|
|
59
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@openshift-migration-advisor/shared-components",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"description": "Shared React UI components for Migration Advisor apps (report charts, forms, OS support tiers)",
|
|
5
|
+
"author": "Red Hat",
|
|
6
|
+
"license": "Apache-2.0",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/kubev2v/migration-planner-agent-ui.git",
|
|
10
|
+
"directory": "packages/shared-components"
|
|
11
|
+
},
|
|
12
|
+
"type": "module",
|
|
13
|
+
"exports": {
|
|
14
|
+
".": {
|
|
15
|
+
"development": "./src/index.ts",
|
|
16
|
+
"import": "./dist/index.js",
|
|
17
|
+
"types": "./dist/index.d.ts",
|
|
18
|
+
"default": "./dist/index.js"
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"typesVersions": {
|
|
22
|
+
"*": {
|
|
23
|
+
".": [
|
|
24
|
+
"./dist/index.d.ts"
|
|
25
|
+
]
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
"sideEffects": false,
|
|
29
|
+
"publishConfig": {
|
|
30
|
+
"access": "public"
|
|
31
|
+
},
|
|
32
|
+
"scripts": {
|
|
33
|
+
"build": "yarn run -T tsc -b",
|
|
34
|
+
"bundle": "yarn build && yarn pack --out ../../out/%s-%v.tgz",
|
|
35
|
+
"clean": "rm -rf node_modules dist .tmp",
|
|
36
|
+
"check": "yarn run -T biome check .",
|
|
37
|
+
"check:fix": "yarn run -T biome check --write .",
|
|
38
|
+
"test": "yarn run -T vitest run",
|
|
39
|
+
"typecheck": "yarn run -T tsc -b --noEmit"
|
|
40
|
+
},
|
|
41
|
+
"files": [
|
|
42
|
+
"dist",
|
|
43
|
+
"src",
|
|
44
|
+
"LICENSE",
|
|
45
|
+
"README.md"
|
|
46
|
+
],
|
|
47
|
+
"peerDependencies": {
|
|
48
|
+
"@emotion/css": "^11.13.0",
|
|
49
|
+
"@patternfly/react-charts": "^8.0.0",
|
|
50
|
+
"@patternfly/react-core": "^6.5.0",
|
|
51
|
+
"@patternfly/react-icons": "^6.5.0",
|
|
52
|
+
"@patternfly/react-table": "^6.5.0",
|
|
53
|
+
"@patternfly/react-tokens": "^6.5.0",
|
|
54
|
+
"react": "^18.3.0",
|
|
55
|
+
"react-dom": "^18.3.0",
|
|
56
|
+
"react-hook-form": "^7.0.0"
|
|
57
|
+
},
|
|
58
|
+
"devDependencies": {
|
|
59
|
+
"@testing-library/jest-dom": "^6.9.1",
|
|
60
|
+
"@testing-library/react": "^16.3.2",
|
|
61
|
+
"@testing-library/user-event": "^14.6.1"
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
import { css } from "@emotion/css";
|
|
2
|
+
import { ChartDonut, ChartLabel } from "@patternfly/react-charts/victory";
|
|
3
|
+
import {
|
|
4
|
+
EmptyState,
|
|
5
|
+
EmptyStateVariant,
|
|
6
|
+
Flex,
|
|
7
|
+
FlexItem,
|
|
8
|
+
} from "@patternfly/react-core";
|
|
9
|
+
import { InboxIcon } from "@patternfly/react-icons";
|
|
10
|
+
import type { FC } from "react";
|
|
11
|
+
import { useCallback, useMemo } from "react";
|
|
12
|
+
|
|
13
|
+
export interface MigrationDonutChartDatum {
|
|
14
|
+
name: string;
|
|
15
|
+
count: number;
|
|
16
|
+
legendCategory: string;
|
|
17
|
+
countDisplay?: string;
|
|
18
|
+
diskRange?: { min: number; max?: number };
|
|
19
|
+
memoryRange?: { min: number; max?: number };
|
|
20
|
+
clusterNames?: string[];
|
|
21
|
+
networkNames?: string[];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface MigrationDonutChartProps {
|
|
25
|
+
data: MigrationDonutChartDatum[];
|
|
26
|
+
legend?: Record<string, string>;
|
|
27
|
+
customColors?: Record<string, string>;
|
|
28
|
+
height?: number;
|
|
29
|
+
width?: number;
|
|
30
|
+
title?: string;
|
|
31
|
+
subTitle?: string;
|
|
32
|
+
titleColor?: string;
|
|
33
|
+
subTitleColor?: string;
|
|
34
|
+
marginLeft?: string;
|
|
35
|
+
titleFontSize?: number;
|
|
36
|
+
subTitleFontSize?: number;
|
|
37
|
+
donutThickness?: number;
|
|
38
|
+
padAngle?: number;
|
|
39
|
+
tooltipLabelFormatter?: (args: {
|
|
40
|
+
datum: {
|
|
41
|
+
x: string;
|
|
42
|
+
y: number;
|
|
43
|
+
countDisplay?: string | number;
|
|
44
|
+
legendCategory: string;
|
|
45
|
+
};
|
|
46
|
+
percent: number;
|
|
47
|
+
total: number;
|
|
48
|
+
}) => string;
|
|
49
|
+
onItemClick?: (item: MigrationDonutChartDatum) => void;
|
|
50
|
+
onTitleClick?: () => void;
|
|
51
|
+
/** When set, used for legend label instead of "name (countDisplay)". */
|
|
52
|
+
legendLabelFormatter?: (item: {
|
|
53
|
+
x: string;
|
|
54
|
+
countDisplay?: string | number;
|
|
55
|
+
}) => string;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const legendColors = ["#0066cc", "#5e40be", "#b6a6e9", "#b98412"];
|
|
59
|
+
|
|
60
|
+
const styles = {
|
|
61
|
+
legendIcon: css`
|
|
62
|
+
margin-right: 4px;
|
|
63
|
+
`,
|
|
64
|
+
cursorPointer: css`
|
|
65
|
+
cursor: pointer;
|
|
66
|
+
`,
|
|
67
|
+
cursorDefault: css`
|
|
68
|
+
cursor: default;
|
|
69
|
+
`,
|
|
70
|
+
chartContainer: css`
|
|
71
|
+
padding: 1em 0;
|
|
72
|
+
`,
|
|
73
|
+
donutWrapper: css`
|
|
74
|
+
position: relative;
|
|
75
|
+
display: inline-block;
|
|
76
|
+
`,
|
|
77
|
+
legendContainer: css`
|
|
78
|
+
overflow: hidden;
|
|
79
|
+
min-height: 40px;
|
|
80
|
+
`,
|
|
81
|
+
legendButton: css`
|
|
82
|
+
gap: var(--pf-t--global--spacer--lg);
|
|
83
|
+
cursor: pointer;
|
|
84
|
+
border: none;
|
|
85
|
+
background: none;
|
|
86
|
+
padding: var(--pf-t--global--spacer--xs) var(--pf-t--global--spacer--ml);
|
|
87
|
+
margin: 0;
|
|
88
|
+
transition: opacity var(--pf-t--global--motion--duration--short);
|
|
89
|
+
white-space: nowrap;
|
|
90
|
+
|
|
91
|
+
&:hover {
|
|
92
|
+
opacity: 0.7;
|
|
93
|
+
}
|
|
94
|
+
`,
|
|
95
|
+
legendLabel: css`
|
|
96
|
+
gap: var(--pf-t--global--spacer--lg);
|
|
97
|
+
padding: var(--pf-t--global--spacer--xs) var(--pf-t--global--spacer--ml);
|
|
98
|
+
white-space: nowrap;
|
|
99
|
+
`,
|
|
100
|
+
legendInner: css`
|
|
101
|
+
max-width: 680px;
|
|
102
|
+
padding: var(--pf-t--global--spacer--ml);
|
|
103
|
+
`,
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
export const MigrationDonutChart: FC<MigrationDonutChartProps> = ({
|
|
107
|
+
data,
|
|
108
|
+
legend,
|
|
109
|
+
customColors,
|
|
110
|
+
height = 260,
|
|
111
|
+
width = 420,
|
|
112
|
+
title,
|
|
113
|
+
subTitle,
|
|
114
|
+
titleColor = "#000000",
|
|
115
|
+
subTitleColor = "#000000",
|
|
116
|
+
marginLeft = "0%",
|
|
117
|
+
titleFontSize = 28,
|
|
118
|
+
subTitleFontSize = 14,
|
|
119
|
+
donutThickness = 45,
|
|
120
|
+
padAngle = 1,
|
|
121
|
+
tooltipLabelFormatter,
|
|
122
|
+
onItemClick,
|
|
123
|
+
onTitleClick,
|
|
124
|
+
legendLabelFormatter,
|
|
125
|
+
}) => {
|
|
126
|
+
const dynamicLegend = useMemo(() => {
|
|
127
|
+
return data.reduce(
|
|
128
|
+
(acc, current) => {
|
|
129
|
+
const key = `${current.legendCategory}`;
|
|
130
|
+
if (!acc.seen.has(key)) {
|
|
131
|
+
acc.seen.add(key);
|
|
132
|
+
const color =
|
|
133
|
+
customColors?.[key] ??
|
|
134
|
+
legendColors[(acc.seen.size - 1) % legendColors.length];
|
|
135
|
+
acc.result.push({ [key]: color });
|
|
136
|
+
}
|
|
137
|
+
return acc;
|
|
138
|
+
},
|
|
139
|
+
{ seen: new Set(), result: [] } as {
|
|
140
|
+
seen: Set<string>;
|
|
141
|
+
result: Record<string, string>[];
|
|
142
|
+
},
|
|
143
|
+
).result;
|
|
144
|
+
}, [data, customColors]);
|
|
145
|
+
|
|
146
|
+
const chartLegend = legend ? legend : Object.assign({}, ...dynamicLegend);
|
|
147
|
+
const getColor = useCallback(
|
|
148
|
+
(name: string): string => chartLegend[name],
|
|
149
|
+
[chartLegend],
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
const chartData = useMemo(() => {
|
|
153
|
+
return data.map((item) => ({
|
|
154
|
+
x: item.name,
|
|
155
|
+
y: item.count,
|
|
156
|
+
legendCategory: item.legendCategory,
|
|
157
|
+
countDisplay: item.countDisplay ?? item.count,
|
|
158
|
+
}));
|
|
159
|
+
}, [data]);
|
|
160
|
+
|
|
161
|
+
const colorScale = useMemo(() => {
|
|
162
|
+
return chartData.map((item) => getColor(item.legendCategory));
|
|
163
|
+
}, [chartData, getColor]);
|
|
164
|
+
|
|
165
|
+
const innerRadius = useMemo(() => {
|
|
166
|
+
const outerApprox = Math.min(width, height) / 2;
|
|
167
|
+
const computed = outerApprox - donutThickness;
|
|
168
|
+
return computed > 0 ? computed : 0;
|
|
169
|
+
}, [width, height, donutThickness]);
|
|
170
|
+
|
|
171
|
+
const totalY = useMemo(() => {
|
|
172
|
+
return chartData.reduce((sum, item) => sum + (Number(item.y) || 0), 0);
|
|
173
|
+
}, [chartData]);
|
|
174
|
+
|
|
175
|
+
const handleClick = useCallback(
|
|
176
|
+
// biome-ignore lint/suspicious/noExplicitAny: Victory chart types are not well-typed
|
|
177
|
+
(props: any) => {
|
|
178
|
+
if (!onItemClick) return;
|
|
179
|
+
|
|
180
|
+
const datum = props?.datum;
|
|
181
|
+
if (!datum) return;
|
|
182
|
+
|
|
183
|
+
let clickedItem = data.find((item) => item.name === datum.x);
|
|
184
|
+
|
|
185
|
+
if (!clickedItem && typeof props.index === "number") {
|
|
186
|
+
clickedItem = data[props.index];
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (!clickedItem) {
|
|
190
|
+
clickedItem = data.find(
|
|
191
|
+
(item) => item.legendCategory === datum.legendCategory,
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (clickedItem) {
|
|
196
|
+
onItemClick(clickedItem);
|
|
197
|
+
}
|
|
198
|
+
},
|
|
199
|
+
[onItemClick, data],
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
const chartEvents = useMemo(() => {
|
|
203
|
+
if (!onItemClick) return undefined;
|
|
204
|
+
|
|
205
|
+
return [
|
|
206
|
+
{
|
|
207
|
+
target: "data" as const,
|
|
208
|
+
eventHandlers: {
|
|
209
|
+
onClick: () => [
|
|
210
|
+
{
|
|
211
|
+
target: "data" as const,
|
|
212
|
+
// biome-ignore lint/suspicious/noExplicitAny: Victory chart types are not well-typed
|
|
213
|
+
mutation: (props: any) => {
|
|
214
|
+
handleClick(props);
|
|
215
|
+
return null;
|
|
216
|
+
},
|
|
217
|
+
},
|
|
218
|
+
],
|
|
219
|
+
},
|
|
220
|
+
},
|
|
221
|
+
];
|
|
222
|
+
}, [onItemClick, handleClick]);
|
|
223
|
+
|
|
224
|
+
if (!data || data.length === 0) {
|
|
225
|
+
return (
|
|
226
|
+
<EmptyState
|
|
227
|
+
headingLevel="h3"
|
|
228
|
+
titleText="No data available"
|
|
229
|
+
icon={InboxIcon}
|
|
230
|
+
variant={EmptyStateVariant.xs}
|
|
231
|
+
/>
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return (
|
|
236
|
+
<Flex
|
|
237
|
+
direction={{ default: "column" }}
|
|
238
|
+
alignItems={{ default: "alignItemsCenter" }}
|
|
239
|
+
className={`${onItemClick ? styles.cursorPointer : styles.cursorDefault} ${styles.chartContainer}`}
|
|
240
|
+
>
|
|
241
|
+
<div className={styles.donutWrapper}>
|
|
242
|
+
<ChartDonut
|
|
243
|
+
ariaDesc="Migration data donut chart"
|
|
244
|
+
data={chartData}
|
|
245
|
+
events={chartEvents}
|
|
246
|
+
labels={({
|
|
247
|
+
datum,
|
|
248
|
+
}: {
|
|
249
|
+
datum: {
|
|
250
|
+
x: string;
|
|
251
|
+
y: number;
|
|
252
|
+
legendCategory: string;
|
|
253
|
+
countDisplay?: string | number;
|
|
254
|
+
};
|
|
255
|
+
}) => {
|
|
256
|
+
const percent = totalY > 0 ? (Number(datum.y) / totalY) * 100 : 0;
|
|
257
|
+
return tooltipLabelFormatter
|
|
258
|
+
? tooltipLabelFormatter({
|
|
259
|
+
datum: {
|
|
260
|
+
x: datum.x,
|
|
261
|
+
y: Number(datum.y),
|
|
262
|
+
countDisplay: datum.countDisplay,
|
|
263
|
+
legendCategory: datum.legendCategory,
|
|
264
|
+
},
|
|
265
|
+
percent,
|
|
266
|
+
total: totalY,
|
|
267
|
+
})
|
|
268
|
+
: `${datum.x}: ${datum.countDisplay ?? datum.y}`;
|
|
269
|
+
}}
|
|
270
|
+
colorScale={colorScale}
|
|
271
|
+
constrainToVisibleArea
|
|
272
|
+
innerRadius={innerRadius}
|
|
273
|
+
padAngle={padAngle}
|
|
274
|
+
title={title}
|
|
275
|
+
subTitle={subTitle}
|
|
276
|
+
height={height}
|
|
277
|
+
width={width}
|
|
278
|
+
padding={{
|
|
279
|
+
bottom: 5,
|
|
280
|
+
left: 20,
|
|
281
|
+
right: 20,
|
|
282
|
+
top: 0,
|
|
283
|
+
}}
|
|
284
|
+
titleComponent={
|
|
285
|
+
title ? (
|
|
286
|
+
<ChartLabel
|
|
287
|
+
style={[
|
|
288
|
+
{
|
|
289
|
+
fill: titleColor,
|
|
290
|
+
fontSize: titleFontSize,
|
|
291
|
+
fontWeight: "bold",
|
|
292
|
+
},
|
|
293
|
+
]}
|
|
294
|
+
/>
|
|
295
|
+
) : undefined
|
|
296
|
+
}
|
|
297
|
+
subTitleComponent={
|
|
298
|
+
subTitle ? (
|
|
299
|
+
<ChartLabel
|
|
300
|
+
style={[
|
|
301
|
+
{
|
|
302
|
+
fill: subTitleColor,
|
|
303
|
+
fontSize: subTitleFontSize,
|
|
304
|
+
},
|
|
305
|
+
]}
|
|
306
|
+
/>
|
|
307
|
+
) : undefined
|
|
308
|
+
}
|
|
309
|
+
/>
|
|
310
|
+
{onTitleClick && title && (
|
|
311
|
+
// biome-ignore lint/a11y/useSemanticElements: Transparent overlay requires precise positioning and styling that button element would interfere with
|
|
312
|
+
<div
|
|
313
|
+
onClick={onTitleClick}
|
|
314
|
+
className={css`
|
|
315
|
+
position: absolute;
|
|
316
|
+
top: 50%;
|
|
317
|
+
left: 50%;
|
|
318
|
+
transform: translate(-50%, -50%);
|
|
319
|
+
width: ${innerRadius * 2}px;
|
|
320
|
+
height: ${innerRadius * 2}px;
|
|
321
|
+
cursor: pointer;
|
|
322
|
+
border-radius: 50%;
|
|
323
|
+
z-index: 10;
|
|
324
|
+
`}
|
|
325
|
+
title="Click to view all VMs"
|
|
326
|
+
role="button"
|
|
327
|
+
tabIndex={0}
|
|
328
|
+
onKeyDown={(e) => {
|
|
329
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
330
|
+
e.preventDefault();
|
|
331
|
+
onTitleClick();
|
|
332
|
+
}
|
|
333
|
+
}}
|
|
334
|
+
/>
|
|
335
|
+
)}
|
|
336
|
+
</div>
|
|
337
|
+
<Flex
|
|
338
|
+
className={`${styles.legendContainer} ${css`margin-left: ${marginLeft};`}`}
|
|
339
|
+
justifyContent={{ default: "justifyContentCenter" }}
|
|
340
|
+
alignItems={{ default: "alignItemsFlexStart" }}
|
|
341
|
+
>
|
|
342
|
+
<Flex
|
|
343
|
+
className={styles.legendInner}
|
|
344
|
+
spaceItems={{ default: "spaceItemsMd" }}
|
|
345
|
+
justifyContent={{ default: "justifyContentCenter" }}
|
|
346
|
+
alignItems={{ default: "alignItemsCenter" }}
|
|
347
|
+
flexWrap={{ default: "wrap" }}
|
|
348
|
+
>
|
|
349
|
+
{data.map((item) => {
|
|
350
|
+
const label = legendLabelFormatter
|
|
351
|
+
? legendLabelFormatter({
|
|
352
|
+
x: item.name,
|
|
353
|
+
countDisplay: item.countDisplay,
|
|
354
|
+
})
|
|
355
|
+
: item.name;
|
|
356
|
+
|
|
357
|
+
const content = (
|
|
358
|
+
<>
|
|
359
|
+
<svg
|
|
360
|
+
width="10"
|
|
361
|
+
height="10"
|
|
362
|
+
aria-hidden="true"
|
|
363
|
+
className={styles.legendIcon}
|
|
364
|
+
>
|
|
365
|
+
<title>Legend color indicator</title>
|
|
366
|
+
<rect
|
|
367
|
+
width="10"
|
|
368
|
+
height="10"
|
|
369
|
+
fill={getColor(item.legendCategory)}
|
|
370
|
+
/>
|
|
371
|
+
</svg>
|
|
372
|
+
<span>{label}</span>
|
|
373
|
+
</>
|
|
374
|
+
);
|
|
375
|
+
|
|
376
|
+
return (
|
|
377
|
+
<FlexItem key={`${item.legendCategory}-${item.name}`}>
|
|
378
|
+
{onItemClick ? (
|
|
379
|
+
<button
|
|
380
|
+
type="button"
|
|
381
|
+
onClick={() => onItemClick(item)}
|
|
382
|
+
className={styles.legendButton}
|
|
383
|
+
>
|
|
384
|
+
{content}
|
|
385
|
+
</button>
|
|
386
|
+
) : (
|
|
387
|
+
<span className={styles.legendLabel}>{content}</span>
|
|
388
|
+
)}
|
|
389
|
+
</FlexItem>
|
|
390
|
+
);
|
|
391
|
+
})}
|
|
392
|
+
</Flex>
|
|
393
|
+
</Flex>
|
|
394
|
+
</Flex>
|
|
395
|
+
);
|
|
396
|
+
};
|
|
397
|
+
|
|
398
|
+
export default MigrationDonutChart;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { Checkbox, FormGroup } from "@patternfly/react-core";
|
|
2
|
+
import { Controller, type FieldError, useFormContext } from "react-hook-form";
|
|
3
|
+
|
|
4
|
+
import FormFieldHelperText from "./FormFieldHelperText.js";
|
|
5
|
+
import type { FormGroupProps } from "./types.js";
|
|
6
|
+
|
|
7
|
+
type CheckboxFormGroupProps = Omit<FormGroupProps, "placeholder">;
|
|
8
|
+
|
|
9
|
+
export default function CheckboxFormGroup({
|
|
10
|
+
id,
|
|
11
|
+
label,
|
|
12
|
+
name,
|
|
13
|
+
isRequired = false,
|
|
14
|
+
isDisabled = false,
|
|
15
|
+
helpText,
|
|
16
|
+
...props
|
|
17
|
+
}: CheckboxFormGroupProps) {
|
|
18
|
+
const methods = useFormContext();
|
|
19
|
+
const error = methods.formState.errors[name] as FieldError | undefined;
|
|
20
|
+
const isTouched = methods.formState.touchedFields[name] as
|
|
21
|
+
| boolean
|
|
22
|
+
| undefined;
|
|
23
|
+
const isSubmitted = methods.formState.isSubmitted;
|
|
24
|
+
const showError = error && (isTouched || isSubmitted);
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<FormGroup isRequired={isRequired} fieldId={id} {...props}>
|
|
28
|
+
<Controller
|
|
29
|
+
name={name}
|
|
30
|
+
control={methods.control}
|
|
31
|
+
render={({ field }) => {
|
|
32
|
+
return (
|
|
33
|
+
<Checkbox
|
|
34
|
+
label={label}
|
|
35
|
+
id={id}
|
|
36
|
+
isRequired={isRequired}
|
|
37
|
+
isDisabled={isDisabled}
|
|
38
|
+
isChecked={field.value as boolean}
|
|
39
|
+
onChange={field.onChange}
|
|
40
|
+
onBlur={field.onBlur}
|
|
41
|
+
name={field.name}
|
|
42
|
+
ref={field.ref}
|
|
43
|
+
/>
|
|
44
|
+
);
|
|
45
|
+
}}
|
|
46
|
+
/>
|
|
47
|
+
<FormFieldHelperText
|
|
48
|
+
helpText={helpText}
|
|
49
|
+
errorMessage={showError ? error?.message : undefined}
|
|
50
|
+
/>
|
|
51
|
+
</FormGroup>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import {
|
|
2
|
+
FormHelperText,
|
|
3
|
+
HelperText,
|
|
4
|
+
HelperTextItem,
|
|
5
|
+
} from "@patternfly/react-core";
|
|
6
|
+
import { ExclamationCircleIcon } from "@patternfly/react-icons";
|
|
7
|
+
|
|
8
|
+
interface FormFieldHelperTextProps {
|
|
9
|
+
helpText?: string;
|
|
10
|
+
errorMessage?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export default function FormFieldHelperText({
|
|
14
|
+
helpText,
|
|
15
|
+
errorMessage,
|
|
16
|
+
}: FormFieldHelperTextProps) {
|
|
17
|
+
if (!helpText && !errorMessage) return null;
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<FormHelperText>
|
|
21
|
+
<HelperText>
|
|
22
|
+
{errorMessage ? (
|
|
23
|
+
<HelperTextItem icon={<ExclamationCircleIcon />} variant="error">
|
|
24
|
+
{errorMessage}
|
|
25
|
+
</HelperTextItem>
|
|
26
|
+
) : (
|
|
27
|
+
helpText && <HelperTextItem>{helpText}</HelperTextItem>
|
|
28
|
+
)}
|
|
29
|
+
</HelperText>
|
|
30
|
+
</FormHelperText>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { FormGroup, Radio } from "@patternfly/react-core";
|
|
2
|
+
import { Controller, type FieldError, useFormContext } from "react-hook-form";
|
|
3
|
+
|
|
4
|
+
import FormFieldHelperText from "./FormFieldHelperText.js";
|
|
5
|
+
import type { FormGroupProps } from "./types.js";
|
|
6
|
+
|
|
7
|
+
interface RadioOption {
|
|
8
|
+
value: string;
|
|
9
|
+
label: string;
|
|
10
|
+
id: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface RadioButtonFormGroupProps
|
|
14
|
+
extends Omit<FormGroupProps, "placeholder"> {
|
|
15
|
+
options: RadioOption[];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export default function RadioButtonFormGroup({
|
|
19
|
+
id,
|
|
20
|
+
label,
|
|
21
|
+
name,
|
|
22
|
+
isRequired = false,
|
|
23
|
+
options,
|
|
24
|
+
helpText,
|
|
25
|
+
...props
|
|
26
|
+
}: RadioButtonFormGroupProps) {
|
|
27
|
+
const methods = useFormContext();
|
|
28
|
+
const error = methods.formState.errors[name] as FieldError | undefined;
|
|
29
|
+
const isTouched = methods.formState.touchedFields[name] as
|
|
30
|
+
| boolean
|
|
31
|
+
| undefined;
|
|
32
|
+
const isSubmitted = methods.formState.isSubmitted;
|
|
33
|
+
const showError = error && (isTouched || isSubmitted);
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<FormGroup label={label} isRequired={isRequired} fieldId={id} {...props}>
|
|
37
|
+
<Controller
|
|
38
|
+
name={name}
|
|
39
|
+
control={methods.control}
|
|
40
|
+
render={({ field }) => (
|
|
41
|
+
<div style={{ display: "flex", gap: "16px" }} role="radiogroup">
|
|
42
|
+
{options.map((option) => (
|
|
43
|
+
<Radio
|
|
44
|
+
key={option.value}
|
|
45
|
+
id={option.id}
|
|
46
|
+
name={name}
|
|
47
|
+
label={option.label}
|
|
48
|
+
isChecked={field.value === option.value}
|
|
49
|
+
onChange={() => field.onChange(option.value)}
|
|
50
|
+
onBlur={field.onBlur}
|
|
51
|
+
/>
|
|
52
|
+
))}
|
|
53
|
+
</div>
|
|
54
|
+
)}
|
|
55
|
+
/>
|
|
56
|
+
<FormFieldHelperText
|
|
57
|
+
helpText={helpText}
|
|
58
|
+
errorMessage={showError ? error?.message : undefined}
|
|
59
|
+
/>
|
|
60
|
+
</FormGroup>
|
|
61
|
+
);
|
|
62
|
+
}
|