@openshift-migration-advisor/shared-components 0.24.0 → 0.25.0-1c27267d2ea6

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 (143) hide show
  1. package/README.md +31 -0
  2. package/dist/charts/index.d.ts +1 -1
  3. package/dist/charts/index.d.ts.map +1 -1
  4. package/dist/forms/RadioButtonFormGroup.d.ts.map +1 -1
  5. package/dist/forms/RadioButtonFormGroup.js +2 -2
  6. package/dist/report/ChartDownloadButton.d.ts +11 -0
  7. package/dist/report/ChartDownloadButton.d.ts.map +1 -0
  8. package/dist/report/ChartDownloadButton.js +24 -0
  9. package/dist/report/ChartExportProvider.d.ts +22 -0
  10. package/dist/report/ChartExportProvider.d.ts.map +1 -0
  11. package/dist/report/ChartExportProvider.js +222 -0
  12. package/dist/report/ChartExportSurface.d.ts +12 -0
  13. package/dist/report/ChartExportSurface.d.ts.map +1 -0
  14. package/dist/report/ChartExportSurface.js +32 -0
  15. package/dist/report/ErrorTable.d.ts +18 -0
  16. package/dist/report/ErrorTable.d.ts.map +1 -0
  17. package/dist/report/ErrorTable.js +20 -0
  18. package/dist/report/FeatureStatusBadge.d.ts +7 -0
  19. package/dist/report/FeatureStatusBadge.d.ts.map +1 -0
  20. package/dist/report/FeatureStatusBadge.js +13 -0
  21. package/dist/report/HostPowerStates.d.ts +8 -0
  22. package/dist/report/HostPowerStates.d.ts.map +1 -0
  23. package/dist/report/HostPowerStates.js +11 -0
  24. package/dist/report/InfrastructureSummary.d.ts +7 -0
  25. package/dist/report/InfrastructureSummary.d.ts.map +1 -0
  26. package/dist/report/InfrastructureSummary.js +9 -0
  27. package/dist/report/NetworkOverview.d.ts +31 -0
  28. package/dist/report/NetworkOverview.d.ts.map +1 -0
  29. package/dist/report/NetworkOverview.js +201 -0
  30. package/dist/report/OSDistribution.d.ts +0 -2
  31. package/dist/report/OSDistribution.d.ts.map +1 -1
  32. package/dist/report/OSDistribution.js +10 -15
  33. package/dist/report/OsSupportTiersHelpPopover.d.ts.map +1 -1
  34. package/dist/report/OsSupportTiersHelpPopover.js +2 -31
  35. package/dist/report/OsUpgradeNotice.d.ts.map +1 -1
  36. package/dist/report/OsUpgradeNotice.js +1 -5
  37. package/dist/report/OsUpgradeRecommendationPopover.d.ts +0 -1
  38. package/dist/report/OsUpgradeRecommendationPopover.d.ts.map +1 -1
  39. package/dist/report/OsUpgradeRecommendationPopover.js +4 -3
  40. package/dist/report/PowerStateCard.d.ts +16 -0
  41. package/dist/report/PowerStateCard.d.ts.map +1 -0
  42. package/dist/report/PowerStateCard.js +9 -0
  43. package/dist/report/ReportExportMenu.d.ts +19 -0
  44. package/dist/report/ReportExportMenu.d.ts.map +1 -0
  45. package/dist/report/ReportExportMenu.js +31 -0
  46. package/dist/report/ReportTable.d.ts +14 -0
  47. package/dist/report/ReportTable.d.ts.map +1 -0
  48. package/dist/report/ReportTable.js +23 -0
  49. package/dist/report/SupportTierBadge.d.ts +0 -1
  50. package/dist/report/SupportTierBadge.d.ts.map +1 -1
  51. package/dist/report/SupportTierBadge.js +1 -19
  52. package/dist/report/VCenterClusterDetails.d.ts +9 -0
  53. package/dist/report/VCenterClusterDetails.d.ts.map +1 -0
  54. package/dist/report/VCenterClusterDetails.js +31 -0
  55. package/dist/report/VmPowerStates.d.ts +8 -0
  56. package/dist/report/VmPowerStates.d.ts.map +1 -0
  57. package/dist/report/VmPowerStates.js +11 -0
  58. package/dist/report/captureChartElement.d.ts +8 -0
  59. package/dist/report/captureChartElement.d.ts.map +1 -0
  60. package/dist/report/captureChartElement.js +284 -0
  61. package/dist/report/chartExport.d.ts +57 -0
  62. package/dist/report/chartExport.d.ts.map +1 -0
  63. package/dist/report/chartExport.js +162 -0
  64. package/dist/report/chartExportContext.d.ts +28 -0
  65. package/dist/report/chartExportContext.d.ts.map +1 -0
  66. package/dist/report/chartExportContext.js +36 -0
  67. package/dist/report/chartExportFilenames.d.ts +4 -0
  68. package/dist/report/chartExportFilenames.d.ts.map +1 -0
  69. package/dist/report/chartExportFilenames.js +16 -0
  70. package/dist/report/constants.d.ts +3 -0
  71. package/dist/report/constants.d.ts.map +1 -1
  72. package/dist/report/constants.js +3 -0
  73. package/dist/report/dashboardStyles.d.ts +0 -4
  74. package/dist/report/dashboardStyles.d.ts.map +1 -1
  75. package/dist/report/dashboardStyles.js +0 -19
  76. package/dist/report/htmlExport.d.ts +9 -0
  77. package/dist/report/htmlExport.d.ts.map +1 -0
  78. package/dist/report/htmlExport.js +65 -0
  79. package/dist/report/index.d.ts +26 -0
  80. package/dist/report/index.d.ts.map +1 -1
  81. package/dist/report/index.js +24 -0
  82. package/dist/report/infrastructureSummaryModel.d.ts +82 -0
  83. package/dist/report/infrastructureSummaryModel.d.ts.map +1 -0
  84. package/dist/report/infrastructureSummaryModel.js +139 -0
  85. package/dist/report/pdfExport.d.ts +3 -0
  86. package/dist/report/pdfExport.d.ts.map +1 -0
  87. package/dist/report/pdfExport.js +82 -0
  88. package/dist/report/pdfPage.d.ts +15 -0
  89. package/dist/report/pdfPage.d.ts.map +1 -0
  90. package/dist/report/pdfPage.js +44 -0
  91. package/dist/report/powerStates.d.ts +11 -0
  92. package/dist/report/powerStates.d.ts.map +1 -0
  93. package/dist/report/powerStates.js +113 -0
  94. package/dist/report/reportExportOptions.d.ts +17 -0
  95. package/dist/report/reportExportOptions.d.ts.map +1 -0
  96. package/dist/report/reportExportOptions.js +31 -0
  97. package/dist/report/zipChartPngs.d.ts +3 -0
  98. package/dist/report/zipChartPngs.d.ts.map +1 -0
  99. package/dist/report/zipChartPngs.js +8 -0
  100. package/package.json +7 -2
  101. package/src/charts/index.ts +1 -0
  102. package/src/forms/RadioButtonFormGroup.tsx +10 -3
  103. package/src/report/ChartDownloadButton.tsx +59 -0
  104. package/src/report/ChartExportProvider.tsx +384 -0
  105. package/src/report/ChartExportSurface.tsx +60 -0
  106. package/src/report/ErrorTable.tsx +85 -0
  107. package/src/report/FeatureStatusBadge.tsx +30 -0
  108. package/src/report/HostPowerStates.tsx +38 -0
  109. package/src/report/InfrastructureSummary.tsx +73 -0
  110. package/src/report/NetworkOverview.tsx +405 -0
  111. package/src/report/OSDistribution.tsx +96 -99
  112. package/src/report/OsSupportTiersHelpPopover.tsx +32 -62
  113. package/src/report/OsUpgradeNotice.tsx +1 -7
  114. package/src/report/OsUpgradeRecommendationPopover.tsx +3 -4
  115. package/src/report/PowerStateCard.tsx +86 -0
  116. package/src/report/ReportExportMenu.tsx +119 -0
  117. package/src/report/ReportTable.tsx +81 -0
  118. package/src/report/SupportTierBadge.tsx +12 -38
  119. package/src/report/VCenterClusterDetails.tsx +233 -0
  120. package/src/report/VmPowerStates.tsx +38 -0
  121. package/src/report/__tests__/ChartExportProvider.test.tsx +474 -0
  122. package/src/report/__tests__/HostPowerStates.test.tsx +47 -0
  123. package/src/report/__tests__/InfrastructureSummary.test.tsx +126 -0
  124. package/src/report/__tests__/NetworkOverview.test.tsx +269 -0
  125. package/src/report/__tests__/OSDistribution.test.tsx +10 -11
  126. package/src/report/__tests__/captureChartElement.test.ts +132 -0
  127. package/src/report/__tests__/chartExportPackagers.test.ts +224 -0
  128. package/src/report/__tests__/infrastructureSummaryModel.test.ts +224 -0
  129. package/src/report/__tests__/powerStates.test.ts +70 -0
  130. package/src/report/captureChartElement.ts +368 -0
  131. package/src/report/chartExport.ts +237 -0
  132. package/src/report/chartExportContext.tsx +84 -0
  133. package/src/report/chartExportFilenames.ts +21 -0
  134. package/src/report/constants.ts +3 -0
  135. package/src/report/dashboardStyles.ts +0 -23
  136. package/src/report/htmlExport.ts +83 -0
  137. package/src/report/index.ts +142 -0
  138. package/src/report/infrastructureSummaryModel.ts +281 -0
  139. package/src/report/pdfExport.ts +154 -0
  140. package/src/report/pdfPage.ts +83 -0
  141. package/src/report/powerStates.ts +169 -0
  142. package/src/report/reportExportOptions.ts +53 -0
  143. package/src/report/zipChartPngs.ts +10 -0
