Skip to content

feat(lineage): show impact badges on direct upstream/downstream in Lineage tab#1352

Merged
gcko merged 10 commits into
mainfrom
feature/drc-3344-lineage-tab-show-impact-badges-on-direct-upstreamdownstream
May 6, 2026
Merged

feat(lineage): show impact badges on direct upstream/downstream in Lineage tab#1352
gcko merged 10 commits into
mainfrom
feature/drc-3344-lineage-tab-show-impact-badges-on-direct-upstreamdownstream

Conversation

@wcchang1115
Copy link
Copy Markdown
Collaborator

@wcchang1115 wcchang1115 commented May 4, 2026

PR checklist

  • Ensure you have added or ran the appropriate tests for your PR.
  • DCO signed

What type of PR is this?

feat / enhancement (UI)

What this PR does / why we need it:

Brings the canvas Impact Analysis signal into the Model Detail panel's Lineage tab so reviewers don't have to cross-reference rows against the graph.

When the focused model and a direct neighbor are both in the impact chain, the row is decorated with a 3-px amber left rail, an 8% amber background tint, and a trailing amber arrow icon (tooltip: "Impacts this model" / "Impacted by this model" depending on direction). The section header gains an amber count chip ("N impacting" / "N impacted") that doubles as a per-side toggle: click it to filter that rail to only impacted models; click again to restore. The status dot also picks up the impacted amber on unchanged-but-impacted neighbors so the panel reads consistently with the canvas border.

The two impact sets are derived directly from CLL — impactedNodeIds mirrors the canonical cll.impacted = true flag (used for the downstream rail and dot color), and impactingNodeIds adds the two cases CLL deliberately omits despite propagating impact (partial_breaking modified nodes and removed nodes). This avoids the bug where a self-modified-but-non_breaking parent (e.g. an stg_* that adds an unused column) would be wrongly marked as impacting downstream.

Canvas coloring is untouched — the existing LineageViewContext.impactedNodeIds continues to drive the amber border.

Which issue(s) this PR fixes:

DRC-3344

Special notes for your reviewer:

  • The chip's tooltip wording uses "models" to match the existing per-row tooltip ("Impacts this model" / "Impacted by this model") and the design copy.
  • LineageTabContent accepts both impact sets as optional props, so existing tests and Storybook mounts that don't wrap LineageViewProvider keep rendering without marks.
  • Tests cover: the partial_breaking bridge, focus-as-source propagation, upstream/downstream isolation, the chip toggle, and the unchanged-but-impacted dot color.
  • Storybook gains ImpactAnalysisActive, ImpactAnalysisPartialBreaking, and ImpactAnalysisDense (the redundant focus-outside-set variant was dropped — covered by unit test).

Does this PR introduce a user-facing change?:

The Model Detail panel's Lineage tab now mirrors the canvas Impact Analysis: direct upstream and downstream rows that participate in the impact chain are marked with an amber rail, tint, arrow, and tooltip; section headers carry a clickable amber count chip that filters the side to only impacted models.

@wcchang1115 wcchang1115 marked this pull request as draft May 4, 2026 08:24
@wcchang1115 wcchang1115 marked this pull request as ready for review May 4, 2026 16:00
@wcchang1115 wcchang1115 self-assigned this May 4, 2026
wcchang1115 and others added 7 commits May 5, 2026 16:31
Brings the canvas Impact Analysis signal into the Model Detail panel's
Lineage tab so users don't have to cross-reference rows against the graph.

When the focused node is in the impact set, direct neighbors that also
participate are decorated with an amber left rail, an 8% amber tint, and
a trailing arrow icon. Tooltip differs by direction: upstream rows show
"Impacts this model"; downstream rows show "Impacted by this model". The
section header gains a count chip ("N impacting" / "N impacted") and stays
silent at zero, matching design DRC-3344 (variant D).

NodeViewOss reads impactedNodeIds from LineageViewContext and forwards it
to LineageTabContent as an optional prop, so existing tests and Storybook
mounts that don't wrap the provider keep rendering as today.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Signed-off-by: Wei-Chun, Chang <wcchang@infuseai.io>
Two changes on top of the initial Lineage-tab impact marks:

