@openshift-migration-advisor/shared-components 0.25.0 → 0.27.0-6438cd962b5f

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 (124) 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/ErrorTable.d.ts +18 -0
  12. package/dist/report/ErrorTable.d.ts.map +1 -0
  13. package/dist/report/ErrorTable.js +20 -0
  14. package/dist/report/HostPowerStates.d.ts +0 -1
  15. package/dist/report/HostPowerStates.d.ts.map +1 -1
  16. package/dist/report/HostPowerStates.js +2 -2
  17. package/dist/report/HostsOverview.d.ts +13 -0
  18. package/dist/report/HostsOverview.d.ts.map +1 -0
  19. package/dist/report/HostsOverview.js +84 -0
  20. package/dist/report/InfrastructureSummary.d.ts.map +1 -1
  21. package/dist/report/InfrastructureSummary.js +6 -2
  22. package/dist/report/NetworkOverview.d.ts +31 -0
  23. package/dist/report/NetworkOverview.d.ts.map +1 -0
  24. package/dist/report/NetworkOverview.js +201 -0
  25. package/dist/report/OSDistribution.d.ts +0 -2
  26. package/dist/report/OSDistribution.d.ts.map +1 -1
  27. package/dist/report/OSDistribution.js +8 -5
  28. package/dist/report/OsUpgradeRecommendationPopover.d.ts +0 -1
  29. package/dist/report/OsUpgradeRecommendationPopover.d.ts.map +1 -1
  30. package/dist/report/OsUpgradeRecommendationPopover.js +4 -3
  31. package/dist/report/PowerStateCard.d.ts +0 -1
  32. package/dist/report/PowerStateCard.d.ts.map +1 -1
  33. package/dist/report/PowerStateCard.js +4 -2
  34. package/dist/report/ReportExportMenu.d.ts +19 -0
  35. package/dist/report/ReportExportMenu.d.ts.map +1 -0
  36. package/dist/report/ReportExportMenu.js +31 -0
  37. package/dist/report/ReportTable.d.ts +14 -0
  38. package/dist/report/ReportTable.d.ts.map +1 -0
  39. package/dist/report/ReportTable.js +23 -0
  40. package/dist/report/SupportTierBadge.d.ts +0 -1
  41. package/dist/report/SupportTierBadge.d.ts.map +1 -1
  42. package/dist/report/SupportTierBadge.js +1 -21
  43. package/dist/report/VCenterClusterDetails.d.ts +0 -1
  44. package/dist/report/VCenterClusterDetails.d.ts.map +1 -1
  45. package/dist/report/VCenterClusterDetails.js +9 -4
  46. package/dist/report/VmPowerStates.d.ts +0 -1
  47. package/dist/report/VmPowerStates.d.ts.map +1 -1
  48. package/dist/report/VmPowerStates.js +2 -2
  49. package/dist/report/WarningsTable.d.ts +9 -0
  50. package/dist/report/WarningsTable.d.ts.map +1 -0
  51. package/dist/report/WarningsTable.js +20 -0
  52. package/dist/report/captureChartElement.d.ts +8 -0
  53. package/dist/report/captureChartElement.d.ts.map +1 -0
  54. package/dist/report/captureChartElement.js +284 -0
  55. package/dist/report/chartExport.d.ts +73 -0
  56. package/dist/report/chartExport.d.ts.map +1 -0
  57. package/dist/report/chartExport.js +162 -0
  58. package/dist/report/chartExportContext.d.ts +32 -0
  59. package/dist/report/chartExportContext.d.ts.map +1 -0
  60. package/dist/report/chartExportContext.js +36 -0
  61. package/dist/report/chartExportFilenames.d.ts +4 -0
  62. package/dist/report/chartExportFilenames.d.ts.map +1 -0
  63. package/dist/report/chartExportFilenames.js +16 -0
  64. package/dist/report/constants.d.ts +2 -0
  65. package/dist/report/constants.d.ts.map +1 -1
  66. package/dist/report/constants.js +2 -0
  67. package/dist/report/dashboardStyles.d.ts +0 -4
  68. package/dist/report/dashboardStyles.d.ts.map +1 -1
  69. package/dist/report/dashboardStyles.js +0 -19
  70. package/dist/report/htmlExport.d.ts +9 -0
  71. package/dist/report/htmlExport.d.ts.map +1 -0
  72. package/dist/report/htmlExport.js +65 -0
  73. package/dist/report/index.d.ts +18 -0
  74. package/dist/report/index.d.ts.map +1 -1
  75. package/dist/report/index.js +18 -0
  76. package/dist/report/pdfExport.d.ts +3 -0
  77. package/dist/report/pdfExport.d.ts.map +1 -0
  78. package/dist/report/pdfExport.js +131 -0
  79. package/dist/report/pdfPage.d.ts +15 -0
  80. package/dist/report/pdfPage.d.ts.map +1 -0
  81. package/dist/report/pdfPage.js +44 -0
  82. package/dist/report/reportExportOptions.d.ts +17 -0
  83. package/dist/report/reportExportOptions.d.ts.map +1 -0
  84. package/dist/report/reportExportOptions.js +31 -0
  85. package/dist/report/zipChartPngs.d.ts +3 -0
  86. package/dist/report/zipChartPngs.d.ts.map +1 -0
  87. package/dist/report/zipChartPngs.js +8 -0
  88. package/package.json +7 -2
  89. package/src/report/ChartDownloadButton.tsx +59 -0
  90. package/src/report/ChartExportProvider.tsx +392 -0
  91. package/src/report/ChartExportSurface.tsx +60 -0
  92. package/src/report/ErrorTable.tsx +85 -0
  93. package/src/report/HostPowerStates.tsx +0 -3
  94. package/src/report/HostsOverview.tsx +185 -0
  95. package/src/report/InfrastructureSummary.tsx +48 -31
  96. package/src/report/NetworkOverview.tsx +405 -0
  97. package/src/report/OSDistribution.tsx +46 -42
  98. package/src/report/OsUpgradeRecommendationPopover.tsx +3 -4
  99. package/src/report/PowerStateCard.tsx +49 -36
  100. package/src/report/ReportExportMenu.tsx +119 -0
  101. package/src/report/ReportTable.tsx +81 -0
  102. package/src/report/SupportTierBadge.tsx +12 -40
  103. package/src/report/VCenterClusterDetails.tsx +38 -16
  104. package/src/report/VmPowerStates.tsx +0 -3
  105. package/src/report/WarningsTable.tsx +75 -0
  106. package/src/report/__tests__/ChartExportProvider.test.tsx +542 -0
  107. package/src/report/__tests__/HostsOverview.test.tsx +145 -0
  108. package/src/report/__tests__/InfrastructureSummary.test.tsx +0 -22
  109. package/src/report/__tests__/NetworkOverview.test.tsx +269 -0
  110. package/src/report/__tests__/OSDistribution.test.tsx +10 -11
  111. package/src/report/__tests__/captureChartElement.test.ts +132 -0
  112. package/src/report/__tests__/chartExportPackagers.test.ts +282 -0
  113. package/src/report/captureChartElement.ts +368 -0
  114. package/src/report/chartExport.ts +255 -0
  115. package/src/report/chartExportContext.tsx +92 -0
  116. package/src/report/chartExportFilenames.ts +21 -0
  117. package/src/report/constants.ts +2 -0
  118. package/src/report/dashboardStyles.ts +0 -23
  119. package/src/report/htmlExport.ts +83 -0
  120. package/src/report/index.ts +98 -0
  121. package/src/report/pdfExport.ts +233 -0
  122. package/src/report/pdfPage.ts +83 -0
  123. package/src/report/reportExportOptions.ts +53 -0
  124. package/src/report/zipChartPngs.ts +10 -0
