Skip to content

feat(ui): share kit - graph export and copy links - #2534

Merged
yohamta0 merged 18 commits into
mainfrom
ui-share-kit
Aug 8, 2026
Merged

yohamta0 merged 18 commits into
mainfrom
ui-share-kit

Conversation

@yohamta0

@yohamta0 yohamta0 commented Aug 8, 2026 •

Copy link
Copy Markdown
Member

Summary

The audit's weakest area against the adoption goal was shareability: the rendered DAG graph SVG sat in the DOM with no way out, and sharing anything meant OS screenshots or the address bar.

  • Graph export. The graph control bar (both normal and expanded views, status and config graphs) gains Export as PNG and Export as SVG. The serialized clone strips the on-screen zoom transform, pins explicit dimensions from the viewBox, converts mermaid's HTML (`foreignObject`) labels into positioned SVG `` styled from the rendered labels, and bakes the card background so dark-theme exports stay readable outside the app. The label conversion matters twice: `foreignObject` content taints the canvas in Chromium (PNG export would throw `SecurityError`), and it does not render in non-browser SVG tools. PNG renders at 2x with the longest edge clamped to 8192px.
  • Copy workflow link. The DAG header gains a copy-link button beside copy-name, honoring the workspace/remote-node scoping of `buildScopedUrl` and the configured base path.
  • Shared download helper. The identical blob-and-anchor download sequence copied across StepLog, ExecutionLog, and ArtifactsTab consolidates into `lib/download` (bearer auth + Content-Disposition filename).

Stacked on #2531 (uses its `useCopyFeedback` hook).

