Skip to content

feat(lineage): add materialization badge for model nodes#1237

Merged
gcko merged 9 commits intomainfrom
feature/materialization-badge
Mar 25, 2026
Merged

feat(lineage): add materialization badge for model nodes#1237
gcko merged 9 commits intomainfrom
feature/materialization-badge

Conversation

@danyelf
Copy link
Contributor

@danyelf danyelf commented Mar 24, 2026

PR checklist

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

What type of PR is this?

Feature + Refactor

What this PR does / why we need it:

Adds materialization badges to model nodes in the lineage graph. Instead of showing the generic "model" resource type, model nodes now display their materialization strategy (table, view, incremental, ephemeral, materialized_view) with a distinct icon.

Key design decision: materialization is only meaningful for models, so ResourceTypeTag and MaterializationTag are unified into a single NodeTag component. It takes resourceType and optional materialized — when the resource is a model with materialization data, it shows the materialization; otherwise it shows the resource type.

Each materialization type gets a distinct icon:

  • table: solid cube (reuses existing model icon)
  • view: eye icon
  • incremental: 2/3 solid + 1/3 dashed cube
  • ephemeral: fully dashed cube
  • materialized_view: solid cube with small eye overlay

Special notes for your reviewer:

  • NodeTag replaces both ResourceTypeTag and MaterializationTag — two separate components collapsed into one
  • The branching logic that was in NodeViewOss.tsx now lives inside NodeTag itself
  • SVG clipPath IDs use useId() to avoid collisions when multiple incremental icons render
  • Storybook stories cover all resource types and all materialization variants in one story file

Does this PR introduce a user-facing change?:

Model nodes in the lineage graph now show their materialization strategy (table, view, incremental, ephemeral, materialized view) instead of the generic "model" label.

image image

  Show the materialization strategy (table, view, incremental, ephemeral,
  materialized_view) on model nodes instead of the generic resource type
  icon. Each materialization type gets a distinct icon from the cube family:

  - table: solid cube (reuses existing model icon)
  - view: eye icon
  - incremental: 2/3 solid + 1/3 dashed cube
  - ephemeral: fully dashed cube
  - materialized_view: solid cube with small eye overlay

  The badge appears in both the canvas graph nodes and the sidebar detail
  view. Non-model nodes continue to show the resource type tag.

  Also adds ResourceTypeTag stories and MaterializationTag stories, and
  updates LineageCanvas/NodeView fixtures with realistic materialization
  data across all dbt layers (staging=view, intermediate=ephemeral,
  fact=incremental, dimension=table, mart=materialized_view).

  Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

Signed-off-by: Danyel Fisher <danyel@gmail.com>
@danyelf
Copy link
Contributor Author

danyelf commented Mar 24, 2026

Code Review — PR 1237

Summary

Adds materialization badges (table, view, incremental, ephemeral, materialized_view) to model nodes in the lineage graph and sidebar. Clean implementation with proper fallback to ResourceTypeTag for non-model nodes. Type checks and lint pass.

Findings

[Critical] SVG clipPath ID collision in IconIncremental

File: js/packages/ui/src/components/lineage/styles.tsx:356-359
Issue: The IconIncremental component uses hardcoded id="incremental-bottom" and id="incremental-top" for its <clipPath> definitions. When multiple incremental model nodes are rendered simultaneously in a lineage graph (which is the common case — e.g., the largeGraph fixture has 18 fact nodes all using incremental), every instance shares the same DOM IDs. The clipPath="url(#incremental-bottom)" reference resolves to the first matching element in the document, so all but the first icon may render incorrectly depending on browser behavior.
Suggestion: Use React's useId() hook to generate unique IDs per instance. Since IconComponent is a plain function component, switching to useId() is the cleanest approach:

const IconIncremental: IconComponent = (props) => {
  const id = useId();
  const bottomId = `inc-bottom-${id}`;
  const topId = `inc-top-${id}`;
  // use bottomId/topId in clipPath defs and references
};

Alternatively, use a CSS clip-path: inset() or <mask> to avoid IDs entirely.

[Warning] Dangling JSDoc comment in primitives.ts

File: js/packages/ui/src/primitives.ts:67-72
Issue: The "Resource type tag" JSDoc comment block sits above a second JSDoc comment ("Materialization tag"), and neither is attached to a declaration — they both float above the single export { ... } block. The first JSDoc is orphaned and misleading.
Suggestion: Merge into a single JSDoc block, or remove both since the export names are self-documenting.

Verdict

⚠️ Issues found — the clipPath ID collision is a real rendering bug that will manifest whenever multiple incremental models appear in the same graph. The JSDoc nit is cosmetic. Overall the architecture is solid: clean separation of MaterializationTag vs ResourceTypeTag, proper fallback logic, good Storybook coverage.