- Source impact sets from the CLL `impacted` flag instead of the broader
  canvas `impactedNodeIds`, with `impactingNodeIds` bridging the cases
  CLL deliberately omits (`partial_breaking` source nodes, `removed`).
  Fixes self-modified-but-non_breaking parents being wrongly marked.
- Section-header amber chip becomes a toggle: click to filter the side
  to impacted models only. Per-side, with directional tooltips and
  pagination reset.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Signed-off-by: Wei-Chun, Chang <wcchang@infuseai.io>
The canvas already shows the impacted (amber) style on unchanged nodes
that the CLL flags as `impacted = true`. The Lineage tab's status dot
ignored that and rendered them with the neutral "unchanged" gray —
visually inconsistent with the graph (e.g. customer_order_pattern in
the customers panel).

`StatusDot` and `FocusCard` now reuse `cllChangeStatusColors.impacted`
when status is unchanged AND the node carries `cll.impacted = true`. The
focus card's status badge text follows ("impacted" instead of
"unchanged"). The dot exposes a `data-status` attribute for testing.

Also drop `ImpactAnalysisFocusOutsideSet` Storybook variant — it
rendered identically to `Default` and the behavior stays covered by the
unit test.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Signed-off-by: Wei-Chun, Chang <wcchang@infuseai.io>
…im comments

Unify Lineage tab impact marks (rail, arrow, chip, dot) on
cllChangeStatusColors.impacted so they match the canvas "!" badge
and impacted node border, replacing rgb(255 173 21) which the canvas
never actually used. Mirror schema's --schema-badge-impacted-* palette
for the chip; active state uses a deeper amber so white text stays
legible.

Also strip internal issue identifiers and tighten doc comments to keep
only the non-obvious "why" (partial_breaking / removed propagation,
focus-in-chain gating, no downstream→upstream bleed).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Signed-off-by: Wei-Chun, Chang <wcchang@infuseai.io>
Cleanups to the Lineage tab impact-mark code introduced in this branch:

- Extract effectiveStatus / colorForEffective helpers — single source for
  the "unchanged + cllImpacted → impacted" mapping that was duplicated
  across StatusDot, getNodeStatusColor, and FocusCard.
- Split the impact chip into ChipButton (interactive) and ChipBadge
  (display-only), removing the polymorphic component={x ? "button" :
  "span"} pattern and its dangling type="button" attribute.
- Flatten the triple-nested chipTitle ternary into a getChipTitle
  helper with early returns.
- Rename DirectRow props for distinct intent: impacted →
  decorateAsImpacted (drives the rail/tint/arrow), cllImpacted →
  dotImpacted (drives the dot color).
- Add a comment marking the trailing arrow icon as direction-agnostic
  (the tooltip carries direction).

Also fix amber legibility on light backgrounds: the canvas amber
rgb(252 211 77) reads fine as a filled shape (dot, rail, chip bg) but
fails contrast as a thin-stroke icon or small text on white. Add an
impactedTextColor() helper (deeper amber rgb(146 64 14) in light mode,
light yellow in dark mode) and apply it to:
- the trailing impact-mark arrow icon in DirectRow
- the FocusCard "Impacted" pill text and border (when status is
  impacted)

The other impact visuals — left rail, dot, chip background — keep the
canvas amber since brightness reads correctly on filled shapes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Signed-off-by: Wei-Chun, Chang <wcchang@infuseai.io>
…le fallback

Extract NodeViewOss's CLL→impact-sets transformation into
computeLineageTabImpactSets so the partial_breaking/removed bridge has
direct unit coverage — previously the only tests passed the sets in
already-built, leaving silent-regression risk if someone simplified the
branching to `if (n.impacted) ...` and dropped the bridge.

Also simplify getChipTitle: it was typed `string | undefined` only to
support a non-existent `interactive=false` caller, with the call site
papering over it via `?? label`. Drop the parameter and tighten the
return type.

