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 diagrams
Next
Snowstorm