Hardcoded clipPath IDs caused collisions when multiple incremental
model nodes rendered in the same graph. Use React useId() to generate
unique IDs per instance.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Danyel Fisher <danyel@gmail.com>
@danyelf danyelf requested a review from gcko March 24, 2026 05:25
@danyelf danyelf self-assigned this Mar 24, 2026
danyelf and others added 2 commits March 24, 2026 00:16
…NodeTag

Materialization is only meaningful for model resources, so these two
concepts belong in a single component. NodeTag takes resourceType and
optional materialized — when resourceType is "model" with a
materialization, it shows the materialization icon/label; otherwise it
shows the resource type.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Danyel Fisher <danyel@gmail.com>
Main refactored NodeView's inline types to use NodeData from api/info.
Took main's version since NodeData already includes config.materialized.
Fixed NodeView.stories.tsx to use NodeViewNodeData instead of
LineageGraphNode to match tightened DI prop types.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Danyel Fisher <danyel@gmail.com>
@danyelf
Copy link
Contributor Author

danyelf commented Mar 24, 2026

Code Review — PR #1237

Summary

Materialization badges for model nodes in the lineage graph, with a clean unification of ResourceTypeTag and MaterializationTag into a single NodeTag. All verification passes (type check, lint, 17/17 tag tests). One potential test bug found, plus a few minor gaps.

Findings

[Warning] SchemaSummary test may rely on a testId that nothing renders

File: js/src/components/summary/__tests__/SchemaSummary.test.tsx:272
Issue: The test asserts data-testid="resource-type-tag" exists, but SchemaSummary.tsx:31 renders <NodeTag resourceType={...} /> without passing a data-testid prop. The test passes only because the mock at line 64 injects that testId — so the test validates the mock, not the real component. If the mock is ever removed or the test switches to a real render, this will break.
Suggestion: Either add data-testid="resource-type-tag" to the <NodeTag> call in SchemaSummary.tsx, or change the test to assert on rendered text content instead.

[Minor] Missing dynamic_table materialization type

File: js/packages/ui/src/components/lineage/styles.tsx:558-563 and tags/NodeTag.tsx:16-22
Issue: dbt supports dynamic_table as a materialization (Snowflake, BigQuery, Databricks). It's absent from the MaterializationType union and the label map. The fallback handles it gracefully (renders raw string, no icon), so this isn't broken — just incomplete.

[Minor] No story exercising a materialization change (base vs current differ)

File: js/packages/storybook/stories/lineage/NodeView.stories.tsx:81-83
Issue: The createNode factory applies the same config to both base and current. There's no story where base=view and current=table, which is a real scenario (someone changes a model's materialization in a PR). The component would show the current value (correct), but the visual case is unexercised.

[Minor] SVG icons lack aria-hidden="true"

File: js/packages/ui/src/components/lineage/styles.tsx (all icon components)
Issue: All materialization icons are decorative (the adjacent text label provides the meaning), but none carry aria-hidden="true". Screen readers will traverse the SVGs. The Tooltip partially mitigates this, but aria-hidden on the SVGs would be more correct.

[Minor] Style tests assert nothing about actual styles

File: js/packages/ui/src/components/lineage/tags/__tests__/NodeTag.test.tsx:77-89
Issue: Both "applies light mode styling" and "applies dark mode styling" only assert toBeInTheDocument(). They'd pass even if useIsDark had no effect on the output. These inflate coverage without catching regressions.

Verification Results

Check Result
pnpm type:check Pass
pnpm lint Pass (578 files, 0 errors)
Tag unit tests Pass (17/17)

Verdict

No critical issues. The testId concern in SchemaSummary is worth a quick look but isn't blocking. Clean integration, good test coverage of the core branching logic, and the NodeTag unification is well-motivated. 👍

danyelf and others added 2 commits March 24, 2026 10:18
- Fix SchemaSummary test to assert on rendered text instead of mock's data-testid
- Add dynamic_table and streaming_table materialization types (Snowflake, Databricks)
- Add materialization-change story (base=view, current=table)
- Add aria-hidden to decorative SVG icons in NodeTag and LineageNode
- Improve style tests to verify useIsDark is called
- Consolidate NodeTag stories into unified AllTypes view
- Remove redundant individual materialization substories from NodeView

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Danyel Fisher <danyel@gmail.com>
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds model materialization badges to lineage UI nodes by introducing a unified NodeTag component that renders either a resource type or a model materialization (with distinct icons), and wires materialization data through the lineage graph/node views and Storybook fixtures.

Changes:

  • Replace ResourceTypeTag with a unified NodeTag that can display model materialization (table/view/incremental/ephemeral/materialized_view, etc.).
  • Add materialization icon support to lineage styling utilities and propagate materialized through graph/node data.
  • Update tests and Storybook stories/fixtures to cover the new tag behavior.

Reviewed changes