No behavior change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Signed-off-by: Wei-Chun, Chang <wcchang@infuseai.io>
Move the impact chip's bg/fg/active-accent constants out of
LineageTabContent into lineage/styles.tsx as cllImpactedAccent /
cllImpactedBadgeBg / cllImpactedBadgeFg. Light accent and dark badge fg
reference cllChangeStatusColors.impacted directly; badge backgrounds
compose alpha onto private hue triplets via space-separated rgb()
syntax. Update schema/style.css's cross-file sync note to point at the
new exports.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Signed-off-by: Wei-Chun, Chang <wcchang@infuseai.io>
@wcchang1115 wcchang1115 force-pushed the feature/drc-3344-lineage-tab-show-impact-badges-on-direct-upstreamdownstream branch from 802a11a to 91867b2 Compare May 5, 2026 08:35
wcchang1115 and others added 2 commits May 5, 2026 17:59
The chip went from non-interactive (initial commit) to a toggle
(9ae2402) and was later split into ChipButton/ChipBadge to clean up a
polymorphic `component={x ? "button" : "span"}` pattern (d8efd07). The
display-only ChipBadge survived the split, but both SectionHeader call
sites always pass `onToggleOnlyImpact`, leaving the badge branch
unreachable.

Drop ChipBadge, tighten `onlyImpact` and `onToggleOnlyImpact` to
required on the prop type, and inline the chip render. Removes the
shared `data-testid="lineage-impact-chip"` between two component
variants and the `!!onlyImpact` coercion that only existed to bridge
the optional.

No behavior change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Signed-off-by: Wei-Chun, Chang <wcchang@infuseai.io>
@wcchang1115 wcchang1115 requested a review from gcko May 6, 2026 03:25
@gcko
Copy link
Copy Markdown
Contributor

gcko commented May 6, 2026

Code Review: PR #1352

Reviewed commit: cea2d55a (PR branch after merging current origin/main)
Branch: feature/drc-3344-lineage-tab-show-impact-badges-on-direct-upstreamdownstream
Files reviewed: 8 (3 new, 5 modified)
Categories: Frontend, Tests, Storybook, Styles
Passes run: A (Correctness), B (Security), C (Cross-Reference), D (Error Handling), E (Tests), F (Diff-Specific), H (Async)
Pass skipped: G (Performance) — no unbounded-iteration / N+1-shaped concerns introduced

Validation Results

Pass A: Correctness & Logic — PASS

Traced the impact-set wiring end-to-end:

  • computeLineageTabImpactSets (computeLineageTabImpactSets.ts:36-46) iterates cll.current.nodes once. The impacted === true branch puts the node in both sets; the partial_breaking | removed branch is mutually exclusive (it sits inside the else if) and only adds to impactingNodeIds. Strict equality (=== true) correctly excludes undefined (initial CLL load before impact analysis ran), matching the design that requires impacted to be a populated boolean.
  • The "non_breaking self-modified parent" guard the PR description calls out is a direct consequence of the else if ordering: a non_breaking change with impacted === false falls through both branches and is correctly excluded from both sets. Test excludes non_breaking self-modified nodes from BOTH sets (computeLineageTabImpactSets.test.ts:89-104) locks this in.
  • effectiveStatus (LineageTabContent.tsx:108-113) only promotes unchanged → impactedadded/removed/modified are never overridden. Confirmed via the focus-card path where cllImpacted=true on an added focus would still render "added" (tested implicitly by the unchanged-but-impacted dot test at line 451).
  • focusInImpact gating (LineageTabContent.tsx:787-790) requires the focus to be in either set before any neighbor decoration / chip count fires. Verified via the test at line 288 ("renders no impact marks when the focused node is not in either set").
  • isImpactedNeighbor correctly resolves direction-to-set: upstream uses impactingNodeIds, downstream uses impactedNodeIds. Asserted directly by test at line 330 ("does NOT mark an upstream parent that is only in impactedNodeIds").
  • Pagination + filter + only-impact toggle: chip is hidden when impactCount === 0 (line 389), so toggling-on with no candidates is unreachable from UI. Filter input + only-impact stack correctly: baseFiltered (text filter applied) is then narrowed by isImpactedNeighbor — when the result is empty, EmptyRow label="no matches" falls through naturally.
  • State reset on node change (useEffect([node.id])) clears query, visible page size, AND the onlyImpact toggles. Test at line 513 covers filter+pagination reset; the onlyImpact reset is included in the same effect by inspection.

No logic inversion, no off-by-one, no null deref, no boundary violation.

Pass B: Security — PASS

