Skip to content
CSA Loom — the Microsoft Fabric experience for Azure tenants where Fabric isn't yet available: lakehouses, warehouses, notebooks, semantic models, Activator rules, Data Agents, across Commercial, GCC, GCC-High, and DoD IL5

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:

  1. Activities pane (left) — searchable, categorized tree of every activity (Move & transform, Orchestration/Synapse-Databricks, General, Iteration & conditionals, …). Drag onto canvas to add.
  2. 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.
  3. 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).
  4. Pipeline configurations pane — Parameters, Variables, (General) Settings (concurrency, annotations, description), Output (run monitor).
  5. 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: PipelineDesigner holds a drillPath: 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 via onActivitiesChange — so PipelineEditorCore's properties.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:

  1. An unbounded dock. pipeline-designer.tsx's dock was flexShrink: 0 with no height, so it took its CONTENT height. With nothing selected that content was an EmptyState whose minHeight: 320px is a hard floor under box-sizing: border-box. Arithmetic inside the 560px region: 552 − 32 gaps − 50 nav − 36 errors − 320 dock − 18 status ≈ 96px of canvas. Fixed by the vertical SplitPane above — 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 in data-pipeline-editor.tsx; it was never applied here, because that editor only renders when templateId is set and DEFAULT_PIPELINE_RUNTIME='adf' routes ordinary pipelines through pipeline-designer.tsx instead.
  2. A React Flow shell floor larger than its clip. canvas.tsx's shell had minHeight: 400px. A floor here is worse than useless: React Flow measures the shell to frame fitView, so a floor bigger than the clip centres the nodes off-screen and anchors the CanvasRightRail / MiniMap to an invisible bottom edge. Now minHeight: 0 — every caller must render this canvas in a height-bounded flex column.
  3. A region that never grew. The region was a fixed 560px on every screen and ignored window resizes (a G3 violation). ResizableCanvasRegion now takes an opt-in fill: until the user's first drag on that storage key the region grows into the height genuinely available below its top edge (floored at defaultPx, 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.