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 controls — zoom in/out/fit, auto-align | ✅ built — CanvasToolbar (bottom-right) + toolbar Auto align / Zoom to fit | 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.