UI-only feature. No new API surface, no user-controlled URLs, no dangerouslySetInnerHTML, no eval-style paths. getDisplayName(id, nodesById) falls back to the raw id if not found — node IDs come from lineageGraph (server-provided), and the value is rendered as text inside <Box title={name}>{name}</Box>. React escapes by default; no XSS surface.

Pass C: Cross-Reference Consistency — PASS

  • ColumnLineageData.current.nodes: Record<string, CllNodeData> (api/cll.ts:47-56) matches Object.entries(cllNodes) iteration in computeLineageTabImpactSets. CllNodeData.impacted?: boolean, change_status?: "added" | "removed" | "modified", change_category?: "breaking" | "non_breaking" | "partial_breaking" | "unknown" — all string-literal checks in the new util are valid members or correctly use literal narrowing. No type drift.
  • LineageViewContextType.cll: ColumnLineageData | undefined (contexts/lineage/types.ts:214) matches computeLineageTabImpactSets(cll: ColumnLineageData | undefined | null) — the function's null tolerance is wider than the type contract, defensively safe (verified by the null test).
  • useLineageViewContext() (the non-throwing variant) returns LineageViewContextType | undefined. NodeViewOss correctly uses optional chaining lineageViewCtx?.cll — no provider crash in tests/Storybook. The naming is confusing (useLineageViewContextSafe is the throwing one in LineageViewContext.tsx) but the PR picks the right one.
  • LineageTabContentProps.impactingNodeIds?: Set<string> matches the Set<string> | undefined return shape from computeLineageTabImpactSets. Storybook's buildArgs correctly converts string[] arg to Set before passing.
  • The new cllImpactedAccent/cllImpactedBadgeBg/cllImpactedBadgeFg exports in styles.tsx are imported by name in LineageTabContent.tsx:35-41. No barrel export changes needed (these are direct imports). No collision with existing tokens.
  • The "two sources of truth for impact" question — LineageViewContext.impactedNodeIds (canvas) vs the new computeLineageTabImpactSets(cll).impactedNodeIds (tab) — was traced. The canvas snapshot is published only when cllInput.change_analysis && !cllInput.column (LineageViewOss.tsx:633), while the tab recomputes live from cll. Both ultimately derive from cll.impacted === true for impactedNodeIds, but the tab adds bridge cases in impactingNodeIds. The PR description is accurate about this design.

Pass D: Error Handling & Edge Cases — PASS

  • cll === undefined | null → returns { undefined, undefined } → caller receives undefined sets → impactActive = false → no marks rendered. Verified by tests.
  • cll.current.nodes === {} → both sets initialize to empty SetimpactActive = false → no marks. Verified.
  • Object.keys(node.data.parents ?? {}) — handles missing parents/children. useMemo deps are stable.
  • No try/catch, no async error paths, no resource ownership.