Validation

  • `cd ui && pnpm test` (629 tests; new download-helper suite; Graph suite covers SVG serialization: dimensions pinned, background rect, transform stripped, labels converted to text, filename)
  • `pnpm exec tsc --noEmit`, `pnpm build`
  • Live browser verification (headless Chromium against a real server): PNG download carries real PNG magic bytes with the `{dag}-graph.png` filename, SVG download is self-contained with node markup, and the copy-link button puts the workflow URL on the clipboard. The taint bug and an earlier selector bug (export grabbing the toolbar's first icon SVG instead of the graph) were both caught by this verification.

Summary by CodeRabbit

  • New Features

    • Added PNG and SVG export controls for DAG graphs, using the DAG name in downloaded filenames.
    • Added a button to copy the current DAG page link.
    • Improved graph exports with readable labels, theme-aware backgrounds, and expanded-view support.
  • Improvements

    • Standardized artifact, execution log, and step log downloads.
    • Added clearer handling for download filenames and download errors.

yohamta0 added 13 commits August 8, 2026 13:14
The mount-time formatDocument rewrote files the user never touched, lighting up Save/Discard and arming the external-change conflict dialog on first open. formatOnType reformatted the whole document on every Enter. Manual format (Shift+Alt+F) still works.
Backend validation errors (line:column prefixes and caret excerpts) were passed as the modal's hint, which collapses newlines into one muted proportional-font paragraph. Add a details channel rendered monospace with line breaks preserved, and stop treating an empty errors array as a failure on spec save.
The retry action re-executes only the selected step, but the tooltip and dialog title said "Retry from this step". Rename to match the API contract, and give the success path the same toast plus refresh the run-level retry already has instead of silently closing the dialog.
The header button said Enqueue while its tooltip, the modal it opens, and the CLI all say start. The modal still switches to Enqueue wording when the queue toggle is on.
The executions list rendered "No DAG runs found" while the first page was still loading, then swapped in the table. Thread the pagination hook's initial-loading flag into both list views and show a quiet inline loading row instead.
The workflow list told users with zero workflows to adjust filters they never set. Branch the copy: a pristine all-workflows view now invites creating the first workflow. The runs empty state is reworded to be range-aware instead of presuming filters, and the duplicated workflow empty-state markup is extracted into one component.
The schema doc sidebar has an Examples panel, and monaco-yaml surfaces schema examples in hover, value completion, and property snippets, but the schema carried none. Seed concise examples for the most-used DAG and step properties.
Every tab, bookmark, and history entry read the static "Dagu". Pages already publish their titles through AppBarContext; mirror that into document.title.
Sharing a workflow required select-all inside Monaco, and sharing a run meant grabbing the address bar. Add a Copy button to the spec editor header (visible to read-only viewers too) and a Copy link button on the run header. The three hand-rolled copy-feedback blocks consolidate into a useCopyFeedback hook.
…used

The v1 executor key was rejected with a bare invalid-keys error while 70+ other legacy keys get migration hints, and renamed keys (precondition, dir) were mislabeled as snake_case fixes. Split the hint map into casing renames and removed keys with full replacement clauses, and drop the dead run->call entry (run is a valid v2 step key).
The same blob-and-anchor download sequence was copied across the step log, execution log, and artifacts tab. Consolidate into lib/download with Content-Disposition filename handling and bearer auth.
The rendered graph SVG existed in the DOM with no way out; sharing it meant an OS screenshot. The graph control bar gains Export as PNG and Export as SVG actions: the serialized clone strips the on-screen zoom transform, pins dimensions from the viewBox, and bakes the card background so dark-theme exports stay readable. Mermaid embeds its styles and the status strokes are inline, so the file is self-contained. The DAG header also gains a copy-link button beside copy-name.
@coderabbitai

coderabbitai Bot commented Aug 8, 2026 •

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 2f077203-7e7f-47d8-8af4-13748c3fdb26

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The PR adds shared download helpers, centralizes artifact and log downloads, adds DAG page-link copying, and adds named PNG/SVG export controls for Mermaid-rendered graphs.

Changes

DAG UI enhancements

Layer / File(s) Summary
Shared download utilities and integrations
ui/src/lib/download.ts, ui/src/lib/__tests__/download.test.ts, ui/src/features/dags/components/artifacts/ArtifactsTab.tsx, ui/src/features/dags/components/dag-execution/ExecutionLog.tsx, ui/src/features/dags/components/dag-execution/StepLog.tsx
Shared helpers now handle blob downloads, authenticated URL downloads, filename selection, response errors, and URL cleanup. Artifact and log downloads use these helpers.
Graph export serialization and rasterization
ui/src/features/dags/components/visualization/exportGraph.ts
Graph export utilities serialize Mermaid SVG output, replace foreignObject labels, add backgrounds and dimensions, and rasterize bounded PNG output.
Graph export controls and named graph wiring
ui/src/features/dags/components/visualization/Graph.tsx, ui/src/features/dags/components/visualization/DAGGraph.tsx, ui/src/features/dags/components/DAGStatus.tsx, ui/src/features/dags/components/dag-editor/DAGSpec.tsx, ui/src/features/dags/components/dag-editor/DAGSpecReadOnly.tsx, ui/src/features/dags/components/visualization/__tests__/Graph.test.tsx
Graph accepts a DAG name, provides PNG and SVG export controls, forwards the name to expanded views, and receives names from its callers. Tests cover SVG export output and PNG control rendering.
DAG page-link copying
ui/src/features/dags/components/dag-details/DAGHeader.tsx
DAGHeader builds a base-path-aware DAG URL and provides a copy button with copied-state and accessible status feedback.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Graph
  participant exportGraphSvg
  participant exportGraphPng
  participant downloadBlob
  Graph->>exportGraphSvg: Export Mermaid SVG
  exportGraphSvg->>downloadBlob: Download serialized SVG
  Graph->>exportGraphPng: Export Mermaid PNG
  exportGraphPng->>downloadBlob: Download rasterized PNG
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description gives a detailed summary and validation results but omits the required Changes, Related Issues, and Checklist sections. Add the required Changes, Related Issues, and Checklist sections, and record the applicable issue and checklist status.
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main changes: graph export and link sharing in the UI.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch ui-share-kit

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Chromium marks a canvas tainted when the drawn SVG contains foreignObject, so PNG export threw SecurityError at toBlob (caught in live browser verification). Replace mermaid's HTML labels with positioned SVG text in the export clone, styled from the rendered labels; this also makes the exported SVG render in non-browser tools.
Constructing Response from a Blob requires Blob.stream, which the CI jsdom/Node combination does not provide; the suite failed there with 'object.stream is not a function'. Plain response stubs cover the same behavior.
Base automatically changed from ui-ux-cheap-wins to main August 8, 2026 06:52
# Conflicts:
#	ui/src/__tests__/App.test.tsx
#	ui/src/features/dags/components/dag-details/DAGHeader.tsx
#	ui/src/features/dags/components/visualization/Graph.tsx

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@ui/src/features/dags/components/visualization/exportGraph.ts`:
- Around line 41-51: The label conversion must preserve the source foreignObject
position. In the conversion logic around the created SVG text element, read the
source x and y attributes and offset the centered text coordinates by those
values while retaining width/height centering; add a regression test covering a
foreignObject label positioned away from the origin.

In `@ui/src/lib/download.ts`:
- Around line 12-13: Defer URL cleanup in the download helper after link.click()
using a scheduled delay rather than revoking the object URL in the same task.
Update ui/src/lib/download.ts lines 12-13 accordingly, and modify
ui/src/lib/__tests__/download.test.ts lines 34-38 to use fake timers and verify
revocation occurs only after the scheduled delay.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 6c61cee9-10e2-43f7-aab6-e64731553ab1

📥 Commits

Reviewing files that changed from the base of the PR and between 1768fb0 and f5d886f.

📒 Files selected for processing (13)
  • ui/src/features/dags/components/DAGStatus.tsx
  • ui/src/features/dags/components/artifacts/ArtifactsTab.tsx
  • ui/src/features/dags/components/dag-details/DAGHeader.tsx
  • ui/src/features/dags/components/dag-editor/DAGSpec.tsx
  • ui/src/features/dags/components/dag-editor/DAGSpecReadOnly.tsx
  • ui/src/features/dags/components/dag-execution/ExecutionLog.tsx
  • ui/src/features/dags/components/dag-execution/StepLog.tsx
  • ui/src/features/dags/components/visualization/DAGGraph.tsx
  • ui/src/features/dags/components/visualization/Graph.tsx
  • ui/src/features/dags/components/visualization/__tests__/Graph.test.tsx
  • ui/src/features/dags/components/visualization/exportGraph.ts
  • ui/src/lib/__tests__/download.test.ts
  • ui/src/lib/download.ts

Comment on lines +41 to +51
const width = Number(label.getAttribute('width') ?? 0);
const height = Number(label.getAttribute('height') ?? 0);

const text = document.createElementNS(
'http://www.w3.org/2000/svg',
'text'
);
text.setAttribute('x', String(width / 2));
text.setAttribute('y', String(height / 2));
text.setAttribute('text-anchor', 'middle');
text.setAttribute('dominant-baseline', 'central');

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== locate file =="
fd -a 'exportGraph\.ts$' . || true

echo "== file outline/contents =="
if [ -f ui/src/features/dags/components/visualization/exportGraph.ts ]; then
  wc -l ui/src/features/dags/components/visualization/exportGraph.ts
  cat -n ui/src/features/dags/components/visualization/exportGraph.ts
fi

echo "== related tests =="
git ls-files | rg 'exportGraph|visualization|mermaid|dags/components/visualization' || true

echo "== searches for replaceForeignObjectLabels/usages =="
rg -n "replaceForeignObjectLabels|foreignObject|exportGraph" .

Repository: dagucloud/dagu

Length of output: 8947


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Graph tests around export behavior =="
sed -n '140,230p' ui/src/features/dags/components/visualization/__tests__/Graph.test.tsx | cat -n

echo "== mermaid fixtures/usages mentioning foreignObject? =="
rg -n "foreignObject|x=\"|y=|rendered\"|serializeGraphSvg|exportGraphSvg" ui/src/components/ui ui/src/features/dags/components/visualization --glob '*.ts' --glob '*.tsx' || true

Repository: dagucloud/dagu

Length of output: 6778


🌐 Web query:

MDN SVG foreignObject x y width height coordinate system semantics

💡 Result:

The SVG foreignObject element is a container that allows for the inclusion of elements from a different XML namespace (most commonly (X)HTML) within an SVG document [1]. It acts as a bridge, enabling the rendering of non-SVG content inside an SVG graphic [2][3]. Coordinate System and Positioning The foreignObject element uses the SVG coordinate system to define its placement and size [2][4]. Its positioning and dimensions are governed by four primary attributes: - x and y: These attributes define the coordinate of the top-left corner of the rectangular region where the foreign content will be rendered [1][3]. They default to 0 [1]. - width and height: These attributes define the dimensions of this rectangular region [1][3]. They default to auto [1]. In SVG2, these four attributes are categorized as Geometry Properties, meaning they can be set either as XML attributes or as CSS properties [1]. Semantics and Layout The foreignObject element serves a specific dual purpose regarding layout: 1. Positioning Rectangle: The x, y, width, and height attributes define a "positioning rectangle" in the SVG coordinate space [2][5]. This rectangle functions as the bounding box for the element [5]. 2. CSS Containing Block: For the child content (such as HTML), the foreignObject establishes a new CSS containing block [2][6]. This effectively resets the scope of layout for that child content [4][6]. The child content is laid out within this block using standard CSS rules [2]. Any absolutely positioned elements inside the foreignObject are positioned relative to this containing block [2][5]. Interaction with SVG - Transformations: The foreign content is subject to the same SVG transformations (e.g., rotate, scale, translate), filters, clipping, and masking as any other SVG element [2][3]. - Overflow: The CSS overflow property determines whether child content that extends outside the defined positioning rectangle is clipped or visible [2][5]. - Rendering: Because the foreignObject container is an SVG element, it is part of the SVG rendering tree. However, the browser uses different processes to render the nested foreign content [2][3]. If you wish to nest SVG elements inside the foreignObject, you must include a root svg element within it to re-establish an SVG document fragment and viewport [2][4]. Top Results: [1][2][4][6][5]

Citations:


🌐 Web query:

SVG text dominant-baseline central default behavior x y text-anchor middle

💡 Result:

In SVG, the dominant-baseline and text-anchor attributes work together to define the position of the text relative to the coordinates specified by the x and y attributes [1]. Dominant-Baseline The dominant-baseline attribute controls the vertical alignment of the text by specifying which baseline should be aligned to the y coordinate [2][1]. - Default Behavior: The default value is auto [2][3]. For a horizontal writing mode, auto computes to alphabetic [2][4]. For a vertical writing mode, auto computes to central [2][4][3]. - Central: When dominant-baseline="central" is explicitly set, the baseline identifier is set to central, effectively aligning the vertical midpoint of the font's em-box (or the constructed baseline table) with the y coordinate [2][3]. Text-Anchor The text-anchor attribute controls the horizontal alignment of the text relative to the x coordinate [5][1]. - Middle: When text-anchor="middle" is used, the geometric midpoint of the rendered text string is aligned exactly with the x coordinate [5][6]. Combined Interaction When you use dominant-baseline="central" and text-anchor="middle" on an SVG text element, you are instructing the rendering engine to place the geometric center of the text exactly at the point defined by the x and y attributes [5][1]. This is a common technique for centering text elements within a specific area or relative to a coordinate point [1]. Summary table for defaults (when auto is used): Writing Mode | Dominant-Baseline (Computed) | Text-Anchor (Default) --- | --- | --- Horizontal | Alphabetic | Start Vertical | Central | Start

Citations:


Preserve the source foreignObject coordinates when converting labels.

foreignObject uses x, y, width, and height to define its rectangle. Replacing it with centered text without adding the source x and y shifts labels that are not at the origin. Preserve the source coordinates before centering, and add a regression test with a positioned foreignObject label.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ui/src/features/dags/components/visualization/exportGraph.ts` around lines 41
- 51, The label conversion must preserve the source foreignObject position. In
the conversion logic around the created SVG text element, read the source x and
y attributes and offset the centered text coordinates by those values while
retaining width/height centering; add a regression test covering a foreignObject
label positioned away from the origin.

Comment thread ui/src/lib/download.ts Outdated
Comment on lines +12 to +13
link.click();
URL.revokeObjectURL(objectUrl);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Defer object URL cleanup after browser download navigation.

The helper revokes the object URL in the same task as link.click(). The test preserves that unsafe timing.

  • ui/src/lib/download.ts#L12-L13: schedule URL.revokeObjectURL(objectUrl) after link.click().
  • ui/src/lib/__tests__/download.test.ts#L34-L38: use fake timers and verify cleanup after the scheduled delay.
Proposed fix
-  URL.revokeObjectURL(objectUrl);
+  window.setTimeout(() => URL.revokeObjectURL(objectUrl), 0);
📍 Affects 2 files
  • ui/src/lib/download.ts#L12-L13 (this comment)
  • ui/src/lib/__tests__/download.test.ts#L34-L38
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ui/src/lib/download.ts` around lines 12 - 13, Defer URL cleanup in the
download helper after link.click() using a scheduled delay rather than revoking
the object URL in the same task. Update ui/src/lib/download.ts lines 12-13
accordingly, and modify ui/src/lib/__tests__/download.test.ts lines 34-38 to use
fake timers and verify revocation occurs only after the scheduled delay.

Exported label text picked up mermaid's shape-oriented stylesheet inside the SVG (cream fill plus a thick stroke in the node color), rendering as unreadable outlines; inline styles on the replacement text now carry the rendered label's color and font. Labels also keep the source foreignObject's x/y offset instead of assuming the origin. Object URL revocation moves out of the click task so the download cannot be aborted by immediate cleanup.
@yohamta0
yohamta0 merged commit 30924c8 into main Aug 8, 2026
11 checks passed
@yohamta0
yohamta0 deleted the ui-share-kit branch August 8, 2026 09:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant