@openshift-migration-advisor/shared-components 0.25.0 → 0.27.0-3f8ae10b086b

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 (138) hide show
  1. package/README.md +25 -0
  2. package/dist/report/ChartDownloadButton.d.ts +11 -0
  3. package/dist/report/ChartDownloadButton.d.ts.map +1 -0
  4. package/dist/report/ChartDownloadButton.js +24 -0
  5. package/dist/report/ChartExportProvider.d.ts +23 -0
  6. package/dist/report/ChartExportProvider.d.ts.map +1 -0
  7. package/dist/report/ChartExportProvider.js +222 -0
  8. package/dist/report/ChartExportSurface.d.ts +12 -0
  9. package/dist/report/ChartExportSurface.d.ts.map +1 -0
  10. package/dist/report/ChartExportSurface.js +32 -0
  11. package/dist/report/ClustersOverview.d.ts +24 -0
  12. package/dist/report/ClustersOverview.d.ts.map +1 -0
  13. package/dist/report/ClustersOverview.js +228 -0
  14. package/dist/report/ClustersOverviewStyles.d.ts +12 -0
  15. package/dist/report/ClustersOverviewStyles.d.ts.map +1 -0
  16. package/dist/report/ClustersOverviewStyles.js +101 -0
  17. package/dist/report/CpuAndMemoryOverview.d.ts +19 -0
  18. package/dist/report/CpuAndMemoryOverview.d.ts.map +1 -0
  19. package/dist/report/CpuAndMemoryOverview.js +112 -0
  20. package/dist/report/ErrorTable.d.ts +18 -0
  21. package/dist/report/ErrorTable.d.ts.map +1 -0
  22. package/dist/report/ErrorTable.js +20 -0
  23. package/dist/report/HostPowerStates.d.ts +0 -1
  24. package/dist/report/HostPowerStates.d.ts.map +1 -1
  25. package/dist/report/HostPowerStates.js +2 -2
  26. package/dist/report/HostsOverview.d.ts +13 -0
  27. package/dist/report/HostsOverview.d.ts.map +1 -0
  28. package/dist/report/HostsOverview.js +84 -0
  29. package/dist/report/InfrastructureSummary.d.ts.map +1 -1
  30. package/dist/report/InfrastructureSummary.js +6 -2
  31. package/dist/report/NetworkOverview.d.ts +31 -0
  32. package/dist/report/NetworkOverview.d.ts.map +1 -0
  33. package/dist/report/NetworkOverview.js +201 -0
  34. package/dist/report/OSDistribution.d.ts +0 -2
  35. package/dist/report/OSDistribution.d.ts.map +1 -1
  36. package/dist/report/OSDistribution.js +8 -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 +0 -1
  41. package/dist/report/PowerStateCard.d.ts.map +1 -1
  42. package/dist/report/PowerStateCard.js +4 -2
  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 -21
  52. package/dist/report/VCenterClusterDetails.d.ts +0 -1
  53. package/dist/report/VCenterClusterDetails.d.ts.map +1 -1
  54. package/dist/report/VCenterClusterDetails.js +9 -4
  55. package/dist/report/VmPowerStates.d.ts +0 -1
  56. package/dist/report/VmPowerStates.d.ts.map +1 -1
  57. package/dist/report/VmPowerStates.js +2 -2
  58. package/dist/report/WarningsTable.d.ts +9 -0
  59. package/dist/report/WarningsTable.d.ts.map +1 -0
  60. package/dist/report/WarningsTable.js +20 -0
  61. package/dist/report/captureChartElement.d.ts +8 -0
  62. package/dist/report/captureChartElement.d.ts.map +1 -0
  63. package/dist/report/captureChartElement.js +304 -0
  64. package/dist/report/chartExport.d.ts +80 -0
  65. package/dist/report/chartExport.d.ts.map +1 -0
  66. package/dist/report/chartExport.js +182 -0
  67. package/dist/report/chartExportContext.d.ts +32 -0
  68. package/dist/report/chartExportContext.d.ts.map +1 -0
  69. package/dist/report/chartExportContext.js +36 -0
  70. package/dist/report/chartExportFilenames.d.ts +4 -0
  71. package/dist/report/chartExportFilenames.d.ts.map +1 -0
  72. package/dist/report/chartExportFilenames.js +16 -0
  73. package/dist/report/constants.d.ts +2 -0
  74. package/dist/report/constants.d.ts.map +1 -1
  75. package/dist/report/constants.js +2 -0
  76. package/dist/report/dashboardStyles.d.ts +0 -4
  77. package/dist/report/dashboardStyles.d.ts.map +1 -1
  78. package/dist/report/dashboardStyles.js +0 -19
  79. package/dist/report/htmlExport.d.ts +9 -0
  80. package/dist/report/htmlExport.d.ts.map +1 -0
  81. package/dist/report/htmlExport.js +65 -0
  82. package/dist/report/index.d.ts +20 -0
  83. package/dist/report/index.d.ts.map +1 -1
  84. package/dist/report/index.js +20 -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 +131 -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/reportExportOptions.d.ts +17 -0
  92. package/dist/report/reportExportOptions.d.ts.map +1 -0
  93. package/dist/report/reportExportOptions.js +31 -0
  94. package/dist/report/zipChartPngs.d.ts +3 -0
  95. package/dist/report/zipChartPngs.d.ts.map +1 -0
  96. package/dist/report/zipChartPngs.js +8 -0
  97. package/package.json +7 -2
  98. package/src/report/ChartDownloadButton.tsx +59 -0
  99. package/src/report/ChartExportProvider.tsx +392 -0
  100. package/src/report/ChartExportSurface.tsx +60 -0
  101. package/src/report/ClustersOverview.tsx +451 -0
  102. package/src/report/ClustersOverviewStyles.ts +110 -0
  103. package/src/report/CpuAndMemoryOverview.tsx +277 -0
  104. package/src/report/ErrorTable.tsx +85 -0
  105. package/src/report/HostPowerStates.tsx +0 -3
  106. package/src/report/HostsOverview.tsx +185 -0
  107. package/src/report/InfrastructureSummary.tsx +48 -31
  108. package/src/report/NetworkOverview.tsx +405 -0
  109. package/src/report/OSDistribution.tsx +46 -42
  110. package/src/report/OsUpgradeRecommendationPopover.tsx +3 -4
  111. package/src/report/PowerStateCard.tsx +49 -36
  112. package/src/report/ReportExportMenu.tsx +119 -0
  113. package/src/report/ReportTable.tsx +81 -0
  114. package/src/report/SupportTierBadge.tsx +12 -40
  115. package/src/report/VCenterClusterDetails.tsx +38 -16
  116. package/src/report/VmPowerStates.tsx +0 -3
  117. package/src/report/WarningsTable.tsx +75 -0
  118. package/src/report/__tests__/ChartExportProvider.test.tsx +582 -0
  119. package/src/report/__tests__/ClustersOverview.test.tsx +241 -0
  120. package/src/report/__tests__/CpuAndMemoryOverview.test.tsx +200 -0
  121. package/src/report/__tests__/HostsOverview.test.tsx +145 -0
  122. package/src/report/__tests__/InfrastructureSummary.test.tsx +0 -22
  123. package/src/report/__tests__/NetworkOverview.test.tsx +269 -0
  124. package/src/report/__tests__/OSDistribution.test.tsx +10 -11
  125. package/src/report/__tests__/captureChartElement.test.ts +205 -0
  126. package/src/report/__tests__/chartExportPackagers.test.ts +299 -0
  127. package/src/report/captureChartElement.ts +398 -0
  128. package/src/report/chartExport.ts +277 -0
  129. package/src/report/chartExportContext.tsx +92 -0
  130. package/src/report/chartExportFilenames.ts +21 -0
  131. package/src/report/constants.ts +2 -0
  132. package/src/report/dashboardStyles.ts +0 -23
  133. package/src/report/htmlExport.ts +83 -0
  134. package/src/report/index.ts +107 -0
  135. package/src/report/pdfExport.ts +233 -0
  136. package/src/report/pdfPage.ts +83 -0
  137. package/src/report/reportExportOptions.ts +53 -0
  138. package/src/report/zipChartPngs.ts +10 -0
