Skip to content

feat(lineage): add Lineage tab to Model Detail panel#1345

Merged
gcko merged 10 commits into
mainfrom
feature/drc-3306-integrate-upstreamdownstream-index-into-model-detail-panel-tab
Apr 29, 2026
Merged

feat(lineage): add Lineage tab to Model Detail panel#1345
gcko merged 10 commits into
mainfrom
feature/drc-3306-integrate-upstreamdownstream-index-into-model-detail-panel-tab

Conversation

@wcchang1115
Copy link
Copy Markdown
Collaborator

@wcchang1115 wcchang1115 commented Apr 29, 2026

PR checklist

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

What type of PR is this?

Feature / enhancement (UI).

What this PR does / why we need it:

Adds a Lineage tab as the first tab in the Model Detail panel that shows the focused node's direct upstream parents and direct downstream children, replacing the previously prototyped collapsible "Upstream & Downstream" section above the Columns/Code tabs.

Tab body:

  • Toolbar with a back arrow and a Path <previous> › <current> breadcrumb that surfaces one step of navigation history (older entries stay in the stack and are reachable by clicking back repeatedly).
  • Upstream and Downstream sections, each with a per-side filter (appears once direct rows exceed 8) and pagination (initial page size 8, +20 per click). Each row refocuses the panel to that node; canvas pan/zoom is decoupled.
  • Focus card with the node name, an inline center-on-canvas icon, a change-status badge, and a left accent bar colored by added / removed / modified / unchanged.
  • Theme-aware via useThemeColors — section headers, toolbar, and focus card adapt to dark mode.

API:

  • NodeView gains a lineageTabContent slot so the OSS shell injects the tab without coupling NodeView to the lineage graph context.
  • LineageViewOss exposes historyTrail and a new navigateToHistoryIndex callback so the breadcrumb can jump to a specific history entry.
  • LineageTabContent and LineageTabContentProps are now exported from @datarecce/ui/advanced.

Which issue(s) this PR fixes:

DRC-3306

Special notes for your reviewer:

The branch went through two design iterations on the same scope:

  1. Earlier commits (ddc41bae, 006b6173) added a V2 collapsible section.
  2. After review the design moved to a dedicated tab. c24d78c4 removes V2 entirely (component, test, export, lineageIndexSlot prop, OSS wiring), and d8cbb138 introduces the Lineage tab.

Net diff vs main is "Lineage tab + supporting infra" — the V2 commits are intermediate history. Reviewing the squash-merge / final diff is easier than going commit-by-commit.

Test coverage:

  • 13 unit tests in LineageTabContent.test.tsx covering rows, filter threshold, pagination, navigation, breadcrumb (single-step rule, fallback to onBack), focus reset on node change, etc.
  • 7 Storybook stories under Lineage/LineageTabContent covering default / change-statuses / with-history / many-downstream / source / leaf / isolated.

Does this PR introduce a user-facing change?:

A new Lineage tab appears as the first tab in the Model Detail panel, showing direct upstream and downstream nodes for the focused model and a path breadcrumb of recent navigation. Replaces the previously prototyped "Upstream & Downstream" collapsible section.

🤖 Generated with Claude Code

wcchang1115 and others added 8 commits April 29, 2026 09:57
Folds the lineage index into the existing Model Detail panel as a
collapsible section above the Columns/Code tabs, so users can navigate
direct upstream/downstream without a separate panel. Includes a per-column
filter, 200px scroll cap, and paginated "show N more" to keep staging
models with large fan-out (e.g. stg_orders with 140 direct children) from
overwhelming the panel.

Refs DRC-3306.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Signed-off-by: Wei-Chun, Chang <wcchang@infuseai.io>
Row clicks in Upstream & Downstream now only refocus the panel; the canvas
is only re-centered when the user clicks the new crosshair icon. A back
button appears in the section header while navigation history is non-empty
(row-click history only — clicking a node on the canvas clears it).

Refs DRC-3306.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Signed-off-by: Wei-Chun, Chang <wcchang@infuseai.io>
The collapsible Upstream & Downstream section above the Columns/Code
tabs is being replaced by a dedicated Lineage tab. Drop the V2
component, its test, the index export, the lineageIndexSlot prop on
NodeView, and the OSS wiring.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Signed-off-by: Wei-Chun, Chang <wcchang@infuseai.io>
Adds a "Lineage" tab as the first tab in the Model Detail panel that
shows the focused node's direct upstream parents and direct downstream
children. The body has:

  - Toolbar with a back button and a "Path <previous> > <current>"
    breadcrumb that exposes one step of navigation history (older
    entries stay in the stack and are reachable via back).
  - Upstream and Downstream sections with per-side filter and
    pagination. Each row navigates the panel to that node.
  - Focused-node card with the node name, an inline center-on-canvas
    button, and a change-status badge. Accent bar uses the
    added/removed/modified/unchanged color.

NodeView gains a `lineageTabContent` slot so the OSS shell injects the
tab without coupling NodeView to the lineage graph context.
LineageViewOss exposes `historyTrail` and a `navigateToHistoryIndex`
callback so the breadcrumb can jump back to a specific entry.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Signed-off-by: Wei-Chun, Chang <wcchang@infuseai.io>
Covers direct upstream/downstream rendering, empty hints, row click
navigation, filter threshold and narrowing, pagination "show more",
back-button visibility, the in-card center-on-canvas button, and the
breadcrumb behavior — including the rule that only the most recent
previous step is shown and that clicking it falls back to onBack when
onJumpToHistory is not provided. Also covers state reset when the
focused node changes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Signed-off-by: Wei-Chun, Chang <wcchang@infuseai.io>
Exports LineageTabContent and LineageTabContentProps from
@datarecce/ui/advanced and adds a Lineage/LineageTabContent story file
covering: a default mid-graph node, mixed change-status neighbors, the
back-path breadcrumb (rendered only when historyTrail is non-empty),
the filter+pagination case, and the source/leaf/isolated edge cases.

The back arrow and breadcrumb only render on the WithHistory story —
matching how the OSS wrapper only passes onBack when the navigation
history is non-empty.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Signed-off-by: Wei-Chun, Chang <wcchang@infuseai.io>
Replace the hardcoded grey.50 section / toolbar backgrounds with
useThemeColors().background.subtle, and swap the focus card's hardcoded
light-pink (rgb(255 245 241)) for background.emphasized when isDark, so
the focused-node card stays distinguishable in both themes. Light mode
keeps its original orange-tinted card.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Signed-off-by: Wei-Chun, Chang <wcchang@infuseai.io>
MUI v9 dropped SystemProps shortcuts on Stack, so `alignItems` is no
longer a valid prop. Move it into `sx` on all five Stack instances in
LineageTabContent so `next build` type-checks cleanly.

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-3306-integrate-upstreamdownstream-index-into-model-detail-panel-tab branch from 1618219 to 31d27ce Compare April 29, 2026 02:37
wcchang1115 and others added 2 commits April 29, 2026 11:21
navigateBack and navigateToHistoryIndex were calling setFocusedNodeId
inside the setFocusedHistory updater function. React updater functions
must be pure — StrictMode invokes them twice in development, firing the
nested setter twice per click. Read state up front, then dispatch both
setters separately.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Signed-off-by: Wei-Chun, Chang <wcchang@infuseai.io>
PathBreadcrumb hardcoded textDecorationColor as 25% black, which is
nearly invisible on the dark background.subtle (neutral[800]) — the
"this previous step is clickable" affordance disappeared at rest.
Read isDark from useThemeColors and pick a translucent white in dark
mode while keeping the existing 25% black in light mode.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Signed-off-by: Wei-Chun, Chang <wcchang@infuseai.io>
@wcchang1115 wcchang1115 self-assigned this Apr 29, 2026
@wcchang1115 wcchang1115 marked this pull request as ready for review April 29, 2026 03:31
@wcchang1115 wcchang1115 requested a review from gcko April 29, 2026 03:58
@gcko
Copy link
Copy Markdown
Contributor

gcko commented Apr 29, 2026

Code Review: PR #1345

Reviewed at: bab342bd (feature/drc-3306-integrate-upstreamdownstream-index-into-model-detail-panel-tab)
Files reviewed: 8
Categories: Frontend (UI components, tests, stories, exports)
Passes run: A, C, D, E, F, G, H

Validation Results

Pass A: Correctness & Logic — PASS

  • getChangeStatus (LineageTabContent.tsx:70-72) correctly maps the underlying "added" | "removed" | "modified" | undefined to the local "added" | "removed" | "modified" | "unchanged" union.
  • historyTrail[historyTrail.length - 1] access at LineageTabContent.tsx:637 is gated by hasTrail && historyTrail where hasTrail = (historyTrail?.length ?? 0) > 0, so the index is always valid.
  • LineageViewOss.navigateToNode (LineageViewOss.tsx:668-674) reads the closure's focusedNodeId for the history append before calling setFocusedNodeId — correct under React 18 batching (and matches the recent fix at 274fcc26 that hoisted setFocusedNodeId out of the updater).
  • navigateToHistoryIndex truncation (h.slice(0, index)) followed by setting focus to focusedHistory[index] correctly produces a state where the back button can continue popping older entries — matches the PR description's "older entries stay reachable by clicking back."
  • Pagination math: setVisible((n) => Math.min(filtered.length, n + PAGE_STEP)) is bounded; slice(0, visible) plus hidden = filtered.length - visibleIds.length handles the case where visible > filtered.length after a filter narrows results.
  • useEffect([node.id]) correctly resets per-side filter and pagination on focus change — verified by the "filter and pagination reset" test.

Pass C: Cross-Reference Consistency — PASS

  • LineageGraphNode.data shape ({ name, parents: Record<string, LineageGraphEdge>, children: Record<string, LineageGraphEdge>, changeStatus? }, types.ts:34-51) matches everything LineageTabContent reads.
  • LineageGraph.nodes: Record<string, LineageGraphNode> (types.ts:87) matches the nodesById prop shape used by the tab and passed from LineageViewOss (lineageGraph?.nodes).
  • useThemeColors returns { isDark, background.subtle, background.emphasized, ... } (useThemeColors.ts:59-128) — every key consumed by the tab/breadcrumb/section header exists.
  • changeStatusColors is Record<ChangeStatus | "unchanged", string> (styles.tsx:629-634) — matches the [status] indexing pattern in StatusDot, FocusCard, and the badge.
  • lineageTabContent?: ReactNode in NodeViewProps (NodeView.tsx:169) ↔ LineageTabContent JSX from NodeViewOss.tsx:375-387LineageViewOss props for historyTrail/onJumpToHistory/onCenterFocused/onBack/onNavigateToNode — all signatures align.
  • V2 cleanup confirmed: no remaining lineageIndexSlot or UpstreamDownstreamIndex references anywhere under js/.

Pass D: Error Handling & Edge Cases — PASS

  • Object.keys(node.data.parents ?? {}) and the equivalent for children — ?? covers both null and undefined.
  • nodesById?.[id]?.data.name ?? id falls back to the raw id when a parent/child id isn't in the lookup (e.g., partially-loaded graph). Same fallback for changeStatus.
  • navigateToNode / navigateToHistoryIndex / navigateBack all guard on lineageGraph?.nodes[target] before mutating state — stale ids are silently ignored, no crash.
  • Empty/source/leaf paths covered: separate empty hints ((source — no upstream), (leaf — no downstream), no matches).

Pass E: Test Coverage & Quality — PASS

  • 13 unit tests cover rendering, navigation, filter threshold, pagination, breadcrumb single-step rule, breadcrumb fallback, and state reset on node change. Assertions are specific (toHaveBeenCalledWith("p1"), toHaveBeenCalledWith(2)).
  • 7 Storybook stories: Default / WithChangeStatuses / WithHistory / ManyDownstream / SourceNode / LeafNode / Isolated.
  • Test fixtures use {} as never to stub edge values (LineageTabContent.test.tsx, lines 45-49, 69, 87-89, etc.). Works because the component only reads Object.keys, but the cast bypasses type safety — see Notes.
  • Existing LineageView.component.test.tsx:248 mock for NodeViewOss only spreads { node, onCloseNode } and silently drops the new onNavigateToNode/onBack/onCenterFocused/historyTrail/onJumpToHistory props. Mock still type-checks (excess props on a vi.fn factory), and the file is testing a different component. Not a regression.

