mermaid workflow coloring

This commit is contained in:
Ian Gulliver
2026-10-16 11:25:53 -07:00
parent 6abd1d8cfa
commit 4dff9b7471
8 changed files with 98 additions and 11 deletions
+18
View File
@@ -42,6 +42,24 @@ export function diagram(src) {
void render(el, src);
return el;
}
const latest = new WeakMap();
export async function redraw(el, src) {
const id = next++;
latest.set(el, id);
try {
const m = await load();
const { svg } = await m.render(`mermaid-${id}`, src);
if (latest.get(el) === id) {
el.innerHTML = svg;
}
}
catch (err) {
if (latest.get(el) !== id) {
return;
}
el.replaceChildren(h("pre", { class: "mermaid-source" }, src), h("div", { class: "mermaid-error" }, `diagram not rendered: ${errorText(err)}`));
}
}
async function render(el, src) {
try {
const m = await load();
+1 -1
View File
@@ -1 +1 @@
{"version":3,"file":"mermaid.js","sourceRoot":"","sources":["../src/mermaid.ts"],"names":[],"mappings":"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"}
{"version":3,"file":"mermaid.js","sourceRoot":"","sources":["../src/mermaid.ts"],"names":[],"mappings":"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"}
+28 -4
View File
@@ -1,13 +1,27 @@
import { get } from "./api.js";
import { errorText, h, replace } from "./dom.js";
import { markdown } from "./markdown.js";
import { diagram } from "./mermaid.js";
import { diagram, redraw } from "./mermaid.js";
import { findTask, focusTask, onEach, state } from "./state.js";
const stateColors = {
waiting: ["--surface", "--border", "--muted"],
running: ["--accent-tint", "--accent", "--text"],
idle: ["--warn-tint", "--warn", "--text"],
finished: ["--ok-tint", "--ok", "--text"],
merged: ["--ok-tint", "--ok", "--text"],
cleaned_up: ["--ok-tint", "--ok", "--text"],
discarded: ["--danger-tint", "--danger", "--text"],
};
function graph(run) {
const css = getComputedStyle(document.documentElement);
const v = (name) => css.getPropertyValue(name).trim();
const id = new Map(run.stages.map((s, i) => [s.name, `s${i}`]));
const lines = ["flowchart TD"];
for (const [state, [fill, stroke, text]] of Object.entries(stateColors)) {
lines.push(` classDef ${state} fill:${v(fill)},stroke:${v(stroke)},color:${v(text)}`);
}
for (const s of run.stages) {
lines.push(` ${id.get(s.name)}["${s.name.replaceAll('"', "#quot;")}"]`);
lines.push(` ${id.get(s.name)}["${s.name.replaceAll('"', "#quot;")}"]:::${s.state}`);
}
for (const s of run.stages) {
for (const a of s.after) {
@@ -75,6 +89,8 @@ export function mountWorkflow(root) {
const list = h("div", { class: "workflow-body" });
let run;
let drawnRun = "";
let drawnSrc = "";
let drawn;
let error = "";
let target = 0;
let loading = false;
@@ -122,6 +138,7 @@ export function mountWorkflow(root) {
replace(head);
replace(graphBox);
drawnRun = "";
drawn = undefined;
replace(list, h("div", { class: "empty" }, task ? "This task isn't part of a workflow run." : "No task selected."));
return;
}
@@ -134,9 +151,16 @@ export function mountWorkflow(root) {
return;
}
replace(head, h("span", { class: "workflow-name" }, run.name), h("span", { class: "workflow-title" }, run.title), h("span", { class: `workflow-state ${run.finished ? "finished" : "running"}` }, run.finished ? "workflow finished" : "in progress"));
if (drawnRun !== run.run) {
const src = graph(run);
if (drawnRun !== run.run || !drawn) {
drawnRun = run.run;
replace(graphBox, diagram(graph(run)));
drawnSrc = src;
drawn = diagram(src);
replace(graphBox, drawn);
}
else if (drawnSrc !== src) {
drawnSrc = src;
void redraw(drawn, src);
}
replace(list, run.brief.trim() ? h("details", { class: "workflow-brief" }, h("summary", {}, "Brief"), markdown(run.brief, "md", { taskId: task.id })) : null, h("ol", { class: "workflow-stages" }, ...run.stages.map((s) => stageCard(run, s))));
};
+1 -1
View File
File diff suppressed because one or more lines are too long
+19
View File
@@ -57,6 +57,25 @@ export function diagram(src: string): HTMLElement {
return el;
}
const latest = new WeakMap<HTMLElement, number>();
export async function redraw(el: HTMLElement, src: string) {
const id = next++;
latest.set(el, id);
try {
const m = await load();
const { svg } = await m.render(`mermaid-${id}`, src);
if (latest.get(el) === id) {
el.innerHTML = svg;
}
} catch (err) {
if (latest.get(el) !== id) {
return;
}
el.replaceChildren(h("pre", { class: "mermaid-source" }, src), h("div", { class: "mermaid-error" }, `diagram not rendered: ${errorText(err)}`));
}
}
async function render(el: HTMLElement, src: string) {
try {
const m = await load();
+28 -4
View File
@@ -1,18 +1,33 @@
import { get, type ByreEvent, type Schemas } from "./api.js";
import { errorText, h, replace } from "./dom.js";
import { markdown } from "./markdown.js";
import { diagram } from "./mermaid.js";
import { diagram, redraw } from "./mermaid.js";
import { findTask, focusTask, onEach, state } from "./state.js";
type WorkflowRun = Schemas["WorkflowRun"];
type RunStage = Schemas["WorkflowRunStage"];
type Attempt = Schemas["StageAttempt"];
const stateColors: Record<RunStage["state"], [fill: string, stroke: string, text: string]> = {
waiting: ["--surface", "--border", "--muted"],
running: ["--accent-tint", "--accent", "--text"],
idle: ["--warn-tint", "--warn", "--text"],
finished: ["--ok-tint", "--ok", "--text"],
merged: ["--ok-tint", "--ok", "--text"],
cleaned_up: ["--ok-tint", "--ok", "--text"],
discarded: ["--danger-tint", "--danger", "--text"],
};
function graph(run: WorkflowRun): string {
const css = getComputedStyle(document.documentElement);
const v = (name: string) => css.getPropertyValue(name).trim();
const id = new Map(run.stages.map((s, i) => [s.name, `s${i}`]));
const lines = ["flowchart TD"];
for (const [state, [fill, stroke, text]] of Object.entries(stateColors)) {
lines.push(` classDef ${state} fill:${v(fill)},stroke:${v(stroke)},color:${v(text)}`);
}
for (const s of run.stages) {
lines.push(` ${id.get(s.name)}["${s.name.replaceAll('"', "#quot;")}"]`);
lines.push(` ${id.get(s.name)}["${s.name.replaceAll('"', "#quot;")}"]:::${s.state}`);
}
for (const s of run.stages) {
for (const a of s.after) {
@@ -113,6 +128,8 @@ export function mountWorkflow(root: HTMLElement): () => void {
const list = h("div", { class: "workflow-body" });
let run: WorkflowRun | undefined;
let drawnRun = "";
let drawnSrc = "";
let drawn: HTMLElement | undefined;
let error = "";
let target = 0;
let loading = false;
@@ -161,6 +178,7 @@ export function mountWorkflow(root: HTMLElement): () => void {
replace(head);
replace(graphBox);
drawnRun = "";
drawn = undefined;
replace(list, h("div", { class: "empty" }, task ? "This task isn't part of a workflow run." : "No task selected."));
return;
}
@@ -178,9 +196,15 @@ export function mountWorkflow(root: HTMLElement): () => void {
h("span", { class: "workflow-title" }, run.title),
h("span", { class: `workflow-state ${run.finished ? "finished" : "running"}` }, run.finished ? "workflow finished" : "in progress"),
);
if (drawnRun !== run.run) {
const src = graph(run);
if (drawnRun !== run.run || !drawn) {
drawnRun = run.run;
replace(graphBox, diagram(graph(run)));
drawnSrc = src;
drawn = diagram(src);
replace(graphBox, drawn);
} else if (drawnSrc !== src) {
drawnSrc = src;
void redraw(drawn, src);
}
replace(
list,
+2
View File
@@ -49,6 +49,7 @@
--warn: #855600;
--warn-tint: #fdf3dc;
--ok: #1a7f37;
--ok-tint: #e5f4e9;
--task-0: #a4273f;
--task-1: #9f3501;
--task-2: #376e00;
@@ -81,6 +82,7 @@
--warn: #f2c66d;
--warn-tint: #352a14;
--ok: #7fd88f;
--ok-tint: #1b3322;
--task-0: #fe9fa6;
--task-1: #ffa383;
--task-2: #99d079;