Copilot reviewed 16 out of 16 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
js/src/components/summary/tests/SchemaSummary.test.tsx Updates mocks/assertions to use NodeTag instead of ResourceTypeTag.
js/packages/ui/src/primitives.ts Re-exports NodeTag via primitives entrypoint.
js/packages/ui/src/components/summary/SchemaSummary.tsx Switches summary card tag from ResourceTypeTag to NodeTag.
js/packages/ui/src/components/lineage/tags/index.ts Barrel now exports NodeTag instead of ResourceTypeTag.
js/packages/ui/src/components/lineage/tags/tests/ResourceTypeTag.test.tsx Removes tests for deleted ResourceTypeTag.
js/packages/ui/src/components/lineage/tags/tests/NodeTag.test.tsx Adds unit tests for NodeTag resource-type/materialization behavior.
js/packages/ui/src/components/lineage/tags/ResourceTypeTag.tsx Removes the old ResourceTypeTag component.
js/packages/ui/src/components/lineage/tags/NodeTag.tsx Adds the unified tag component (resource type + optional materialization).
js/packages/ui/src/components/lineage/styles.tsx Adds materialization icons + getIconForMaterialization.
js/packages/ui/src/components/lineage/nodes/LineageNode.tsx Uses materialization icon when node is a model with materialized.
js/packages/ui/src/components/lineage/NodeViewOss.tsx Injected tag now derives materialized from node config and renders NodeTag.
js/packages/ui/src/components/lineage/GraphNodeOss.tsx Populates materialized into node data from base/current config.
js/packages/ui/src/api/info.ts Extends NodeData with config.materialized typing.
js/packages/storybook/stories/lineage/fixtures.ts Adds materialized to fixtures and seeds example graphs with materializations.
js/packages/storybook/stories/lineage/NodeView.stories.tsx Uses NodeTag for the “ResourceTypeTag” injection + adds materialization-change story.
js/packages/storybook/stories/lineage/NodeTag.stories.tsx New Storybook stories covering resource types and materialization variants.

@gcko
Copy link
Contributor

gcko commented Mar 25, 2026

Code Review — PR #1237

Summary

Adds materialization badges to model nodes by unifying ResourceTypeTag and the new MaterializationTag into a single NodeTag component. Clean refactor with good test coverage and Storybook stories. All tests, lint, and type checks pass.

Findings

[Warning] SchemaSummary does not pass materialized to NodeTag

File: js/packages/ui/src/components/summary/SchemaSummary.tsx:31
Issue: SchemaSummary renders <NodeTag resourceType={node.data.resourceType} /> without passing materialized. The data is available via node.data.data.current?.config?.materialized ?? node.data.data.base?.config?.materialized (the same pattern used in GraphNodeOss.tsx:364 and NodeViewOss.tsx:62). This means models in the schema summary will still show "model" while models in the lineage graph show their materialization — inconsistent UX.
Suggestion: Pass the materialized prop, consistent with the two other call sites:

<NodeTag
  resourceType={node.data.resourceType}
  materialized={
    node.data.data.current?.config?.materialized ??
    node.data.data.base?.config?.materialized
  }
/>

[Warning] Dead type export: MaterializationType

File: js/packages/ui/src/components/lineage/styles.tsx:558
Issue: MaterializationType is declared and exported but never imported or used anywhere in the codebase. Dead code.
Suggestion: Remove the type declaration (lines 558-565), or use it to type the materialization parameter of getIconForMaterialization if you want compile-time safety.

Verification Results

  • Tests: 142 files, 3536 passed, 5 skipped — all green
  • Lint (Biome): 579 files checked, no issues
  • Type check (tsc): Clean, no errors
  • Import integrity: No dangling imports of the deleted ResourceTypeTag component

Verdict

No critical bugs. Two minor issues flagged above — the SchemaSummary inconsistency is the more user-facing one. Overall this is a well-structured PR. Approving with comments.

Copy link
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. Two minor findings posted as comment — SchemaSummary missing materialized prop (inconsistent UX) and unused MaterializationType export.

gcko and others added 3 commits March 25, 2026 14:44
SchemaSummary was missing the materialized prop on NodeTag, causing
models to show "model" instead of their materialization strategy.
Also removes unused MaterializationType export from styles.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Jared Scott <jared.scott@datarecce.io>
Use `!= null` instead of truthy check for showMaterialization so
TypeScript explicitly narrows `materialized` to `string` in the
truthy branch. Addresses Copilot review feedback.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Jared Scott <jared.scott@datarecce.io>
@danyelf
Copy link
Contributor Author

danyelf commented Mar 25, 2026

Claude Code Review: No critical issues found. Two minor findings posted as comment — SchemaSummary missing materialized prop (inconsistent UX) and unused MaterializationType export.

Thanks for catching the SchemaSummary prop! I thought I'd gotten that one.

@gcko gcko merged commit fbeb09c into main Mar 25, 2026
7 checks passed
@gcko gcko deleted the feature/materialization-badge branch March 25, 2026 06:52
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.

3 participants