@openshift-migration-advisor/shared-components 0.25.0-fce5293f01c1 → 0.27.0-5b72aa3d4938

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.
Files changed (61) hide show
  1. package/dist/report/ChartExportProvider.d.ts +3 -2
  2. package/dist/report/ChartExportProvider.d.ts.map +1 -1
  3. package/dist/report/ChartExportProvider.js +3 -3
  4. package/dist/report/ClustersOverview.d.ts +24 -0
  5. package/dist/report/ClustersOverview.d.ts.map +1 -0
  6. package/dist/report/ClustersOverview.js +228 -0
  7. package/dist/report/ClustersOverviewStyles.d.ts +12 -0
  8. package/dist/report/ClustersOverviewStyles.d.ts.map +1 -0
  9. package/dist/report/ClustersOverviewStyles.js +101 -0
  10. package/dist/report/CpuAndMemoryOverview.d.ts +19 -0
  11. package/dist/report/CpuAndMemoryOverview.d.ts.map +1 -0
  12. package/dist/report/CpuAndMemoryOverview.js +112 -0
  13. package/dist/report/ErrorTable.d.ts +18 -0
  14. package/dist/report/ErrorTable.d.ts.map +1 -0
  15. package/dist/report/ErrorTable.js +20 -0
  16. package/dist/report/HostsOverview.d.ts +13 -0
  17. package/dist/report/HostsOverview.d.ts.map +1 -0
  18. package/dist/report/HostsOverview.js +84 -0
  19. package/dist/report/ReportTable.d.ts +14 -0
  20. package/dist/report/ReportTable.d.ts.map +1 -0
  21. package/dist/report/ReportTable.js +23 -0
  22. package/dist/report/WarningsTable.d.ts +9 -0
  23. package/dist/report/WarningsTable.d.ts.map +1 -0
  24. package/dist/report/WarningsTable.js +20 -0
  25. package/dist/report/captureChartElement.d.ts.map +1 -1
  26. package/dist/report/captureChartElement.js +28 -8
  27. package/dist/report/chartExport.d.ts +23 -0
  28. package/dist/report/chartExport.d.ts.map +1 -1
  29. package/dist/report/chartExport.js +20 -0
  30. package/dist/report/chartExportContext.d.ts +6 -2
  31. package/dist/report/chartExportContext.d.ts.map +1 -1
  32. package/dist/report/constants.d.ts +2 -0
  33. package/dist/report/constants.d.ts.map +1 -1
  34. package/dist/report/constants.js +2 -0
  35. package/dist/report/index.d.ts +8 -2
  36. package/dist/report/index.d.ts.map +1 -1
  37. package/dist/report/index.js +6 -0
  38. package/dist/report/pdfExport.d.ts +2 -2
  39. package/dist/report/pdfExport.d.ts.map +1 -1
  40. package/dist/report/pdfExport.js +52 -3
  41. package/package.json +2 -2
  42. package/src/report/ChartExportProvider.tsx +13 -5
  43. package/src/report/ClustersOverview.tsx +451 -0
  44. package/src/report/ClustersOverviewStyles.ts +110 -0
  45. package/src/report/CpuAndMemoryOverview.tsx +277 -0
  46. package/src/report/ErrorTable.tsx +85 -0
  47. package/src/report/HostsOverview.tsx +185 -0
  48. package/src/report/ReportTable.tsx +81 -0
  49. package/src/report/WarningsTable.tsx +75 -0
  50. package/src/report/__tests__/ChartExportProvider.test.tsx +110 -2
  51. package/src/report/__tests__/ClustersOverview.test.tsx +241 -0
  52. package/src/report/__tests__/CpuAndMemoryOverview.test.tsx +200 -0
  53. package/src/report/__tests__/HostsOverview.test.tsx +145 -0
  54. package/src/report/__tests__/captureChartElement.test.ts +73 -0
  55. package/src/report/__tests__/chartExportPackagers.test.ts +75 -0
  56. package/src/report/captureChartElement.ts +37 -7
  57. package/src/report/chartExport.ts +40 -0
  58. package/src/report/chartExportContext.tsx +9 -1
  59. package/src/report/constants.ts +2 -0
  60. package/src/report/index.ts +27 -0
  61. package/src/report/pdfExport.ts +81 -2
@@ -1,9 +1,10 @@
1
1
  import { type FC, type ReactNode } from "react";
2
- import { type ChartCaptureSource, type ChartExportFile } from "./chartExport.js";
2
+ import { type ChartCaptureSource, type ChartExportFile, type PdfTextPage } from "./chartExport.js";
3
3
  export type CaptureChartElement = (element: HTMLElement) => Promise<HTMLCanvasElement>;
4
4
  export type ZipChartFiles = (files: ChartExportFile[]) => Promise<Blob>;
5
5
  export type EncodeChartPng = (canvas: HTMLCanvasElement) => Promise<Blob>;
6
6
  export type BuildChartDocument = (charts: ChartCaptureSource[], documentTitle: string) => Promise<Blob>;
7
+ export type BuildPdfDocument = (charts: ChartCaptureSource[], documentTitle: string, extraPages?: PdfTextPage[]) => Promise<Blob>;
7
8
  export type { ChartExportApi, ChartExportFormat, } from "./chartExportContext.js";
8
9
  export { useChartExport, useRegisterChart } from "./chartExportContext.js";
