@openshift-migration-advisor/shared-components 0.22.0-dd51551836b7 → 0.23.0-54185b7f1be8

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 (68) hide show
  1. package/README.md +6 -0
  2. package/dist/charts/MigrationDonutChart.d.ts +18 -1
  3. package/dist/charts/MigrationDonutChart.d.ts.map +1 -1
  4. package/dist/charts/MigrationDonutChart.js +103 -43
  5. package/dist/charts/index.d.ts +1 -1
  6. package/dist/charts/index.d.ts.map +1 -1
  7. package/dist/forms/RadioButtonFormGroup.d.ts.map +1 -1
  8. package/dist/forms/RadioButtonFormGroup.js +2 -2
  9. package/dist/report/FeatureStatusBadge.d.ts +7 -0
  10. package/dist/report/FeatureStatusBadge.d.ts.map +1 -0
  11. package/dist/report/FeatureStatusBadge.js +13 -0
  12. package/dist/report/HostPowerStates.d.ts +9 -0
  13. package/dist/report/HostPowerStates.d.ts.map +1 -0
  14. package/dist/report/HostPowerStates.js +11 -0
  15. package/dist/report/InfrastructureSummary.d.ts +7 -0
  16. package/dist/report/InfrastructureSummary.d.ts.map +1 -0
  17. package/dist/report/InfrastructureSummary.js +5 -0
  18. package/dist/report/OSDistribution.d.ts.map +1 -1
  19. package/dist/report/OSDistribution.js +5 -13
  20. package/dist/report/OsSupportTiersHelpPopover.d.ts.map +1 -1
  21. package/dist/report/OsSupportTiersHelpPopover.js +2 -31
  22. package/dist/report/OsUpgradeNotice.d.ts.map +1 -1
  23. package/dist/report/OsUpgradeNotice.js +1 -5
  24. package/dist/report/PowerStateCard.d.ts +17 -0
  25. package/dist/report/PowerStateCard.d.ts.map +1 -0
  26. package/dist/report/PowerStateCard.js +7 -0
  27. package/dist/report/SupportTierBadge.d.ts.map +1 -1
  28. package/dist/report/SupportTierBadge.js +4 -2
  29. package/dist/report/VCenterClusterDetails.d.ts +10 -0
  30. package/dist/report/VCenterClusterDetails.d.ts.map +1 -0
  31. package/dist/report/VCenterClusterDetails.js +26 -0
  32. package/dist/report/VmPowerStates.d.ts +9 -0
  33. package/dist/report/VmPowerStates.d.ts.map +1 -0
  34. package/dist/report/VmPowerStates.js +11 -0
  35. package/dist/report/constants.d.ts +2 -0
  36. package/dist/report/constants.d.ts.map +1 -1
  37. package/dist/report/constants.js +2 -0
  38. package/dist/report/index.d.ts +10 -0
  39. package/dist/report/index.d.ts.map +1 -1
  40. package/dist/report/index.js +8 -0
  41. package/dist/report/infrastructureSummary.d.ts +82 -0
  42. package/dist/report/infrastructureSummary.d.ts.map +1 -0
  43. package/dist/report/infrastructureSummary.js +139 -0
  44. package/dist/report/powerStates.d.ts +11 -0
  45. package/dist/report/powerStates.d.ts.map +1 -0
  46. package/dist/report/powerStates.js +113 -0
  47. package/package.json +1 -1
  48. package/src/charts/MigrationDonutChart.tsx +232 -111
  49. package/src/charts/index.ts +1 -0
  50. package/src/forms/RadioButtonFormGroup.tsx +10 -3
  51. package/src/report/FeatureStatusBadge.tsx +30 -0
  52. package/src/report/HostPowerStates.tsx +41 -0
  53. package/src/report/InfrastructureSummary.tsx +56 -0
  54. package/src/report/OSDistribution.tsx +60 -67
  55. package/src/report/OsSupportTiersHelpPopover.tsx +32 -61
  56. package/src/report/OsUpgradeNotice.tsx +1 -7
  57. package/src/report/PowerStateCard.tsx +73 -0
  58. package/src/report/SupportTierBadge.tsx +4 -2
  59. package/src/report/VCenterClusterDetails.tsx +211 -0
  60. package/src/report/VmPowerStates.tsx +41 -0
  61. package/src/report/__tests__/HostPowerStates.test.tsx +47 -0
  62. package/src/report/__tests__/InfrastructureSummary.test.tsx +148 -0
  63. package/src/report/__tests__/infrastructureSummary.test.ts +224 -0
  64. package/src/report/__tests__/powerStates.test.ts +70 -0
  65. package/src/report/constants.ts +2 -0
  66. package/src/report/index.ts +56 -0
  67. package/src/report/infrastructureSummary.ts +281 -0
  68. package/src/report/powerStates.ts +169 -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
  };
@@ -1,5 +1,6 @@
1
1
  export {
2
2
  MigrationDonutChart,
3
3
  type MigrationDonutChartDatum,
4
+ type MigrationDonutChartLegendVariant,
4
5
  type MigrationDonutChartProps,
5
6
  } from "./MigrationDonutChart.js";
