From 489b6603af744cbc949af9d8a462b44c47f4e7a2 Mon Sep 17 00:00:00 2001 From: Diego Imbert Date: Wed, 30 Sep 2026 11:30:00 +0200 Subject: [PATCH] feat: one card for pipeline nodes, and the pipeline pages in the page header Co-Authored-By: Claude Opus 5.5 (1M context) --- .../assets/AssetGraph/AssetGraphCanvas.svelte | 28 +- .../assets/AssetGraph/AssetNode.svelte | 261 ++++++++--------- .../assets/AssetGraph/PipelineNodeCard.svelte | 103 +++++++ .../assets/AssetGraph/RunnableNode.svelte | 240 ++++++++-------- .../assets/AssetGraph/TriggerNode.svelte | 271 +++++++----------- .../lib/components/assets/AssetGraph/types.ts | 5 +- .../(root)/(logged)/pipeline/+page.svelte | 18 +- .../(logged)/pipeline/[folder]/+page.svelte | 113 ++++---- 8 files changed, 531 insertions(+), 508 deletions(-) create mode 100644 frontend/src/lib/components/assets/AssetGraph/PipelineNodeCard.svelte diff --git a/frontend/src/lib/components/assets/AssetGraph/AssetGraphCanvas.svelte b/frontend/src/lib/components/assets/AssetGraph/AssetGraphCanvas.svelte index e4b15d4e01..ce3a503855 100644 --- a/frontend/src/lib/components/assets/AssetGraph/AssetGraphCanvas.svelte +++ b/frontend/src/lib/components/assets/AssetGraph/AssetGraphCanvas.svelte @@ -529,6 +529,7 @@ runnable_kind: r.usage_kind, path: r.path, summary: r.summary, + language: r.language, in_pipeline: r.in_pipeline ?? false, partition_kind: r.partition_kind, freshness: r.freshness, @@ -1126,30 +1127,30 @@ // the "happening now" signal. switch (e.kind) { case 'lineage-write': - style = 'stroke: rgb(59 130 246); stroke-width: 2px;' + style = 'stroke: rgb(59 130 246); stroke-width: 1.25px;' animated = flowAnimated markerColor = 'rgb(59 130 246)' break case 'lineage-read': - style = 'stroke: rgb(156 163 175); stroke-width: 1.25px;' + style = 'stroke: rgb(156 163 175); stroke-width: 1px;' animated = flowAnimated break case 'data-test': // Asset → its custom-test script: dashed, muted, no run // animation (the test isn't a producing step). - style = 'stroke: rgb(156 163 175); stroke-width: 1.25px;' + style = 'stroke: rgb(156 163 175); stroke-width: 1px;' strokeDasharray = '4 3' markerColor = 'rgb(156 163 175)' break case 'trigger-asset': - style = 'stroke: rgb(107 114 128); stroke-width: 2px;' + style = 'stroke: rgb(107 114 128); stroke-width: 1.25px;' animated = flowAnimated markerColor = 'rgb(107 114 128)' label = 'triggers' labelStyle = 'fill: rgb(107 114 128); font-size: 10px; font-weight: 600;' break case 'trigger-native': - style = 'stroke: rgb(107 114 128); stroke-width: 2px;' + style = 'stroke: rgb(107 114 128); stroke-width: 1.25px;' strokeDasharray = '6 3' markerColor = 'rgb(107 114 128)' label = 'triggers' @@ -1159,7 +1160,7 @@ // Library → consumer: violet dashed, visually apart from both // lineage (blue/gray solid) and trigger (gray dashed) families — // it's a code dependency, not data flow or execution. - style = 'stroke: rgb(139 92 246); stroke-width: 1.25px;' + style = 'stroke: rgb(139 92 246); stroke-width: 1px;' strokeDasharray = '5 3' markerColor = 'rgb(139 92 246)' label = e.via_use ? 'uses lib' : 'macros' @@ -1169,7 +1170,7 @@ // Producer → tested script: amber dashed ordering link. Not // data flow (blue/gray) nor execution trigger (gray "triggers") // — it only says "the test needs this asset to exist first". - style = 'stroke: rgb(217 119 6); stroke-width: 1.25px;' + style = 'stroke: rgb(217 119 6); stroke-width: 1px;' strokeDasharray = '5 3' markerColor = 'rgb(217 119 6)' label = 'test needs' @@ -1179,7 +1180,7 @@ // model → model inside one dbt project. Orange, matching the // dbt badges, and dashed because the edge is dbt's own lineage // rather than a Windmill read/write the cascade acts on. - style = 'stroke: rgb(234 88 12); stroke-width: 1.25px;' + style = 'stroke: rgb(234 88 12); stroke-width: 1px;' strokeDasharray = '4 3' markerColor = 'rgb(234 88 12)' label = 'ref' @@ -1216,7 +1217,7 @@ // (red dashed dimmed — fresh draft annotation that also has // no matching row, which is the common case). if (e.missing) { - style = 'stroke: rgb(239 68 68); stroke-width: 2px;' + style = 'stroke: rgb(239 68 68); stroke-width: 1.25px;' strokeDasharray = '3 3' markerColor = 'rgb(239 68 68)' label = 'missing trigger' @@ -1254,8 +1255,8 @@ style, markerEnd: { type: MarkerType.ArrowClosed, - width: 14, - height: 14, + width: 12, + height: 12, color: markerColor } } @@ -1397,11 +1398,8 @@ :global(.svelte-flow__controls-button:hover) { @apply bg-surface-hover; } - :global(.svelte-flow__node.selected .drop-shadow-sm) { - @apply outline outline-2 outline-blue-500; - } /* Activity-panel emphasis — soft, monochromatic, less prominent than the - blue details selection above. Hover is a thin neutral ring (transient); + node's own selected state. Hover is a thin neutral ring (transient); pinning an expanded run is a soft-blue ring. */ /* A dbt project node and the models it materializes, related by badge rather than by edges — hovering either lights up the whole set. */ diff --git a/frontend/src/lib/components/assets/AssetGraph/AssetNode.svelte b/frontend/src/lib/components/assets/AssetGraph/AssetNode.svelte index 3564a841cf..4ec8a39f55 100644 --- a/frontend/src/lib/components/assets/AssetGraph/AssetNode.svelte +++ b/frontend/src/lib/components/assets/AssetGraph/AssetNode.svelte @@ -3,8 +3,7 @@ import { twMerge } from 'tailwind-merge' import AssetGenericIcon from '$lib/components/icons/AssetGenericIcon.svelte' import { formatAssetKind, formatShortAssetPath, type AssetKind } from '$lib/components/assets/lib' - import { NODE } from '$lib/components/graph/util' - import { PIPELINE_NODE_HEIGHT } from './assetGraphLayout' + import PipelineNodeCard from './PipelineNodeCard.svelte' import PipelineInsertMenu, { type PipelineInsertPick } from './PipelineInsertMenu.svelte' import { ArrowUpRight, @@ -241,124 +240,121 @@ {/if} -
- - - {#if data.runStatus} - + + {/snippet} + {#snippet leading()} + {#if data.runStatus} + - - {#if data.runStatus === 'running'} - - {:else if data.runStatus === 'failed'} - - {:else} - - {/if} - - - {#if data.runStatus === 'materialized' && data.runRowCount != undefined} - - {Intl.NumberFormat().format(data.runRowCount)} - + {#if data.runStatus === 'materialized' && data.runRowCount != undefined} + + {Intl.NumberFormat().format(data.runRowCount)} + + {/if} {/if} - {/if} -
- - {formatAssetKind(asset)} - - {formatShortAssetPath(asset)} -
- - {#if data.fork_materialization === 'deferred'} - - - parent - - {:else if data.fork_materialization === 'fork'} - - - fork - - {/if} - - {#if data.dbt} - - {/if} - - {#if data.derived_from} - - - - {/if} -
+ {#if data.derived_from} + + + + {/if} + {/snippet} + {#if showGuardBadge} -
- - {#snippet trigger()} - +
+
+
+ + {#snippet trigger()} + - - {/snippet} - + + {/snippet} + +
{/if}
diff --git a/frontend/src/lib/components/assets/AssetGraph/PipelineNodeCard.svelte b/frontend/src/lib/components/assets/AssetGraph/PipelineNodeCard.svelte new file mode 100644 index 0000000000..77f591b3c3 --- /dev/null +++ b/frontend/src/lib/components/assets/AssetGraph/PipelineNodeCard.svelte @@ -0,0 +1,103 @@ + + + +{#snippet body()} + {@render icon()} + {@render leading?.()} + + {kindLabel} + {title} + + {@render trailing?.()} +{/snippet} + +{#if onclick} + +{:else} +
+ {@render body()} +
+{/if} diff --git a/frontend/src/lib/components/assets/AssetGraph/RunnableNode.svelte b/frontend/src/lib/components/assets/AssetGraph/RunnableNode.svelte index 72446e1249..a11c7f90d9 100644 --- a/frontend/src/lib/components/assets/AssetGraph/RunnableNode.svelte +++ b/frontend/src/lib/components/assets/AssetGraph/RunnableNode.svelte @@ -24,8 +24,9 @@ import type { GraphUsageKind } from './types' import type { RunnableRunState } from './activeRunnables.svelte' import { parseDurationSecs } from './parsePipelineAnnotations' - import { NODE } from '$lib/components/graph/util' - import { PIPELINE_NODE_HEIGHT } from './assetGraphLayout' + import LanguageIcon from '$lib/components/common/languageIcons/LanguageIcon.svelte' + import PipelineNodeCard from './PipelineNodeCard.svelte' + import type { ScriptLang } from '$lib/gen' import DropdownV2 from '$lib/components/DropdownV2.svelte' import Popover from '$lib/components/meltComponents/Popover.svelte' import type { Item } from '$lib/utils' @@ -39,6 +40,7 @@ runnable_kind: GraphUsageKind path: string summary?: string + language?: ScriptLang in_pipeline?: boolean partition_kind?: 'daily' | 'hourly' | 'weekly' | 'monthly' | 'dynamic' freshness?: string @@ -217,134 +219,132 @@ -
- -
- - {kindLabel}{data.unsaved ? ' · draft' : ''} - - {data.summary || data.path} -
- - {#if data.partition_kind} -
- - {data.partition_kind} -
- {/if} - - {#if data.freshness} -
- - {data.freshness} -
- {/if} - {#if data.tag} -
- - {data.tag} -
- {/if} - {#if data.retry} - {@const r = data.retry} -
- - ×{r.count} -
- {/if} - {#if data.macros && data.macros.length > 0} -
1 ? 's' : ''}:\n${data.macros - .map((m) => `• ${m.name}(${m.params})${m.is_table ? ' → table' : ''}`) - .join('\n')}`} - > - - ×{data.macros.length} -
- {/if} - {#if data.dbt} -
- - ×{data.dbt.model_count} -
- {/if} - {#if data.runState} - {@const rs = data.runState} -
0 ? ` — ran ${rs.runs}× this session` : ''}`} - > - {#if rs.status === 'running'} - - {:else if rs.status === 'success'} - - {:else} - - {/if} - {#if rs.runs > 0} - ×{rs.runs} - {/if} -
- {/if} -
+ : freshnessState === 'stale' + ? 'bg-amber-100 dark:bg-amber-900/40 text-amber-700 dark:text-amber-300' + : 'bg-surface-secondary text-secondary' + )} + title={freshnessTooltip} + > + + {data.freshness} + + {/if} + {#if data.tag} +
+ + {data.tag} +
+ {/if} + {#if data.retry} + {@const r = data.retry} +
+ + ×{r.count} +
+ {/if} + {#if data.macros && data.macros.length > 0} +
1 ? 's' : ''}:\n${data.macros + .map((m) => `• ${m.name}(${m.params})${m.is_table ? ' → table' : ''}`) + .join('\n')}`} + > + + ×{data.macros.length} +
+ {/if} + {#if data.dbt} +
+ + ×{data.dbt.model_count} +
+ {/if} + {#if data.runState} + {@const rs = data.runState} +
0 ? ` — ran ${rs.runs}× this session` : ''}`} + > + {#if rs.status === 'running'} + + {:else if rs.status === 'success'} + + {:else} + + {/if} + {#if rs.runs > 0} + ×{rs.runs} + {/if} +
+ {/if} + {/snippet} + {#if showRun}
(hover = true)} onmouseleave={() => (hover = false)}> - {#if canCreate} - - - {:else if canEdit} - - - {:else if canOpenWebhook} - - - {:else if canOpenDataUpload} - - - {:else} -
- -
- - {style.label}{displayMissing ? ' · missing' : data.unsaved ? ' · unsaved' : ''} - - - {displayMissing ? 'no trigger row' : data.ref} - -
-
- {/if} + + {#snippet icon()} + {#if canOpenDataUpload} + + {:else} + + {/if} + {/snippet} + {#if menuItems.length > 0} -
+
setMode(m)} /> {#if mode === 'view' && pendingCount > 0} @@ -2702,9 +2702,10 @@ title="Refresh" /> -
+{/snippet} +
{#if graphRes.loading && !graphRes.current}