9
10
  export interface ChartExportProviderProps {
@@ -11,7 +12,7 @@ export interface ChartExportProviderProps {
11
12
  capture?: CaptureChartElement;
12
13
  zipFiles?: ZipChartFiles;
13
14
  encodePng?: EncodeChartPng;
14
- buildPdf?: BuildChartDocument;
15
+ buildPdf?: BuildPdfDocument;
15
16
  buildHtml?: BuildChartDocument;
16
17
  downloadFile?: (blob: Blob, filename: string) => void;
17
18
  getZipFilename?: () => string;
@@ -1 +1 @@
1
- {"version":3,"file":"ChartExportProvider.d.ts","sourceRoot":"","sources":["../../src/report/ChartExportProvider.tsx"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,EAAE,EACP,KAAK,SAAS,EAKf,MAAM,OAAO,CAAC;AACf,OAAO,EACL,KAAK,kBAAkB,EACvB,KAAK,eAAe,EAQrB,MAAM,kBAAkB,CAAC;AAU1B,MAAM,MAAM,mBAAmB,GAAG,CAChC,OAAO,EAAE,WAAW,KACjB,OAAO,CAAC,iBAAiB,CAAC,CAAC;AAChC,MAAM,MAAM,aAAa,GAAG,CAAC,KAAK,EAAE,eAAe,EAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;AACxE,MAAM,MAAM,cAAc,GAAG,CAAC,MAAM,EAAE,iBAAiB,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;AAC1E,MAAM,MAAM,kBAAkB,GAAG,CAC/B,MAAM,EAAE,kBAAkB,EAAE,EAC5B,aAAa,EAAE,MAAM,KAClB,OAAO,CAAC,IAAI,CAAC,CAAC;AAEnB,YAAY,EACV,cAAc,EACd,iBAAiB,GAClB,MAAM,yBAAyB,CAAC;AACjC,OAAO,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAE3E,MAAM,WAAW,wBAAwB;IACvC,QAAQ,EAAE,SAAS,CAAC;IACpB,OAAO,CAAC,EAAE,mBAAmB,CAAC;IAC9B,QAAQ,CAAC,EAAE,aAAa,CAAC;IACzB,SAAS,CAAC,EAAE,cAAc,CAAC;IAC3B,QAAQ,CAAC,EAAE,kBAAkB,CAAC;IAC9B,SAAS,CAAC,EAAE,kBAAkB,CAAC;IAC/B,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;IACtD,cAAc,CAAC,EAAE,MAAM,MAAM,CAAC;IAC9B,cAAc,CAAC,EAAE,MAAM,MAAM,CAAC;IAC9B,eAAe,CAAC,EAAE,MAAM,MAAM,CAAC;CAChC;AAsED,eAAO,MAAM,mBAAmB,EAAE,EAAE,CAAC,wBAAwB,CAgQ5D,CAAC"}
1
+ {"version":3,"file":"ChartExportProvider.d.ts","sourceRoot":"","sources":["../../src/report/ChartExportProvider.tsx"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,EAAE,EACP,KAAK,SAAS,EAKf,MAAM,OAAO,CAAC;AACf,OAAO,EACL,KAAK,kBAAkB,EACvB,KAAK,eAAe,EAGpB,KAAK,WAAW,EAMjB,MAAM,kBAAkB,CAAC;AAU1B,MAAM,MAAM,mBAAmB,GAAG,CAChC,OAAO,EAAE,WAAW,KACjB,OAAO,CAAC,iBAAiB,CAAC,CAAC;AAChC,MAAM,MAAM,aAAa,GAAG,CAAC,KAAK,EAAE,eAAe,EAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;AACxE,MAAM,MAAM,cAAc,GAAG,CAAC,MAAM,EAAE,iBAAiB,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;AAC1E,MAAM,MAAM,kBAAkB,GAAG,CAC/B,MAAM,EAAE,kBAAkB,EAAE,EAC5B,aAAa,EAAE,MAAM,KAClB,OAAO,CAAC,IAAI,CAAC,CAAC;AAEnB,MAAM,MAAM,gBAAgB,GAAG,CAC7B,MAAM,EAAE,kBAAkB,EAAE,EAC5B,aAAa,EAAE,MAAM,EACrB,UAAU,CAAC,EAAE,WAAW,EAAE,KACvB,OAAO,CAAC,IAAI,CAAC,CAAC;AAEnB,YAAY,EACV,cAAc,EACd,iBAAiB,GAClB,MAAM,yBAAyB,CAAC;AACjC,OAAO,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAE3E,MAAM,WAAW,wBAAwB;IACvC,QAAQ,EAAE,SAAS,CAAC;IACpB,OAAO,CAAC,EAAE,mBAAmB,CAAC;IAC9B,QAAQ,CAAC,EAAE,aAAa,CAAC;IACzB,SAAS,CAAC,EAAE,cAAc,CAAC;IAC3B,QAAQ,CAAC,EAAE,gBAAgB,CAAC;IAC5B,SAAS,CAAC,EAAE,kBAAkB,CAAC;IAC/B,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;IACtD,cAAc,CAAC,EAAE,MAAM,MAAM,CAAC;IAC9B,cAAc,CAAC,EAAE,MAAM,MAAM,CAAC;IAC9B,eAAe,CAAC,EAAE,MAAM,MAAM,CAAC;CAChC;AAsED,eAAO,MAAM,mBAAmB,EAAE,EAAE,CAAC,wBAAwB,CAiQ5D,CAAC"}
@@ -156,17 +156,17 @@ export const ChartExportProvider = ({ children, capture, zipFiles, encodePng = c
156
156
  runCapture,
157
157
  zipFiles,
158
158
  ]);
159
- const downloadPdf = useCallback(async (documentTitle) => {
159
+ const downloadPdf = useCallback(async (documentTitle, extraPages) => {
160
160
  await runCapture(async () => {
161
161
  const charts = listCharts();
162
- if (charts.length === 0) {
162
+ if (charts.length === 0 && (extraPages?.length ?? 0) === 0) {
163
163
  throw new Error("The report is not ready to export yet.");
164
164
  }
165
165
  const captureFn = await resolveCapture(capture);
166
166
  const prepareElement = await resolvePrepareElement(capture);
167
167
  const build = await resolvePdf(buildPdf);
168
168
  try {
169
- downloadFile(await build(toCaptureSources(charts, captureFn, prepareElement), documentTitle), getPdfFilename());
169
+ downloadFile(await build(toCaptureSources(charts, captureFn, prepareElement), documentTitle, extraPages), getPdfFilename());
170
170
  }
171
171
  finally {
172
172
  await restoreChartExportViews(charts);
@@ -0,0 +1,24 @@
1
+ import type { FC } from "react";
2
+ import { type MigrationDonutChartLegendVariant } from "../charts/MigrationDonutChart.js";
3
+ /**
4
+ * Cluster inventory fields this card reads.
5
+ * agent-sdk and planner-sdk inventories both satisfy it. `cpuOverCommitment`
6
+ * stays `unknown` because generated models type it as a number while older
7
+ * payloads send a ratio string such as `"1:4"`.
8
+ */
9
+ export interface InventoryDataLike {
10
+ infra?: {
11
+ cpuOverCommitment?: unknown;
12
+ };
13
+ vms?: {
14
+ total?: number;
15
+ };
16
+ }
17
+ export interface ClustersOverviewProps {
18
+ clustersPerDatacenter?: number[];
19
+ clusters?: Record<string, InventoryDataLike>;
20
+ /** `"html"` in agent-ui; ui-app passes `"chart"`. */
21
+ legendVariant?: MigrationDonutChartLegendVariant;
22
+ }
23
+ export declare const ClustersOverview: FC<ClustersOverviewProps>;
24
+ //# sourceMappingURL=ClustersOverview.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ClustersOverview.d.ts","sourceRoot":"","sources":["../../src/report/ClustersOverview.tsx"],"names":[],"mappings":"AAyBA,OAAO,KAAK,EAAE,EAAE,EAAO,MAAM,OAAO,CAAC;AAErC,OAAO,EAGL,KAAK,gCAAgC,EACtC,MAAM,kCAAkC,CAAC;AAS1C;;;;;GAKG;AACH,MAAM,WAAW,iBAAiB;IAChC,KAAK,CAAC,EAAE;QACN,iBAAiB,CAAC,EAAE,OAAO,CAAC;KAC7B,CAAC;IACF,GAAG,CAAC,EAAE;QACJ,KAAK,CAAC,EAAE,MAAM,CAAC;KAChB,CAAC;CACH;AAED,MAAM,WAAW,qBAAqB;IACpC,qBAAqB,CAAC,EAAE,MAAM,EAAE,CAAC;IACjC,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAAC;IAC7C,qDAAqD;IACrD,aAAa,CAAC,EAAE,gCAAgC,CAAC;CAClD;AAyOD,eAAO,MAAM,gBAAgB,EAAE,EAAE,CAAC,qBAAqB,CA2JtD,CAAC"}
@@ -0,0 +1,228 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { Card, CardBody, CardTitle, Dropdown, DropdownItem, DropdownList, Flex, FlexItem, MenuToggle, } from "@patternfly/react-core";
3
+ import { DatabaseIcon } from "@patternfly/react-icons";
4
+ import { chart_color_black_500, chart_color_blue_300, chart_color_green_300, chart_color_orange_100, chart_color_purple_100, chart_color_purple_300, chart_color_red_orange_200, chart_color_teal_200, chart_color_teal_400, chart_color_yellow_400, } from "@patternfly/react-tokens";
5
+ import { useMemo, useState } from "react";
6
+ import { MigrationDonutChart, } from "../charts/MigrationDonutChart.js";
7
+ import { CardEmptyState } from "./CardEmptyState.js";
8
+ import { ChartHeaderActions } from "./ChartDownloadButton.js";
9
+ import { ChartExportSurface } from "./ChartExportSurface.js";
10
+ import { clustersOverviewStyles } from "./ClustersOverviewStyles.js";
11
+ import { chartExportViewsFromLabels } from "./chartExport.js";
12
+ import { REPORT_CARD_EMPTY_STATE_TITLES } from "./constants.js";
13
+ import { dashboardStyles } from "./dashboardStyles.js";
14
+ const VIEW_MODE_LABELS = {
15
+ dataCenterDistribution: "Cluster distribution by data center",
16
+ vmByCluster: "VM distribution by cluster",
17
+ cpuOverCommitment: "Cluster CPU over commitment",
18
+ };
19
+ const TOP_CLUSTERS = 4;
20
+ const TOP_CPU_RATIOS = 5;
21
+ const REST_OF_CLUSTERS_LABEL = "Rest of clusters";
22
+ const REST_OF_DATACENTERS_LABEL = "Rest of datacenters";
23
+ const CHART_ID = "clusters-overview";
24
+ const CHART_TITLE = "Clusters";
25
+ /**
26
+ * Same categorical order as the previous cluster hex palette, mapped onto
27
+ * PatternFly `chart_color_*` tokens (nearest token where the old hex was
28
+ * not itself a token).
29
+ */
30
+ const CLUSTER_COLORS = [
31
+ chart_color_blue_300.value,
32
+ chart_color_purple_300.value,
33
+ chart_color_purple_100.value,
34
+ chart_color_teal_200.value,
35
+ chart_color_yellow_400.value,
36
+ chart_color_green_300.value,
37
+ chart_color_orange_100.value,
38
+ chart_color_red_orange_200.value,
39
+ chart_color_teal_400.value,
40
+ chart_color_black_500.value,
41
+ ];
42
+ const EMPTY_CHART = {
43
+ chartData: [],
44
+ legend: {},
45
+ title: "",
46
+ subTitle: "",
47
+ };
48
+ const isViewMode = (value) => value === "dataCenterDistribution" ||
49
+ value === "vmByCluster" ||
50
+ value === "cpuOverCommitment";
51
+ const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
52
+ const legendFor = (categories) => {
53
+ const legend = {};
54
+ categories.forEach((category, index) => {
55
+ const color = CLUSTER_COLORS[index % CLUSTER_COLORS.length];
56
+ if (color) {
57
+ legend[category] = color;
58
+ }
59
+ });
60
+ return legend;
61
+ };
62
+ const readCounts = (values) => {
63
+ if (!Array.isArray(values)) {
64
+ return [];
65
+ }
66
+ return values.filter((value) => typeof value === "number" && Number.isFinite(value) && value >= 0);
67
+ };
68
+ const readClusters = (clusters) => {
69
+ if (!isRecord(clusters)) {
70
+ return [];
71
+ }
72
+ return Object.values(clusters);
73
+ };
74
+ const readVmTotal = (cluster) => {
75
+ if (!isRecord(cluster) || !isRecord(cluster.vms)) {
76
+ return 0;
77
+ }
78
+ const total = cluster.vms.total;
79
+ return typeof total === "number" && Number.isFinite(total) && total >= 0
80
+ ? total
81
+ : 0;
82
+ };
83
+ /**
84
+ * Read `infra.cpuOverCommitment` without assuming either SDK's generated
85
+ * shape. Missing infra, a non-object infra, or an absent property yield
86
+ * `undefined` instead of throwing.
87
+ */
88
+ const readCpuOverCommitment = (cluster) => {
89
+ if (!isRecord(cluster) || !isRecord(cluster.infra)) {
90
+ return undefined;
91
+ }
92
+ if (!("cpuOverCommitment" in cluster.infra)) {
93
+ return undefined;
94
+ }
95
+ return cluster.infra.cpuOverCommitment;
96
+ };
97
+ const parseCpuOverCommitment = (raw) => {
98
+ if (typeof raw === "number") {
99
+ return Number.isFinite(raw) ? raw : Number.NaN;
100
+ }
101
+ if (typeof raw !== "string") {
102
+ return Number.NaN;
103
+ }
104
+ const trimmed = raw.trim();
105
+ if (trimmed === "") {
106
+ return Number.NaN;
107
+ }
108
+ const match = trimmed.match(/^\d+\s*:\s*(\d+(?:\.\d+)?)$/);
109
+ if (match?.[1] != null) {
110
+ return Number(match[1]);
111
+ }
112
+ const asNum = Number(trimmed);
113
+ return Number.isFinite(asNum) ? asNum : Number.NaN;
114
+ };
115
+ const formatRatio = (ratio) => {
116
+ const formatted = ratio % 1 === 0 ? ratio.toFixed(0) : ratio.toFixed(2);
117
+ return formatted.replace(/(?:\.0+|(\.\d*[1-9]))0+$/, "$1");
118
+ };
119
+ const buildRankedDonut = (counts, options) => {
120
+ const total = counts.reduce((sum, count) => sum + count, 0);
121
+ const ranked = counts
122
+ .map((count, index) => ({ count, index }))
123
+ .sort((a, b) => b.count - a.count);
124
+ const top = ranked.slice(0, TOP_CLUSTERS);
125
+ const restSum = ranked
126
+ .slice(TOP_CLUSTERS)
127
+ .reduce((sum, item) => sum + item.count, 0);
128
+ const slices = top.map((item, index) => {
129
+ const name = options.sliceName(index + 1);
130
+ return {
131
+ name,
132
+ count: item.count,
133
+ countDisplay: `${item.count} ${options.countSuffix}`,
134
+ legendCategory: name,
135
+ };
136
+ });
137
+ if (restSum > 0) {
138
+ slices.push({
139
+ name: options.restName,
140
+ count: restSum,
141
+ countDisplay: `${restSum} ${options.countSuffix}`,
142
+ legendCategory: options.restName,
143
+ });
144
+ }
145
+ return {
146
+ chartData: slices,
147
+ legend: legendFor(slices.map((slice) => slice.legendCategory)),
148
+ title: `${total}`,
149
+ subTitle: options.subTitle,
150
+ };
151
+ };
152
+ const buildVmByCluster = (clusters) => {
153
+ const counts = readClusters(clusters).map(readVmTotal);
154
+ if (counts.length === 0) {
155
+ return EMPTY_CHART;
156
+ }
157
+ return buildRankedDonut(counts, {
158
+ sliceName: (rank) => `Cluster ${rank}`,
159
+ countSuffix: "VMs",
160
+ restName: REST_OF_CLUSTERS_LABEL,
161
+ subTitle: "VMs",
162
+ });
163
+ };
164
+ const buildDatacenterDistribution = (clustersPerDatacenter) => {
165
+ const counts = readCounts(clustersPerDatacenter);
166
+ if (counts.length === 0) {
167
+ return EMPTY_CHART;
168
+ }
169
+ return buildRankedDonut(counts, {
170
+ sliceName: (rank) => `Data center ${rank}`,
171
+ countSuffix: "clusters",
172
+ restName: REST_OF_DATACENTERS_LABEL,
173
+ subTitle: "Clusters",
174
+ });
175
+ };
176
+ const buildCpuOverCommitment = (clusters) => {
177
+ const ranked = readClusters(clusters)
178
+ .map((cluster) => parseCpuOverCommitment(readCpuOverCommitment(cluster)))
179
+ .filter((value) => Number.isFinite(value) && value >= 0)
180
+ .sort((a, b) => b - a)
181
+ .slice(0, TOP_CPU_RATIOS);
182
+ const slices = ranked.map((value, index) => {
183
+ const legendCategory = `Cluster ${index + 1}`;
184
+ const display = formatRatio(value);
185
+ return {
186
+ name: display,
187
+ count: value,
188
+ countDisplay: display,
189
+ legendCategory,
190
+ };
191
+ });
192
+ return {
193
+ chartData: slices,
194
+ legend: legendFor(slices.map((slice) => slice.legendCategory)),
195
+ title: "",
196
+ subTitle: "",
197
+ };
198
+ };
199
+ export const ClustersOverview = ({ clustersPerDatacenter, clusters, legendVariant = "html", }) => {
200
+ const [viewMode, setViewMode] = useState("vmByCluster");
201
+ const [isDropdownOpen, setIsDropdownOpen] = useState(false);
202
+ const chart = useMemo(() => {
203
+ if (viewMode === "cpuOverCommitment") {
204
+ return buildCpuOverCommitment(clusters);
205
+ }
206
+ if (viewMode === "vmByCluster") {
207
+ return buildVmByCluster(clusters);
208
+ }
209
+ return buildDatacenterDistribution(clustersPerDatacenter);
210
+ }, [viewMode, clustersPerDatacenter, clusters]);
211
+ const chartTitle = `${CHART_TITLE} — ${VIEW_MODE_LABELS[viewMode]}`;
212
+ const emptyTitle = viewMode === "cpuOverCommitment"
213
+ ? REPORT_CARD_EMPTY_STATE_TITLES.cpuOvercommitment
214
+ : REPORT_CARD_EMPTY_STATE_TITLES.clusters;
215
+ return (_jsx(ChartExportSurface, { id: CHART_ID, title: chartTitle, exportViews: chartExportViewsFromLabels(CHART_TITLE, VIEW_MODE_LABELS), activeExportViewId: viewMode, onExportViewChange: (viewId) => {
216
+ if (isViewMode(viewId)) {
217
+ setViewMode(viewId);
218
+ }
219
+ }, children: _jsxs(Card, { className: dashboardStyles.card, children: [_jsx(CardTitle, { children: _jsxs(Flex, { className: clustersOverviewStyles.titleRow, justifyContent: { default: "justifyContentSpaceBetween" }, alignItems: { default: "alignItemsCenter" }, children: [_jsx(FlexItem, { children: _jsxs("div", { children: [_jsxs("div", { children: [_jsx(DatabaseIcon, {}), " ", CHART_TITLE] }), _jsx("div", { className: clustersOverviewStyles.cardSubtitle, children: viewMode === "dataCenterDistribution"
220
+ ? "Top 5 datacenters"
221
+ : "Top 5 clusters" })] }) }), _jsx(ChartHeaderActions, { chartId: CHART_ID, title: chartTitle, children: _jsx(Dropdown, { isOpen: isDropdownOpen, onSelect: (_event, value) => {
222
+ if (isViewMode(value)) {
223
+ setViewMode(value);
224
+ }
225
+ setIsDropdownOpen(false);
226
+ }, onOpenChange: setIsDropdownOpen, toggle: (toggleRef) => (_jsx(MenuToggle, { ref: toggleRef, onClick: () => setIsDropdownOpen((open) => !open), isExpanded: isDropdownOpen, className: clustersOverviewStyles.menuToggleMinWidth, children: VIEW_MODE_LABELS[viewMode] })), children: _jsxs(DropdownList, { children: [_jsx(DropdownItem, { value: "vmByCluster", children: VIEW_MODE_LABELS.vmByCluster }, "vmByCluster"), _jsx(DropdownItem, { value: "cpuOverCommitment", children: VIEW_MODE_LABELS.cpuOverCommitment }, "cpuOverCommitment"), _jsx(DropdownItem, { value: "dataCenterDistribution", children: VIEW_MODE_LABELS.dataCenterDistribution }, "dataCenterDistribution")] }) }) })] }) }), _jsx(CardBody, { className: dashboardStyles.cardBodyScrollable, children: chart.chartData.length === 0 ? (_jsx(CardEmptyState, { title: emptyTitle })) : viewMode === "cpuOverCommitment" ? (_jsxs(_Fragment, { children: [_jsx("div", { className: clustersOverviewStyles.cpuOvercommitBoxes, children: chart.chartData.map((item) => (_jsx("div", { className: clustersOverviewStyles.cpuOvercommitBox, style: { background: chart.legend[item.legendCategory] }, children: item.countDisplay }, `cpu-box-${item.legendCategory}`))) }), _jsx("div", { className: clustersOverviewStyles.cpuOvercommitLegend, children: chart.chartData.map((item) => (_jsxs("div", { className: clustersOverviewStyles.cpuOvercommitLegendItem, children: [_jsx("span", { className: clustersOverviewStyles.cpuOvercommitLegendSwatch, style: { background: chart.legend[item.legendCategory] } }), _jsxs("span", { className: clustersOverviewStyles.cpuOvercommitLegendText, children: [item.legendCategory, " (", item.countDisplay, ")"] })] }, `cpu-legend-${item.legendCategory}`))) })] })) : (_jsx(MigrationDonutChart, { legendVariant: legendVariant, data: chart.chartData, height: 300, width: 420, donutThickness: 18, titleFontSize: 34, legend: chart.legend, title: chart.title, subTitle: chart.subTitle, subTitleColor: "var(--pf-t--global--text--color--subtle)", tooltipLabelFormatter: ({ datum, percent }) => `${datum.countDisplay}\n${percent.toFixed(1)}%` })) })] }) }));
227
+ };
228
+ ClustersOverview.displayName = "ClustersOverview";
@@ -0,0 +1,12 @@
1
+ export declare const clustersOverviewStyles: {
2
+ titleRow: string;
3
+ cardSubtitle: string;
4
+ menuToggleMinWidth: string;
5
+ cpuOvercommitBoxes: string;
6
+ cpuOvercommitBox: string;
7
+ cpuOvercommitLegend: string;
8
+ cpuOvercommitLegendItem: string;
9
+ cpuOvercommitLegendSwatch: string;
10
+ cpuOvercommitLegendText: string;
11
+ };
12
+ //# sourceMappingURL=ClustersOverviewStyles.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ClustersOverviewStyles.d.ts","sourceRoot":"","sources":["../../src/report/ClustersOverviewStyles.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,sBAAsB;;;;;;;;;;CA2GlC,CAAC"}
@@ -0,0 +1,101 @@
1
+ import { css } from "@emotion/css";
2
+ export const clustersOverviewStyles = {
3
+ titleRow: css `
4
+ width: 100%;
5
+ `,
6
+ cardSubtitle: css `
7
+ color: var(--pf-t--global--text--color--subtle);
8
+ font-size: 0.85rem;
9
+ `,
10
+ menuToggleMinWidth: css `
11
+ min-width: 250px;
12
+ `,
13
+ cpuOvercommitBoxes: css `
14
+ display: flex;
15
+ justify-content: center;
16
+ gap: 12px;
17
+ margin: 124px 0 16px;
18
+ flex-wrap: wrap;
19
+
20
+ @media (max-width: 1200px) {
21
+ margin: 72px 0 12px;
22
+ gap: 20px;
23
+ }
24
+
25
+ @media (max-width: 768px) {
26
+ margin: 40px 0 8px;
27
+ gap: 16px;
28
+ }
29
+ `,
30
+ cpuOvercommitBox: css `
31
+ color: #000;
32
+ min-width: 120px;
33
+ height: 64px;
34
+ border-radius: 12px;
35
+ display: flex;
36
+ align-items: center;
37
+ justify-content: center;
38
+ font-size: 22px;
39
+ padding: 0 16px;
40
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
41
+
42
+ @media (max-width: 768px) {
43
+ min-width: 96px;
44
+ height: 56px;
45
+ font-size: 18px;
46
+ padding: 0 12px;
47
+ border-radius: 10px;
48
+ }
49
+
50
+ @media (max-width: 480px) {
51
+ min-width: 88px;
52
+ height: 48px;
53
+ font-size: 16px;
54
+ border-radius: 8px;
55
+ }
56
+ `,
57
+ cpuOvercommitLegend: css `
58
+ display: flex;
59
+ justify-content: center;
60
+ gap: 6px 24px;
61
+ flex-wrap: wrap;
62
+ margin-top: 20%;
63
+
64
+ @media (max-width: 1200px) {
65
+ margin-top: 20%;
66
+ gap: 6px 20px;
67
+ }
68
+
69
+ @media (max-width: 768px) {
70
+ margin-top: 20%;
71
+ gap: 6px 16px;
72
+ }
73
+ `,
74
+ cpuOvercommitLegendItem: css `
75
+ display: flex;
76
+ align-items: center;
77
+ gap: 12px;
78
+ `,
79
+ cpuOvercommitLegendSwatch: css `
80
+ display: inline-block;
81
+ width: 12px;
82
+ height: 12px;
83
+ border-radius: 2px;
84
+
85
+ @media (max-width: 768px) {
86
+ width: 10px;
87
+ height: 10px;
88
+ }
89
+ `,
90
+ cpuOvercommitLegendText: css `
91
+ font-size: 16px;
92
+
93
+ @media (max-width: 768px) {
94
+ font-size: 16px;
95
+ }
96
+
97
+ @media (max-width: 480px) {
98
+ font-size: 14px;
99
+ }
100
+ `,
101
+ };
@@ -0,0 +1,19 @@
1
+ import type { FC } from "react";
2
+ import { type MigrationDonutChartLegendVariant } from "../charts/MigrationDonutChart.js";
3
+ export interface CpuAndMemoryOverviewProps {
4
+ cpuTierDistribution?: Record<string, number>;
5
+ memoryTierDistribution?: Record<string, number>;
6
+ memoryTotalGB?: number;
7
+ cpuTotalCores?: number;
8
+ /**
9
+ * Memory-tier slice click. The argument is the distribution key.
10
+ * Agent-ui maps it to a VM memory filter; ui-app omits it.
11
+ */
12
+ onTierSelect?: (tierLabel: string) => void;
13
+ /** Center-title click. Agent-ui opens the VM list with no extra filters. */
14
+ onTitleClick?: () => void;
15
+ /** `"html"` in agent-ui; ui-app passes `"chart"`. */
16
+ legendVariant?: MigrationDonutChartLegendVariant;
17
+ }
18
+ export declare const CpuAndMemoryOverview: FC<CpuAndMemoryOverviewProps>;
19
+ //# sourceMappingURL=CpuAndMemoryOverview.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CpuAndMemoryOverview.d.ts","sourceRoot":"","sources":["../../src/report/CpuAndMemoryOverview.tsx"],"names":[],"mappings":"AAcA,OAAO,KAAK,EAAE,EAAE,EAAO,MAAM,OAAO,CAAC;AAErC,OAAO,EAGL,KAAK,gCAAgC,EACtC,MAAM,kCAAkC,CAAC;AAQ1C,MAAM,WAAW,yBAAyB;IACxC,mBAAmB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC7C,sBAAsB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChD,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;;OAGG;IACH,YAAY,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAC;IAC3C,4EAA4E;IAC5E,YAAY,CAAC,EAAE,MAAM,IAAI,CAAC;IAC1B,qDAAqD;IACrD,aAAa,CAAC,EAAE,gCAAgC,CAAC;CAClD;AAuFD,eAAO,MAAM,oBAAoB,EAAE,EAAE,CAAC,yBAAyB,CAiJ9D,CAAC"}
@@ -0,0 +1,112 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { css } from "@emotion/css";
3
+ import { Card, CardBody, CardTitle, Dropdown, DropdownItem, DropdownList, Flex, FlexItem, MenuToggle, } from "@patternfly/react-core";
4
+ import { DataProcessorIcon } from "@patternfly/react-icons";
5
+ import { useMemo, useState } from "react";
6
+ import { MigrationDonutChart, } from "../charts/MigrationDonutChart.js";
7
+ import { CardEmptyState } from "./CardEmptyState.js";
8
+ import { ChartHeaderActions } from "./ChartDownloadButton.js";
9
+ import { ChartExportSurface } from "./ChartExportSurface.js";
10
+ import { chartExportViewsFromLabels } from "./chartExport.js";
11
+ import { REPORT_CARD_EMPTY_STATE_TITLES } from "./constants.js";
12
+ import { dashboardStyles } from "./dashboardStyles.js";
13
+ const VIEW_MODE_LABELS = {
14
+ memoryTiers: "VM distribution by memory size tier",
15
+ vcpuTiers: "VM distribution by vCPU count tier",
16
+ };
17
+ const VIEW_SUBTITLES = {
18
+ memoryTiers: "Memory size tiers",
19
+ vcpuTiers: "vCPU count tiers",
20
+ };
21
+ /**
22
+ * Categorical order shared by the agent-ui and ui-app CPU/memory donuts.
23
+ */
24
+ const TIER_COLORS = [
25
+ "#0066cc",
26
+ "#5e40be",
27
+ "#b6a6e9",
28
+ "#73c5c5",
29
+ "#b98412",
30
+ "#28a745",
31
+ "#f0ad4e",
32
+ "#d9534f",
33
+ "#009596",
34
+ "#6a6e73",
35
+ ];
36
+ const CHART_ID = "cpu-memory-overview";
37
+ const CHART_TITLE = "CPU & memory";
38
+ const cardSubtitle = css `
39
+ color: var(--pf-t--global--text--color--subtle);
40
+ font-size: 0.85rem;
41
+ `;
42
+ const menuToggleMinWidth = css `
43
+ min-width: 290px;
44
+ `;
45
+ const titleRow = css `
46
+ width: 100%;
47
+ `;
48
+ const isViewMode = (value) => value === "memoryTiers" || value === "vcpuTiers";
49
+ const tierSortKey = (label) => {
50
+ const value = Number.parseInt(label.trim(), 10);
51
+ return Number.isFinite(value) ? value : Number.MAX_SAFE_INTEGER;
52
+ };
53
+ const hasUnit = (label, unit) => unit === "GB" ? /gb$/i.test(label) : /cores?$/i.test(label);
54
+ const slicesFromDistribution = (distribution, unit) => Object.entries(distribution ?? {})
55
+ .filter(([, count]) => Number(count) > 0)
56
+ .sort(([left], [right]) => tierSortKey(left) - tierSortKey(right))
57
+ .map(([tier, count]) => {
58
+ const trimmed = tier.trim();
59
+ const name = hasUnit(trimmed, unit) ? trimmed : `${trimmed} ${unit}`;
60
+ const vmCount = Number(count);
61
+ return {
62
+ name,
63
+ count: vmCount,
64
+ countDisplay: `${vmCount} VMs`,
65
+ legendCategory: tier,
66
+ };
67
+ });
68
+ const legendFor = (categories) => {
69
+ const legend = {};
70
+ categories.forEach((category, index) => {
71
+ const color = TIER_COLORS[index % TIER_COLORS.length];
72
+ if (color) {
73
+ legend[category] = color;
74
+ }
75
+ });
76
+ return legend;
77
+ };
78
+ export const CpuAndMemoryOverview = ({ cpuTierDistribution, memoryTierDistribution, memoryTotalGB, cpuTotalCores, onTierSelect, onTitleClick, legendVariant = "html", }) => {
79
+ const [viewMode, setViewMode] = useState("memoryTiers");
80
+ const [isDropdownOpen, setIsDropdownOpen] = useState(false);
81
+ const memorySlices = useMemo(() => slicesFromDistribution(memoryTierDistribution, "GB"), [memoryTierDistribution]);
82
+ const vcpuSlices = useMemo(() => slicesFromDistribution(cpuTierDistribution, "cores"), [cpuTierDistribution]);
83
+ const activeSlices = viewMode === "memoryTiers" ? memorySlices : vcpuSlices;
84
+ const legend = useMemo(() => legendFor(activeSlices.map((slice) => slice.legendCategory)), [activeSlices]);
85
+ const totalVMs = useMemo(() => activeSlices.reduce((sum, slice) => sum + slice.count, 0), [activeSlices]);
86
+ const chartTitle = `${CHART_TITLE} — ${VIEW_MODE_LABELS[viewMode]}`;
87
+ const subTitle = viewMode === "memoryTiers"
88
+ ? typeof memoryTotalGB === "number"
89
+ ? `${memoryTotalGB.toLocaleString()} GB`
90
+ : undefined
91
+ : typeof cpuTotalCores === "number"
92
+ ? `${cpuTotalCores.toLocaleString()} Cores`
93
+ : undefined;
94
+ const emptyTitle = viewMode === "memoryTiers"
95
+ ? REPORT_CARD_EMPTY_STATE_TITLES.memory
96
+ : REPORT_CARD_EMPTY_STATE_TITLES.cpu;
97
+ return (_jsx(ChartExportSurface, { id: CHART_ID, title: chartTitle, exportViews: chartExportViewsFromLabels(CHART_TITLE, VIEW_MODE_LABELS), activeExportViewId: viewMode, onExportViewChange: (viewId) => {
98
+ if (isViewMode(viewId)) {
99
+ setViewMode(viewId);
100
+ }
101
+ }, children: _jsxs(Card, { className: dashboardStyles.card, children: [_jsx(CardTitle, { children: _jsxs(Flex, { className: titleRow, justifyContent: { default: "justifyContentSpaceBetween" }, alignItems: { default: "alignItemsCenter" }, children: [_jsx(FlexItem, { children: _jsxs("div", { children: [_jsxs("div", { children: [_jsx(DataProcessorIcon, {}), " ", CHART_TITLE] }), _jsx("div", { className: cardSubtitle, children: VIEW_SUBTITLES[viewMode] })] }) }), _jsx(ChartHeaderActions, { chartId: CHART_ID, title: chartTitle, children: _jsx(Dropdown, { isOpen: isDropdownOpen, onSelect: (_event, value) => {
102
+ if (isViewMode(value)) {
103
+ setViewMode(value);
104
+ }
105
+ setIsDropdownOpen(false);
106
+ }, onOpenChange: setIsDropdownOpen, toggle: (toggleRef) => (_jsx(MenuToggle, { ref: toggleRef, onClick: () => setIsDropdownOpen((open) => !open), isExpanded: isDropdownOpen, className: menuToggleMinWidth, children: VIEW_MODE_LABELS[viewMode] })), children: _jsxs(DropdownList, { children: [_jsx(DropdownItem, { value: "memoryTiers", children: VIEW_MODE_LABELS.memoryTiers }, "memoryTiers"), _jsx(DropdownItem, { value: "vcpuTiers", children: VIEW_MODE_LABELS.vcpuTiers }, "vcpuTiers")] }) }) })] }) }), _jsx(CardBody, { className: dashboardStyles.cardBodyScrollable, children: activeSlices.length === 0 ? (_jsx(CardEmptyState, { title: emptyTitle })) : (_jsx(MigrationDonutChart, { legendVariant: legendVariant, data: activeSlices, legend: legend, height: 300, width: 420, donutThickness: 18, titleFontSize: 34, title: `${totalVMs} VMs`, subTitle: subTitle, subTitleColor: "var(--pf-t--global--text--color--subtle)", legendLabelFormatter: ({ x, countDisplay }) => `${x} (${countDisplay})`, tooltipLabelFormatter: ({ datum, percent }) => `${datum.countDisplay}\n${percent.toFixed(1)}%`, itemsPerRow: legendVariant === "chart"
107
+ ? Math.ceil(activeSlices.length / 2)
108
+ : undefined, labelFontSize: legendVariant === "chart" ? 18 : undefined, marginLeft: legendVariant === "chart" ? "52%" : undefined, onItemClick: viewMode === "memoryTiers" && onTierSelect
109
+ ? (item) => onTierSelect(item.legendCategory)
110
+ : undefined, onTitleClick: onTitleClick })) })] }) }));
111
+ };
112
+ CpuAndMemoryOverview.displayName = "CpuAndMemoryOverview";
@@ -0,0 +1,18 @@
1
+ import type React from "react";
2
+ /**
3
+ * Minimal shape of a migration issue consumed by {@link ErrorTable}. Declared
4
+ * locally so this package does not depend on a specific SDK (`agent-sdk` or
5
+ * `planner-sdk`); callers pass the subset of `MigrationIssue` fields read here.
6
+ */
7
+ export interface MigrationIssueLike {
8
+ label: string;
9
+ assessment: string;
10
+ count: number;
11
+ }
12
+ export interface ErrorTableProps {
13
+ errors: MigrationIssueLike[];
14
+ /** Optional drill-down callback invoked with an issue's `label`. */
15
+ onConcernClick?: (concernLabel: string) => void;
16
+ }
17
+ export declare const ErrorTable: React.FC<ErrorTableProps>;
18
+ //# sourceMappingURL=ErrorTable.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ErrorTable.d.ts","sourceRoot":"","sources":["../../src/report/ErrorTable.tsx"],"names":[],"mappings":"AASA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAQ/B;;;;GAIG;AACH,MAAM,WAAW,kBAAkB;IACjC,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,kBAAkB,EAAE,CAAC;IAC7B,oEAAoE;IACpE,cAAc,CAAC,EAAE,CAAC,YAAY,EAAE,MAAM,KAAK,IAAI,CAAC;CACjD;AAED,eAAO,MAAM,UAAU,EAAE,KAAK,CAAC,EAAE,CAAC,eAAe,CAgDhD,CAAC"}
@@ -0,0 +1,20 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Card, CardBody, CardTitle, Flex, FlexItem, Icon, } from "@patternfly/react-core";
3
+ import { ExclamationCircleIcon } from "@patternfly/react-icons";
4
+ import { CardEmptyState } from "./CardEmptyState.js";
5
+ import { ChartHeaderActions } from "./ChartDownloadButton.js";
6
+ import { ChartExportSurface } from "./ChartExportSurface.js";
7
+ import { REPORT_CARD_EMPTY_STATE_TITLES } from "./constants.js";
8
+ import { dashboardStyles } from "./dashboardStyles.js";
9
+ import { ReportTable } from "./ReportTable.js";
10
+ export const ErrorTable = ({ errors, onConcernClick, }) => {
11
+ const handleRowClick = (issue) => {
12
+ if (issue.label && onConcernClick) {
13
+ onConcernClick(issue.label);
14
+ }
15
+ };
16
+ const chartId = "errors-table";
17
+ const chartTitle = "Errors";
18
+ return (_jsx(ChartExportSurface, { id: chartId, title: chartTitle, children: _jsxs(Card, { className: dashboardStyles.card, children: [_jsx(CardTitle, { children: _jsxs(Flex, { justifyContent: { default: "justifyContentSpaceBetween" }, alignItems: { default: "alignItemsCenter" }, children: [_jsxs(FlexItem, { children: [_jsx(Icon, { status: "danger", children: _jsx(ExclamationCircleIcon, {}) }), " ", "Errors"] }), _jsx(ChartHeaderActions, { chartId: chartId, title: chartTitle })] }) }), _jsx(CardBody, { className: dashboardStyles.cardBodyScrollable, children: errors.length === 0 ? (_jsx(CardEmptyState, { title: REPORT_CARD_EMPTY_STATE_TITLES.errors })) : (_jsx("div", { children: _jsx(ReportTable, { data: errors, columns: ["Description", "Total VMs"], fields: ["assessment", "count"], onRowClick: onConcernClick ? handleRowClick : undefined, clickableFields: onConcernClick ? ["assessment"] : [] }) })) })] }) }));
19
+ };
20
+ ErrorTable.displayName = "ErrorTable";
@@ -0,0 +1,13 @@
1
+ import type { FC } from "react";
2
+ import { type MigrationDonutChartLegendVariant } from "../charts/MigrationDonutChart.js";
3
+ /** Smallest host shape this card reads. Apps pass SDK hosts structurally. */
4
+ export interface HostLike {
5
+ model?: string | number;
6
+ }
7
+ export interface HostsOverviewProps {
8
+ hosts?: HostLike[];
9
+ /** `"html"` in agent-ui; ui-app passes `"chart"`. */
10
+ legendVariant?: MigrationDonutChartLegendVariant;
11
+ }
12
+ export declare const HostsOverview: FC<HostsOverviewProps>;
13
+ //# sourceMappingURL=HostsOverview.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"HostsOverview.d.ts","sourceRoot":"","sources":["../../src/report/HostsOverview.tsx"],"names":[],"mappings":"AAiBA,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC;AAEhC,OAAO,EAGL,KAAK,gCAAgC,EACtC,MAAM,kCAAkC,CAAC;AAO1C,6EAA6E;AAC7E,MAAM,WAAW,QAAQ;IACvB,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,kBAAkB;IACjC,KAAK,CAAC,EAAE,QAAQ,EAAE,CAAC;IACnB,qDAAqD;IACrD,aAAa,CAAC,EAAE,gCAAgC,CAAC;CAClD;AAuFD,eAAO,MAAM,aAAa,EAAE,EAAE,CAAC,kBAAkB,CAwDhD,CAAC"}