diff --git a/frontend/src/lib/components/assets/AssetGraph/AssetGraphCanvas.svelte b/frontend/src/lib/components/assets/AssetGraph/AssetGraphCanvas.svelte index 39fc1fa9d2..aef3469414 100644 --- a/frontend/src/lib/components/assets/AssetGraph/AssetGraphCanvas.svelte +++ b/frontend/src/lib/components/assets/AssetGraph/AssetGraphCanvas.svelte @@ -25,6 +25,7 @@ assetsOnlyView, upstreamDeletion, NO_ASSET_NODE_ID, + withoutPassiveReads, type AssetUpstream, type AssetUpstreamDelete } from './assetsOnlyView' @@ -1359,7 +1360,21 @@ } return { ...m, nodes, edges } } - let view = $derived(assetsOnly ? assetsOnlyModel(model) : model) + // Reads that start nothing (lookups, reference tables) are often most of a + // graph's edges; hiding them leaves what makes the pipeline run. Remembered + // across pipelines. + const SHOW_PASSIVE_READS_KEY = 'pipeline-show-passive-reads' + let showPassiveReads = $state(true) + try { + showPassiveReads = localStorage.getItem(SHOW_PASSIVE_READS_KEY) !== 'false' + } catch {} + let withoutPassive = $derived(withoutPassiveReads(model.nodes, model.edges)) + let shownModel = $derived( + showPassiveReads || withoutPassive.dropped === 0 + ? model + : { ...model, nodes: withoutPassive.nodes, edges: withoutPassive.edges } + ) + let view = $derived(assetsOnly ? assetsOnlyModel(shownModel) : shownModel) // dbt association, surfaced by emphasis instead of edges. Hovering a model's // dbt badge lights up the project node that materializes it; hovering the @@ -1901,7 +1916,7 @@ {#if assetsOnlyToggle} - + + {#if withoutPassive.dropped > 0} + { + showPassiveReads = e.detail + try { + localStorage.setItem(SHOW_PASSIVE_READS_KEY, String(e.detail)) + } catch {} + }} + /> + {/if} {/if} {#if showMinimap} diff --git a/frontend/src/lib/components/assets/AssetGraph/assetsOnlyView.test.ts b/frontend/src/lib/components/assets/AssetGraph/assetsOnlyView.test.ts index 8c68991e0b..12a0303178 100644 --- a/frontend/src/lib/components/assets/AssetGraph/assetsOnlyView.test.ts +++ b/frontend/src/lib/components/assets/AssetGraph/assetsOnlyView.test.ts @@ -3,6 +3,7 @@ import { assetsOnlyView, upstreamDeletion, NO_ASSET_NODE_ID, + withoutPassiveReads, type AssetUpstream } from './assetsOnlyView' @@ -134,3 +135,28 @@ describe('upstreamDeletion', () => { expect(upstreamDeletion(u, script, all)).toBeUndefined() }) }) + +describe('withoutPassiveReads', () => { + it('drops reads that trigger nothing, and the assets only they drew', () => { + const nodes = [ + node('asset:src', 'asset'), + node('asset:lookup', 'asset'), + node('asset:out', 'asset'), + node('script:a', 'runnable') + ] + const edges = [ + edge('asset:src', 'script:a', 'lineage-read'), + edge('asset:src', 'script:a', 'trigger-asset'), + edge('asset:lookup', 'script:a', 'lineage-read'), + edge('asset:out', 'script:a', 'lineage-read'), + edge('script:a', 'asset:out', 'lineage-write') + ] + const r = withoutPassiveReads(nodes, edges) + expect(r.nodes.map((n) => n.id)).toEqual(['asset:src', 'asset:out', 'script:a']) + expect(r.edges.map((e) => e.id)).toEqual([ + 'lineage-read:asset:src->script:a', + 'trigger-asset:asset:src->script:a', + 'lineage-write:script:a->asset:out' + ]) + }) +}) diff --git a/frontend/src/lib/components/assets/AssetGraph/assetsOnlyView.ts b/frontend/src/lib/components/assets/AssetGraph/assetsOnlyView.ts index 0cef27f8c6..d9c102018c 100644 --- a/frontend/src/lib/components/assets/AssetGraph/assetsOnlyView.ts +++ b/frontend/src/lib/components/assets/AssetGraph/assetsOnlyView.ts @@ -186,3 +186,32 @@ export function upstreamDeletion( trigger: ownTrigger ? { kind, path: t.data.ref, draft: !!t.data.draft } : undefined } } + +/** + * The graph without its reads that start nothing: a read edge with no asset + * trigger along it, and the assets only such reads drew. + */ +export function withoutPassiveReads( + nodes: N[], + edges: E[] +): { nodes: N[]; edges: E[]; dropped: number } { + const triggers = new Set( + edges.filter((e) => e.kind === 'trigger-asset').map((e) => `${e.source}\n${e.target}`) + ) + const passive = (e: E) => + e.kind === 'lineage-read' && !triggers.has(`${e.source}\n${e.target}`) + const kept = edges.filter((e) => !passive(e)) + const dropped = edges.length - kept.length + if (dropped === 0) return { nodes, edges, dropped } + const linked = new Set( + kept.filter((e) => e.kind !== 'add-anchor').flatMap((e) => [e.source, e.target]) + ) + const readOnly = new Set( + edges.filter(passive).map((e) => e.source).filter((id) => !linked.has(id)) + ) + return { + nodes: nodes.filter((n) => !readOnly.has(n.id)), + edges: kept.filter((e) => !readOnly.has(e.source) && !readOnly.has(e.target)), + dropped + } +}