@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,224 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import {
3
+ booleanToFeatureStatus,
4
+ buildClusterDetailRows,
5
+ buildClusterDetails,
6
+ buildInfrastructureSummary,
7
+ countVCenters,
8
+ formatVSphereVersion,
9
+ hostCapabilityStatus,
10
+ type InventoryCluster,
11
+ type InventoryHost,
12
+ type InventoryInfra,
13
+ visibleNetworks,
14
+ vsanStatus,
15
+ } from "../infrastructureSummaryModel.js";
16
+
17
+ const createInfra = (
18
+ overrides: Partial<InventoryInfra> = {},
19
+ ): InventoryInfra => ({
20
+ totalHosts: 0,
21
+ networks: [],
22
+ datastores: [],
23
+ ...overrides,
24
+ });
25
+
26
+ describe("formatVSphereVersion", () => {
27
+ it("formats API versions as vSphere labels and drops trailing build zeros", () => {
28
+ expect(formatVSphereVersion("7.0.3.0")).toBe("vSphere 7.0.3");
29
+ expect(formatVSphereVersion("8.0.3.0")).toBe("vSphere 8.0.3");
30
+ });
31
+
32
+ it("keeps an existing vSphere prefix and missing values", () => {
33
+ expect(formatVSphereVersion("vSphere 7.0.3")).toBe("vSphere 7.0.3");
34
+ expect(formatVSphereVersion(undefined)).toBe("—");
35
+ expect(formatVSphereVersion("")).toBe("—");
36
+ });
37
+ });
38
+
39
+ describe("feature status helpers", () => {
40
+ it("maps booleans to enabled, disabled, or unknown", () => {
41
+ expect(booleanToFeatureStatus(true)).toBe("enabled");
42
+ expect(booleanToFeatureStatus(false)).toBe("disabled");
43
+ expect(booleanToFeatureStatus(undefined)).toBe("unknown");
44
+ });
45
+
46
+ it("treats vMotion as enabled when any host supports it", () => {
47
+ const hosts: InventoryHost[] = [
48
+ { vmotionSupported: false },
49
+ { vmotionSupported: true },
50
+ ];
51
+ expect(hostCapabilityStatus(hosts, "vmotionSupported")).toBe("enabled");
52
+ });
53
+
54
+ it("treats vMotion as disabled when every known host reports false", () => {
55
+ const hosts: InventoryHost[] = [
56
+ { vmotionSupported: false },
57
+ { vmotionSupported: false },
58
+ ];
59
+ expect(hostCapabilityStatus(hosts, "vmotionSupported")).toBe("disabled");
60
+ });
61
+
62
+ it("treats missing host capability flags as unknown", () => {
63
+ expect(hostCapabilityStatus([{}], "vmotionSupported")).toBe("unknown");
64
+ expect(hostCapabilityStatus([], "vmotionSupported")).toBe("unknown");
65
+ });
66
+
67
+ it("detects vSAN from datastore type or disk type", () => {
68
+ expect(
69
+ vsanStatus(
70
+ createInfra({
71
+ datastores: [{ type: "vSAN" }],
72
+ }),
73
+ ),
74
+ ).toBe("enabled");
75
+ expect(
76
+ vsanStatus(createInfra(), { total: 10, diskTypes: { vSAN: {} } }),
77
+ ).toBe("enabled");
78
+ expect(vsanStatus(createInfra({ datastores: [] }))).toBe("disabled");
79
+ expect(vsanStatus()).toBe("unknown");
80
+ });
81
+ });
82
+
83
+ describe("buildInfrastructureSummary", () => {
84
+ it("maps inventory metadata into the summary card model", () => {
85
+ const summary = buildInfrastructureSummary({
86
+ infra: createInfra({
87
+ totalHosts: 12,
88
+ totalDatacenters: 1,
89
+ }),
90
+ vcenterVersion: "7.0.3.0",
91
+ vcenterId: "vc-1",
92
+ clusters: {},
93
+ });
94
+
95
+ expect(summary).toEqual({
96
+ vmwareVersion: "vSphere 7.0.3",
97
+ datacenters: 1,
98
+ vCenters: 1,
99
+ esxiHosts: 12,
100
+ });
101
+ });
102
+
103
+ it("falls back to clustersPerDatacenter length and unique vCenter ids", () => {
104
+ const summary = buildInfrastructureSummary({
105
+ infra: createInfra({
106
+ totalHosts: 7,
107
+ clustersPerDatacenter: [1, 1],
108
+ }),
109
+ clusters: {
110
+ A: {
111
+ infra: createInfra(),
112
+ vms: { total: 1 },
113
+ vcenter: { id: "vc-a" },
114
+ },
115
+ B: {
116
+ infra: createInfra(),
117
+ vms: { total: 1 },
118
+ vcenter: { id: "vc-b" },
119
+ },
120
+ },
121
+ });
122
+
123
+ expect(summary.datacenters).toBe(2);
124
+ expect(summary.vCenters).toBe(2);
125
+ });
126
+ });
127
+
128
+ describe("countVCenters", () => {
129
+ it("returns undefined when no vCenter identity is present", () => {
130
+ expect(countVCenters(undefined, undefined)).toBeUndefined();
131
+ });
132
+ });
133
+
134
+ describe("cluster details", () => {
135
+ it("builds sorted aggregate rows from cluster features", () => {
136
+ const clusters: Record<string, InventoryCluster> = {
137
+ "Cluster-Dev-02": {
138
+ infra: createInfra({
139
+ totalHosts: 7,
140
+ hosts: [{ vmotionSupported: true }],
141
+ datastores: [],
142
+ }),
143
+ vms: { total: 350 },
144
+ clusterFeatures: { drsEnabled: false, haEnabled: true },
145
+ },
146
+ "Cluster-Prod-01": {
147
+ infra: createInfra({
148
+ totalHosts: 5,
149
+ hosts: [{ vmotionSupported: true }],
150
+ datastores: [{ type: "vSAN" }],
151
+ }),
152
+ vms: { total: 280 },
153
+ clusterFeatures: { drsEnabled: true, haEnabled: true },
154
+ },
155
+ };
156
+
157
+ const rows = buildClusterDetailRows(clusters);
158
+ expect(rows.map((row) => row.name)).toEqual([
159
+ "Cluster-Dev-02",
160
+ "Cluster-Prod-01",
161
+ ]);
162
+ expect(rows[0]).toMatchObject({
163
+ hosts: 7,
164
+ vms: 350,
165
+ vmotion: "enabled",
166
+ drs: "disabled",
167
+ vsan: "disabled",
168
+ });
169
+ expect(rows[1]).toMatchObject({
170
+ hosts: 5,
171
+ vms: 280,
172
+ drs: "enabled",
173
+ vsan: "enabled",
174
+ });
175
+ });
176
+
177
+ it("builds the detailed cluster model including HA and network labels", () => {
178
+ const details = buildClusterDetails({
179
+ infra: createInfra({
180
+ totalHosts: 7,
181
+ hosts: [{ vmotionSupported: true }],
182
+ networks: [
183
+ { type: "distributed", name: "VDS-Dev", vlanId: "10" },
184
+ { type: "distributed", name: "VDS-Dev", vlanId: "11" },
185
+ { type: "dvswitch", name: "vDSwitch0" },
186
+ ],
187
+ }),
188
+ vms: { total: 350 },
189
+ clusterFeatures: { drsEnabled: false, haEnabled: true },
190
+ });
191
+
192
+ expect(details).toMatchObject({
193
+ hosts: 7,
194
+ vms: 350,
195
+ networksDetected: 2,
196
+ vmotion: "enabled",
197
+ drs: "disabled",
198
+ ha: "enabled",
199
+ vsan: "disabled",
200
+ });
201
+ expect(details?.networks.map((network) => network.displayName)).toEqual([
202
+ "VDS-Dev (VLAN 10)",
203
+ "VDS-Dev (VLAN 11)",
204
+ ]);
205
+ });
206
+ });
207
+
208
+ describe("visibleNetworks", () => {
209
+ it("omits unnamed networks and dvswitches", () => {
210
+ expect(
211
+ visibleNetworks(
212
+ createInfra({
213
+ networks: [
214
+ { type: "distributed", name: " " },
215
+ { type: "dvswitch", name: "switch" },
216
+ { type: "standard", name: "VM Network" },
217
+ ],
218
+ }),
219
+ ),
220
+ ).toEqual([
221
+ { name: "VM Network", vlanId: undefined, displayName: "VM Network" },
222
+ ]);
223
+ });
224
+ });
@@ -0,0 +1,70 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import {
3
+ buildHostPowerStateChart,
4
+ buildVmPowerStateChart,
5
+ } from "../powerStates.js";
6
+
7
+ describe("buildHostPowerStateChart", () => {
8
+ it("maps overallStatus green to powered on and keeps zero categories", () => {
9
+ const chart = buildHostPowerStateChart({ green: 7 });
10
+
11
+ expect(chart.total).toBe(7);
12
+ expect(chart.slices.map((slice) => [slice.name, slice.count])).toEqual([
13
+ ["Powered on", 7],
14
+ ["Powered off", 0],
15
+ ["Standby", 0],
16
+ ["Not responding", 0],
17
+ ["Maintenance", 0],
18
+ ]);
19
+ expect(chart.slices[0].countDisplay).toBe("7 hosts");
20
+ });
21
+
22
+ it("maps explicit host power and maintenance keys", () => {
23
+ const chart = buildHostPowerStateChart({
24
+ poweredOn: 5,
25
+ poweredOff: 1,
26
+ standBy: 1,
27
+ notResponding: 1,
28
+ inMaintenanceMode: 2,
29
+ });
30
+
31
+ expect(chart.total).toBe(10);
32
+ expect(
33
+ chart.slices.find((slice) => slice.name === "Maintenance")?.count,
34
+ ).toBe(2);
35
+ expect(
36
+ chart.slices.find((slice) => slice.name === "Not responding")?.count,
37
+ ).toBe(1);
38
+ });
39
+ });
40
+
41
+ describe("buildVmPowerStateChart", () => {
42
+ it("maps VM power states and omits unused suspended", () => {
43
+ const chart = buildVmPowerStateChart({
44
+ poweredOff: 297,
45
+ poweredOn: 53,
46
+ });
47
+
48
+ expect(chart.total).toBe(350);
49
+ expect(chart.slices.map((slice) => [slice.name, slice.count])).toEqual([
50
+ ["Powered off", 297],
51
+ ["Powered on", 53],
52
+ ]);
53
+ expect(chart.slices[0].countDisplay).toBe("297 VMs");
54
+ });
55
+
56
+ it("includes suspended when present", () => {
57
+ const chart = buildVmPowerStateChart({
58
+ poweredOn: 10,
59
+ poweredOff: 2,
60
+ suspended: 1,
61
+ });
62
+
63
+ expect(chart.slices.map((slice) => slice.name)).toEqual([
64
+ "Powered off",
65
+ "Powered on",
66
+ "Suspended",
67
+ ]);
68
+ expect(chart.slices[2].countDisplay).toBe("1 VM");
69
+ });
70
+ });
@@ -0,0 +1,368 @@
1
+ import html2canvas from "html2canvas-pro";
2
+ import {
3
+ CHART_EXPORT_CAPTURING_ATTR,
4
+ CHART_EXPORT_SCROLL_ATTR,
5
+ shouldIgnoreChartExportElement,
6
+ } from "./chartExport.js";
7
+
8
+ /** CSS-pixel scale; 2x made zip-all of ~15 live cards too slow. */
9
+ const CHART_CAPTURE_SCALE = 1;
10
+ const HIDDEN_LAYOUT_STYLE_KEYS = [
11
+ "display",
12
+ "position",
13
+ "left",
14
+ "top",
15
+ "width",
16
+ "maxHeight",
17
+ "overflow",
18
+ "zIndex",
19
+ ] as const;
20
+
21
+ type HiddenLayoutStyleKey = (typeof HIDDEN_LAYOUT_STYLE_KEYS)[number];
22
+
23
+ type HiddenAncestorSnapshot = {
24
+ node: HTMLElement;
25
+ hidden: HTMLElement["hidden"];
26
+ styles: Record<HiddenLayoutStyleKey, string>;
27
+ };
28
+
29
+ function isDisplayNone(node: HTMLElement): boolean {
30
+ return window.getComputedStyle(node).display === "none";
31
+ }
32
+
33
+ /**
34
+ * Tab panels stay mounted with `hidden` / `display:none` when another tab is
35
+ * active. html2canvas then measures a 0×0 box. Reveal those ancestors off-screen
36
+ * for the duration of the capture, then restore.
37
+ */
38
+ export function revealHiddenChartAncestors(element: HTMLElement): () => void {
39
+ const saved: HiddenAncestorSnapshot[] = [];
40
+ let node: HTMLElement | null = element;
41
+
42
+ while (node && node !== document.documentElement) {
43
+ if (node.hidden || isDisplayNone(node)) {
44
+ saved.push({
45
+ node,
46
+ hidden: node.hidden,
47
+ styles: {
48
+ display: node.style.display,
49
+ position: node.style.position,
50
+ left: node.style.left,
51
+ top: node.style.top,
52
+ width: node.style.width,
53
+ maxHeight: node.style.maxHeight,
54
+ overflow: node.style.overflow,
55
+ zIndex: node.style.zIndex,
56
+ },
57
+ });
58
+ }
59
+ node = node.parentElement;
60
+ }
61
+
62
+ if (saved.length === 0) {
63
+ return () => undefined;
64
+ }
65
+
66
+ const outermost = saved[saved.length - 1].node;
67
+ const parentWidth = outermost.parentElement?.clientWidth ?? 0;
68
+ const layoutWidth =
69
+ parentWidth || document.documentElement.clientWidth || 1024;
70
+
71
+ for (const entry of saved) {
72
+ entry.node.hidden = false;
73
+ entry.node.style.display = "block";
74
+ entry.node.style.position = "fixed";
75
+ entry.node.style.left = "-10000px";
76
+ entry.node.style.top = "0";
77
+ entry.node.style.width = `${layoutWidth}px`;
78
+ entry.node.style.maxHeight = "none";
79
+ entry.node.style.overflow = "visible";
80
+ entry.node.style.zIndex = "-1";
81
+ }
82
+
83
+ return () => {
84
+ for (const entry of saved) {
85
+ entry.node.hidden = entry.hidden;
86
+ for (const key of HIDDEN_LAYOUT_STYLE_KEYS) {
87
+ entry.node.style[key] = entry.styles[key];
88
+ }
89
+ }
90
+ };
91
+ }
92
+
93
+ const PADDED_CAPTURE_SELECTOR =
94
+ ".pf-v6-c-card__title, .pf-v6-c-card__header, .pf-v6-c-card__body";
95
+ const CARD_CAPTURE_SELECTOR = ".pf-v6-c-card";
96
+ const TRANSPARENT_FILLS = new Set(["", "transparent", "rgba(0, 0, 0, 0)"]);
97
+
98
+ function queryCards(root: HTMLElement): HTMLElement[] {
99
+ const cards = Array.from(
100
+ root.querySelectorAll<HTMLElement>(CARD_CAPTURE_SELECTOR),
101
+ );
102
+ if (root.matches(CARD_CAPTURE_SELECTOR)) {
103
+ cards.unshift(root);
104
+ }
105
+ return cards;
106
+ }
107
+
108
+ function resolvedFill(color: string): string {
109
+ return TRANSPARENT_FILLS.has(color) ? "#ffffff" : color;
110
+ }
111
+
112
+ function applyComputedPadding(source: HTMLElement, dest: HTMLElement): void {
113
+ const computed = window.getComputedStyle(source);
114
+ dest.style.paddingTop = computed.paddingTop;
115
+ dest.style.paddingRight = computed.paddingRight;
116
+ dest.style.paddingBottom = computed.paddingBottom;
117
+ dest.style.paddingLeft = computed.paddingLeft;
118
+ }
119
+
120
+ function readCardBorder(source: HTMLElement): {
121
+ width: string;
122
+ style: string;
123
+ color: string;
124
+ } | null {
125
+ if (source.style.borderWidth && source.style.borderStyle) {
126
+ return {
127
+ width: source.style.borderWidth,
128
+ style: source.style.borderStyle,
129
+ color: source.style.borderColor,
130
+ };
131
+ }
132
+ const computed = window.getComputedStyle(source);
133
+ const before = window.getComputedStyle(source, "::before");
134
+ const width =
135
+ before.borderTopWidth && before.borderTopWidth !== "0px"
136
+ ? before.borderTopWidth
137
+ : computed.borderTopWidth;
138
+ const style =
139
+ before.borderTopStyle && before.borderTopStyle !== "none"
140
+ ? before.borderTopStyle
141
+ : computed.borderTopStyle;
142
+ const color =
143
+ before.borderTopColor && before.borderTopColor !== "rgba(0, 0, 0, 0)"
144
+ ? before.borderTopColor
145
+ : computed.borderTopColor;
146
+ if (width === "0px" || style === "none") {
147
+ return null;
148
+ }
149
+ return { width, style, color };
150
+ }
151
+
152
+ function applyCardChrome(source: HTMLElement, dest: HTMLElement): void {
153
+ const computed = window.getComputedStyle(source);
154
+ dest.style.borderRadius = computed.borderRadius;
155
+ dest.style.backgroundColor = resolvedFill(computed.backgroundColor);
156
+ dest.style.overflow = "hidden";
157
+ const border = readCardBorder(source);
158
+ if (border) {
159
+ dest.style.borderWidth = border.width;
160
+ dest.style.borderStyle = border.style;
161
+ dest.style.borderColor = border.color;
162
+ }
163
+ }
164
+
165
+ function hideCardBeforePseudo(): () => void {
166
+ const style = document.createElement("style");
167
+ style.setAttribute("data-chart-export-hide-before", "");
168
+ style.textContent = `
169
+ [${CHART_EXPORT_CAPTURING_ATTR}] .pf-v6-c-card::before,
170
+ [${CHART_EXPORT_CAPTURING_ATTR}].pf-v6-c-card::before {
171
+ border: none !important;
172
+ box-shadow: none !important;
173
+ }
174
+ `;
175
+ document.head.append(style);
176
+ return () => {
177
+ style.remove();
178
+ };
179
+ }
180
+
181
+ function copyComputedPadding(node: HTMLElement): {
182
+ paddingTop: string;
183
+ paddingRight: string;
184
+ paddingBottom: string;
185
+ paddingLeft: string;
186
+ } {
187
+ const previous = {
188
+ paddingTop: node.style.paddingTop,
189
+ paddingRight: node.style.paddingRight,
190
+ paddingBottom: node.style.paddingBottom,
191
+ paddingLeft: node.style.paddingLeft,
192
+ };
193
+ applyComputedPadding(node, node);
194
+ return previous;
195
+ }
196
+
197
+ function copyCardChrome(node: HTMLElement): {
198
+ borderRadius: string;
199
+ backgroundColor: string;
200
+ overflow: string;
201
+ overflowPriority: string;
202
+ borderWidth: string;
203
+ borderStyle: string;
204
+ borderColor: string;
205
+ minHeight: string;
206
+ height: string;
207
+ maxHeight: string;
208
+ } {
209
+ const previous = {
210
+ borderRadius: node.style.borderRadius,
211
+ backgroundColor: node.style.backgroundColor,
212
+ overflow: node.style.getPropertyValue("overflow"),
213
+ overflowPriority: node.style.getPropertyPriority("overflow"),
214
+ borderWidth: node.style.borderWidth,
215
+ borderStyle: node.style.borderStyle,
216
+ borderColor: node.style.borderColor,
217
+ minHeight: node.style.minHeight,
218
+ height: node.style.height,
219
+ maxHeight: node.style.maxHeight,
220
+ };
221
+ applyCardChrome(node, node);
222
+ node.style.minHeight = "0px";
223
+ node.style.height = "auto";
224
+ node.style.maxHeight = "none";
225
+ return previous;
226
+ }
227
+
228
+ function applyComputedStylesToClone(
229
+ sourceRoot: HTMLElement,
230
+ clonedRoot: HTMLElement,
231
+ ): void {
232
+ const sourceCards = queryCards(sourceRoot);
233
+ const cloneCards = queryCards(clonedRoot);
234
+ sourceCards.forEach((source, index) => {
235
+ const dest = cloneCards[index];
236
+ if (dest) {
237
+ applyCardChrome(source, dest);
238
+ }
239
+ });
240
+
241
+ const sources = sourceRoot.querySelectorAll<HTMLElement>(
242
+ PADDED_CAPTURE_SELECTOR,
243
+ );
244
+ const dests = clonedRoot.querySelectorAll<HTMLElement>(
245
+ PADDED_CAPTURE_SELECTOR,
246
+ );
247
+ sources.forEach((source, index) => {
248
+ const dest = dests[index];
249
+ if (dest) {
250
+ applyComputedPadding(source, dest);
251
+ }
252
+ });
253
+ }
254
+
255
+ function prepareCaptureLayout(element: HTMLElement): () => void {
256
+ const expandNodes = [
257
+ element,
258
+ ...Array.from(
259
+ element.querySelectorAll<HTMLElement>(
260
+ `[${CHART_EXPORT_SCROLL_ATTR}], .pf-v6-c-card__body`,
261
+ ),
262
+ ),
263
+ ];
264
+ const cards = queryCards(element);
265
+ const paddedNodes = Array.from(
266
+ element.querySelectorAll<HTMLElement>(PADDED_CAPTURE_SELECTOR),
267
+ );
268
+ const previousExpand = expandNodes.map((node) => ({
269
+ node,
270
+ overflow: node.style.getPropertyValue("overflow"),
271
+ overflowPriority: node.style.getPropertyPriority("overflow"),
272
+ maxHeight: node.style.maxHeight,
273
+ height: node.style.height,
274
+ }));
275
+ const previousChrome = cards.map((node) => ({
276
+ node,
277
+ ...copyCardChrome(node),
278
+ }));
279
+ const previousPadding = paddedNodes.map((node) => ({
280
+ node,
281
+ ...copyComputedPadding(node),
282
+ }));
283
+
284
+ for (const node of expandNodes) {
285
+ node.style.setProperty("overflow", "visible", "important");
286
+ node.style.maxHeight = "none";
287
+ node.style.height = "auto";
288
+ }
289
+
290
+ return () => {
291
+ for (const entry of previousExpand) {
292
+ if (entry.overflow) {
293
+ entry.node.style.setProperty(
294
+ "overflow",
295
+ entry.overflow,
296
+ entry.overflowPriority,
297
+ );
298
+ } else {
299
+ entry.node.style.removeProperty("overflow");
300
+ }
301
+ entry.node.style.maxHeight = entry.maxHeight;
302
+ entry.node.style.height = entry.height;
303
+ }
304
+ for (const entry of previousChrome) {
305
+ entry.node.style.borderRadius = entry.borderRadius;
306
+ entry.node.style.backgroundColor = entry.backgroundColor;
307
+ if (entry.overflow) {
308
+ entry.node.style.setProperty(
309
+ "overflow",
310
+ entry.overflow,
311
+ entry.overflowPriority,
312
+ );
313
+ } else {
314
+ entry.node.style.removeProperty("overflow");
315
+ }
316
+ entry.node.style.borderWidth = entry.borderWidth;
317
+ entry.node.style.borderStyle = entry.borderStyle;
318
+ entry.node.style.borderColor = entry.borderColor;
319
+ entry.node.style.minHeight = entry.minHeight;
320
+ entry.node.style.height = entry.height;
321
+ entry.node.style.maxHeight = entry.maxHeight;
322
+ }
323
+ for (const entry of previousPadding) {
324
+ entry.node.style.paddingTop = entry.paddingTop;
325
+ entry.node.style.paddingRight = entry.paddingRight;
326
+ entry.node.style.paddingBottom = entry.paddingBottom;
327
+ entry.node.style.paddingLeft = entry.paddingLeft;
328
+ }
329
+ };
330
+ }
331
+
332
+ export async function captureChartElement(
333
+ element: HTMLElement,
334
+ ): Promise<HTMLCanvasElement> {
335
+ const restoreHidden = revealHiddenChartAncestors(element);
336
+ const restoreLayout = prepareCaptureLayout(element);
337
+ element.setAttribute(CHART_EXPORT_CAPTURING_ATTR, "");
338
+ const restoreCardBefore = hideCardBeforePseudo();
339
+
340
+ try {
341
+ await new Promise<void>((resolve) => {
342
+ requestAnimationFrame(() => resolve());
343
+ });
344
+ if (element.getBoundingClientRect().width < 1) {
345
+ await new Promise<void>((resolve) => {
346
+ requestAnimationFrame(() => resolve());
347
+ });
348
+ }
349
+ return html2canvas(element, {
350
+ useCORS: true,
351
+ backgroundColor: null,
352
+ logging: false,
353
+ scale: CHART_CAPTURE_SCALE,
354
+ imageTimeout: 0,
355
+ ignoreElements: (node) =>
356
+ node instanceof Element &&
357
+ shouldIgnoreChartExportElement(node, element),
358
+ onclone: (_clonedDoc, clonedElement) => {
359
+ applyComputedStylesToClone(element, clonedElement);
360
+ },
361
+ });
362
+ } finally {
363
+ restoreCardBefore();
364
+ element.removeAttribute(CHART_EXPORT_CAPTURING_ATTR);
365
+ restoreLayout();
366
+ restoreHidden();
367
+ }
368
+ }