@@ -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";
@@ -21,6 +21,9 @@ import { DesktopIcon } from "@patternfly/react-icons";
21
21
  import { Table, Tbody, Td, Th, Thead, Tr } from "@patternfly/react-table";
22
22
  import type { FC, Ref } from "react";
23
23
  import { CardEmptyState } from "./CardEmptyState.js";
24
+ import { ChartHeaderActions } from "./ChartDownloadButton.js";
25
+ import { ChartExportSurface } from "./ChartExportSurface.js";
26
+ import { chartExportHideProps, chartExportScrollProps } from "./chartExport.js";
24
27
  import { REPORT_CARD_EMPTY_STATE_TITLES } from "./constants.js";
25
28
  import { dashboardStyles, tableFullWidthStyle } from "./dashboardStyles.js";
26
29
  import { EmptySearchResults } from "./EmptySearchResults.js";
@@ -43,47 +46,55 @@ const tableScrollStyle = css`
43
46
  max-height: 350px;
44
47
  `;
45
48
 
49
+ const OS_DISTRIBUTION_CHART_ID = "os-distribution";
50
+ const OS_DISTRIBUTION_TITLE = "Operating system distribution";
51
+
46
52
  interface OSDistributionProps {
47
53
  osData: Record<string, OSDistributionEntry>;
48
- isExportMode?: boolean;
49
54
  }
