Mermaid sequential pipeline with custom shapes
Paste a Mermaid flowchart or graph, run a simulated CI-style DAG on the nodes (merge = wait for all parents), and retry failed steps from the canvas.
/* eslint-disable react-hooks/rules-of-hooks */
/**
* Mermaid flowchart → DAG pipeline demo: paste flowchart/graph source, apply to the canvas, run simulated steps.
* - `blueprintRender.mapNodeToRenderSpec` maps each flowchart vertex to `flowchart-util` + `mermaidNodeId`.
* - After import, graph and layer badges come from arrows + bindings (`extractFlowchartPipelineFromEditor`).
*/
import { useCallback, useState } from 'react'
import { TLComponents, Tldraw, TldrawUiButton, useEditor, useValue } from 'tldraw'
import 'tldraw/tldraw.css'
import { FlowchartShapeUtil } from './customMermaidShapeUtil'
import './custom-shape-mermaid.css'
import { getFlowchartSourceError } from './flowchartSourceGuard'
import { mapNodeToRenderSpec } from './mermaidPipelineBlueprint'
import { type StepStatus, pipelineStateAtom, runFullPipeline } from './mermaidPipelineState'
import { applyPipelineStepIndices, extractFlowchartPipelineFromEditor } from './pipelineFromEditor'
const components: TLComponents = {
TopPanel: TopPanel,
}
const customShapes = [FlowchartShapeUtil]
const DEFAULT_MERMAID = `flowchart LR
s1[Checkout] --> s2[Build]
s2 --> s3[Unit tests]
s2 --> s4[Integration tests]
s3 --> s5[Deploy]
s4 --> s5`
export default function MermaidDiagramsCustomShapes() {
return (
<div className="tldraw__editor">
<Tldraw components={components} shapeUtils={customShapes} />
</div>
)
}
function TopPanel() {
const editor = useEditor()
const [mermaidText, setMermaidText] = useState(DEFAULT_MERMAID)
const [isApplying, setIsApplying] = useState(false)
const pipeline = useValue(pipelineStateAtom)
const canRun =
!pipeline.parseError && pipeline.nodeIds.length > 0 && !pipeline.isRunning && !isApplying
const applyWorkflow = useCallback(async () => {
if (isApplying) return
setIsApplying(true)
try {
const sourceError = getFlowchartSourceError(mermaidText)
if (sourceError) {
pipelineStateAtom.set({
nodeIds: [],
edges: [],
statusByNodeId: {},
parseError: sourceError,
isRunning: false,
})
return
}
const [{ createMermaidDiagram }, { default: mermaid }] = await Promise.all([
import('@tldraw/mermaid'),
import('mermaid'),
])
mermaid.initialize({
startOnLoad: false,
flowchart: { useMaxWidth: false, nodeSpacing: 80, rankSpacing: 80, padding: 20 },
})
editor.selectAll()
editor.deleteShapes(editor.getSelectedShapes())
try {
await createMermaidDiagram(editor, mermaidText, {
blueprintRender: {
position: { x: 200, y: 400 },
centerOnPosition: false,
mapNodeToRenderSpec,
},
})
} catch {
pipelineStateAtom.set({
nodeIds: [],
edges: [],
statusByNodeId: {},
parseError: `An error occurred; please make sure your diagram is valid.`,
isRunning: false,
})
return
}
const parsed = extractFlowchartPipelineFromEditor(editor)
if (!parsed.ok) {
pipelineStateAtom.set({
nodeIds: [],
edges: [],
statusByNodeId: {},
parseError: parsed.error,
isRunning: false,
})
} else {
pipelineStateAtom.set({
nodeIds: parsed.nodeIds,
edges: parsed.edges,
statusByNodeId: Object.fromEntries(
parsed.nodeIds.map((id) => [id, 'pending' as const])
) as Record<string, StepStatus>,
parseError: null,
isRunning: false,
})
applyPipelineStepIndices(editor, parsed.stepIndexByNodeId)
}
editor.selectNone()
} catch {
pipelineStateAtom.update((s) => ({
...s,
parseError: `An error occurred; please make sure your diagram is valid.`,
}))
} finally {
setIsApplying(false)
}
}, [editor, isApplying, mermaidText])
const runPipeline = useCallback(() => {
void runFullPipeline()
}, [])
return (
<div className="custom-shape-mermaid">
<div>
Paste a Mermaid <strong>flowchart</strong> or <strong>graph</strong> (branching is ok; merge
nodes run after <strong>all</strong> incoming steps pass). Apply runs Mermaid import, then
builds the DAG from <strong>arrows on the canvas</strong>. Run simulates steps; failures can
be retried on the shape. Step badges are Kahn layers, not a global sequence. Status is only
in memory for this demo.
</div>
<textarea
value={mermaidText}
onChange={(e) => setMermaidText(e.target.value)}
rows={7}
spellCheck={false}
className="custom-shape-mermaid__textarea"
/>
{pipeline.parseError && (
<div className="custom-shape-mermaid__error">{pipeline.parseError}</div>
)}
<div className="custom-shape-mermaid__controls">
<TldrawUiButton type="normal" onClick={applyWorkflow} disabled={isApplying}>
{isApplying ? 'Applying…' : 'Apply workflow'}
</TldrawUiButton>
<TldrawUiButton type="low" onClick={runPipeline} disabled={!canRun}>
Run pipeline
</TldrawUiButton>
</div>
{pipeline.isRunning && (
<div className="custom-shape-mermaid__notice">Running simulated steps…</div>
)}
</div>
)
}
This example accepts flowchart / graph source only (validated before import). It uses @tldraw/mermaid with blueprintRender.mapNodeToRenderSpec so vertices become custom flowchart-util shapes with mermaidNodeId. After import, edges must form a DAG (no cycles). Step schedule follows AND-join semantics: a node runs when all its predecessors have passed. Step badges are Kahn layers (same layer = same badge number). The graph is read from tldraw arrow bindings (extractFlowchartPipelineFromEditor), not from parsing edge syntax in the text box.
Is this page helpful?
Prev
Mermaid diagramsNext
Snowstorm