@openshift-migration-advisor/shared-components 0.25.0-fce5293f01c1 → 0.27.0-5d0cc20d809e
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/dist/report/ChartExportProvider.d.ts +3 -2
- package/dist/report/ChartExportProvider.d.ts.map +1 -1
- package/dist/report/ChartExportProvider.js +3 -3
- package/dist/report/CpuAndMemoryOverview.d.ts +19 -0
- package/dist/report/CpuAndMemoryOverview.d.ts.map +1 -0
- package/dist/report/CpuAndMemoryOverview.js +112 -0
- package/dist/report/ErrorTable.d.ts +18 -0
- package/dist/report/ErrorTable.d.ts.map +1 -0
- package/dist/report/ErrorTable.js +20 -0
- package/dist/report/HostsOverview.d.ts +13 -0
- package/dist/report/HostsOverview.d.ts.map +1 -0
- package/dist/report/HostsOverview.js +84 -0
- package/dist/report/ReportTable.d.ts +14 -0
- package/dist/report/ReportTable.d.ts.map +1 -0
- package/dist/report/ReportTable.js +23 -0
- package/dist/report/WarningsTable.d.ts +9 -0
- package/dist/report/WarningsTable.d.ts.map +1 -0
- package/dist/report/WarningsTable.js +20 -0
- package/dist/report/captureChartElement.d.ts.map +1 -1
- package/dist/report/captureChartElement.js +28 -8
- package/dist/report/chartExport.d.ts +23 -0
- package/dist/report/chartExport.d.ts.map +1 -1
- package/dist/report/chartExport.js +20 -0
- package/dist/report/chartExportContext.d.ts +6 -2
- package/dist/report/chartExportContext.d.ts.map +1 -1
- 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/index.d.ts +7 -2
- package/dist/report/index.d.ts.map +1 -1
- package/dist/report/index.js +5 -0
- package/dist/report/pdfExport.d.ts +2 -2
- package/dist/report/pdfExport.d.ts.map +1 -1
- package/dist/report/pdfExport.js +52 -3
- package/package.json +2 -2
- package/src/report/ChartExportProvider.tsx +13 -5
- package/src/report/CpuAndMemoryOverview.tsx +277 -0
- package/src/report/ErrorTable.tsx +85 -0
- package/src/report/HostsOverview.tsx +185 -0
- package/src/report/ReportTable.tsx +81 -0
- package/src/report/WarningsTable.tsx +75 -0
- package/src/report/__tests__/ChartExportProvider.test.tsx +110 -2
- package/src/report/__tests__/CpuAndMemoryOverview.test.tsx +200 -0
- package/src/report/__tests__/HostsOverview.test.tsx +145 -0
- package/src/report/__tests__/captureChartElement.test.ts +73 -0
- package/src/report/__tests__/chartExportPackagers.test.ts +75 -0
- package/src/report/captureChartElement.ts +37 -7
- package/src/report/chartExport.ts +40 -0
- package/src/report/chartExportContext.tsx +9 -1
- package/src/report/constants.ts +2 -0
- package/src/report/index.ts +22 -0
- package/src/report/pdfExport.ts +81 -2
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import "@testing-library/jest-dom";
|
|
2
|
+
import {
|
|
3
|
+
chart_color_blue_300,
|
|
4
|
+
chart_color_green_300,
|
|
5
|
+
chart_color_purple_100,
|
|
6
|
+
chart_color_purple_300,
|
|
7
|
+
chart_color_teal_200,
|
|
8
|
+
chart_color_yellow_400,
|
|
9
|
+
} from "@patternfly/react-tokens";
|
|
10
|
+
import { cleanup, render, screen } from "@testing-library/react";
|
|
11
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
12
|
+
import type { MigrationDonutChartProps } from "../../charts/MigrationDonutChart.js";
|
|
13
|
+
import { HostsOverview } from "../HostsOverview.js";
|
|
14
|
+
|
|
15
|
+
const donutProps = vi.hoisted(() => ({
|
|
16
|
+
current: null as MigrationDonutChartProps | null,
|
|
17
|
+
}));
|
|
18
|
+
|
|
19
|
+
vi.mock("../../charts/MigrationDonutChart.js", () => ({
|
|
20
|
+
MigrationDonutChart: (props: MigrationDonutChartProps): JSX.Element => {
|
|
21
|
+
donutProps.current = props;
|
|
22
|
+
return (
|
|
23
|
+
<div data-testid="hosts-donut">
|
|
24
|
+
{props.title} {props.subTitle}
|
|
25
|
+
</div>
|
|
26
|
+
);
|
|
27
|
+
},
|
|
28
|
+
}));
|
|
29
|
+
|
|
30
|
+
afterEach(() => {
|
|
31
|
+
donutProps.current = null;
|
|
32
|
+
cleanup();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const renderedDonut = (): MigrationDonutChartProps => {
|
|
36
|
+
const props = donutProps.current;
|
|
37
|
+
if (!props) {
|
|
38
|
+
throw new Error("expected the host model donut to render");
|
|
39
|
+
}
|
|
40
|
+
return props;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
describe("HostsOverview", () => {
|
|
44
|
+
it("shows the shared empty state when no hosts are available", () => {
|
|
45
|
+
render(<HostsOverview />);
|
|
46
|
+
|
|
47
|
+
expect(screen.getByText("Host distribution by model")).toBeInTheDocument();
|
|
48
|
+
expect(screen.getByText("Top 5 models")).toBeInTheDocument();
|
|
49
|
+
expect(screen.getByText("Host data not collected")).toBeInTheDocument();
|
|
50
|
+
expect(screen.queryByTestId("hosts-donut")).not.toBeInTheDocument();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("buckets host counts into the top 5 models plus Other models", () => {
|
|
54
|
+
render(
|
|
55
|
+
<HostsOverview
|
|
56
|
+
hosts={[
|
|
57
|
+
{ model: "M1" },
|
|
58
|
+
{ model: "M1" },
|
|
59
|
+
{ model: "M1" },
|
|
60
|
+
{ model: "M1" },
|
|
61
|
+
{ model: "M1" },
|
|
62
|
+
{ model: "M1" },
|
|
63
|
+
{ model: "M2" },
|
|
64
|
+
{ model: "M2" },
|
|
65
|
+
{ model: "M2" },
|
|
66
|
+
{ model: "M2" },
|
|
67
|
+
{ model: "M2" },
|
|
68
|
+
{ model: "M3" },
|
|
69
|
+
{ model: "M3" },
|
|
70
|
+
{ model: "M3" },
|
|
71
|
+
{ model: "M3" },
|
|
72
|
+
{ model: "M4" },
|
|
73
|
+
{ model: "M4" },
|
|
74
|
+
{ model: "M4" },
|
|
75
|
+
{ model: "M5" },
|
|
76
|
+
{ model: "M5" },
|
|
77
|
+
{ model: "M6" },
|
|
78
|
+
{ model: "M7" },
|
|
79
|
+
]}
|
|
80
|
+
/>,
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
expect(screen.getByTestId("hosts-donut")).toHaveTextContent("22 Hosts");
|
|
84
|
+
|
|
85
|
+
const props = renderedDonut();
|
|
86
|
+
expect(props.data.map((slice) => [slice.name, slice.count])).toEqual([
|
|
87
|
+
["M1", 6],
|
|
88
|
+
["M2", 5],
|
|
89
|
+
["M3", 4],
|
|
90
|
+
["M4", 3],
|
|
91
|
+
["M5", 2],
|
|
92
|
+
["Other models", 2],
|
|
93
|
+
]);
|
|
94
|
+
expect(props.data[5]?.countDisplay).toBe("2 hosts");
|
|
95
|
+
expect(props.legend).toEqual({
|
|
96
|
+
M1: chart_color_blue_300.value,
|
|
97
|
+
M2: chart_color_purple_300.value,
|
|
98
|
+
M3: chart_color_purple_100.value,
|
|
99
|
+
M4: chart_color_teal_200.value,
|
|
100
|
+
M5: chart_color_yellow_400.value,
|
|
101
|
+
"Other models": chart_color_green_300.value,
|
|
102
|
+
});
|
|
103
|
+
expect(props.legendVariant).toBe("html");
|
|
104
|
+
expect(props.height).toBe(300);
|
|
105
|
+
expect(props.width).toBe(420);
|
|
106
|
+
expect(props.donutThickness).toBe(18);
|
|
107
|
+
expect(props.titleFontSize).toBe(34);
|
|
108
|
+
expect(props.legendWidth).toBe(680);
|
|
109
|
+
expect(props.itemsPerRow).toBe(2);
|
|
110
|
+
expect(props.labelFontSize).toBe(16);
|
|
111
|
+
expect(props.marginLeft).toBe("0%");
|
|
112
|
+
expect(props.subTitleColor).toBe(
|
|
113
|
+
"var(--pf-t--global--text--color--subtle)",
|
|
114
|
+
);
|
|
115
|
+
expect(
|
|
116
|
+
props.tooltipLabelFormatter?.({
|
|
117
|
+
datum: {
|
|
118
|
+
x: "M1",
|
|
119
|
+
y: 6,
|
|
120
|
+
countDisplay: "6 hosts",
|
|
121
|
+
legendCategory: "M1",
|
|
122
|
+
},
|
|
123
|
+
percent: 27.2727,
|
|
124
|
+
total: 22,
|
|
125
|
+
}),
|
|
126
|
+
).toBe("6 hosts\n27.3%");
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("treats numeric models as labels and blank models as unknown", () => {
|
|
130
|
+
render(
|
|
131
|
+
<HostsOverview
|
|
132
|
+
legendVariant="chart"
|
|
133
|
+
hosts={[{ model: 42 }, { model: " " }, { model: " R640 " }]}
|
|
134
|
+
/>,
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
expect(renderedDonut().data.map((slice) => slice.name)).toEqual([
|
|
138
|
+
"42",
|
|
139
|
+
"Unknown model",
|
|
140
|
+
"R640",
|
|
141
|
+
]);
|
|
142
|
+
expect(renderedDonut().legendVariant).toBe("chart");
|
|
143
|
+
expect(renderedDonut().title).toBe("3");
|
|
144
|
+
});
|
|
145
|
+
});
|
|
@@ -16,6 +16,12 @@ const mockedHtml2Canvas = vi.mocked(html2canvas);
|
|
|
16
16
|
afterEach(() => {
|
|
17
17
|
mockedHtml2Canvas.mockClear();
|
|
18
18
|
document.body.replaceChildren();
|
|
19
|
+
document.documentElement.classList.remove(
|
|
20
|
+
"pf-v6-theme-dark",
|
|
21
|
+
"pf-v5-theme-dark",
|
|
22
|
+
);
|
|
23
|
+
document.documentElement.style.colorScheme = "";
|
|
24
|
+
document.body.style.colorScheme = "";
|
|
19
25
|
});
|
|
20
26
|
|
|
21
27
|
describe("captureChartElement", () => {
|
|
@@ -129,4 +135,71 @@ describe("captureChartElement", () => {
|
|
|
129
135
|
|
|
130
136
|
await captureChartElement(root);
|
|
131
137
|
});
|
|
138
|
+
|
|
139
|
+
it("applies light theme on the clone and leaves the live document dark", async () => {
|
|
140
|
+
document.documentElement.classList.add("pf-v6-theme-dark");
|
|
141
|
+
const root = document.createElement("div");
|
|
142
|
+
const card = document.createElement("div");
|
|
143
|
+
card.className = "pf-v6-c-card";
|
|
144
|
+
card.style.backgroundColor = "rgb(30, 30, 30)";
|
|
145
|
+
card.style.border = "1px solid rgb(80, 80, 80)";
|
|
146
|
+
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
|
147
|
+
const text = document.createElementNS("http://www.w3.org/2000/svg", "text");
|
|
148
|
+
text.style.fill = "rgb(255, 255, 255)";
|
|
149
|
+
text.textContent = "42";
|
|
150
|
+
svg.append(text);
|
|
151
|
+
card.append(svg);
|
|
152
|
+
root.append(card);
|
|
153
|
+
document.body.append(root);
|
|
154
|
+
|
|
155
|
+
mockedHtml2Canvas.mockImplementation(async (_element, options) => {
|
|
156
|
+
expect(
|
|
157
|
+
document.documentElement.classList.contains("pf-v6-theme-dark"),
|
|
158
|
+
).toBe(true);
|
|
159
|
+
|
|
160
|
+
const clonedDoc = document.implementation.createHTMLDocument("export");
|
|
161
|
+
clonedDoc.documentElement.classList.add("pf-v6-theme-dark");
|
|
162
|
+
const cloneRoot = clonedDoc.createElement("div");
|
|
163
|
+
const cloneCard = clonedDoc.createElement("div");
|
|
164
|
+
cloneCard.className = "pf-v6-c-card";
|
|
165
|
+
cloneCard.style.backgroundColor = "rgb(30, 30, 30)";
|
|
166
|
+
const cloneSvg = clonedDoc.createElementNS(
|
|
167
|
+
"http://www.w3.org/2000/svg",
|
|
168
|
+
"svg",
|
|
169
|
+
);
|
|
170
|
+
const cloneText = clonedDoc.createElementNS(
|
|
171
|
+
"http://www.w3.org/2000/svg",
|
|
172
|
+
"text",
|
|
173
|
+
);
|
|
174
|
+
cloneText.style.fill = "rgb(255, 255, 255)";
|
|
175
|
+
cloneSvg.append(cloneText);
|
|
176
|
+
cloneCard.append(cloneSvg);
|
|
177
|
+
cloneRoot.append(cloneCard);
|
|
178
|
+
clonedDoc.body.append(cloneRoot);
|
|
179
|
+
|
|
180
|
+
await options?.onclone?.(clonedDoc, cloneRoot);
|
|
181
|
+
|
|
182
|
+
expect(
|
|
183
|
+
clonedDoc.documentElement.classList.contains("pf-v6-theme-dark"),
|
|
184
|
+
).toBe(false);
|
|
185
|
+
expect(
|
|
186
|
+
document.documentElement.classList.contains("pf-v6-theme-dark"),
|
|
187
|
+
).toBe(true);
|
|
188
|
+
expect(cloneCard.style.backgroundColor).toBe("rgb(255, 255, 255)");
|
|
189
|
+
expect(cloneCard.style.borderColor).toBe("rgb(224, 224, 224)");
|
|
190
|
+
expect(cloneText.style.fill).toBe("rgb(21, 21, 21)");
|
|
191
|
+
|
|
192
|
+
const canvas = document.createElement("canvas");
|
|
193
|
+
canvas.width = 1;
|
|
194
|
+
canvas.height = 1;
|
|
195
|
+
return canvas;
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
await captureChartElement(root);
|
|
199
|
+
|
|
200
|
+
expect(
|
|
201
|
+
document.documentElement.classList.contains("pf-v6-theme-dark"),
|
|
202
|
+
).toBe(true);
|
|
203
|
+
expect(card.style.backgroundColor).toBe("rgb(30, 30, 30)");
|
|
204
|
+
});
|
|
132
205
|
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest";
|
|
2
2
|
import {
|
|
3
|
+
applyLightThemeToDocument,
|
|
3
4
|
chartExportViewsFromLabels,
|
|
4
5
|
restoreChartExportViews,
|
|
5
6
|
sortRegisteredChartsByDocumentOrder,
|
|
@@ -7,6 +8,7 @@ import {
|
|
|
7
8
|
} from "../chartExport.js";
|
|
8
9
|
import { getChartExportFilename } from "../chartExportFilenames.js";
|
|
9
10
|
import { buildHtmlReport, escapeHtml } from "../htmlExport.js";
|
|
11
|
+
import { buildPdfFromCharts } from "../pdfExport.js";
|
|
10
12
|
import {
|
|
11
13
|
fitPdfImageSize,
|
|
12
14
|
placePdfBlock,
|
|
@@ -198,6 +200,63 @@ describe("placePdfBlock", () => {
|
|
|
198
200
|
});
|
|
199
201
|
});
|
|
200
202
|
|
|
203
|
+
describe("buildPdfFromCharts with extra text pages", () => {
|
|
204
|
+
it("appends a text page even when there are no chart captures", async () => {
|
|
205
|
+
const blob = await buildPdfFromCharts([], "Report", [
|
|
206
|
+
{
|
|
207
|
+
title: "Cluster sizing recommendations",
|
|
208
|
+
items: [
|
|
209
|
+
{ label: "Cluster name", value: "cluster-1" },
|
|
210
|
+
{ label: "Target platform", value: "Bare metal" },
|
|
211
|
+
{
|
|
212
|
+
label: "Total nodes",
|
|
213
|
+
value: "6 (3 workers + 3 control plane)",
|
|
214
|
+
},
|
|
215
|
+
{ label: "Failover capacity", value: "1 failover nodes" },
|
|
216
|
+
{ label: "Worker node size", value: "8 CPU, 32 GB memory" },
|
|
217
|
+
{
|
|
218
|
+
label: "Control plane node size",
|
|
219
|
+
value: "16 CPU, 64 GB memory",
|
|
220
|
+
},
|
|
221
|
+
],
|
|
222
|
+
footer: "Note: estimates only.",
|
|
223
|
+
},
|
|
224
|
+
]);
|
|
225
|
+
|
|
226
|
+
expect(blob).toBeInstanceOf(Blob);
|
|
227
|
+
expect(blob.size).toBeGreaterThan(0);
|
|
228
|
+
expect(blob.type).toBe("application/pdf");
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it("paginates extra pages with many rows without throwing", async () => {
|
|
232
|
+
const manyItems = Array.from({ length: 60 }, (_, index) => ({
|
|
233
|
+
label: `Field ${index}`,
|
|
234
|
+
value: `Value ${index}`,
|
|
235
|
+
}));
|
|
236
|
+
|
|
237
|
+
const blob = await buildPdfFromCharts([], "Report", [
|
|
238
|
+
{ title: "Cluster sizing recommendations", items: manyItems },
|
|
239
|
+
]);
|
|
240
|
+
|
|
241
|
+
expect(blob.size).toBeGreaterThan(0);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it("supports multiple extra pages (e.g. one per sized cluster)", async () => {
|
|
245
|
+
const blob = await buildPdfFromCharts([], "Report", [
|
|
246
|
+
{
|
|
247
|
+
title: "Cluster sizing recommendations — cluster-a",
|
|
248
|
+
items: [{ label: "Cluster name", value: "cluster-a" }],
|
|
249
|
+
},
|
|
250
|
+
{
|
|
251
|
+
title: "Cluster sizing recommendations — cluster-b",
|
|
252
|
+
items: [{ label: "Cluster name", value: "cluster-b" }],
|
|
253
|
+
},
|
|
254
|
+
]);
|
|
255
|
+
|
|
256
|
+
expect(blob.size).toBeGreaterThan(0);
|
|
257
|
+
});
|
|
258
|
+
});
|
|
259
|
+
|
|
201
260
|
describe("buildHtmlReport", () => {
|
|
202
261
|
it("embeds captured chart images and escapes the title", () => {
|
|
203
262
|
const html = buildHtmlReport(
|
|
@@ -222,3 +281,19 @@ describe("buildHtmlReport", () => {
|
|
|
222
281
|
expect(escapeHtml("it's")).toBe("it's");
|
|
223
282
|
});
|
|
224
283
|
});
|
|
284
|
+
|
|
285
|
+
describe("applyLightThemeToDocument", () => {
|
|
286
|
+
it("removes PatternFly dark theme classes from the given document", () => {
|
|
287
|
+
const clone = document.implementation.createHTMLDocument("export");
|
|
288
|
+
clone.documentElement.classList.add("pf-v6-theme-dark");
|
|
289
|
+
clone.body.style.colorScheme = "dark";
|
|
290
|
+
|
|
291
|
+
applyLightThemeToDocument(clone);
|
|
292
|
+
|
|
293
|
+
expect(clone.documentElement.classList.contains("pf-v6-theme-dark")).toBe(
|
|
294
|
+
false,
|
|
295
|
+
);
|
|
296
|
+
expect(clone.documentElement.style.colorScheme).toBe("light");
|
|
297
|
+
expect(clone.body.style.colorScheme).toBe("light");
|
|
298
|
+
});
|
|
299
|
+
});
|
|
@@ -1,5 +1,11 @@
|
|
|
1
|
+
import {
|
|
2
|
+
t_color_gray_20,
|
|
3
|
+
t_color_white,
|
|
4
|
+
t_global_text_color_regular,
|
|
5
|
+
} from "@patternfly/react-tokens";
|
|
1
6
|
import html2canvas from "html2canvas-pro";
|
|
2
7
|
import {
|
|
8
|
+
applyLightThemeToDocument,
|
|
3
9
|
CHART_EXPORT_CAPTURING_ATTR,
|
|
4
10
|
CHART_EXPORT_SCROLL_ATTR,
|
|
5
11
|
shouldIgnoreChartExportElement,
|
|
@@ -94,6 +100,10 @@ const PADDED_CAPTURE_SELECTOR =
|
|
|
94
100
|
".pf-v6-c-card__title, .pf-v6-c-card__header, .pf-v6-c-card__body";
|
|
95
101
|
const CARD_CAPTURE_SELECTOR = ".pf-v6-c-card";
|
|
96
102
|
const TRANSPARENT_FILLS = new Set(["", "transparent", "rgba(0, 0, 0, 0)"]);
|
|
103
|
+
/** PatternFly light-theme chrome. html2canvas inlines live computed SVG styles. */
|
|
104
|
+
const LIGHT_EXPORT_BACKGROUND = t_color_white.value;
|
|
105
|
+
const LIGHT_EXPORT_BORDER = t_color_gray_20.value;
|
|
106
|
+
const LIGHT_EXPORT_TEXT = t_global_text_color_regular.value;
|
|
97
107
|
|
|
98
108
|
function queryCards(root: HTMLElement): HTMLElement[] {
|
|
99
109
|
const cards = Array.from(
|
|
@@ -149,16 +159,34 @@ function readCardBorder(source: HTMLElement): {
|
|
|
149
159
|
return { width, style, color };
|
|
150
160
|
}
|
|
151
161
|
|
|
152
|
-
function applyCardChrome(
|
|
162
|
+
function applyCardChrome(
|
|
163
|
+
source: HTMLElement,
|
|
164
|
+
dest: HTMLElement,
|
|
165
|
+
colors: "computed" | "light",
|
|
166
|
+
): void {
|
|
153
167
|
const computed = window.getComputedStyle(source);
|
|
154
168
|
dest.style.borderRadius = computed.borderRadius;
|
|
155
|
-
dest.style.backgroundColor = resolvedFill(computed.backgroundColor);
|
|
156
169
|
dest.style.overflow = "hidden";
|
|
170
|
+
dest.style.backgroundColor =
|
|
171
|
+
colors === "light"
|
|
172
|
+
? LIGHT_EXPORT_BACKGROUND
|
|
173
|
+
: resolvedFill(computed.backgroundColor);
|
|
157
174
|
const border = readCardBorder(source);
|
|
158
175
|
if (border) {
|
|
159
176
|
dest.style.borderWidth = border.width;
|
|
160
177
|
dest.style.borderStyle = border.style;
|
|
161
|
-
dest.style.borderColor =
|
|
178
|
+
dest.style.borderColor =
|
|
179
|
+
colors === "light" ? LIGHT_EXPORT_BORDER : border.color;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function applyLightThemeSvgText(root: HTMLElement): void {
|
|
184
|
+
for (const node of root.querySelectorAll("text, tspan")) {
|
|
185
|
+
if (!(node instanceof SVGElement)) {
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
node.style.fill = LIGHT_EXPORT_TEXT;
|
|
189
|
+
node.style.color = LIGHT_EXPORT_TEXT;
|
|
162
190
|
}
|
|
163
191
|
}
|
|
164
192
|
|
|
@@ -218,7 +246,7 @@ function copyCardChrome(node: HTMLElement): {
|
|
|
218
246
|
height: node.style.height,
|
|
219
247
|
maxHeight: node.style.maxHeight,
|
|
220
248
|
};
|
|
221
|
-
applyCardChrome(node, node);
|
|
249
|
+
applyCardChrome(node, node, "computed");
|
|
222
250
|
node.style.minHeight = "0px";
|
|
223
251
|
node.style.height = "auto";
|
|
224
252
|
node.style.maxHeight = "none";
|
|
@@ -234,7 +262,7 @@ function applyComputedStylesToClone(
|
|
|
234
262
|
sourceCards.forEach((source, index) => {
|
|
235
263
|
const dest = cloneCards[index];
|
|
236
264
|
if (dest) {
|
|
237
|
-
applyCardChrome(source, dest);
|
|
265
|
+
applyCardChrome(source, dest, "light");
|
|
238
266
|
}
|
|
239
267
|
});
|
|
240
268
|
|
|
@@ -348,15 +376,17 @@ export async function captureChartElement(
|
|
|
348
376
|
}
|
|
349
377
|
return html2canvas(element, {
|
|
350
378
|
useCORS: true,
|
|
351
|
-
backgroundColor:
|
|
379
|
+
backgroundColor: LIGHT_EXPORT_BACKGROUND,
|
|
352
380
|
logging: false,
|
|
353
381
|
scale: CHART_CAPTURE_SCALE,
|
|
354
382
|
imageTimeout: 0,
|
|
355
383
|
ignoreElements: (node) =>
|
|
356
384
|
node instanceof Element &&
|
|
357
385
|
shouldIgnoreChartExportElement(node, element),
|
|
358
|
-
onclone: (
|
|
386
|
+
onclone: (clonedDoc, clonedElement) => {
|
|
387
|
+
applyLightThemeToDocument(clonedDoc);
|
|
359
388
|
applyComputedStylesToClone(element, clonedElement);
|
|
389
|
+
applyLightThemeSvgText(clonedElement);
|
|
360
390
|
},
|
|
361
391
|
});
|
|
362
392
|
} finally {
|
|
@@ -51,6 +51,24 @@ export type ChartCaptureSource = {
|
|
|
51
51
|
capture: () => Promise<HTMLCanvasElement>;
|
|
52
52
|
};
|
|
53
53
|
|
|
54
|
+
/** One label/value row rendered on a {@link PdfTextPage}. */
|
|
55
|
+
export type PdfTextPageItem = {
|
|
56
|
+
label: string;
|
|
57
|
+
value: string;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* A native-text PDF page appended after the captured chart images.
|
|
62
|
+
*
|
|
63
|
+
* Used for content that is not a chart/card (e.g. calculated recommendations)
|
|
64
|
+
* but still needs to be part of the exported PDF.
|
|
65
|
+
*/
|
|
66
|
+
export type PdfTextPage = {
|
|
67
|
+
title: string;
|
|
68
|
+
items: PdfTextPageItem[];
|
|
69
|
+
footer?: string;
|
|
70
|
+
};
|
|
71
|
+
|
|
54
72
|
export function releaseCanvas(canvas: HTMLCanvasElement): void {
|
|
55
73
|
canvas.width = 0;
|
|
56
74
|
canvas.height = 0;
|
|
@@ -67,6 +85,28 @@ export function waitForChartExportPaint(): Promise<void> {
|
|
|
67
85
|
});
|
|
68
86
|
}
|
|
69
87
|
|
|
88
|
+
/** PatternFly applies dark tokens when this class is on `:root`. */
|
|
89
|
+
export const PATTERNFLY_DARK_THEME_CLASSES = [
|
|
90
|
+
"pf-v6-theme-dark",
|
|
91
|
+
"pf-v5-theme-dark",
|
|
92
|
+
] as const;
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Force PatternFly light tokens on a document (the html2canvas clone).
|
|
96
|
+
* Does not touch the live page.
|
|
97
|
+
*/
|
|
98
|
+
export function applyLightThemeToDocument(root: Document): void {
|
|
99
|
+
for (const node of [root.documentElement, root.body]) {
|
|
100
|
+
if (!node) {
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
for (const className of PATTERNFLY_DARK_THEME_CLASSES) {
|
|
104
|
+
node.classList.remove(className);
|
|
105
|
+
}
|
|
106
|
+
node.style.colorScheme = "light";
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
70
110
|
export function chartExportViewsFromLabels(
|
|
71
111
|
titlePrefix: string,
|
|
72
112
|
labels: Record<string, string>,
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
type ChartExportMeta,
|
|
10
10
|
type ChartExportView,
|
|
11
11
|
chartPngFilename,
|
|
12
|
+
type PdfTextPage,
|
|
12
13
|
type RegisteredChart,
|
|
13
14
|
} from "./chartExport.js";
|
|
14
15
|
|
|
@@ -17,7 +18,14 @@ export type ChartExportFormat = "pdf" | "png" | "html";
|
|
|
17
18
|
export type ChartExportApi = {
|
|
18
19
|
downloadChart: (id: string) => Promise<void>;
|
|
19
20
|
downloadAll: () => Promise<void>;
|
|
20
|
-
|
|
21
|
+
/**
|
|
22
|
+
* @param extraPages - Native-text pages appended after the captured chart
|
|
23
|
+
* images (e.g. calculated recommendations that aren't rendered as charts).
|
|
24
|
+
*/
|
|
25
|
+
downloadPdf: (
|
|
26
|
+
documentTitle: string,
|
|
27
|
+
extraPages?: PdfTextPage[],
|
|
28
|
+
) => Promise<void>;
|
|
21
29
|
downloadHtml: (documentTitle: string) => Promise<void>;
|
|
22
30
|
downloadingChartId: string | null;
|
|
23
31
|
exportingFormat: ChartExportFormat | null;
|
package/src/report/constants.ts
CHANGED
|
@@ -25,6 +25,8 @@ 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
|
+
errors: "Error data not collected",
|
|
29
|
+
warnings: "Warning data not collected",
|
|
28
30
|
hostPowerStates: "Host power state data not collected",
|
|
29
31
|
vmPowerStates: "VM power state data not collected",
|
|
30
32
|
} as const;
|
package/src/report/index.ts
CHANGED
|
@@ -9,6 +9,7 @@ export {
|
|
|
9
9
|
} from "./ChartDownloadButton.js";
|
|
10
10
|
export {
|
|
11
11
|
type BuildChartDocument,
|
|
12
|
+
type BuildPdfDocument,
|
|
12
13
|
type CaptureChartElement,
|
|
13
14
|
ChartExportProvider,
|
|
14
15
|
type ChartExportProviderProps,
|
|
@@ -16,6 +17,10 @@ export {
|
|
|
16
17
|
type ZipChartFiles,
|
|
17
18
|
} from "./ChartExportProvider.js";
|
|
18
19
|
export { ChartExportSurface } from "./ChartExportSurface.js";
|
|
20
|
+
export {
|
|
21
|
+
CpuAndMemoryOverview,
|
|
22
|
+
type CpuAndMemoryOverviewProps,
|
|
23
|
+
} from "./CpuAndMemoryOverview.js";
|
|
19
24
|
export { captureChartElement } from "./captureChartElement.js";
|
|
20
25
|
export {
|
|
21
26
|
CHART_EXPORT_CAPTURING_ATTR,
|
|
@@ -32,6 +37,8 @@ export {
|
|
|
32
37
|
chartExportViewsFromLabels,
|
|
33
38
|
chartPngFilename,
|
|
34
39
|
downloadBlob,
|
|
40
|
+
type PdfTextPage,
|
|
41
|
+
type PdfTextPageItem,
|
|
35
42
|
type RegisteredChart,
|
|
36
43
|
releaseCanvas,
|
|
37
44
|
restoreChartExportViews,
|
|
@@ -65,6 +72,11 @@ export {
|
|
|
65
72
|
EmptySearchResults,
|
|
66
73
|
type EmptySearchResultsProps,
|
|
67
74
|
} from "./EmptySearchResults.js";
|
|
75
|
+
export {
|
|
76
|
+
ErrorTable,
|
|
77
|
+
type ErrorTableProps,
|
|
78
|
+
type MigrationIssueLike,
|
|
79
|
+
} from "./ErrorTable.js";
|
|
68
80
|
export {
|
|
69
81
|
FeatureStatusBadge,
|
|
70
82
|
type FeatureStatusBadgeProps,
|
|
@@ -73,6 +85,11 @@ export {
|
|
|
73
85
|
HostPowerStates,
|
|
74
86
|
type HostPowerStatesProps,
|
|
75
87
|
} from "./HostPowerStates.js";
|
|
88
|
+
export {
|
|
89
|
+
type HostLike,
|
|
90
|
+
HostsOverview,
|
|
91
|
+
type HostsOverviewProps,
|
|
92
|
+
} from "./HostsOverview.js";
|
|
76
93
|
export {
|
|
77
94
|
buildHtmlFromCharts,
|
|
78
95
|
buildHtmlReport,
|
|
@@ -167,6 +184,7 @@ export {
|
|
|
167
184
|
ReportExportMenu,
|
|
168
185
|
type ReportExportMenuProps,
|
|
169
186
|
} from "./ReportExportMenu.js";
|
|
187
|
+
export { ReportTable, type ReportTableProps } from "./ReportTable.js";
|
|
170
188
|
export {
|
|
171
189
|
type ReportExportOption,
|
|
172
190
|
type StandardReportExportHandlers,
|
|
@@ -187,4 +205,8 @@ export {
|
|
|
187
205
|
VmPowerStates,
|
|
188
206
|
type VmPowerStatesProps,
|
|
189
207
|
} from "./VmPowerStates.js";
|
|
208
|
+
export {
|
|
209
|
+
WarningsTable,
|
|
210
|
+
type WarningsTableProps,
|
|
211
|
+
} from "./WarningsTable.js";
|
|
190
212
|
export { zipChartPngs } from "./zipChartPngs.js";
|
package/src/report/pdfExport.ts
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import jsPDF from "jspdf";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
type ChartCaptureSource,
|
|
4
|
+
type PdfTextPage,
|
|
5
|
+
releaseCanvas,
|
|
6
|
+
} from "./chartExport.js";
|
|
3
7
|
import { fitPdfImageSize, placePdfBlock, sliceCanvas } from "./pdfPage.js";
|
|
4
8
|
|
|
5
9
|
const MARGIN_MM = 10;
|
|
@@ -10,6 +14,7 @@ const PAGE_BACKGROUND = "#ffffff";
|
|
|
10
14
|
export async function buildPdfFromCharts(
|
|
11
15
|
charts: ChartCaptureSource[],
|
|
12
16
|
documentTitle: string,
|
|
17
|
+
extraPages: PdfTextPage[] = [],
|
|
13
18
|
): Promise<Blob> {
|
|
14
19
|
const pdf = new jsPDF("p", "mm", "a4");
|
|
15
20
|
const pageWidth = pdf.internal.pageSize.getWidth();
|
|
@@ -22,7 +27,7 @@ export async function buildPdfFromCharts(
|
|
|
22
27
|
addCoverPage(
|
|
23
28
|
pdf,
|
|
24
29
|
documentTitle,
|
|
25
|
-
charts.map((chart) => chart.title),
|
|
30
|
+
[...charts.map((chart) => chart.title), ...extraPages.map((p) => p.title)],
|
|
26
31
|
pageWidth,
|
|
27
32
|
pageHeight,
|
|
28
33
|
);
|
|
@@ -63,6 +68,10 @@ export async function buildPdfFromCharts(
|
|
|
63
68
|
}
|
|
64
69
|
}
|
|
65
70
|
|
|
71
|
+
if (extraPages.length > 0) {
|
|
72
|
+
addExtraPages(pdf, extraPages, pageWidth, pageHeight);
|
|
73
|
+
}
|
|
74
|
+
|
|
66
75
|
addPageNumbers(pdf, pageWidth, pageHeight);
|
|
67
76
|
return pdf.output("blob");
|
|
68
77
|
}
|
|
@@ -138,6 +147,76 @@ function addChartImage(
|
|
|
138
147
|
}
|
|
139
148
|
}
|
|
140
149
|
|
|
150
|
+
/**
|
|
151
|
+
* Append one native-text PDF page per entry in `extraPages`, rendered as
|
|
152
|
+
* label/value rows rather than a captured image. Each page starts fresh so
|
|
153
|
+
* its content never bleeds into a chart page.
|
|
154
|
+
*/
|
|
155
|
+
function addExtraPages(
|
|
156
|
+
pdf: jsPDF,
|
|
157
|
+
extraPages: PdfTextPage[],
|
|
158
|
+
pageWidth: number,
|
|
159
|
+
pageHeight: number,
|
|
160
|
+
): void {
|
|
161
|
+
const LABEL_COL_WIDTH = 68; // mm — enough for the longest label
|
|
162
|
+
const VALUE_COL_X = MARGIN_MM + LABEL_COL_WIDTH;
|
|
163
|
+
const VALUE_COL_WIDTH = pageWidth - MARGIN_MM - LABEL_COL_WIDTH;
|
|
164
|
+
const LINE_HEIGHT = 7; // mm between rows
|
|
165
|
+
|
|
166
|
+
for (const page of extraPages) {
|
|
167
|
+
pdf.addPage();
|
|
168
|
+
let y = MARGIN_MM + 8;
|
|
169
|
+
|
|
170
|
+
pdf.setFontSize(16);
|
|
171
|
+
pdf.setFont("helvetica", "bold");
|
|
172
|
+
pdf.text(page.title, MARGIN_MM, y);
|
|
173
|
+
y += 10;
|
|
174
|
+
|
|
175
|
+
pdf.setDrawColor(0, 103, 187);
|
|
176
|
+
pdf.setLineWidth(0.5);
|
|
177
|
+
pdf.line(MARGIN_MM, y, pageWidth - MARGIN_MM, y);
|
|
178
|
+
y += 10;
|
|
179
|
+
|
|
180
|
+
pdf.setFontSize(11);
|
|
181
|
+
|
|
182
|
+
for (const item of page.items) {
|
|
183
|
+
pdf.setFont("helvetica", "bold");
|
|
184
|
+
const labelLines = pdf.splitTextToSize(
|
|
185
|
+
item.label,
|
|
186
|
+
LABEL_COL_WIDTH - 4,
|
|
187
|
+
) as string[];
|
|
188
|
+
pdf.text(labelLines, MARGIN_MM, y);
|
|
189
|
+
|
|
190
|
+
pdf.setFont("helvetica", "normal");
|
|
191
|
+
const valueLines = pdf.splitTextToSize(
|
|
192
|
+
item.value,
|
|
193
|
+
VALUE_COL_WIDTH,
|
|
194
|
+
) as string[];
|
|
195
|
+
pdf.text(valueLines, VALUE_COL_X, y);
|
|
196
|
+
|
|
197
|
+
y += LINE_HEIGHT * Math.max(labelLines.length, valueLines.length);
|
|
198
|
+
|
|
199
|
+
if (y > pageHeight - MARGIN_MM - 25) {
|
|
200
|
+
pdf.addPage();
|
|
201
|
+
y = MARGIN_MM + 10;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (page.footer) {
|
|
206
|
+
y += 4;
|
|
207
|
+
pdf.setFontSize(9);
|
|
208
|
+
pdf.setFont("helvetica", "bolditalic");
|
|
209
|
+
const footerLines = pdf.splitTextToSize(
|
|
210
|
+
page.footer,
|
|
211
|
+
pageWidth - MARGIN_MM * 2,
|
|
212
|
+
) as string[];
|
|
213
|
+
pdf.text(footerLines, MARGIN_MM, y);
|
|
214
|
+
pdf.setFont("helvetica", "normal");
|
|
215
|
+
pdf.setFontSize(11);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
141
220
|
function addPageNumbers(
|
|
142
221
|
pdf: jsPDF,
|
|
143
222
|
pageWidth: number,
|