50
55
 
51
- export const OSDistribution: FC<OSDistributionProps> = ({
52
- osData,
53
- isExportMode = false,
54
- }) => (
55
- <Card
56
- className={isExportMode ? dashboardStyles.cardPrint : dashboardStyles.card}
57
- id="os-distribution"
56
+ export const OSDistribution: FC<OSDistributionProps> = ({ osData }) => (
57
+ <ChartExportSurface
58
+ id={OS_DISTRIBUTION_CHART_ID}
59
+ title={OS_DISTRIBUTION_TITLE}
58
60
  >
59
- <CardTitle>
60
- <Flex
61
- alignItems={{ default: "alignItemsCenter" }}
62
- spaceItems={{ default: "spaceItemsSm" }}
63
- >
64
- <FlexItem>
65
- <DesktopIcon /> Operating Systems
66
- </FlexItem>
67
- <FlexItem>
68
- <OsSupportTiersHelpPopover />
69
- </FlexItem>
70
- </Flex>
71
- </CardTitle>
72
- <CardBody>
73
- <OSBarChart osData={osData} isExportMode={isExportMode} />
74
- </CardBody>
75
- </Card>
61
+ <Card className={dashboardStyles.card}>
62
+ <CardTitle>
63
+ <Flex
64
+ justifyContent={{ default: "justifyContentSpaceBetween" }}
65
+ alignItems={{ default: "alignItemsCenter" }}
66
+ >
67
+ <FlexItem>
68
+ <Flex
69
+ alignItems={{ default: "alignItemsCenter" }}
70
+ spaceItems={{ default: "spaceItemsSm" }}
71
+ >
72
+ <FlexItem>
73
+ <DesktopIcon /> Operating Systems
74
+ </FlexItem>
75
+ <FlexItem {...chartExportHideProps}>
76
+ <OsSupportTiersHelpPopover />
77
+ </FlexItem>
78
+ </Flex>
79
+ </FlexItem>
80
+ <ChartHeaderActions
81
+ chartId={OS_DISTRIBUTION_CHART_ID}
82
+ title={OS_DISTRIBUTION_TITLE}
83
+ />
84
+ </Flex>
85
+ </CardTitle>
86
+ <CardBody>
87
+ <OSBarChart osData={osData} />
88
+ </CardBody>
89
+ </Card>
90
+ </ChartExportSurface>
76
91
  );
77
92
 
78
93
  interface OSBarChartProps {
79
94
  osData: Record<string, OSDistributionEntry>;
80
- isExportMode?: boolean;
81
95
  }
82
96
 
83
- export const OSBarChart: FC<OSBarChartProps> = ({
84
- osData,
85
- isExportMode = false,
86
- }) => {
97
+ export const OSBarChart: FC<OSBarChartProps> = ({ osData }) => {
87
98
  const vm = useOsBarChartViewModel(osData);
88
99
 
89
100
  if (vm.tableRows.length === 0) {
@@ -94,7 +105,7 @@ export const OSBarChart: FC<OSBarChartProps> = ({
94
105
 
95
106
  return (
96
107
  <>
97
- {!isExportMode ? (
108
+ <div {...chartExportHideProps}>
98
109
  <Toolbar hasNoPadding>
99
110
  <ToolbarContent>
100
111
  <ToolbarItem>
@@ -138,7 +149,7 @@ export const OSBarChart: FC<OSBarChartProps> = ({
138
149
  </ToolbarItem>
139
150
  </ToolbarContent>
140
151
  </Toolbar>
141
- ) : null}
152
+ </div>
142
153
 
143
154
  <Stack hasGutter>
144
155
  {vm.showUpgradeNotice ? (
@@ -148,11 +159,8 @@ export const OSBarChart: FC<OSBarChartProps> = ({
148
159
  ) : null}
149
160
  <StackItem>
150
161
  <div
151
- className={
152
- isExportMode
153
- ? tableFullWidthStyle
154
- : `${tableScrollStyle} ${tableFullWidthStyle}`
155
- }
162
+ className={`${tableScrollStyle} ${tableFullWidthStyle}`}
163
+ {...chartExportScrollProps}
156
164
  >
157
165
  <Table aria-label="Operating systems" variant="compact">
158
166
  <Thead>
@@ -176,14 +184,10 @@ export const OSBarChart: FC<OSBarChartProps> = ({
176
184
  <OsNameCell
177
185
  osName={row.osName}
178
186
  upgradeRecommendation={row.upgradeRecommendation}
179
- isExportMode={isExportMode}
180
187
  />
181
188
  </Td>
182
189
  <Td dataLabel="Tier">
183
- <SupportTierBadge
184
- tier={row.tier}
185
- isExportMode={isExportMode}
186
- />
190
+ <SupportTierBadge tier={row.tier} />
187
191
  </Td>
188
192
  <Td dataLabel="VMs">{row.count}</Td>
189
193
  </Tr>
@@ -2,6 +2,7 @@ import { css } from "@emotion/css";
2
2
  import { Button, Flex, FlexItem, Popover } from "@patternfly/react-core";
3
3
  import { InfoCircleIcon } from "@patternfly/react-icons";
4
4
  import type { FC } from "react";
5
+ import { chartExportHideProps } from "./chartExport.js";
5
6
 
6
7
  const upgradeRecommendationPopoverCloseButton = css`
7
8
  .pf-v6-c-popover__close .pf-v6-c-button.pf-m-plain,
@@ -44,18 +45,16 @@ OsUpgradeRecommendationPopover.displayName = "OsUpgradeRecommendationPopover";
44
45
  interface OsNameCellProps {
45
46
  osName: string;
46
47
  upgradeRecommendation?: string;
47
- isExportMode?: boolean;
48
48
  }
49
49
 
50
50
  export const OsNameCell: FC<OsNameCellProps> = ({
51
51
  osName,
52
52
  upgradeRecommendation,
53
- isExportMode = false,
54
53
  }) => {
55
54
  const hasUpgradeRecommendation =
56
55
  upgradeRecommendation && upgradeRecommendation.trim() !== "";
57
56
 
58
- if (!hasUpgradeRecommendation || isExportMode) {
57
+ if (!hasUpgradeRecommendation) {
59
58
  return <>{osName}</>;
60
59
  }
61
60
 
@@ -66,7 +65,7 @@ export const OsNameCell: FC<OsNameCellProps> = ({
66
65
  flexWrap={{ default: "nowrap" }}
67
66
  >
68
67
  <FlexItem>{osName}</FlexItem>
69
- <FlexItem shrink={{ default: "shrink" }}>
68
+ <FlexItem shrink={{ default: "shrink" }} {...chartExportHideProps}>
70
69
  <OsUpgradeRecommendationPopover
71
70
  upgradeRecommendation={upgradeRecommendation}
72
71
  />
@@ -1,4 +1,10 @@
1
- import { Card, CardBody, CardTitle } from "@patternfly/react-core";
1
+ import {
2
+ Card,
3
+ CardBody,
4
+ CardTitle,
5
+ Flex,
6
+ FlexItem,
7
+ } from "@patternfly/react-core";
2
8
  import type { FC, ReactNode } from "react";
3
9
  import {
4
10
  MigrationDonutChart,
@@ -6,6 +12,8 @@ import {
6
12
  type MigrationDonutChartLegendVariant,
7
13
  } from "../charts/MigrationDonutChart.js";
8
14
  import { CardEmptyState } from "./CardEmptyState.js";
15
+ import { ChartHeaderActions } from "./ChartDownloadButton.js";
16
+ import { ChartExportSurface } from "./ChartExportSurface.js";
9
17
  import { dashboardStyles } from "./dashboardStyles.js";
10
18
 
11
19
  export interface PowerStateCardProps {
@@ -17,7 +25,6 @@ export interface PowerStateCardProps {
17
25
  legend: Record<string, string>;
18
26
  total: number;
19
27
  subTitle: string;
20
- isExportMode?: boolean;
21
28
  itemsPerRow?: number;
22
29
  legendVariant?: MigrationDonutChartLegendVariant;
23
30
  }
@@ -31,43 +38,49 @@ export const PowerStateCard: FC<PowerStateCardProps> = ({
31
38
  legend,
32
39
  total,
33
40
  subTitle,
34
- isExportMode = false,
35
41
  itemsPerRow = 2,
36
42
  legendVariant = "html",
37
43
  }) => (
38
- <Card
39
- className={isExportMode ? dashboardStyles.cardPrint : dashboardStyles.card}
40
- id={id}
41
- >
42
- <CardTitle>
43
- {icon} {title}
44
- </CardTitle>
45
- <CardBody>
46
- {total === 0 ? (
47
- <CardEmptyState title={emptyTitle} />
48
- ) : (
49
- <MigrationDonutChart
50
- legendVariant={legendVariant}
51
- data={slices}
52
- legend={legend}
53
- height={300}
54
- width={420}
55
- donutThickness={18}
56
- padAngle={1}
57
- title={`${total}`}
58
- subTitle={subTitle}
59
- subTitleColor="var(--pf-t--global--text--color--subtle)"
60
- titleFontSize={34}
61
- labelFontSize={16}
62
- itemsPerRow={itemsPerRow}
63
- marginLeft="0%"
64
- tooltipLabelFormatter={({ datum, percent }) =>
65
- `${datum.countDisplay}\n${percent.toFixed(1)}%`
66
- }
67
- />
68
- )}
69
- </CardBody>
70
- </Card>
44
+ <ChartExportSurface id={id} title={title}>
45
+ <Card className={dashboardStyles.card}>
46
+ <CardTitle>
47
+ <Flex
48
+ justifyContent={{ default: "justifyContentSpaceBetween" }}
49
+ alignItems={{ default: "alignItemsCenter" }}
50
+ >
51
+ <FlexItem>
52
+ {icon} {title}
53
+ </FlexItem>
54
+ <ChartHeaderActions chartId={id} title={title} />
55
+ </Flex>
56
+ </CardTitle>
57
+ <CardBody>
58
+ {total === 0 ? (
59
+ <CardEmptyState title={emptyTitle} />
60
+ ) : (
61
+ <MigrationDonutChart
62
+ legendVariant={legendVariant}
63
+ data={slices}
64
+ legend={legend}
65
+ height={300}
66
+ width={420}
67
+ donutThickness={18}
68
+ padAngle={1}
69
+ title={`${total}`}
70
+ subTitle={subTitle}
71
+ subTitleColor="var(--pf-t--global--text--color--subtle)"
72
+ titleFontSize={34}
73
+ labelFontSize={16}
74
+ itemsPerRow={itemsPerRow}
75
+ marginLeft="0%"
76
+ tooltipLabelFormatter={({ datum, percent }) =>
77
+ `${datum.countDisplay}\n${percent.toFixed(1)}%`
78
+ }
79
+ />
80
+ )}
81
+ </CardBody>
82
+ </Card>
83
+ </ChartExportSurface>
71
84
  );
72
85
 
73
86
  PowerStateCard.displayName = "PowerStateCard";