Pass E: Test Coverage & Quality — PASS

  • computeLineageTabImpactSets.test.ts covers undefined input, null input, empty nodes, the impacted=true → both rule, both bridge cases (partial_breaking, removed), the non_breaking exclusion, the added-with-impacted=false exclusion, AND a mixed-graph integration. Each test asserts on the exact set contents, not just size — no vacuous assertions.
  • LineageTabContent.test.tsx adds 9 new tests covering: omitted props, empty sets, focus-not-in-impact gate, upstream-only marking, anti-leak (impactedNodeIds doesn't bleed into upstream rail), partial_breaking, downstream tooltip text, chip count, chip toggle round-trip, unchanged-but-impacted dot color, and focus-as-source. Every test asserts specific DOM attributes (aria-label, data-status, aria-pressed).
  • No mock drift — the test makes its own LineageGraphNode shapes via makeNode and doesn't mock the new util (it tests the wiring through component-level props, which is the correct boundary).
  • 155 test files passed, 3691 tests passed locally.

Pass F: Diff-Specific Checks — PASS

  • New file computeLineageTabImpactSets.ts is self-contained — only consumers are NodeViewOss.tsx and the new test. Verified via grep.
  • LineageTabContentProps adds two optional props (impactingNodeIds?, impactedNodeIds?) — backward compatible with all existing call sites (the OSS app's NodeViewOss, Storybook stories, tests). Verified the existing tests that don't pass these props still pass.
  • NodeViewOss.tsx adds useLineageViewContext() — the non-throwing variant is intentional; tests/Storybook that mount NodeViewOss without a LineageViewProvider would otherwise crash. The current LineageView component test (LineageView.component.test.tsx:248) already mocks NodeViewOss wholesale, so this hook addition has zero blast on existing tests.
  • styles.tsx additions are pure additions — no existing exports modified. The doc comment correctly calls out the cross-file sync convention with schema/style.css.
  • No partial renames, no signature shifts on existing functions, no migrations, no feature flags.

Pass H: Async/Concurrency — PASS

No async code in any changed file. No promises, no useEffect race conditions (the only effect is a synchronous state reset on node.id).

Verification Results

  • pnpm lint — clean (609 files).
  • pnpm type:check — clean.
  • pnpm test — 3691 passed, 5 skipped, 155 test files.
  • GitHub CI (last polled): lint, both Analyze jobs, CodeQL, DCO SUCCESS; test-and-build IN_PROGRESS at review time but local equivalent passes.

Verdict: GO

All validation passes clean. No BLOCKERs, no ISSUEs.

Notes

  1. Pre-existing DirectRow keyboard accessibility gap. Clickable rows in the Lineage tab use <Box onClick={...} cursor: "pointer"> without role="button", tabIndex={0}, or onKeyDown — keyboard users cannot navigate via Enter/Space, while the new chip and the existing ShowMoreRow/breadcrumb both wire keyboard support correctly. Not introduced by this PR (predates it) but the PR's chip toggle is keyboard-accessible by virtue of being a real <button>, which puts the row click target in awkward inconsistency. Worth a follow-up ticket — out of scope for this review.

  2. Hand-synced JS-CSS token pair. The new cllImpactedAccent / cllImpactedBadgeBg / cllImpactedBadgeFg in styles.tsx are documented as mirroring --schema-color-impacted-accent / --schema-badge-impacted-{bg,fg} in schema/style.css, with no build-time check. This follows the existing convention for cllChangeStatusColors. Practically, the new JS tokens are consumed only by the Lineage tab (not the schema view), so a divergence wouldn't silently break the schema view — but if a future PR ever flips that and the JS-side tokens drift from the CSS variables, the visual will diverge across surfaces. The doc comment is the only safeguard.

What I could not verify

  • Live visual confirmation against recce server with real CLL data (requires a dbt project with change_analysis triggered).
  • That LineageViewContext.impactedNodeIds (canvas snapshot, published once) and the new tab-side impactedNodeIds (live-derived) stay aligned in real impact-analysis flows where the user clicks a column afterward (CLL response gets scoped to the column). Unit tests cover the pure derivation; an integration test on the canvas-tab interaction would close that gap.

What I looked for and did not find

  • Logic inversions in the impacted === true / else if (partial_breaking | removed) ordering
  • Set leak from impactedNodeIds into upstream-rail decoration (covered by the explicit anti-leak test)
  • Stale onlyImpact toggle after node change (the useEffect([node.id]) includes both toggles)
  • Stale closure / unstable callback issues in isImpactedNeighbor (used inline only, not passed as a prop)
  • Set-construction perf cliff (single linear pass over cll.current.nodes; small payloads in practice)
  • Type drift between CllNodeData.change_category literals and the else if branch
  • React rerender storm from new object/array literals in props (the useMemo on lineageViewCtx?.cll produces stable refs)
  • New Set creation per render in LineageTabContent (the prop sets are passed through unchanged from NodeViewOss's memoized result)
  • dotImpacted / decorateAsImpacted divergence creating visually inconsistent rows in the partial_breaking case (verified by tracing both signals end-to-end)
  • A11y regressions on the new chip and impact mark (chip is <button aria-pressed>, mark has aria-label and is decorative <span> inside a <Tooltip>)
  • Storybook story prop shape mismatches (Set conversions in buildArgs are correct)

Copy link
Copy Markdown
Contributor

@gcko gcko left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude Code Review: No critical issues found.

Copy link
Copy Markdown
Contributor

@gcko gcko left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude Code Review: No critical issues found.

@gcko gcko merged commit a45e20a into main May 6, 2026
7 checks passed
@gcko gcko deleted the feature/drc-3344-lineage-tab-show-impact-badges-on-direct-upstreamdownstream branch May 6, 2026 04:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants