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}