Pipeline
An orchestration graph: stages on the cadastral canvas, joined by cables that show where data is moving.
The graph
A stage declares a column and a row; a link names two ids. Every coordinate and every cable is computed from those — so moving a stage is one number, and the edges that touch it follow.
Code
<GdPipeline
:stages="stages"
:links="links"
v-model:selected="selected"
interactive
@select="open"
/>
<!-- a stage places itself by CELL, never by pixel -->
{ id: "parse", title: "Parse scheme layers", icon: "layers",
status: "running", progress: 62, column: 2, row: 0 }
<!-- a link names two ids; its appearance is derived -->
{ from: "fetch", to: "parse" }Props
stagesrequiredGdPipelineStage[]—The graph. Each stage carries a `column` and `row` — grid cells, not pixels.
linksGdPipelineLink[][]Dependencies by id. A link naming a stage that is not present draws nothing rather than throwing.
selectedstring—Id of the stage drawn as selected.
interactivebooleanfalseStages render as buttons and emit `select`.
gridbooleantrueDraw the cadastral grid as the canvas ground.
Events
select[id: string]A stage was activated. Only fires with `interactive`.
The inspector rail
Selecting a stage has to open something, or the click is a lie. The rail is a composition, not a component — GdSidePanel holding GdPanelSection blocks — because its sections are one app's content. A GdPipelineInspector that hard-coded "Configuration / Output / Live log" would have exactly one consumer, and the next console would need its own.
What the system does ship is the two primitives those blocks needed and did not have: GdKeyFigures for the output counts, and GdLogStream for the run log.
Dragged, not scrolled
A graph is a place, not a document, so the canvas is dragged. Scrollbars are the giveaway that a spatial surface is being presented as a long page: they occupy two edges, they imply a beginning and an end a graph does not have, and they turn "look at the node to the left" into two gestures.
- Zoom is on the modifier.
Ctrl/⌘and the wheel, which is also what a trackpad pinch arrives as, plus the cluster at the bottom-left and+−01on the keyboard. Zooming keeps the point under the cursor fixed — zooming about the origin is the version everyone writes first, and it throws the graph off-screen as soon as you are not already at 0,0. Fit never zooms past 100%: a three-stage pipeline magnified to fill a 1400px viewport looks broken rather than fitted. - A bare wheel is deliberately not captured. Panning on wheel is the classic map-in-a-page trap — a reader scrolling the page reaches the canvas, the canvas eats the wheel, and the page stops. Dragging is unambiguous, so the wheel is left to the document.
- Removing scrollbars removes keyboard access, which is the easy part to forget: a scroll container moves with the arrow keys for free, a transform does not. The surface takes focus and pans on the arrow keys, and tabbing to a stage brings that stage into view — a focused card off-screen is a focus trap wearing a different hat.
- The ground travels with the content. A fixed grid under a moving graph is worse than no grid: the cards appear to slide over a static sheet, which reads as a rendering fault rather than a camera move.
- Panning is clamped, so the graph can never be dragged off its own canvas and lost.
A cable's appearance is derived
Links carry no styling of their own. The rule is stated once and every edge follows it, so a status change re-colours exactly the right cables and the graph cannot claim a path is live into a stage that finished an hour ago.
- The target is running — accent, dashed, marching. Data is moving now.
- Both ends are done — accent, solid. This path has carried data.
- Anything else — the hairline. Nothing has happened here yet.
The dash offset animates by exactly four dashes, so the loop is seamless; any other figure shows a jump once per cycle. Under prefers-reduced-motion the cable keeps its colour and stops moving — "live" stays legible without a conveyor that never stops.
Six states
They differ in weight as well as colour, because someone scanning forty cards is looking for the two that need them.
done
running
queued
warn
failed
skipped
empty
queuedhas no dot. A status colour for "not started" is a colour that means nothing.failedis not louder thanrunning. A failure is already terminal; a run is the thing being watched.emptyhas no shadow. That is the load-bearing part — a placeholder with a card's shadow reads as a real card that failed to render.
Props
titlerequiredstring—The stage's name — one line, truncated rather than wrapped.
substring—The quiet second line: transport, schedule, tool.
iconGdIconName—A glyph from the registry, on the plaque.
status"done" | "running" | "queued" | "warn" | "failed" | "empty""done"See the six states below. Only `running` animates, and only one stage should hold it.
metastring—The mono line: counts and durations, set in figures that stack.
progressnumber—0–100. Drawn only while `running`.
interactivebooleanfalseRender a `button` and emit `select`.
selectedbooleanfalseDraw the accent edge without claiming the stage is running.
Events
select[]The stage was activated.
Why cables meet the header
Cards are different heights — a running stage carries a progress bar, an empty one has no meta line. Anchoring a cable at the card's vertical centre would make every line into a running stage kink downward relative to its neighbours. The header row sits at a fixed offset from the top in every state, so lines stay level across a row whatever the cards below them are doing.