feat: toggle to hide reads that don't trigger runs in the pipeline graph

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Diego Imbert
2026-09-30 18:17:04 +02:00
co-authored by Claude Opus 5.5
parent a069f3c231
commit 7c840fb54e
3 changed files with 85 additions and 2 deletions
@@ -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 @@
<PanToNode targetId={panToNodeId} {nodes} />
<Controls position="top-right" orientation="horizontal" showLock={false} class="!mr-10" />
{#if assetsOnlyToggle}
<Panel position="top-left" class="!m-3">
<Panel position="top-left" class="!m-3 flex flex-col gap-1.5">
<Toggle
checked={!assetsOnly}
size="xs"
@@ -1911,6 +1926,19 @@
if (assetsOnlyFolder != undefined) setAssetsOnlyFolder(assetsOnlyFolder, assetsOnly)
}}
/>
{#if withoutPassive.dropped > 0}
<Toggle
checked={showPassiveReads}
size="xs"
options={{ right: "Show reads that don't trigger runs" }}
on:change={(e) => {
showPassiveReads = e.detail
try {
localStorage.setItem(SHOW_PASSIVE_READS_KEY, String(e.detail))
} catch {}
}}
/>
{/if}
</Panel>
{/if}
{#if showMinimap}
@@ -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'
])
})
})
@@ -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<N extends ViewNode, E extends ViewEdge>(
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
}
}