adf-pipeline — parity with Azure Data Factory pipeline (Author / Integrate)¶
Source UI: ADF Studio → Author → Pipelines (https://learn.microsoft.com/azure/data-factory/concepts-pipelines-activities) Backend factory: adf-loom-default-eastus2 via ARM REST (Microsoft.DataFactory/factories, api-version 2018-06-01).
The bug this fixes¶
The Loom pipeline item is a Cosmos GUID. The old routes passed that GUID straight to getPipeline() as the Azure pipeline name → ADF returned 404 PipelineNotFound, and the missing /runs route returned a 404 HTML page that crashed res.json(). Fixed by a resource-binding model: the Loom item binds to a real ADF pipeline (persisted in state.pipelineName); every route resolves the bound name via resolveBinding() instead of the route id.
Real ADF Studio pipeline-editor inventory (grounded in Learn)¶
Source: author-visually#authoring-canvas + concepts-pipelines-activities#creating-a-pipeline-with-ui + tutorial-pipeline-failure-error-handling. The real editor has four regions:
- Activities pane (left) — searchable, categorized tree of every activity (Move & transform, Orchestration/Synapse-Databricks, General, Iteration & conditionals, …). Drag onto canvas to add.
- Authoring canvas (center) — activity cards (icon + name + type); drag from palette to add, drag to reposition, click to select. Dependency arrows between cards in the four conditional-path colours: Upon Success (green), Upon Failure (red), Upon Completion (blue), Upon Skip (gray) — each activity exposes the four output ports.
- Bottom configuration panel — when an activity is selected, the panel at the bottom of the canvas edits it (General + activity-specific Source/Sink/Settings + User properties + Activity policy: timeout/retry/retryInterval/secureOutput).
- Pipeline configurations pane — Parameters, Variables, (General) Settings (concurrency, annotations, description), Output (run monitor).
- Toolbar — Save, Validate, Debug, Trigger now / Add trigger; canvas controls zoom in/out/fit/auto-align.
Loom coverage¶
| ADF Studio capability | Loom coverage | Backend (real REST) |
|---|---|---|
| Pick a pipeline from the factory tree | ✅ built — left-panel tree from listPipelines() | GET factories/{f}/pipelines |
| Bind item to an existing pipeline | ✅ built — bind picker (Dropdown + Bind) | POST /api/items/adf-pipeline/[id]/bind → persistBinding (Cosmos) |
| Create a new pipeline | ✅ built — "Create & bind" | PUT factories/{f}/pipelines/{name} then bind |
| Activities pane — searchable categorized palette | ✅ built — ActivityPalette: search + 3 collapsible groups, 24 activity types | n/a (client) |
| Authoring canvas — cards, drag-to-add, drag-to-move, select | ✅ built — PipelineCanvas + ActivityNode: absolute-positioned cards, palette drag-drop, node reposition, pan/zoom, minimap | spec from GET .../pipelines/{name} |
| 4 dependency conditions (success/failure/completion/skip) | ✅ built — each card has 4 coloured output ports; drag a port → another card draws a dependsOn edge with that dependencyCondition; SVG arrows colour-coded | persisted in dependsOn[] on Save |
| Bottom configuration panel for selected activity | ✅ built — PropertiesPanel layout="dock": General / Source-Sink / Settings (policy: timeout, retry, retryInterval, secureOutput) / Parameters / User properties tabs | PUT .../pipelines/{name} |
| Nested control-flow sub-canvases (ForEach / Until / If / Switch drill-in) | ✅ built — pencil button on every container node + "Activities (n)" count; ForEach/Until also double-click to drill; breadcrumb trail (Pipeline > ForEach1 > True) pops back to any ancestor; the same palette / drag-drop / connect / properties operate on inner activities; If/Switch branch tabs (True/False, Default + add/remove cases); ADF nesting limits enforced (If/Switch not inside If/Switch; single-level loop nesting) with disabled palette tiles + tooltips | inner activities round-trip in typeProperties.activities / ifTrue|ifFalseActivities / defaultActivities / cases[].activities on the same PUT .../pipelines/{name} |
| Pipeline Parameters pane | ✅ built — ParametersPane: add/type/default/delete | round-trips properties.parameters on PUT |
| Pipeline Variables pane | ✅ built — VariablesPane: add/type/default/delete | round-trips properties.variables on PUT |
| Pipeline Settings pane (concurrency, annotations, description) | ✅ built — SettingsPane | round-trips properties.{concurrency,annotations,description} on PUT |
Canvas navigation controls on the RIGHT side (Fabric pipeline-canvas-experience#canvas-navigation: search, zoom in/out, zoom to fit, auto-align) | ✅ built — CanvasRightRail on a center-right React Flow Panel (was bottom-left), clear of the top-right power toolbar and the bottom-right MiniMap; toolbar Auto align / Zoom to fit mirror it | n/a (client) |
Configuration pane shows PIPELINE-LEVEL settings when no activity is selected (Fabric pipeline-canvas-experience#canvas-and-pipeline-settings: Parameters, Variables, Settings, Output) | ✅ built — pipeline-level-inspector.tsx: Parameters (live table: name/type/default), Variables (live table), Activities (clickable list with pre-run issue counts → selects the node), Settings (real derived pipeline facts + one-click jump to the host Settings / Output / Parameters panes). Replaces the former "No activity selected" EmptyState. Fabric's Settings and Output editing surfaces live in the host pipeline-configurations tab row (ADF Studio's own arrangement) and the dock links straight to them | client model + PUT .../pipelines/{name} on save |
| Collapsible bottom configuration pane | ✅ built — chevron in the dock header (now present in the no-selection branch too); collapse is lifted to PipelineDesigner so it also shrinks the split pane and returns the space to the canvas | n/a (client) |
| Resizable canvas ↔ configuration split (G3) | ✅ built — one vertical SplitPane (loom.splitpane.pipeline-designer.config-dock); the canvas fills everything above the divider, the dock is the sized pane with its own internal scroll | n/a (client) |
Canvas keyboard shortcuts (Fabric data-factory/keyboard-shortcuts) — I/O zoom, F fit, A auto-align, N toggle nested preview, Shift+arrows pan, Backspace = return to previous canvas | ✅ built — PipelineCanvasInner.handleKeyDown on the focusable (tabIndex=0) canvas shell; ignores keys while a text control is focused | n/a (client) |
Inline nested-activity preview (Fabric "updated canvas experience", data-factory/pipeline-canvas-experience) — container nodes summarise their inner activities (branch name + count + mini tiles) on the parent canvas; N / a "Nested" toolbar toggle shows/hides it | ✅ built — FlowActivityNode renders miniPreviewSections() (True/False, Default/Case, or Activities sections with +N more) when showNestedPreview is on | n/a (client) |
| Large-graph scalability — 200-node pipeline pans smoothly | ✅ built — shouldVirtualize() (≥80 nodes) enables React Flow onlyRenderVisibleElements + a compact ELK layout variant (LARGE_GRAPH_LAYOUT_OPTIONS) | n/a (client) |
| Code (JSON) view | ✅ built — Monaco JSON tab (round-trips to/from the canvas model) | PUT .../pipelines/{name} |
| Save / Publish | ✅ built — toolbar Save + Ctrl+S | PUT factories/{f}/pipelines/{name} (createOrUpdate) |
| Validate | ✅ built — toolbar Validate | POST .../pipelines/{name}/validate or POST factories/{f}/validatePipeline |
| Debug | ✅ built — toolbar Debug | POST .../pipelines/{name}/createRun?isRecovery=false |
| Trigger now | ✅ built — toolbar Trigger now | POST .../pipelines/{name}/createRun |
| Add trigger / Triggers (list / create schedule / start / stop / delete) | ✅ built — Add trigger dialog | GET/PUT/POST .../triggers, .../triggers/{n}/start|stop |
| Output / Run history (Monitor) + window/status filter | ✅ built — Output tab | POST factories/{f}/queryPipelineRuns (filtered PipelineName) |
| Deep activity config needing a linked service / dataset that isn't provisioned | ⚠️ honest-gate — Source/Sink + dataset reference fields render; the referenceName is empty with guidance; Fabric-only activities (Dataflow Gen2, Office 365) marked Save-only with remediation | n/a |
| Infra-gate when factory not provisioned | ⚠️ honest-gate — bind picker shows listError (e.g. "Missing env var: LOOM_ADF_NAME"); full UI still renders | n/a |
Zero ❌. Zero stub banners. The cards/edges ⇄ properties.activities[]/dependsOn[] round-trip, the palette catalog, the per-type default typeProperties, and the 4-condition connect/merge are covered by lib/components/pipeline/__tests__/activities-roundtrip.test.ts (Vitest). The nested control-flow drill model (branch read/write, level walk, immutable tree write-back, ADF nesting limits, popDrill back-navigation, and miniPreviewSections inline preview) is covered by lib/components/pipeline/__tests__/drill-path.test.ts (25 tests).
Nested control-flow sub-canvases (ADF concepts-nested-activities)¶
Source: concepts-nested-activities. ADF container activities edit their inner activities on an inner canvas reached via a pencil icon on the node (ForEach/Until also drill on double-click), with a breadcrumb back to ancestors. Loom mirrors this exactly:
- Inner-activity storage (ADF JSON shape): ForEach/Until →
typeProperties.activities; IfCondition →typeProperties.ifTrueActivities/ifFalseActivities; Switch →typeProperties.defaultActivities+typeProperties.cases[].activities. - Drill state:
PipelineDesignerholds adrillPath: Array<{ name; branch? }>.getLevelActivities(tree, path)walks to the current level;setLevelActivities(tree, path, next)rebuilds the container chain immutably and emits the full top-level tree viaonActivitiesChange— soPipelineEditorCore'sproperties.activities[]⇄ JSON contract is unchanged. - Branch UI: IfCondition shows True/False tabs; Switch shows Default + a tab per case with inline add/remove case (real JSON mutation).
- Nesting limits (
drill-path.ts#canAddTypeAtLevel): If/Switch can be used inside ForEach/Until but not inside another If/Switch; ForEach/Until support a single level of nesting. Disallowed palette tiles are disabled (no drag, no click) with an explanatory tooltip, and the canvas drop handler enforces the same gate. - No new backend. Inner activities serialise inside the same pipeline definition; Save still does one
PUT factories/{f}/pipelines/{name}. No new route, env var, role, Cosmos container, or bicep change.
Backend per control¶
- Binding read/list/create:
/api/items/adf-pipeline/[id]/bind(GET lists real pipelines, POST binds/creates). - Spec GET/PUT/DELETE:
/api/items/adf-pipeline/[id]→resolveBinding→adf-client.{getPipeline,upsertPipeline,deletePipeline}. - Run/Debug:
/api/items/adf-pipeline/[id]/run|debug→runPipeline/debugPipeline(createRun). - Runs:
/api/items/adf-pipeline/[id]/runs→listPipelineRuns(boundName)(queryPipelineRuns). - Validate:
/api/items/adf-pipeline/[id]/validate→validatePipeline. - Triggers:
/api/items/adf-pipeline/[id]/triggers→listTriggers/upsertTrigger/start/stop/delete.
All routes 412 with {ok:false, code:'unbound'} when the item has no state.pipelineName, so the editor shows its bind picker. Every res.json() on the client goes through safePipelineJson (content-type guard) so a non-JSON error page never crashes the editor.
Canvas viewport — the layout defect fixed 2026-08 (and how to not re-introduce it)¶
The designer rendered every control correctly and was still unusable: the canvas region was ~96px tall inside a 560px shell, and React Flow was framing a 400px viewport inside it, so the graph and both floating panels sat below the clip. "You can't even see the Canvas portion of it."
Three compounding causes, all now closed:
- An unbounded dock.
pipeline-designer.tsx's dock wasflexShrink: 0with no height, so it took its CONTENT height. With nothing selected that content was anEmptyStatewhoseminHeight: 320pxis a hard floor underbox-sizing: border-box. Arithmetic inside the 560px region:552 − 32 gaps − 50 nav − 36 errors − 320 dock − 18 status ≈ 96pxof canvas. Fixed by the verticalSplitPaneabove — the dock is now the SIZED pane (default 38% of the split, floored at 200px), so the canvas always keeps the majority. PR #2542 had already established this pattern indata-pipeline-editor.tsx; it was never applied here, because that editor only renders whentemplateIdis set andDEFAULT_PIPELINE_RUNTIME='adf'routes ordinary pipelines throughpipeline-designer.tsxinstead. - A React Flow shell floor larger than its clip.
canvas.tsx's shell hadminHeight: 400px. A floor here is worse than useless: React Flow measures the shell to framefitView, so a floor bigger than the clip centres the nodes off-screen and anchors theCanvasRightRail/MiniMapto an invisible bottom edge. NowminHeight: 0— every caller must render this canvas in a height-bounded flex column. - A region that never grew. The region was a fixed 560px on every screen and ignored window resizes (a G3 violation).
ResizableCanvasRegionnow takes an opt-infill: until the user's first drag on that storage key the region grows into the height genuinely available below its top edge (floored atdefaultPx, so it can only ever gain height) and re-measures on window, container, and following-sibling resize.
Applied to the sibling canvases with the same fixed-region shape: eventstream visual designer, mapping data flow designer, ADX schema diagram, catalog lineage, network topology, mounted-ADF data flow, and the Databricks DLT pipeline editor.
Regression cover: lib/components/pipeline/__tests__/pipeline-canvas-viewport.test.tsx (drives the real designer + the real SplitPane) and the G3 — viewport fill block in lib/components/canvas/__tests__/resizable-canvas.test.tsx.