@@ -0,0 +1,60 @@
1
+ import { type FC, type ReactNode, useCallback, useRef } from "react";
2
+ import {
3
+ type ChartExportView,
4
+ chartExportRootStyle,
5
+ waitForChartExportPaint,
6
+ } from "./chartExport.js";
7
+ import { useRegisterChart } from "./chartExportContext.js";
8
+
9
+ export const ChartExportSurface: FC<{
10
+ id: string;
11
+ title: string;
12
+ filename?: string;
13
+ exportViews?: ChartExportView[];
14
+ activeExportViewId?: string;
15
+ onExportViewChange?: (viewId: string) => void;
16
+ children: ReactNode;
17
+ }> = ({
18
+ id,
19
+ title,
20
+ filename,
21
+ exportViews,
22
+ activeExportViewId,
23
+ onExportViewChange,
24
+ children,
25
+ }) => {
26
+ const activeViewRef = useRef(activeExportViewId);
27
+ activeViewRef.current = activeExportViewId;
28
+ const onExportViewChangeRef = useRef(onExportViewChange);
29
+ onExportViewChangeRef.current = onExportViewChange;
30
+
31
+ const setExportView = useCallback(async (viewId: string) => {
32
+ const change = onExportViewChangeRef.current;
33
+ if (change && activeViewRef.current !== viewId) {
34
+ change(viewId);
35
+ await new Promise<void>((resolve) => {
36
+ window.setTimeout(resolve, 0);
37
+ });
38
+ }
39
+ await waitForChartExportPaint();
40
+ }, []);
41
+
42
+ const ref = useRegisterChart({
43
+ id,
44
+ title,
45
+ filename,
46
+ exportViews,
47
+ activeExportViewId,
48
+ setExportView:
49
+ exportViews && exportViews.length > 1 && onExportViewChange
50
+ ? setExportView
51
+ : undefined,
52
+ });
53
+ return (
54
+ <div ref={ref} id={id} style={chartExportRootStyle}>
55
+ {children}
56
+ </div>
57
+ );
58
+ };
59
+
60
+ ChartExportSurface.displayName = "ChartExportSurface";
@@ -0,0 +1,85 @@
1
+ import {
2
+ Card,
3
+ CardBody,
4
+ CardTitle,
5
+ Flex,
6
+ FlexItem,
7
+ Icon,
8
+ } from "@patternfly/react-core";
9
+ import { ExclamationCircleIcon } from "@patternfly/react-icons";
10
+ import type React from "react";
11
+ import { CardEmptyState } from "./CardEmptyState.js";
12
+ import { ChartHeaderActions } from "./ChartDownloadButton.js";
13
+ import { ChartExportSurface } from "./ChartExportSurface.js";
14
+ import { REPORT_CARD_EMPTY_STATE_TITLES } from "./constants.js";
15
+ import { dashboardStyles } from "./dashboardStyles.js";
16
+ import { ReportTable } from "./ReportTable.js";
17
+
18
+ /**
19
+ * Minimal shape of a migration issue consumed by {@link ErrorTable}. Declared
20
+ * locally so this package does not depend on a specific SDK (`agent-sdk` or
21
+ * `planner-sdk`); callers pass the subset of `MigrationIssue` fields read here.
22
+ */
23
+ export interface MigrationIssueLike {
24
+ label: string;
25
+ assessment: string;
26
+ count: number;
27
+ }
28
+
29
+ export interface ErrorTableProps {
30
+ errors: MigrationIssueLike[];
31
+ /** Optional drill-down callback invoked with an issue's `label`. */
32
+ onConcernClick?: (concernLabel: string) => void;
33
+ }
34
+
35
+ export const ErrorTable: React.FC<ErrorTableProps> = ({
36
+ errors,
37
+ onConcernClick,
38
+ }) => {
39
+ const handleRowClick = (issue: MigrationIssueLike) => {
40
+ if (issue.label && onConcernClick) {
41
+ onConcernClick(issue.label);
42
+ }
43
+ };
44
+
45
+ const chartId = "errors-table";
46
+ const chartTitle = "Errors";
47
+
48
+ return (
49
+ <ChartExportSurface id={chartId} title={chartTitle}>
50
+ <Card className={dashboardStyles.card}>
51
+ <CardTitle>
52
+ <Flex
53
+ justifyContent={{ default: "justifyContentSpaceBetween" }}
54
+ alignItems={{ default: "alignItemsCenter" }}
55
+ >
56
+ <FlexItem>
57
+ <Icon status="danger">
58
+ <ExclamationCircleIcon />
59
+ </Icon>{" "}
60
+ Errors
61
+ </FlexItem>
62
+ <ChartHeaderActions chartId={chartId} title={chartTitle} />
63
+ </Flex>
64
+ </CardTitle>
65
+ <CardBody className={dashboardStyles.cardBodyScrollable}>
66
+ {errors.length === 0 ? (
67
+ <CardEmptyState title={REPORT_CARD_EMPTY_STATE_TITLES.errors} />
68
+ ) : (
69
+ <div>
70
+ <ReportTable<MigrationIssueLike>
71
+ data={errors}
72
+ columns={["Description", "Total VMs"]}
73
+ fields={["assessment", "count"]}
74
+ onRowClick={onConcernClick ? handleRowClick : undefined}
75
+ clickableFields={onConcernClick ? ["assessment"] : []}
76
+ />
77
+ </div>
78
+ )}
79
+ </CardBody>
80
+ </Card>
81
+ </ChartExportSurface>
82
+ );
83
+ };
84
+
85
+ ErrorTable.displayName = "ErrorTable";
@@ -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 "./infrastructureSummaryModel.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,38 @@
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
+ legendVariant?: MigrationDonutChartLegendVariant;
12
+ }
13
+
14
+ export const HostPowerStates: FC<HostPowerStatesProps> = ({
15
+ hostPowerStates,
16
+ legendVariant,
17
+ }) => {
18
+ const chart = useMemo(
19
+ () => buildHostPowerStateChart(hostPowerStates),
20
+ [hostPowerStates],
21
+ );
22
+
23
+ return (
24
+ <PowerStateCard
25
+ id="esxi-host-power-states"
26
+ title="ESXi host power states"
27
+ icon={<ServerIcon />}
28
+ emptyTitle={REPORT_CARD_EMPTY_STATE_TITLES.hostPowerStates}
29
+ slices={chart.slices}
30
+ legend={chart.legend}
31
+ total={chart.total}
32
+ subTitle="Hosts"
33
+ legendVariant={legendVariant}
34
+ />
35
+ );
36
+ };
37
+
38
+ HostPowerStates.displayName = "HostPowerStates";
@@ -0,0 +1,73 @@
1
+ import {
2
+ Card,
3
+ CardBody,
4
+ CardTitle,
5
+ DescriptionList,
6
+ DescriptionListDescription,
7
+ DescriptionListGroup,
8
+ DescriptionListTerm,
9
+ Flex,
10
+ FlexItem,
11
+ } from "@patternfly/react-core";
12
+ import type { FC } from "react";
13
+ import { ChartHeaderActions } from "./ChartDownloadButton.js";
14
+ import { ChartExportSurface } from "./ChartExportSurface.js";
15
+ import type { InfrastructureSummaryModel } from "./infrastructureSummaryModel.js";
16
+
17
+ export interface InfrastructureSummaryProps {
18
+ summary: InfrastructureSummaryModel;
19
+ }
20
+
21
+ const formatCount = (value: number | undefined): string =>
22
+ typeof value === "number" ? String(value) : "—";
23
+
24
+ const INFRA_CHART_ID = "infrastructure-summary";
25
+ const INFRA_TITLE = "Infrastructure summary";
26
+
27
+ export const InfrastructureSummary: FC<InfrastructureSummaryProps> = ({
28
+ summary,
29
+ }) => (
30
+ <ChartExportSurface id={INFRA_CHART_ID} title={INFRA_TITLE}>
31
+ <Card isFullHeight>
32
+ <CardTitle>
33
+ <Flex
34
+ justifyContent={{ default: "justifyContentSpaceBetween" }}
35
+ alignItems={{ default: "alignItemsCenter" }}
36
+ >
37
+ <FlexItem>{INFRA_TITLE}</FlexItem>
38
+ <ChartHeaderActions chartId={INFRA_CHART_ID} title={INFRA_TITLE} />
39
+ </Flex>
40
+ </CardTitle>
41
+ <CardBody>
42
+ <DescriptionList isAutoFit displaySize="lg">
43
+ <DescriptionListGroup>
44
+ <DescriptionListTerm>VMware version</DescriptionListTerm>
45
+ <DescriptionListDescription>
46
+ {summary.vmwareVersion}
47
+ </DescriptionListDescription>
48
+ </DescriptionListGroup>
49
+ <DescriptionListGroup>
50
+ <DescriptionListTerm>Datacenters</DescriptionListTerm>
51
+ <DescriptionListDescription>
52
+ {formatCount(summary.datacenters)}
53
+ </DescriptionListDescription>
54
+ </DescriptionListGroup>
55
+ <DescriptionListGroup>
56
+ <DescriptionListTerm>vCenters</DescriptionListTerm>
57
+ <DescriptionListDescription>
58
+ {formatCount(summary.vCenters)}
59
+ </DescriptionListDescription>
60
+ </DescriptionListGroup>
61
+ <DescriptionListGroup>
62
+ <DescriptionListTerm>ESXi hosts</DescriptionListTerm>
63
+ <DescriptionListDescription>
64
+ {formatCount(summary.esxiHosts)}
65
+ </DescriptionListDescription>
66
+ </DescriptionListGroup>
67
+ </DescriptionList>
68
+ </CardBody>
69
+ </Card>
70
+ </ChartExportSurface>
71
+ );
72
+
73
+ InfrastructureSummary.displayName = "InfrastructureSummary";
@@ -0,0 +1,405 @@
1
+ import { css } from "@emotion/css";
2
+ import {
3
+ Card,
4
+ CardBody,
5
+ CardTitle,
6
+ Dropdown,
7
+ DropdownItem,
8
+ DropdownList,
9
+ Flex,
10
+ FlexItem,
11
+ MenuToggle,
12
+ type MenuToggleElement,
13
+ } from "@patternfly/react-core";
14
+ import { TopologyIcon } from "@patternfly/react-icons";
15
+ import {
16
+ chart_color_black_500,
17
+ chart_color_blue_300,
18
+ chart_color_green_300,
19
+ chart_color_orange_100,
20
+ chart_color_purple_100,
21
+ chart_color_purple_300,
22
+ chart_color_red_orange_200,
23
+ chart_color_teal_200,
24
+ chart_color_teal_400,
25
+ chart_color_yellow_400,
26
+ } from "@patternfly/react-tokens";
27
+ import type { FC, Ref } from "react";
28
+ import { useMemo, useState } from "react";
29
+ import {
30
+ MigrationDonutChart,
31
+ type MigrationDonutChartDatum,
32
+ type MigrationDonutChartLegendVariant,
33
+ } from "../charts/MigrationDonutChart.js";
34
+ import { CardEmptyState } from "./CardEmptyState.js";
35
+ import { ChartHeaderActions } from "./ChartDownloadButton.js";
36
+ import { ChartExportSurface } from "./ChartExportSurface.js";
37
+ import { chartExportViewsFromLabels } from "./chartExport.js";
38
+ import { REPORT_CARD_EMPTY_STATE_TITLES } from "./constants.js";
39
+ import { dashboardStyles } from "./dashboardStyles.js";
40
+
41
+ /** Network fields this card reads. Apps pass SDK networks structurally. */
42
+ export interface NetworkLike {
43
+ vmsCount?: number;
44
+ vlanId?: string | number;
45
+ }
46
+
47
+ /** Infra fields this card reads. */
48
+ export interface NetworkInfraLike {
49
+ networks?: NetworkLike[];
50
+ }
51
+
52
+ /** Deprecated NIC histogram used when `distributionByNicCount` is absent. */
53
+ export interface NicCountHistogram {
54
+ data?: number[];
55
+ minValue?: number;
56
+ step?: number;
57
+ }
58
+
59
+ /** NIC summary fields this card reads from the VM resource breakdown. */
60
+ export interface NicCountSummary {
61
+ total?: number;
62
+ histogram?: NicCountHistogram;
63
+ }
64
+
65
+ export interface NetworkOverviewProps {
66
+ infra?: NetworkInfraLike;
67
+ nicCount?: NicCountSummary;
68
+ distributionByNicCount?: Record<string, number>;
69
+ /** `"html"` in agent-ui; ui-app passes `"chart"`. */
70
+ legendVariant?: MigrationDonutChartLegendVariant;
71
+ }
72
+
73
+ type ViewMode = "networkDistribution" | "nicCount";
74
+
75
+ const VIEW_MODE_LABELS: Record<ViewMode, string> = {
76
+ networkDistribution: "VM distribution by network",
77
+ nicCount: "VM distribution by NIC count",
78
+ };
79
+
80
+ const TOP_NETWORKS = 4;
81
+ const REST_OF_NETWORKS_LABEL = "Rest of networks";
82
+ const CHART_ID = "network-overview";
83
+ const CHART_TITLE = "Networks";
84
+
85
+ /**
86
+ * Categorical order from the ui-app network donut, mapped onto PatternFly
87
+ * `chart_color_*` tokens (nearest token where the old hex was not a token).
88
+ */
89
+ const NETWORK_COLORS = [
90
+ chart_color_blue_300.value,
91
+ chart_color_purple_300.value,
92
+ chart_color_purple_100.value,
93
+ chart_color_teal_200.value,
94
+ chart_color_yellow_400.value,
95
+ chart_color_green_300.value,
96
+ chart_color_orange_100.value,
97
+ chart_color_red_orange_200.value,
98
+ chart_color_teal_400.value,
99
+ chart_color_black_500.value,
100
+ ];
101
+
102
+ const cardSubtitle = css`
103
+ color: var(--pf-t--global--text--color--subtle);
104
+ font-size: 0.85rem;
105
+ `;
106
+
107
+ const menuToggleMinWidth = css`
108
+ min-width: 250px;
109
+ `;
110
+
111
+ const titleRow = css`
112
+ width: 100%;
113
+ `;
114
+
115
+ const isViewMode = (value: string | number | undefined): value is ViewMode =>
116
+ value === "networkDistribution" || value === "nicCount";
117
+
118
+ const legendFor = (categories: string[]): Record<string, string> => {
119
+ const legend: Record<string, string> = {};
120
+ categories.forEach((category, index) => {
121
+ const color = NETWORK_COLORS[index % NETWORK_COLORS.length];
122
+ if (color) {
123
+ legend[category] = color;
124
+ }
125
+ });
126
+ return legend;
127
+ };
128
+
129
+ const vlanLabel = (vlanId: NetworkLike["vlanId"]): string => {
130
+ if (typeof vlanId !== "string" && typeof vlanId !== "number") {
131
+ return "-";
132
+ }
133
+ const value = String(vlanId).trim();
134
+ return value !== "" ? value : "-";
135
+ };
136
+
137
+ const buildNetworkDonut = (
138
+ networks: NetworkLike[] | undefined,
139
+ ): {
140
+ slices: MigrationDonutChartDatum[];
141
+ legend: Record<string, string>;
142
+ title: string;
143
+ vlanBySlice: Record<string, string>;
144
+ } => {
145
+ const items = (Array.isArray(networks) ? networks : [])
146
+ .map((network) => ({
147
+ vmsCount:
148
+ typeof network?.vmsCount === "number" ? network.vmsCount : Number.NaN,
149
+ vlanId: vlanLabel(network?.vlanId),
150
+ }))
151
+ .filter(
152
+ (network) => Number.isFinite(network.vmsCount) && network.vmsCount >= 0,
153
+ )
154
+ .sort((a, b) => b.vmsCount - a.vmsCount);
155
+
156
+ const totalVMs = items.reduce((sum, network) => sum + network.vmsCount, 0);
157
+ const top = items.slice(0, TOP_NETWORKS);
158
+ const restSum = items
159
+ .slice(TOP_NETWORKS)
160
+ .reduce((sum, network) => sum + network.vmsCount, 0);
161
+
162
+ const vlanBySlice: Record<string, string> = {};
163
+ const slices: MigrationDonutChartDatum[] = top.map((network, index) => {
164
+ const name = `Network ${index + 1}`;
165
+ vlanBySlice[name] = network.vlanId;
166
+ return {
167
+ name,
168
+ count: network.vmsCount,
169
+ countDisplay: `${network.vmsCount} VMs`,
170
+ legendCategory: name,
171
+ };
172
+ });
173
+
174
+ if (restSum > 0) {
175
+ slices.push({
176
+ name: REST_OF_NETWORKS_LABEL,
177
+ count: restSum,
178
+ countDisplay: `${restSum} VMs`,
179
+ legendCategory: REST_OF_NETWORKS_LABEL,
180
+ });
181
+ vlanBySlice[REST_OF_NETWORKS_LABEL] = "-";
182
+ }
183
+
184
+ return {
185
+ slices,
186
+ legend: legendFor(slices.map((slice) => slice.legendCategory)),
187
+ title: `${totalVMs}`,
188
+ vlanBySlice,
189
+ };
190
+ };
191
+
192
+ const nicBucketLabel = (bucket: string): string => {
193
+ const count = Number.parseInt(bucket, 10);
194
+ if (bucket.endsWith("+")) {
195
+ return `${count}+ NIC`;
196
+ }
197
+ return count === 1 ? "1 NIC" : `${count} NIC`;
198
+ };
199
+
200
+ const compareNicBuckets = (
201
+ a: { bucket: string },
202
+ b: { bucket: string },
203
+ ): number => {
204
+ const aPlus = a.bucket.endsWith("+");
205
+ const bPlus = b.bucket.endsWith("+");
206
+ if (aPlus !== bPlus) {
207
+ return aPlus ? 1 : -1;
208
+ }
209
+ const aNum = Number.parseInt(a.bucket, 10);
210
+ const bNum = Number.parseInt(b.bucket, 10);
211
+ if (Number.isFinite(aNum) && Number.isFinite(bNum)) {
212
+ return aNum - bNum;
213
+ }
214
+ return a.bucket.localeCompare(b.bucket);
215
+ };
216
+
217
+ const slicesFromBuckets = (
218
+ entries: { bucket: string; count: number }[],
219
+ ): MigrationDonutChartDatum[] =>
220
+ entries.map((entry) => {
221
+ const label = nicBucketLabel(entry.bucket);
222
+ return {
223
+ name: label,
224
+ count: entry.count,
225
+ countDisplay: `${entry.count} VMs`,
226
+ legendCategory: label,
227
+ };
228
+ });
229
+
230
+ const buildNicDonut = (
231
+ distributionByNicCount: Record<string, number> | undefined,
232
+ nicCount: NicCountSummary | undefined,
233
+ ): {
234
+ slices: MigrationDonutChartDatum[];
235
+ legend: Record<string, string>;
236
+ title: string;
237
+ } => {
238
+ if (distributionByNicCount != null) {
239
+ const entries = Object.entries(distributionByNicCount)
240
+ .map(([bucket, value]) => ({
241
+ bucket,
242
+ count: Number.isFinite(Number(value)) ? Number(value) : 0,
243
+ }))
244
+ .filter((entry) => entry.count > 0)
245
+ .sort(compareNicBuckets);
246
+ const slices = slicesFromBuckets(entries);
247
+ const total = entries.reduce((sum, entry) => sum + entry.count, 0);
248
+ return {
249
+ slices,
250
+ legend: legendFor(slices.map((slice) => slice.legendCategory)),
251
+ title: `${total}`,
252
+ };
253
+ }
254
+
255
+ const histogram = nicCount?.histogram;
256
+ const data = Array.isArray(histogram?.data) ? histogram.data : [];
257
+ const minValue =
258
+ typeof histogram?.minValue === "number" ? histogram.minValue : 0;
259
+ const step = typeof histogram?.step === "number" ? histogram.step : 1;
260
+ const buckets = data
261
+ .map((count, index) => ({
262
+ bucket: String(minValue + index * step),
263
+ count: Number(count) || 0,
264
+ }))
265
+ .filter((entry) => entry.count > 0)
266
+ .sort((a, b) => Number(a.bucket) - Number(b.bucket));
267
+ const slices = slicesFromBuckets(buckets);
268
+ const total =
269
+ typeof nicCount?.total === "number"
270
+ ? nicCount.total
271
+ : buckets.reduce((sum, entry) => sum + entry.count, 0);
272
+
273
+ return {
274
+ slices,
275
+ legend: legendFor(slices.map((slice) => slice.legendCategory)),
276
+ title: `${total}`,
277
+ };
278
+ };
279
+
280
+ export const NetworkOverview: FC<NetworkOverviewProps> = ({
281
+ infra,
282
+ nicCount,
283
+ distributionByNicCount,
284
+ legendVariant = "html",
285
+ }) => {
286
+ const [viewMode, setViewMode] = useState<ViewMode>("networkDistribution");
287
+ const [isDropdownOpen, setIsDropdownOpen] = useState(false);
288
+
289
+ const networkChart = useMemo(
290
+ () => buildNetworkDonut(infra?.networks),
291
+ [infra?.networks],
292
+ );
293
+ const nicChart = useMemo(
294
+ () => buildNicDonut(distributionByNicCount, nicCount),
295
+ [distributionByNicCount, nicCount],
296
+ );
297
+
298
+ const chartTitle = `${CHART_TITLE} — ${VIEW_MODE_LABELS[viewMode]}`;
299
+ const activeChart =
300
+ viewMode === "networkDistribution" ? networkChart : nicChart;
301
+ const emptyTitle =
302
+ viewMode === "networkDistribution"
303
+ ? REPORT_CARD_EMPTY_STATE_TITLES.networks
304
+ : REPORT_CARD_EMPTY_STATE_TITLES.nicCount;
305
+
306
+ return (
307
+ <ChartExportSurface
308
+ id={CHART_ID}
309
+ title={chartTitle}
310
+ exportViews={chartExportViewsFromLabels(CHART_TITLE, VIEW_MODE_LABELS)}
311
+ activeExportViewId={viewMode}
312
+ onExportViewChange={(viewId) => {
313
+ if (isViewMode(viewId)) {
314
+ setViewMode(viewId);
315
+ }
316
+ }}
317
+ >
318
+ <Card className={dashboardStyles.card}>
319
+ <CardTitle>
320
+ <Flex
321
+ className={titleRow}
322
+ justifyContent={{ default: "justifyContentSpaceBetween" }}
323
+ alignItems={{ default: "alignItemsCenter" }}
324
+ >
325
+ <FlexItem>
326
+ <div>
327
+ <div>
328
+ <TopologyIcon /> {CHART_TITLE}
329
+ </div>
330
+ {viewMode === "networkDistribution" && (
331
+ <div className={cardSubtitle}>Top 5 networks</div>
332
+ )}
333
+ </div>
334
+ </FlexItem>
335
+ <ChartHeaderActions chartId={CHART_ID} title={chartTitle}>
336
+ <Dropdown
337
+ isOpen={isDropdownOpen}
338
+ onSelect={(_event, value) => {
339
+ if (isViewMode(value)) {
340
+ setViewMode(value);
341
+ }
342
+ setIsDropdownOpen(false);
343
+ }}
344
+ onOpenChange={setIsDropdownOpen}
345
+ toggle={(toggleRef: Ref<MenuToggleElement>) => (
346
+ <MenuToggle
347
+ ref={toggleRef}
348
+ onClick={() => setIsDropdownOpen((open) => !open)}
349
+ isExpanded={isDropdownOpen}
350
+ className={menuToggleMinWidth}
351
+ >
352
+ {VIEW_MODE_LABELS[viewMode]}
353
+ </MenuToggle>
354
+ )}
355
+ >
356
+ <DropdownList>
357
+ <DropdownItem
358
+ value="networkDistribution"
359
+ key="networkDistribution"
360
+ >
361
+ {VIEW_MODE_LABELS.networkDistribution}
362
+ </DropdownItem>
363
+ <DropdownItem value="nicCount" key="nicCount">
364
+ {VIEW_MODE_LABELS.nicCount}
365
+ </DropdownItem>
366
+ </DropdownList>
367
+ </Dropdown>
368
+ </ChartHeaderActions>
369
+ </Flex>
370
+ </CardTitle>
371
+ <CardBody className={dashboardStyles.cardBodyScrollable}>
372
+ {activeChart.slices.length === 0 ? (
373
+ <CardEmptyState title={emptyTitle} />
374
+ ) : (
375
+ <MigrationDonutChart
376
+ legendVariant={legendVariant}
377
+ data={activeChart.slices}
378
+ legend={activeChart.legend}
379
+ height={300}
380
+ width={420}
381
+ donutThickness={18}
382
+ titleFontSize={34}
383
+ title={activeChart.title}
384
+ subTitle="VMs"
385
+ subTitleColor="var(--pf-t--global--text--color--subtle)"
386
+ itemsPerRow={Math.ceil(activeChart.slices.length / 2)}
387
+ labelFontSize={18}
388
+ marginLeft="0%"
389
+ tooltipLabelFormatter={({ datum, percent }) => {
390
+ const summary = `${datum.countDisplay}\n${percent.toFixed(1)}%`;
391
+ if (viewMode !== "networkDistribution") {
392
+ return summary;
393
+ }
394
+ const vlan = networkChart.vlanBySlice[datum.legendCategory];
395
+ return `${summary}\nVLAN: ${vlan ?? "-"}`;
396
+ }}
397
+ />
398
+ )}
399
+ </CardBody>
400
+ </Card>
401
+ </ChartExportSurface>
402
+ );
403
+ };
404
+
405
+ NetworkOverview.displayName = "NetworkOverview";