@openshift-migration-advisor/shared-components 0.24.0 → 0.25.0-6a4104ca5ed2
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.
- package/README.md +31 -0
- package/dist/charts/index.d.ts +1 -1
- package/dist/charts/index.d.ts.map +1 -1
- package/dist/forms/RadioButtonFormGroup.d.ts.map +1 -1
- package/dist/forms/RadioButtonFormGroup.js +2 -2
- package/dist/report/ChartDownloadButton.d.ts +11 -0
- package/dist/report/ChartDownloadButton.d.ts.map +1 -0
- package/dist/report/ChartDownloadButton.js +24 -0
- package/dist/report/ChartExportProvider.d.ts +22 -0
- package/dist/report/ChartExportProvider.d.ts.map +1 -0
- package/dist/report/ChartExportProvider.js +222 -0
- package/dist/report/ChartExportSurface.d.ts +12 -0
- package/dist/report/ChartExportSurface.d.ts.map +1 -0
- package/dist/report/ChartExportSurface.js +32 -0
- package/dist/report/FeatureStatusBadge.d.ts +7 -0
- package/dist/report/FeatureStatusBadge.d.ts.map +1 -0
- package/dist/report/FeatureStatusBadge.js +13 -0
- package/dist/report/HostPowerStates.d.ts +8 -0
- package/dist/report/HostPowerStates.d.ts.map +1 -0
- package/dist/report/HostPowerStates.js +11 -0
- package/dist/report/InfrastructureSummary.d.ts +7 -0
- package/dist/report/InfrastructureSummary.d.ts.map +1 -0
- package/dist/report/InfrastructureSummary.js +9 -0
- package/dist/report/OSDistribution.d.ts +0 -2
- package/dist/report/OSDistribution.d.ts.map +1 -1
- package/dist/report/OSDistribution.js +10 -15
- package/dist/report/OsSupportTiersHelpPopover.d.ts.map +1 -1
- package/dist/report/OsSupportTiersHelpPopover.js +2 -31
- package/dist/report/OsUpgradeNotice.d.ts.map +1 -1
- package/dist/report/OsUpgradeNotice.js +1 -5
- package/dist/report/OsUpgradeRecommendationPopover.d.ts +0 -1
- package/dist/report/OsUpgradeRecommendationPopover.d.ts.map +1 -1
- package/dist/report/OsUpgradeRecommendationPopover.js +4 -3
- package/dist/report/PowerStateCard.d.ts +16 -0
- package/dist/report/PowerStateCard.d.ts.map +1 -0
- package/dist/report/PowerStateCard.js +9 -0
- package/dist/report/ReportExportMenu.d.ts +19 -0
- package/dist/report/ReportExportMenu.d.ts.map +1 -0
- package/dist/report/ReportExportMenu.js +31 -0
- package/dist/report/SupportTierBadge.d.ts +0 -1
- package/dist/report/SupportTierBadge.d.ts.map +1 -1
- package/dist/report/SupportTierBadge.js +1 -19
- package/dist/report/VCenterClusterDetails.d.ts +9 -0
- package/dist/report/VCenterClusterDetails.d.ts.map +1 -0
- package/dist/report/VCenterClusterDetails.js +31 -0
- package/dist/report/VmPowerStates.d.ts +8 -0
- package/dist/report/VmPowerStates.d.ts.map +1 -0
- package/dist/report/VmPowerStates.js +11 -0
- package/dist/report/captureChartElement.d.ts +8 -0
- package/dist/report/captureChartElement.d.ts.map +1 -0
- package/dist/report/captureChartElement.js +264 -0
- package/dist/report/chartExport.d.ts +57 -0
- package/dist/report/chartExport.d.ts.map +1 -0
- package/dist/report/chartExport.js +159 -0
- package/dist/report/chartExportContext.d.ts +28 -0
- package/dist/report/chartExportContext.d.ts.map +1 -0
- package/dist/report/chartExportContext.js +36 -0
- package/dist/report/chartExportFilenames.d.ts +4 -0
- package/dist/report/chartExportFilenames.d.ts.map +1 -0
- package/dist/report/chartExportFilenames.js +16 -0
- package/dist/report/constants.d.ts +2 -0
- package/dist/report/constants.d.ts.map +1 -1
- package/dist/report/constants.js +2 -0
- package/dist/report/dashboardStyles.d.ts +0 -4
- package/dist/report/dashboardStyles.d.ts.map +1 -1
- package/dist/report/dashboardStyles.js +0 -19
- package/dist/report/htmlExport.d.ts +9 -0
- package/dist/report/htmlExport.d.ts.map +1 -0
- package/dist/report/htmlExport.js +67 -0
- package/dist/report/index.d.ts +23 -0
- package/dist/report/index.d.ts.map +1 -1
- package/dist/report/index.js +21 -0
- package/dist/report/infrastructureSummaryModel.d.ts +82 -0
- package/dist/report/infrastructureSummaryModel.d.ts.map +1 -0
- package/dist/report/infrastructureSummaryModel.js +139 -0
- package/dist/report/pdfExport.d.ts +3 -0
- package/dist/report/pdfExport.d.ts.map +1 -0
- package/dist/report/pdfExport.js +87 -0
- package/dist/report/pdfPage.d.ts +7 -0
- package/dist/report/pdfPage.d.ts.map +1 -0
- package/dist/report/pdfPage.js +27 -0
- package/dist/report/powerStates.d.ts +11 -0
- package/dist/report/powerStates.d.ts.map +1 -0
- package/dist/report/powerStates.js +113 -0
- package/dist/report/reportExportOptions.d.ts +17 -0
- package/dist/report/reportExportOptions.d.ts.map +1 -0
- package/dist/report/reportExportOptions.js +31 -0
- package/dist/report/zipChartPngs.d.ts +3 -0
- package/dist/report/zipChartPngs.d.ts.map +1 -0
- package/dist/report/zipChartPngs.js +8 -0
- package/package.json +7 -2
- package/src/charts/index.ts +1 -0
- package/src/forms/RadioButtonFormGroup.tsx +10 -3
- package/src/report/ChartDownloadButton.tsx +59 -0
- package/src/report/ChartExportProvider.tsx +384 -0
- package/src/report/ChartExportSurface.tsx +60 -0
- package/src/report/FeatureStatusBadge.tsx +30 -0
- package/src/report/HostPowerStates.tsx +38 -0
- package/src/report/InfrastructureSummary.tsx +73 -0
- package/src/report/OSDistribution.tsx +96 -99
- package/src/report/OsSupportTiersHelpPopover.tsx +32 -62
- package/src/report/OsUpgradeNotice.tsx +1 -7
- package/src/report/OsUpgradeRecommendationPopover.tsx +3 -4
- package/src/report/PowerStateCard.tsx +86 -0
- package/src/report/ReportExportMenu.tsx +119 -0
- package/src/report/SupportTierBadge.tsx +12 -38
- package/src/report/VCenterClusterDetails.tsx +233 -0
- package/src/report/VmPowerStates.tsx +38 -0
- package/src/report/__tests__/ChartExportProvider.test.tsx +474 -0
- package/src/report/__tests__/HostPowerStates.test.tsx +47 -0
- package/src/report/__tests__/InfrastructureSummary.test.tsx +126 -0
- package/src/report/__tests__/OSDistribution.test.tsx +10 -11
- package/src/report/__tests__/captureChartElement.test.ts +132 -0
- package/src/report/__tests__/chartExportPackagers.test.ts +185 -0
- package/src/report/__tests__/infrastructureSummaryModel.test.ts +224 -0
- package/src/report/__tests__/powerStates.test.ts +70 -0
- package/src/report/captureChartElement.ts +351 -0
- package/src/report/chartExport.ts +234 -0
- package/src/report/chartExportContext.tsx +84 -0
- package/src/report/chartExportFilenames.ts +21 -0
- package/src/report/constants.ts +2 -0
- package/src/report/dashboardStyles.ts +0 -23
- package/src/report/htmlExport.ts +85 -0
- package/src/report/index.ts +128 -0
- package/src/report/infrastructureSummaryModel.ts +281 -0
- package/src/report/pdfExport.ts +161 -0
- package/src/report/pdfPage.ts +54 -0
- package/src/report/powerStates.ts +169 -0
- package/src/report/reportExportOptions.ts +53 -0
- package/src/report/zipChartPngs.ts +10 -0
|
@@ -0,0 +1,351 @@
|
|
|
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
|
+
const CARD_BORDER_WIDTH_VAR = "--pf-v6-c-card--BorderWidth";
|
|
121
|
+
|
|
122
|
+
function applyCardChrome(source: HTMLElement, dest: HTMLElement): void {
|
|
123
|
+
const computed = window.getComputedStyle(source);
|
|
124
|
+
const before = window.getComputedStyle(source, "::before");
|
|
125
|
+
dest.style.borderRadius = computed.borderRadius;
|
|
126
|
+
dest.style.backgroundColor = resolvedFill(computed.backgroundColor);
|
|
127
|
+
dest.style.overflow = "hidden";
|
|
128
|
+
|
|
129
|
+
const borderWidth =
|
|
130
|
+
before.borderTopWidth && before.borderTopWidth !== "0px"
|
|
131
|
+
? before.borderTopWidth
|
|
132
|
+
: computed.borderTopWidth;
|
|
133
|
+
const borderStyle =
|
|
134
|
+
before.borderTopStyle && before.borderTopStyle !== "none"
|
|
135
|
+
? before.borderTopStyle
|
|
136
|
+
: computed.borderTopStyle;
|
|
137
|
+
const borderColor =
|
|
138
|
+
before.borderTopColor && before.borderTopColor !== "rgba(0, 0, 0, 0)"
|
|
139
|
+
? before.borderTopColor
|
|
140
|
+
: computed.borderTopColor;
|
|
141
|
+
if (borderWidth !== "0px" && borderStyle !== "none") {
|
|
142
|
+
dest.style.borderWidth = borderWidth;
|
|
143
|
+
dest.style.borderStyle = borderStyle;
|
|
144
|
+
dest.style.borderColor = borderColor;
|
|
145
|
+
}
|
|
146
|
+
// PF draws the border on ::before via this token. html2canvas still paints
|
|
147
|
+
// that pseudo even if a stylesheet sets content:none, so collapse it here.
|
|
148
|
+
dest.style.setProperty(CARD_BORDER_WIDTH_VAR, "0px");
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function copyComputedPadding(node: HTMLElement): {
|
|
152
|
+
paddingTop: string;
|
|
153
|
+
paddingRight: string;
|
|
154
|
+
paddingBottom: string;
|
|
155
|
+
paddingLeft: string;
|
|
156
|
+
} {
|
|
157
|
+
const previous = {
|
|
158
|
+
paddingTop: node.style.paddingTop,
|
|
159
|
+
paddingRight: node.style.paddingRight,
|
|
160
|
+
paddingBottom: node.style.paddingBottom,
|
|
161
|
+
paddingLeft: node.style.paddingLeft,
|
|
162
|
+
};
|
|
163
|
+
applyComputedPadding(node, node);
|
|
164
|
+
return previous;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function copyCardChrome(node: HTMLElement): {
|
|
168
|
+
borderRadius: string;
|
|
169
|
+
backgroundColor: string;
|
|
170
|
+
overflow: string;
|
|
171
|
+
overflowPriority: string;
|
|
172
|
+
borderWidth: string;
|
|
173
|
+
borderStyle: string;
|
|
174
|
+
borderColor: string;
|
|
175
|
+
minHeight: string;
|
|
176
|
+
height: string;
|
|
177
|
+
maxHeight: string;
|
|
178
|
+
borderWidthVar: string;
|
|
179
|
+
borderWidthVarPriority: string;
|
|
180
|
+
} {
|
|
181
|
+
const previous = {
|
|
182
|
+
borderRadius: node.style.borderRadius,
|
|
183
|
+
backgroundColor: node.style.backgroundColor,
|
|
184
|
+
overflow: node.style.getPropertyValue("overflow"),
|
|
185
|
+
overflowPriority: node.style.getPropertyPriority("overflow"),
|
|
186
|
+
borderWidth: node.style.borderWidth,
|
|
187
|
+
borderStyle: node.style.borderStyle,
|
|
188
|
+
borderColor: node.style.borderColor,
|
|
189
|
+
minHeight: node.style.minHeight,
|
|
190
|
+
height: node.style.height,
|
|
191
|
+
maxHeight: node.style.maxHeight,
|
|
192
|
+
borderWidthVar: node.style.getPropertyValue(CARD_BORDER_WIDTH_VAR),
|
|
193
|
+
borderWidthVarPriority: node.style.getPropertyPriority(
|
|
194
|
+
CARD_BORDER_WIDTH_VAR,
|
|
195
|
+
),
|
|
196
|
+
};
|
|
197
|
+
applyCardChrome(node, node);
|
|
198
|
+
node.style.minHeight = "0px";
|
|
199
|
+
node.style.height = "auto";
|
|
200
|
+
node.style.maxHeight = "none";
|
|
201
|
+
return previous;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function applyComputedStylesToClone(
|
|
205
|
+
sourceRoot: HTMLElement,
|
|
206
|
+
clonedRoot: HTMLElement,
|
|
207
|
+
): void {
|
|
208
|
+
const sourceCards = queryCards(sourceRoot);
|
|
209
|
+
const cloneCards = queryCards(clonedRoot);
|
|
210
|
+
sourceCards.forEach((source, index) => {
|
|
211
|
+
const dest = cloneCards[index];
|
|
212
|
+
if (dest) {
|
|
213
|
+
applyCardChrome(source, dest);
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
const sources = sourceRoot.querySelectorAll<HTMLElement>(
|
|
218
|
+
PADDED_CAPTURE_SELECTOR,
|
|
219
|
+
);
|
|
220
|
+
const dests = clonedRoot.querySelectorAll<HTMLElement>(
|
|
221
|
+
PADDED_CAPTURE_SELECTOR,
|
|
222
|
+
);
|
|
223
|
+
sources.forEach((source, index) => {
|
|
224
|
+
const dest = dests[index];
|
|
225
|
+
if (dest) {
|
|
226
|
+
applyComputedPadding(source, dest);
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function prepareCaptureLayout(element: HTMLElement): () => void {
|
|
232
|
+
const expandNodes = [
|
|
233
|
+
element,
|
|
234
|
+
...Array.from(
|
|
235
|
+
element.querySelectorAll<HTMLElement>(
|
|
236
|
+
`[${CHART_EXPORT_SCROLL_ATTR}], .pf-v6-c-card__body`,
|
|
237
|
+
),
|
|
238
|
+
),
|
|
239
|
+
];
|
|
240
|
+
const cards = queryCards(element);
|
|
241
|
+
const paddedNodes = Array.from(
|
|
242
|
+
element.querySelectorAll<HTMLElement>(PADDED_CAPTURE_SELECTOR),
|
|
243
|
+
);
|
|
244
|
+
const previousExpand = expandNodes.map((node) => ({
|
|
245
|
+
node,
|
|
246
|
+
overflow: node.style.getPropertyValue("overflow"),
|
|
247
|
+
overflowPriority: node.style.getPropertyPriority("overflow"),
|
|
248
|
+
maxHeight: node.style.maxHeight,
|
|
249
|
+
height: node.style.height,
|
|
250
|
+
}));
|
|
251
|
+
const previousChrome = cards.map((node) => ({
|
|
252
|
+
node,
|
|
253
|
+
...copyCardChrome(node),
|
|
254
|
+
}));
|
|
255
|
+
const previousPadding = paddedNodes.map((node) => ({
|
|
256
|
+
node,
|
|
257
|
+
...copyComputedPadding(node),
|
|
258
|
+
}));
|
|
259
|
+
|
|
260
|
+
for (const node of expandNodes) {
|
|
261
|
+
node.style.setProperty("overflow", "visible", "important");
|
|
262
|
+
node.style.maxHeight = "none";
|
|
263
|
+
node.style.height = "auto";
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
return () => {
|
|
267
|
+
for (const entry of previousExpand) {
|
|
268
|
+
if (entry.overflow) {
|
|
269
|
+
entry.node.style.setProperty(
|
|
270
|
+
"overflow",
|
|
271
|
+
entry.overflow,
|
|
272
|
+
entry.overflowPriority,
|
|
273
|
+
);
|
|
274
|
+
} else {
|
|
275
|
+
entry.node.style.removeProperty("overflow");
|
|
276
|
+
}
|
|
277
|
+
entry.node.style.maxHeight = entry.maxHeight;
|
|
278
|
+
entry.node.style.height = entry.height;
|
|
279
|
+
}
|
|
280
|
+
for (const entry of previousChrome) {
|
|
281
|
+
entry.node.style.borderRadius = entry.borderRadius;
|
|
282
|
+
entry.node.style.backgroundColor = entry.backgroundColor;
|
|
283
|
+
if (entry.overflow) {
|
|
284
|
+
entry.node.style.setProperty(
|
|
285
|
+
"overflow",
|
|
286
|
+
entry.overflow,
|
|
287
|
+
entry.overflowPriority,
|
|
288
|
+
);
|
|
289
|
+
} else {
|
|
290
|
+
entry.node.style.removeProperty("overflow");
|
|
291
|
+
}
|
|
292
|
+
entry.node.style.borderWidth = entry.borderWidth;
|
|
293
|
+
entry.node.style.borderStyle = entry.borderStyle;
|
|
294
|
+
entry.node.style.borderColor = entry.borderColor;
|
|
295
|
+
entry.node.style.minHeight = entry.minHeight;
|
|
296
|
+
entry.node.style.height = entry.height;
|
|
297
|
+
entry.node.style.maxHeight = entry.maxHeight;
|
|
298
|
+
if (entry.borderWidthVar) {
|
|
299
|
+
entry.node.style.setProperty(
|
|
300
|
+
CARD_BORDER_WIDTH_VAR,
|
|
301
|
+
entry.borderWidthVar,
|
|
302
|
+
entry.borderWidthVarPriority,
|
|
303
|
+
);
|
|
304
|
+
} else {
|
|
305
|
+
entry.node.style.removeProperty(CARD_BORDER_WIDTH_VAR);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
for (const entry of previousPadding) {
|
|
309
|
+
entry.node.style.paddingTop = entry.paddingTop;
|
|
310
|
+
entry.node.style.paddingRight = entry.paddingRight;
|
|
311
|
+
entry.node.style.paddingBottom = entry.paddingBottom;
|
|
312
|
+
entry.node.style.paddingLeft = entry.paddingLeft;
|
|
313
|
+
}
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
export async function captureChartElement(
|
|
318
|
+
element: HTMLElement,
|
|
319
|
+
): Promise<HTMLCanvasElement> {
|
|
320
|
+
const restoreHidden = revealHiddenChartAncestors(element);
|
|
321
|
+
const restoreLayout = prepareCaptureLayout(element);
|
|
322
|
+
element.setAttribute(CHART_EXPORT_CAPTURING_ATTR, "");
|
|
323
|
+
|
|
324
|
+
try {
|
|
325
|
+
await new Promise<void>((resolve) => {
|
|
326
|
+
requestAnimationFrame(() => resolve());
|
|
327
|
+
});
|
|
328
|
+
if (element.getBoundingClientRect().width < 1) {
|
|
329
|
+
await new Promise<void>((resolve) => {
|
|
330
|
+
requestAnimationFrame(() => resolve());
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
return html2canvas(element, {
|
|
334
|
+
useCORS: true,
|
|
335
|
+
backgroundColor: null,
|
|
336
|
+
logging: false,
|
|
337
|
+
scale: CHART_CAPTURE_SCALE,
|
|
338
|
+
imageTimeout: 0,
|
|
339
|
+
ignoreElements: (node) =>
|
|
340
|
+
node instanceof Element &&
|
|
341
|
+
shouldIgnoreChartExportElement(node, element),
|
|
342
|
+
onclone: (_clonedDoc, clonedElement) => {
|
|
343
|
+
applyComputedStylesToClone(element, clonedElement);
|
|
344
|
+
},
|
|
345
|
+
});
|
|
346
|
+
} finally {
|
|
347
|
+
element.removeAttribute(CHART_EXPORT_CAPTURING_ATTR);
|
|
348
|
+
restoreLayout();
|
|
349
|
+
restoreHidden();
|
|
350
|
+
}
|
|
351
|
+
}
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import type { CSSProperties } from "react";
|
|
2
|
+
|
|
3
|
+
export const CHART_EXPORT_HIDE_ATTR = "data-chart-export-hide";
|
|
4
|
+
export const CHART_EXPORT_SCROLL_ATTR = "data-chart-export-scroll";
|
|
5
|
+
export const CHART_EXPORT_CAPTURING_ATTR = "data-chart-capturing";
|
|
6
|
+
|
|
7
|
+
export const chartExportHideProps = {
|
|
8
|
+
[CHART_EXPORT_HIDE_ATTR]: "",
|
|
9
|
+
} as const;
|
|
10
|
+
|
|
11
|
+
export const chartExportScrollProps = {
|
|
12
|
+
[CHART_EXPORT_SCROLL_ATTR]: "",
|
|
13
|
+
} as const;
|
|
14
|
+
|
|
15
|
+
export const chartExportRootStyle: CSSProperties = { height: "100%" };
|
|
16
|
+
|
|
17
|
+
export type ChartExportMeta = {
|
|
18
|
+
id: string;
|
|
19
|
+
title: string;
|
|
20
|
+
filename?: string;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export type ChartExportView = {
|
|
24
|
+
id: string;
|
|
25
|
+
title: string;
|
|
26
|
+
filename?: string;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export type RegisteredChart = {
|
|
30
|
+
id: string;
|
|
31
|
+
title: string;
|
|
32
|
+
filename: string;
|
|
33
|
+
element: HTMLElement;
|
|
34
|
+
exportViews?: ChartExportView[];
|
|
35
|
+
activeExportViewId?: string;
|
|
36
|
+
setExportView?: (viewId: string) => Promise<void>;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export type ChartExportFile = {
|
|
40
|
+
filename: string;
|
|
41
|
+
blob: Blob;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export type ChartCaptureSource = {
|
|
45
|
+
id: string;
|
|
46
|
+
title: string;
|
|
47
|
+
filename: string;
|
|
48
|
+
capture: () => Promise<HTMLCanvasElement>;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export function releaseCanvas(canvas: HTMLCanvasElement): void {
|
|
52
|
+
canvas.width = 0;
|
|
53
|
+
canvas.height = 0;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function waitForChartExportPaint(): Promise<void> {
|
|
57
|
+
if (typeof requestAnimationFrame !== "function") {
|
|
58
|
+
return Promise.resolve();
|
|
59
|
+
}
|
|
60
|
+
return new Promise((resolve) => {
|
|
61
|
+
requestAnimationFrame(() => {
|
|
62
|
+
requestAnimationFrame(() => resolve());
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function chartExportViewsFromLabels(
|
|
68
|
+
titlePrefix: string,
|
|
69
|
+
labels: Record<string, string>,
|
|
70
|
+
): ChartExportView[] {
|
|
71
|
+
return Object.entries(labels).map(([id, label]) => ({
|
|
72
|
+
id,
|
|
73
|
+
title: `${titlePrefix} — ${label}`,
|
|
74
|
+
}));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function shouldCycleExportViews(chart: RegisteredChart): boolean {
|
|
78
|
+
return Boolean(
|
|
79
|
+
chart.exportViews && chart.exportViews.length > 1 && chart.setExportView,
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function bulkExportViews(chart: RegisteredChart): ChartExportView[] {
|
|
84
|
+
if (shouldCycleExportViews(chart) && chart.exportViews) {
|
|
85
|
+
return chart.exportViews;
|
|
86
|
+
}
|
|
87
|
+
return [
|
|
88
|
+
{
|
|
89
|
+
id: chart.id,
|
|
90
|
+
title: chart.title,
|
|
91
|
+
filename: chart.filename,
|
|
92
|
+
},
|
|
93
|
+
];
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function toBulkChartCaptureSources(
|
|
97
|
+
charts: RegisteredChart[],
|
|
98
|
+
capture: (element: HTMLElement) => Promise<HTMLCanvasElement>,
|
|
99
|
+
prepareElement?: (element: HTMLElement) => () => void,
|
|
100
|
+
): ChartCaptureSource[] {
|
|
101
|
+
return charts.flatMap((chart) => {
|
|
102
|
+
const views = bulkExportViews(chart);
|
|
103
|
+
const cyclesViews = shouldCycleExportViews(chart);
|
|
104
|
+
return views.map((view) => ({
|
|
105
|
+
id: cyclesViews ? `${chart.id}--${view.id}` : chart.id,
|
|
106
|
+
title: view.title,
|
|
107
|
+
filename:
|
|
108
|
+
view.filename ??
|
|
109
|
+
(cyclesViews
|
|
110
|
+
? chartPngFilename(view.title, `${chart.id}-${view.id}`)
|
|
111
|
+
: chart.filename),
|
|
112
|
+
capture: async () => {
|
|
113
|
+
const restore = prepareElement?.(chart.element);
|
|
114
|
+
try {
|
|
115
|
+
if (cyclesViews && chart.setExportView) {
|
|
116
|
+
await chart.setExportView(view.id);
|
|
117
|
+
}
|
|
118
|
+
return await capture(chart.element);
|
|
119
|
+
} finally {
|
|
120
|
+
restore?.();
|
|
121
|
+
}
|
|
122
|
+
},
|
|
123
|
+
}));
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export async function restoreChartExportViews(
|
|
128
|
+
charts: RegisteredChart[],
|
|
129
|
+
): Promise<void> {
|
|
130
|
+
for (const chart of charts) {
|
|
131
|
+
if (
|
|
132
|
+
shouldCycleExportViews(chart) &&
|
|
133
|
+
chart.setExportView &&
|
|
134
|
+
chart.activeExportViewId
|
|
135
|
+
) {
|
|
136
|
+
try {
|
|
137
|
+
await chart.setExportView(chart.activeExportViewId);
|
|
138
|
+
} catch (error) {
|
|
139
|
+
console.error("Failed to restore the selected chart view:", error);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function sortRegisteredChartsByDocumentOrder(
|
|
146
|
+
charts: RegisteredChart[],
|
|
147
|
+
): RegisteredChart[] {
|
|
148
|
+
return [...charts].sort((left, right) => {
|
|
149
|
+
if (left.element === right.element) {
|
|
150
|
+
return 0;
|
|
151
|
+
}
|
|
152
|
+
const position = left.element.compareDocumentPosition(right.element);
|
|
153
|
+
if (position & Node.DOCUMENT_POSITION_FOLLOWING) {
|
|
154
|
+
return -1;
|
|
155
|
+
}
|
|
156
|
+
if (position & Node.DOCUMENT_POSITION_PRECEDING) {
|
|
157
|
+
return 1;
|
|
158
|
+
}
|
|
159
|
+
return 0;
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function canvasToBlob(canvas: HTMLCanvasElement): Promise<Blob> {
|
|
164
|
+
return new Promise((resolve, reject) => {
|
|
165
|
+
if (canvas.width < 1 || canvas.height < 1) {
|
|
166
|
+
reject(new Error("Failed to encode canvas as PNG"));
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
canvas.toBlob((blob) => {
|
|
170
|
+
if (blob) {
|
|
171
|
+
resolve(blob);
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
reject(new Error("Failed to encode canvas as PNG"));
|
|
175
|
+
}, "image/png");
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export function slugifyExportName(value: string): string {
|
|
180
|
+
const slug = value
|
|
181
|
+
.toLowerCase()
|
|
182
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
183
|
+
.replace(/^-|-$/g, "");
|
|
184
|
+
return slug || "chart";
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export function chartPngFilename(title: string, id = "chart"): string {
|
|
188
|
+
return `${slugifyExportName(title) || slugifyExportName(id)}.png`;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const KEEP_TAGS = new Set(["HTML", "HEAD", "BODY", "STYLE", "LINK"]);
|
|
192
|
+
|
|
193
|
+
function isHiddenForChartExport(node: Element): boolean {
|
|
194
|
+
return (
|
|
195
|
+
node.hasAttribute(CHART_EXPORT_HIDE_ATTR) ||
|
|
196
|
+
Boolean(node.closest(`[${CHART_EXPORT_HIDE_ATTR}]`))
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* html2canvas clones the whole document for every capture. Ignore everything
|
|
202
|
+
* except the target card (and ancestors / stylesheet nodes) so zip-all does
|
|
203
|
+
* not re-render every other chart.
|
|
204
|
+
*/
|
|
205
|
+
export function shouldIgnoreChartExportElement(
|
|
206
|
+
node: Element,
|
|
207
|
+
captureRoot?: Element | null,
|
|
208
|
+
): boolean {
|
|
209
|
+
if (!captureRoot) {
|
|
210
|
+
return isHiddenForChartExport(node);
|
|
211
|
+
}
|
|
212
|
+
if (KEEP_TAGS.has(node.tagName)) {
|
|
213
|
+
return false;
|
|
214
|
+
}
|
|
215
|
+
if (
|
|
216
|
+
node !== captureRoot &&
|
|
217
|
+
!captureRoot.contains(node) &&
|
|
218
|
+
!node.contains(captureRoot)
|
|
219
|
+
) {
|
|
220
|
+
return true;
|
|
221
|
+
}
|
|
222
|
+
return isHiddenForChartExport(node);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export function downloadBlob(blob: Blob, filename: string): void {
|
|
226
|
+
const url = URL.createObjectURL(blob);
|
|
227
|
+
const link = document.createElement("a");
|
|
228
|
+
link.href = url;
|
|
229
|
+
link.download = filename;
|
|
230
|
+
document.body.appendChild(link);
|
|
231
|
+
link.click();
|
|
232
|
+
document.body.removeChild(link);
|
|
233
|
+
URL.revokeObjectURL(url);
|
|
234
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createContext,
|
|
3
|
+
type RefCallback,
|
|
4
|
+
useContext,
|
|
5
|
+
useEffect,
|
|
6
|
+
useState,
|
|
7
|
+
} from "react";
|
|
8
|
+
import {
|
|
9
|
+
type ChartExportMeta,
|
|
10
|
+
type ChartExportView,
|
|
11
|
+
chartPngFilename,
|
|
12
|
+
type RegisteredChart,
|
|
13
|
+
} from "./chartExport.js";
|
|
14
|
+
|
|
15
|
+
export type ChartExportFormat = "pdf" | "png" | "html";
|
|
16
|
+
|
|
17
|
+
export type ChartExportApi = {
|
|
18
|
+
downloadChart: (id: string) => Promise<void>;
|
|
19
|
+
downloadAll: () => Promise<void>;
|
|
20
|
+
downloadPdf: (documentTitle: string) => Promise<void>;
|
|
21
|
+
downloadHtml: (documentTitle: string) => Promise<void>;
|
|
22
|
+
downloadingChartId: string | null;
|
|
23
|
+
exportingFormat: ChartExportFormat | null;
|
|
24
|
+
exportLoadingLabel: string | null;
|
|
25
|
+
isExportingAll: boolean;
|
|
26
|
+
isBusy: boolean;
|
|
27
|
+
exportError: string | null;
|
|
28
|
+
clearExportError: () => void;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export type ChartExportRegistry = {
|
|
32
|
+
register: (chart: RegisteredChart) => () => void;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export const ChartExportRegistryContext =
|
|
36
|
+
createContext<ChartExportRegistry | null>(null);
|
|
37
|
+
export const ChartExportApiContext = createContext<ChartExportApi | null>(null);
|
|
38
|
+
|
|
39
|
+
export function useChartExport(): ChartExportApi | null {
|
|
40
|
+
return useContext(ChartExportApiContext);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function useRegisterChart({
|
|
44
|
+
id,
|
|
45
|
+
title,
|
|
46
|
+
filename,
|
|
47
|
+
exportViews,
|
|
48
|
+
activeExportViewId,
|
|
49
|
+
setExportView,
|
|
50
|
+
}: ChartExportMeta & {
|
|
51
|
+
exportViews?: ChartExportView[];
|
|
52
|
+
activeExportViewId?: string;
|
|
53
|
+
setExportView?: (viewId: string) => Promise<void>;
|
|
54
|
+
}): RefCallback<HTMLElement> {
|
|
55
|
+
const registry = useContext(ChartExportRegistryContext);
|
|
56
|
+
const [element, setElement] = useState<HTMLElement | null>(null);
|
|
57
|
+
const resolvedFilename = filename ?? chartPngFilename(title, id);
|
|
58
|
+
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
if (!registry || !element) {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
return registry.register({
|
|
64
|
+
id,
|
|
65
|
+
title,
|
|
66
|
+
filename: resolvedFilename,
|
|
67
|
+
element,
|
|
68
|
+
exportViews,
|
|
69
|
+
activeExportViewId,
|
|
70
|
+
setExportView,
|
|
71
|
+
});
|
|
72
|
+
}, [
|
|
73
|
+
activeExportViewId,
|
|
74
|
+
element,
|
|
75
|
+
exportViews,
|
|
76
|
+
id,
|
|
77
|
+
registry,
|
|
78
|
+
resolvedFilename,
|
|
79
|
+
setExportView,
|
|
80
|
+
title,
|
|
81
|
+
]);
|
|
82
|
+
|
|
83
|
+
return setElement;
|
|
84
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export function getDatedChartExportBasename(date = new Date()): string {
|
|
2
|
+
const year = date.getFullYear();
|
|
3
|
+
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
4
|
+
const day = String(date.getDate()).padStart(2, "0");
|
|
5
|
+
return `migration-export-${year}-${month}-${day}`;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function getChartExportFilename(
|
|
9
|
+
format: "pdf" | "html" | "zip",
|
|
10
|
+
date = new Date(),
|
|
11
|
+
): string {
|
|
12
|
+
const base = getDatedChartExportBasename(date);
|
|
13
|
+
if (format === "zip") {
|
|
14
|
+
return `${base}-charts.zip`;
|
|
15
|
+
}
|
|
16
|
+
return `${base}.${format}`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function getChartZipFilename(date = new Date()): string {
|
|
20
|
+
return getChartExportFilename("zip", date);
|
|
21
|
+
}
|
package/src/report/constants.ts
CHANGED
|
@@ -25,4 +25,6 @@ export const REPORT_CARD_EMPTY_STATE_TITLES = {
|
|
|
25
25
|
operatingSystems: "Operating system data not collected",
|
|
26
26
|
migrationStatus: "Migration status data not collected",
|
|
27
27
|
issuesBreakdown: "Issues breakdown data not collected",
|
|
28
|
+
hostPowerStates: "Host power state data not collected",
|
|
29
|
+
vmPowerStates: "VM power state data not collected",
|
|
28
30
|
} as const;
|