feat(canvas): export canvas dashboards as PDF#9593
Open
nishantmonu51 wants to merge 12 commits into
Open
Conversation
Add a "PDF" tab to the share popover for canvas dashboards that captures the rendered canvas and assembles it into a downloadable PDF. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
Contributor
There was a problem hiding this comment.
Pull request overview
This PR adds a client-side “Export as PDF” flow for canvas dashboards, integrating a new PDF export module in web-common and exposing it via a new “PDF” tab in the share popover for canvas dashboards.
Changes:
- Introduces a new
web-common/src/features/exports/pdf/module to settle rendering, capture canvas blocks, paginate, and assemble/download a PDF viajspdf. - Updates the share UI to show a “PDF” tab only when canvas identifiers are present, and wires it to the canvas PDF export orchestrator.
- Adds an off-screen, read-only canvas header render (
CanvasPdfExportHeader) as a capture target and adds thejspdfdependency.
Reviewed changes
Copilot reviewed 15 out of 16 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| web-common/src/features/exports/pdf/types.ts | Adds shared PDF export types and capture block schema. |
| web-common/src/features/exports/pdf/settle.ts | Adds “settle before capture” utilities for stores, queries, and fonts. |
| web-common/src/features/exports/pdf/layout.ts | Implements pagination/layout logic for captured blocks across PDF pages. |
| web-common/src/features/exports/pdf/layout.spec.ts | Adds vitest coverage for pagination/orientation behavior. |
| web-common/src/features/exports/pdf/filename.ts | Adds filename slug + timestamp helper for PDF downloads. |
| web-common/src/features/exports/pdf/ExportPdfOptions.svelte | Adds share-popover UI for running the PDF export with options/progress. |
| web-common/src/features/exports/pdf/export-canvas-pdf.ts | Orchestrates canvas-to-PDF export (settle → capture → paginate → assemble). |
| web-common/src/features/exports/pdf/capture.ts | Captures filter header + component cards into raster blocks for PDF assembly. |
| web-common/src/features/exports/pdf/CanvasPdfExportHeader.svelte | Off-screen read-only header (time range + filters) for capture. |
| web-common/src/features/exports/pdf/assemble.ts | Renders paginated placements into a jsPDF document and triggers download. |
| web-common/src/features/exports/pdf/assemble.spec.ts | Adds tests for background color parsing used in PDF rendering. |
| web-common/src/features/canvas/CanvasDashboardWrapper.svelte | Mounts the off-screen PDF header capture target for canvases. |
| web-common/package.json | Adds jspdf dependency. |
| web-admin/src/features/projects/ProjectHeader.svelte | Passes canvasName/instanceId to enable the PDF tab for canvas dashboards. |
| web-admin/src/features/dashboards/share/ShareDashboardPopover.svelte | Adds “PDF” tab + hooks it up to exportCanvasPdf. |
| package-lock.json | Updates lockfile for the new dependency and transitive packages. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
capture.spec.ts uses the DOM (document) but is batched with pure-logic specs in the same directory. Under vitest's parallel forks pool the jsdom global was intermittently unavailable, causing 'document is not defined'. Pin the spec to the jsdom environment to make it deterministic.
The off-screen canvas PDF export header was always mounted, so its read-only filter chips duplicated the live filter bar's text and the 'Readonly Filter Chips' aria-label. Off-screen positioning still counts as visible/queryable to Playwright, so this caused strict-mode violations in unrelated canvas e2e tests (web-local default-filters, web-admin bookmarks). Keep it display:none in normal use (excluded from the a11y tree and text/label locators) and have capture.ts reveal it only while measuring and rasterizing.
The previous display:none approach did not work: Playwright locators (getByText/getByLabel) and the accessibility tree match elements regardless of CSS visibility, so the always-mounted off-screen header still duplicated the live filter bar's text and 'Readonly Filter Chips' label, failing web-local default-filters and web-admin bookmarks e2e tests with strict-mode violations. Gate the header behind a canvasPdfExportActive store so it only exists in the DOM during an active export (mirroring the ScreenshotContainer dialog pattern). prepareCanvasForCapture's tick() flushes it into the DOM before capture reads it.
P2: prepareCanvasForCapture force-set every component's visible store to true to bypass lazy-load, but never restored it. Since visible gates component queries, one export left all below-the-fold queries enabled for the rest of the session, re-running on every filter/time change. It now returns a restore closure that exportCanvasPdf calls in finally; the IntersectionObserver keeps observing those components, so lazy-load still works on later scroll. P3: the export button shows a spinner via loading but stays clickable (Button gates onClick on disabled, not loading), and onExport had no early return, so a double-click could start overlapping exports sharing one capture header. Add an early return when already exporting.
P3 (observer race): restoreVisibility set components back to visible=false, but the IntersectionObserver unobserves a component once it intersects and visible is otherwise a one-way latch. A component that intersected during export would end up unobserved AND hidden, so it never lazy-loaded again. CanvasComponent now re-observes whenever visible transitions back to false: genuinely on-screen components snap back to true, off-screen ones lazy-load on later scroll. P3 (missing title): the exported PDF never rendered the dashboard title; assemblePdf only set PDF metadata and drew the footer. It now draws the title as vector text in a band reserved at the top of the first page (paginate's titleReservePt), matching the design the CanvasPdfExportHeader comment already described. Long titles are ellipsized to the content width.
Comment on lines
+67
to
+75
| // UTC, e.g. "2026-06-19 02:20 UTC". | ||
| const generatedAt = `${new Date().toISOString().replace("T", " ").slice(0, 16)} UTC`; | ||
| await assemblePdf(pagination, { | ||
| title, | ||
| filename: buildPdfFilename(title), | ||
| backgroundColor, | ||
| generatedAt, | ||
| dashboardUrl: window.location.href, | ||
| }); |
Comment on lines
+68
to
+76
| export function isCanvasExportQuery( | ||
| queryKey: QueryKey, | ||
| instanceId: string, | ||
| ): boolean { | ||
| const [service] = queryKey; | ||
| if (service === "metrics_sql") return true; | ||
| if (service !== "QueryService") return false; | ||
| return queryKey.includes(instanceId); | ||
| } |
The title reserve started cursorYPt below the margin, but the page-advance guards used 'cursorYPt > marginPt' as a proxy for 'this page already has content'. With the reserve that proxy was always true on page 0, so a tall first component (or any first row that fits a full page but not the title band) advanced to page 1 before placing anything, leaving page 0 with only the title and footer. Track each page's content-top (pageTopPt) and advance only when content was actually placed (atPageTop). Slicing is now title-band-aware: the first slice fills page 0 below the title and the remainder flows onto later full-height pages. Non-title pagination is unchanged (reserve 0).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds a PDF tab to the share popover for canvas dashboards, allowing users to export the rendered canvas as a downloadable PDF.
web-common/src/features/exports/pdf/module: captures the canvas, expands tables, settles async rendering, lays out pages, and assembles the PDF viajspdf.ShareDashboardPopovergains a "PDF" tab, shown only for canvas dashboards (whencanvasNameandinstanceIdare provided).CanvasDashboardWrapperrenders an off-screenCanvasPdfExportHeaderused solely as the PDF capture target.jspdfdependency.Checklist:
Developed in collaboration with Claude Code