@@ -0,0 +1,282 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import {
3
+ chartExportViewsFromLabels,
4
+ restoreChartExportViews,
5
+ sortRegisteredChartsByDocumentOrder,
6
+ toBulkChartCaptureSources,
7
+ } from "../chartExport.js";
8
+ import { getChartExportFilename } from "../chartExportFilenames.js";
9
+ import { buildHtmlReport, escapeHtml } from "../htmlExport.js";
10
+ import { buildPdfFromCharts } from "../pdfExport.js";
11
+ import {
12
+ fitPdfImageSize,
13
+ placePdfBlock,
14
+ splitSegmentForPageHeight,
15
+ } from "../pdfPage.js";
16
+
17
+ describe("chart export filenames", () => {
18
+ const date = new Date("2026-07-01T15:30:00");
19
+
20
+ it("names pdf, html, and zip downloads with the export date", () => {
21
+ expect(getChartExportFilename("pdf", date)).toBe(
22
+ "migration-export-2026-07-01.pdf",
23
+ );
24
+ expect(getChartExportFilename("html", date)).toBe(
25
+ "migration-export-2026-07-01.html",
26
+ );
27
+ expect(getChartExportFilename("zip", date)).toBe(
28
+ "migration-export-2026-07-01-charts.zip",
29
+ );
30
+ });
31
+ });
32
+
33
+ describe("dropdown chart export views", () => {
34
+ it("builds a titled view list from card dropdown labels", () => {
35
+ expect(
36
+ chartExportViewsFromLabels("VM migration status", {
37
+ issuesVsNoIssues: "No issues vs with issues",
38
+ issuesBreakdown: "With issues breakdown",
39
+ }),
40
+ ).toEqual([
41
+ {
42
+ id: "issuesVsNoIssues",
43
+ title: "VM migration status — No issues vs with issues",
44
+ },
45
+ {
46
+ id: "issuesBreakdown",
47
+ title: "VM migration status — With issues breakdown",
48
+ },
49
+ ]);
50
+ });
51
+
52
+ it("captures every dropdown view then restores the visible one", async () => {
53
+ const element = document.createElement("div");
54
+ const activated: string[] = [];
55
+ const charts = [
56
+ {
57
+ id: "vm",
58
+ title: "Visible",
59
+ filename: "visible.png",
60
+ element,
61
+ exportViews: [
62
+ { id: "a", title: "View A" },
63
+ { id: "b", title: "View B" },
64
+ ],
65
+ activeExportViewId: "a",
66
+ setExportView: async (viewId: string) => {
67
+ activated.push(viewId);
68
+ },
69
+ },
70
+ ];
71
+
72
+ const sources = toBulkChartCaptureSources(charts, async () => {
73
+ const canvas = document.createElement("canvas");
74
+ canvas.width = 1;
75
+ canvas.height = 1;
76
+ return canvas;
77
+ });
78
+
79
+ expect(sources.map((source) => source.title)).toEqual(["View A", "View B"]);
80
+ await sources[0].capture();
81
+ await sources[1].capture();
82
+ await restoreChartExportViews(charts);
83
+
84
+ expect(activated).toEqual(["a", "b", "a"]);
85
+ });
86
+
87
+ it("lays out hidden cards before switching dropdown views", async () => {
88
+ const element = document.createElement("div");
89
+ const order: string[] = [];
90
+ const charts = [
91
+ {
92
+ id: "vm",
93
+ title: "Visible",
94
+ filename: "visible.png",
95
+ element,
96
+ exportViews: [
97
+ { id: "a", title: "View A" },
98
+ { id: "b", title: "View B" },
99
+ ],
100
+ activeExportViewId: "a",
101
+ setExportView: async () => {
102
+ order.push("setView");
103
+ },
104
+ },
105
+ ];
106
+
107
+ const [source] = toBulkChartCaptureSources(
108
+ charts,
109
+ async () => {
110
+ order.push("capture");
111
+ const canvas = document.createElement("canvas");
112
+ canvas.width = 1;
113
+ canvas.height = 1;
114
+ return canvas;
115
+ },
116
+ () => {
117
+ order.push("prepare");
118
+ return () => {
119
+ order.push("restore");
120
+ };
121
+ },
122
+ );
123
+
124
+ await source.capture();
125
+ expect(order).toEqual(["prepare", "setView", "capture", "restore"]);
126
+ });
127
+ });
128
+
129
+ describe("sortRegisteredChartsByDocumentOrder", () => {
130
+ it("orders registered nodes by their position in the document", () => {
131
+ const root = document.createElement("div");
132
+ const first = document.createElement("div");
133
+ const second = document.createElement("div");
134
+ root.append(first, second);
135
+ document.body.append(root);
136
+
137
+ const sorted = sortRegisteredChartsByDocumentOrder([
138
+ {
139
+ id: "second",
140
+ title: "Second",
141
+ filename: "second.png",
142
+ element: second,
143
+ },
144
+ {
145
+ id: "first",
146
+ title: "First",
147
+ filename: "first.png",
148
+ element: first,
149
+ },
150
+ ]);
151
+
152
+ expect(sorted.map((chart) => chart.id)).toEqual(["first", "second"]);
153
+ root.remove();
154
+ });
155
+ });
156
+
157
+ describe("splitSegmentForPageHeight", () => {
158
+ it("splits a tall chart across page height", () => {
159
+ expect(
160
+ splitSegmentForPageHeight({ top: 0, height: 250 }, 250, 100),
161
+ ).toEqual([
162
+ { top: 0, height: 100 },
163
+ { top: 100, height: 100 },
164
+ { top: 200, height: 50 },
165
+ ]);
166
+ });
167
+ });
168
+
169
+ describe("fitPdfImageSize", () => {
170
+ it("keeps full content width when the chart fits the page", () => {
171
+ expect(fitPdfImageSize(800, 200, 190, 277)).toEqual({
172
+ widthMm: 190,
173
+ heightMm: 47.5,
174
+ });
175
+ });
176
+
177
+ it("scales down a chart taller than the page", () => {
178
+ expect(fitPdfImageSize(1000, 2000, 200, 100)).toEqual({
179
+ widthMm: 50,
180
+ heightMm: 100,
181
+ });
182
+ });
183
+ });
184
+
185
+ describe("placePdfBlock", () => {
186
+ it("stacks charts on the same page while they fit", () => {
187
+ expect(placePdfBlock(null, 50, 10, 287)).toEqual({
188
+ y: 10,
189
+ needsNewPage: true,
190
+ });
191
+ expect(placePdfBlock(68, 50, 10, 287)).toEqual({
192
+ y: 68,
193
+ needsNewPage: false,
194
+ });
195
+ expect(placePdfBlock(250, 50, 10, 287)).toEqual({
196
+ y: 10,
197
+ needsNewPage: true,
198
+ });
199
+ });
200
+ });
201
+
202
+ describe("buildPdfFromCharts with extra text pages", () => {
203
+ it("appends a text page even when there are no chart captures", async () => {
204
+ const blob = await buildPdfFromCharts([], "Report", [
205
+ {
206
+ title: "Cluster sizing recommendations",
207
+ items: [
208
+ { label: "Cluster name", value: "cluster-1" },
209
+ { label: "Target platform", value: "Bare metal" },
210
+ {
211
+ label: "Total nodes",
212
+ value: "6 (3 workers + 3 control plane)",
213
+ },
214
+ { label: "Failover capacity", value: "1 failover nodes" },
215
+ { label: "Worker node size", value: "8 CPU, 32 GB memory" },
216
+ {
217
+ label: "Control plane node size",
218
+ value: "16 CPU, 64 GB memory",
219
+ },
220
+ ],
221
+ footer: "Note: estimates only.",
222
+ },
223
+ ]);
224
+
225
+ expect(blob).toBeInstanceOf(Blob);
226
+ expect(blob.size).toBeGreaterThan(0);
227
+ expect(blob.type).toBe("application/pdf");
228
+ });
229
+
230
+ it("paginates extra pages with many rows without throwing", async () => {
231
+ const manyItems = Array.from({ length: 60 }, (_, index) => ({
232
+ label: `Field ${index}`,
233
+ value: `Value ${index}`,
234
+ }));
235
+
236
+ const blob = await buildPdfFromCharts([], "Report", [
237
+ { title: "Cluster sizing recommendations", items: manyItems },
238
+ ]);
239
+
240
+ expect(blob.size).toBeGreaterThan(0);
241
+ });
242
+
243
+ it("supports multiple extra pages (e.g. one per sized cluster)", async () => {
244
+ const blob = await buildPdfFromCharts([], "Report", [
245
+ {
246
+ title: "Cluster sizing recommendations — cluster-a",
247
+ items: [{ label: "Cluster name", value: "cluster-a" }],
248
+ },
249
+ {
250
+ title: "Cluster sizing recommendations — cluster-b",
251
+ items: [{ label: "Cluster name", value: "cluster-b" }],
252
+ },
253
+ ]);
254
+
255
+ expect(blob.size).toBeGreaterThan(0);
256
+ });
257
+ });
258
+
259
+ describe("buildHtmlReport", () => {
260
+ it("embeds captured chart images and escapes the title", () => {
261
+ const html = buildHtmlReport(
262
+ 'Report <script>alert("x")</script>',
263
+ [
264
+ {
265
+ name: "Infrastructure summary",
266
+ dataUrl: "data:image/png;base64,abc",
267
+ },
268
+ ],
269
+ new Date("2026-07-01T15:30:00"),
270
+ );
271
+
272
+ expect(html).toContain(
273
+ "Report &lt;script&gt;alert(&quot;x&quot;)&lt;/script&gt;",
274
+ );
275
+ expect(html).toContain('src="data:image/png;base64,abc"');
276
+ expect(html).toContain("Infrastructure summary");
277
+ expect(html).not.toContain("border:");
278
+ expect(html).toContain("width: 100%");
279
+ expect(escapeHtml("<b>")).toBe("&lt;b&gt;");
280
+ expect(escapeHtml("it's")).toBe("it&#39;s");
281
+ });
282
+ });
@@ -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
+ }