Pass F: Diff-Specific Checks — PASS

  • V2 collapsible (lineageIndexSlot, UpstreamDownstreamIndex) fully removed — no orphaned references in js/app, js/packages, or js/src.
  • NodeViewProps adds lineageTabContent?: ReactNode as a purely additive optional slot. Existing consumers without the slot continue to get the prior 2-tab layout (Columns at 0, Code at 1) because the index math in NodeView.tsx:771-773 is conditional on lineageTabContent.
  • Stack alignItems consistently in sx (not as a removed-in-MUI-v9 prop) — confirmed by zero alignItems= matches in the new files.

Pass G: Performance — PASS

  • Filter/pagination computed via useMemo with appropriate deps. Object.keys and filter(...toLowerCase().includes(q)) are O(n) per side — fine for typical dbt graphs.
  • LineageGraph.nodes (a single shared object) is passed by reference to LineageTabContent.nodesById; no per-render copy.

Pass H: Async/Concurrency — PASS

  • All state transitions are synchronous setState calls. navigateToHistoryIndex and navigateToNode each call multiple setters in sequence, which React 18 batches automatically.
  • NodeViewOss.useQuery for modelDetail is keyed on node.id, so navigating between nodes triggers a fresh fetch — no stale-data hazard.

Verification Results

  • pnpm type:check — passing (tsc --noEmit, 0 errors)
  • pnpm lint — passing (Biome, 609 files, 0 errors)
  • pnpm test — passing (155 files, 3699 passing / 5 skipped / 0 failed)

Verdict: GO

No BLOCKERs, no ISSUEs.

Notes

  1. Hardcoded light-mode accent colorLineageTabContent.tsx:318 uses the literal "rgb(255 245 241)" for the light-mode focus card background while dark mode uses background.emphasized from useThemeColors. If this is the intended brand accent, a named token would make it easier to keep the rest of the orange-accent surface in sync.

  2. tabValue persistence across lineageTabContent toggleNodeView.tsx:632 keeps tabValue as local state, and the Lineage/Columns/Code indices shift if lineageTabContent switches between defined/undefined at runtime. In OSS this never happens (always provided when the panel mounts), but external consumers of the public NodeViewProps could hit it. A reset in useEffect([!!lineageTabContent]) would harden the contract.

  3. DirectRow is mouse-onlyLineageTabContent.tsx:119-155 doesn't add role="button", tabIndex, or onKeyDown to the clickable upstream/downstream rows. This is internally inconsistent with ShowMoreRow and PathBreadcrumb in the same file, which do wire keyboard handling. It is, however, consistent with the broader project pattern (the only role="button" under js/packages/ui/src/components outside this file is zero), so not a regression — flagging as an opportunity for the new code to set a stronger bar.

  4. {} as never in test fixturesLineageTabContent.test.tsx uses {} as never for LineageGraphEdge values (e.g. line 45). Safe today because the component only reads Object.keys(parents/children), but it ties the fixture to that implementation detail. A typed factory like makeEdge() (or as unknown as LineageGraphEdge with a comment on why fields are unused) would survive a future component change that touches edge values.

What I could not verify

  • Visual integration in recce server (didn't start the server / open the browser); relied on the test suite, Storybook story coverage, and structural reading.
  • Behavior under live navigation across nodes with both historyTrail and active filter input (covered indirectly by unit tests but not exercised end-to-end).

What I looked for and did not find

  • Logic inversions, off-by-one in pagination, null/undefined hazards in historyTrail indexing.
  • Stale state in the navigate-then-record-history pattern (the 274fcc26 fix is correctly applied).
  • Mock signature drift between NodeViewOss and its component-test mock.
  • Theme token mismatches in useThemeColors return shape.
  • Cross-file regressions from the V2 → tab rename (V2 fully excised).
  • React-rerender hazards from new object/array literals in props (memoization on filtered/visible is in place).
  • TODO/FIXME left in the diff.

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. See PR comment for full review (4 NOTEs, no BLOCKERs/ISSUEs).

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: GO. No critical issues found. See PR comment for full review (4 NOTEs, no BLOCKERs/ISSUEs).

@gcko gcko merged commit 79e24f4 into main Apr 29, 2026
7 checks passed
@gcko gcko deleted the feature/drc-3306-integrate-upstreamdownstream-index-into-model-detail-panel-tab branch April 29, 2026 07:18
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