@@ -33,12 +33,19 @@ export default function RadioButtonFormGroup({
33
33
  const showError = error && (isTouched || isSubmitted);
34
34
 
35
35
  return (
36
- <FormGroup label={label} isRequired={isRequired} fieldId={id} {...props}>
36
+ <FormGroup
37
+ label={label}
38
+ isRequired={isRequired}
39
+ fieldId={id}
40
+ role="radiogroup"
41
+ isInline
42
+ {...props}
43
+ >
37
44
  <Controller
38
45
  name={name}
39
46
  control={methods.control}
40
47
  render={({ field }) => (
41
- <div style={{ display: "flex", gap: "16px" }} role="radiogroup">
48
+ <>
42
49
  {options.map((option) => (
43
50
  <Radio
44
51
  key={option.value}
@@ -50,7 +57,7 @@ export default function RadioButtonFormGroup({
50
57
  onBlur={field.onBlur}
51
58
  />
52
59
  ))}
53
- </div>
60
+ </>
54
61
  )}
55
62
  />
56
63
  <FormFieldHelperText
@@ -0,0 +1,30 @@
1
+ import { Label } from "@patternfly/react-core";
2
+ import { CheckCircleIcon, TimesCircleIcon } from "@patternfly/react-icons";
3
+ import type { FC } from "react";
4
+ import type { FeatureStatus } from "./infrastructureSummary.js";
5
+
6
+ export interface FeatureStatusBadgeProps {
7
+ status: FeatureStatus;
8
+ }
9
+
10
+ export const FeatureStatusBadge: FC<FeatureStatusBadgeProps> = ({ status }) => {
11
+ if (status === "unknown") {
12
+ return <span>—</span>;
13
+ }
14
+
15
+ if (status === "enabled") {
16
+ return (
17
+ <Label color="green" isCompact icon={<CheckCircleIcon />}>
18
+ Enabled
19
+ </Label>
20
+ );
21
+ }
22
+
23
+ return (
24
+ <Label color="grey" isCompact icon={<TimesCircleIcon />}>
25
+ Disabled
26
+ </Label>
27
+ );
28
+ };
29
+
30
+ FeatureStatusBadge.displayName = "FeatureStatusBadge";
@@ -0,0 +1,41 @@
1
+ import { ServerIcon } from "@patternfly/react-icons";
2
+ import type { FC } from "react";
3
+ import { useMemo } from "react";
4
+ import type { MigrationDonutChartLegendVariant } from "../charts/MigrationDonutChart.js";
5
+ import { REPORT_CARD_EMPTY_STATE_TITLES } from "./constants.js";
6
+ import { PowerStateCard } from "./PowerStateCard.js";
7
+ import { buildHostPowerStateChart } from "./powerStates.js";
8
+
9
+ export interface HostPowerStatesProps {
10
+ hostPowerStates?: Record<string, number>;
11
+ isExportMode?: boolean;
12
+ legendVariant?: MigrationDonutChartLegendVariant;
13
+ }
14
+
15
+ export const HostPowerStates: FC<HostPowerStatesProps> = ({
16
+ hostPowerStates,
17
+ isExportMode = false,
18
+ legendVariant,
19
+ }) => {
20
+ const chart = useMemo(
21
+ () => buildHostPowerStateChart(hostPowerStates),
22
+ [hostPowerStates],
23
+ );
24
+
25
+ return (
26
+ <PowerStateCard
27
+ id="esxi-host-power-states"
28
+ title="ESXi host power states"
29
+ icon={<ServerIcon />}
30
+ emptyTitle={REPORT_CARD_EMPTY_STATE_TITLES.hostPowerStates}
31
+ slices={chart.slices}
32
+ legend={chart.legend}
33
+ total={chart.total}
34
+ subTitle="Hosts"
35
+ isExportMode={isExportMode}
36
+ legendVariant={legendVariant}
37
+ />
38
+ );
39
+ };
40
+
41
+ HostPowerStates.displayName = "HostPowerStates";
@@ -0,0 +1,56 @@
1
+ import {
2
+ Card,
3
+ CardBody,
4
+ CardTitle,
5
+ DescriptionList,
6
+ DescriptionListDescription,
7
+ DescriptionListGroup,
8
+ DescriptionListTerm,
9
+ } from "@patternfly/react-core";
10
+ import type { FC } from "react";
11
+ import type { InfrastructureSummaryModel } from "./infrastructureSummary.js";
12
+
13
+ export interface InfrastructureSummaryProps {
14
+ summary: InfrastructureSummaryModel;
15
+ }
16
+
17
+ const formatCount = (value: number | undefined): string =>
18
+ typeof value === "number" ? String(value) : "—";
19
+
20
+ export const InfrastructureSummary: FC<InfrastructureSummaryProps> = ({
21
+ summary,
22
+ }) => (
23
+ <Card isFullHeight id="infrastructure-summary">
24
+ <CardTitle>Infrastructure summary</CardTitle>
25
+ <CardBody>
26
+ <DescriptionList isAutoFit displaySize="lg">
27
+ <DescriptionListGroup>
28
+ <DescriptionListTerm>VMware version</DescriptionListTerm>
29
+ <DescriptionListDescription>
30
+ {summary.vmwareVersion}
31
+ </DescriptionListDescription>
32
+ </DescriptionListGroup>
33
+ <DescriptionListGroup>
34
+ <DescriptionListTerm>Datacenters</DescriptionListTerm>
35
+ <DescriptionListDescription>
36
+ {formatCount(summary.datacenters)}
37
+ </DescriptionListDescription>
38
+ </DescriptionListGroup>
39
+ <DescriptionListGroup>
40
+ <DescriptionListTerm>vCenters</DescriptionListTerm>
41
+ <DescriptionListDescription>
42
+ {formatCount(summary.vCenters)}
43
+ </DescriptionListDescription>
44
+ </DescriptionListGroup>
45
+ <DescriptionListGroup>
46
+ <DescriptionListTerm>ESXi hosts</DescriptionListTerm>
47
+ <DescriptionListDescription>
48
+ {formatCount(summary.esxiHosts)}
49
+ </DescriptionListDescription>
50
+ </DescriptionListGroup>
51
+ </DescriptionList>
52
+ </CardBody>
53
+ </Card>
54
+ );
55
+
56
+ InfrastructureSummary.displayName = "InfrastructureSummary";