diff --git a/js/packages/storybook/stories/lineage/NodeTag.stories.tsx b/js/packages/storybook/stories/lineage/NodeTag.stories.tsx new file mode 100644 index 000000000..cf2e2f155 --- /dev/null +++ b/js/packages/storybook/stories/lineage/NodeTag.stories.tsx @@ -0,0 +1,89 @@ +import { NodeTag } from "@datarecce/ui/primitives"; +import type { Meta, StoryObj } from "@storybook/react-vite"; + +const meta: Meta = { + title: "Lineage/NodeTag", + component: NodeTag, + tags: ["autodocs"], + parameters: { + docs: { + description: { + component: + "Displays the dbt resource type or materialization strategy as a pill-shaped tag with an icon. For model nodes with a materialization, shows the materialization (table, view, incremental, etc.). For all other resource types, shows the resource type.", + }, + }, + layout: "centered", + }, +}; + +export default meta; +type Story = StoryObj; + +// ============================================ +// All Types Overview +// ============================================ + +export const AllTypes: Story = { + name: "All Types", + render: () => ( +
+
+
Resource Types
+
+ {[ + "model", + "source", + "seed", + "snapshot", + "metric", + "exposure", + "semantic_model", + ].map((type) => ( +
+ +
+ ))} +
+
+
+
+ Materializations (model) +
+
+ {[ + "table", + "view", + "incremental", + "ephemeral", + "materialized_view", + "dynamic_table", + "streaming_table", + ].map((type) => ( +
+ +
+ ))} +
+
+
+ ), +}; + +// ============================================ +// Edge Cases +// ============================================ + +export const UnknownResourceType: Story = { + name: "Unknown Resource Type", + args: { resourceType: "unknown_type" }, +}; + +export const UnknownMaterialization: Story = { + name: "Unknown Materialization", + args: { resourceType: "model", materialized: "custom_materialization" }, +}; + +export const Undefined: Story = { + name: "Undefined", + args: {}, +}; diff --git a/js/packages/storybook/stories/lineage/NodeView.stories.tsx b/js/packages/storybook/stories/lineage/NodeView.stories.tsx index f270a07d8..98f8613b0 100644 --- a/js/packages/storybook/stories/lineage/NodeView.stories.tsx +++ b/js/packages/storybook/stories/lineage/NodeView.stories.tsx @@ -4,6 +4,7 @@ import type { SchemaViewProps, } from "@datarecce/ui/advanced"; import { NodeView } from "@datarecce/ui/advanced"; +import { NodeTag } from "@datarecce/ui/primitives"; import Box from "@mui/material/Box"; import Typography from "@mui/material/Typography"; import type { Meta, StoryObj } from "@storybook/react-vite"; @@ -48,6 +49,19 @@ function StubNodeSqlView({ node }: { node: NodeViewNodeData }) { ); } +function ResourceTypeTag({ node }: { node: NodeViewNodeData }) { + const materialized = + node.data.data.current?.config?.materialized ?? + node.data.data.base?.config?.materialized; + + return ( + + ); +} + // ============================================================================= // FIXTURE FACTORIES // ============================================================================= @@ -61,8 +75,16 @@ function createNode( name?: string; resourceType?: string; changeStatus?: string; + materialized?: string; + baseMaterialized?: string; } = {}, ): NodeViewNodeData { + const baseMat = overrides.baseMaterialized ?? overrides.materialized; + const baseConfig = baseMat ? { materialized: baseMat } : undefined; + const currentConfig = overrides.materialized + ? { materialized: overrides.materialized } + : undefined; + return { id: "model.jaffle_shop.stg_orders", data: { @@ -80,6 +102,7 @@ function createNode( customer_id: { name: "customer_id", type: "integer" }, order_date: { name: "order_date", type: "date" }, }, + config: baseConfig, }, current: { id: "stg_orders", @@ -91,6 +114,7 @@ function createNode( customer_id: { name: "customer_id", type: "integer" }, order_date: { name: "order_date", type: "date" }, }, + config: currentConfig, }, }, }, @@ -125,6 +149,7 @@ const meta: Meta = { isSingleEnv: false, SchemaView: StubSchemaView, NodeSqlView: StubNodeSqlView, + ResourceTypeTag, }, }; @@ -138,7 +163,7 @@ type Story = StoryObj; /** No differences in schema or code — no dots shown on either tab. */ export const NoDifferences: Story = { args: { - node: createNode(), + node: createNode({ materialized: "view" }), }, }; @@ -207,6 +232,7 @@ export const SingleEnvMode: Story = { args: { isSingleEnv: true, node: createNode({ + materialized: "table", baseCode: "SELECT 1", currentCode: "SELECT 2", baseColumns: { @@ -219,3 +245,18 @@ export const SingleEnvMode: Story = { }), }, }; + +// ============================================================================= +// MATERIALIZATION CHANGE STORY +// ============================================================================= + +/** Materialization changed from view to table between base and current. */ +export const MaterializationChanged: Story = { + args: { + node: createNode({ + baseMaterialized: "view", + materialized: "table", + changeStatus: "modified", + }), + }, +}; diff --git a/js/packages/storybook/stories/lineage/fixtures.ts b/js/packages/storybook/stories/lineage/fixtures.ts index b87c6907a..705482bcd 100644 --- a/js/packages/storybook/stories/lineage/fixtures.ts +++ b/js/packages/storybook/stories/lineage/fixtures.ts @@ -31,6 +31,7 @@ export interface LineageNodeData extends Record { changeStatus?: NodeChangeStatus; isSelected?: boolean; resourceType?: string; + materialized?: string; packageName?: string; showColumns?: boolean; columns?: Array<{ @@ -55,6 +56,7 @@ interface CreateNodeOptions { position: { x: number; y: number }; changeStatus?: NodeChangeStatus; resourceType?: string; + materialized?: string; showColumns?: boolean; columnCount?: number; data?: Partial; @@ -83,6 +85,7 @@ export function createNode({ position, changeStatus = "unchanged", resourceType = "model", + materialized, showColumns = false, columnCount = 0, data = {}, @@ -96,6 +99,7 @@ export function createNode({ nodeType: resourceType, changeStatus, resourceType, + materialized, showColumns, ...data, }, @@ -144,18 +148,21 @@ export function createDiamondNodes(): Node[] { label: "Model A", position: { x: 400, y: 150 }, changeStatus: "modified", + materialized: "view", }), createNode({ id: "model_b", label: "Model B", position: { x: 400, y: 350 }, changeStatus: "added", + materialized: "incremental", }), createNode({ id: "final", label: "Final Model", position: { x: 800, y: 250 }, changeStatus: "unchanged", + materialized: "table", }), ]; } @@ -282,14 +289,50 @@ export function largeGraph() { "unchanged", ]; - // Layer configuration: x position, node count, prefix + // Layer configuration: x position, node count, prefix, materialization const layers = [ - { x: 0, count: 8, prefix: "src", resourceType: "source" }, - { x: 400, count: 12, prefix: "stg", resourceType: "model" }, - { x: 800, count: 15, prefix: "int", resourceType: "model" }, - { x: 1200, count: 18, prefix: "fct", resourceType: "model" }, - { x: 1600, count: 12, prefix: "dim", resourceType: "model" }, - { x: 2000, count: 5, prefix: "mart", resourceType: "model" }, + { + x: 0, + count: 8, + prefix: "src", + resourceType: "source", + materialized: undefined, + }, + { + x: 400, + count: 12, + prefix: "stg", + resourceType: "model", + materialized: "view" as const, + }, + { + x: 800, + count: 15, + prefix: "int", + resourceType: "model", + materialized: "ephemeral" as const, + }, + { + x: 1200, + count: 18, + prefix: "fct", + resourceType: "model", + materialized: "incremental" as const, + }, + { + x: 1600, + count: 12, + prefix: "dim", + resourceType: "model", + materialized: "table" as const, + }, + { + x: 2000, + count: 5, + prefix: "mart", + resourceType: "model", + materialized: "materialized_view" as const, + }, ]; let nodeIndex = 0; @@ -308,6 +351,7 @@ export function largeGraph() { position: { x: layer.x, y: startY + i * verticalSpacing }, changeStatus: statuses[nodeIndex % statuses.length], resourceType: layer.resourceType, + materialized: layer.materialized, }), ); nodeIndex++; @@ -380,12 +424,15 @@ function createLineageGraphNode( resourceType: string, columns: ColumnDef[], changeStatus?: "added" | "removed" | "modified", + materialized?: string, ): LineageGraphNode { const columnData: Record = {}; for (const col of columns) { columnData[col.name] = { name: col.name, type: col.type }; } + const config = materialized ? { materialized } : undefined; + return { id, type: "lineageGraphNode", @@ -405,6 +452,7 @@ function createLineageGraphNode( resource_type: resourceType, package_name: "demo", columns: columnData, + config, }, current: { id, @@ -413,6 +461,7 @@ function createLineageGraphNode( resource_type: resourceType, package_name: "demo", columns: columnData, + config, }, }, parents: {}, @@ -460,7 +509,7 @@ export function createCllLineageGraph(): LineageGraph { ], ), - // Staging + // Staging (views — lightweight transformations) "model.demo.stg_users": createLineageGraphNode( "model.demo.stg_users", "stg_users", @@ -481,6 +530,7 @@ export function createCllLineageGraph(): LineageGraph { }, ], "modified", + "view", ), "model.demo.stg_orders": createLineageGraphNode( "model.demo.stg_orders", @@ -509,9 +559,11 @@ export function createCllLineageGraph(): LineageGraph { transformationType: "passthrough", }, ], + undefined, + "view", ), - // Dimension + // Dimension (table — pre-built for fast joins) "model.demo.dim_users": createLineageGraphNode( "model.demo.dim_users", "dim_users", @@ -532,9 +584,11 @@ export function createCllLineageGraph(): LineageGraph { changeStatus: "added", }, ], + undefined, + "table", ), - // Fact + // Fact (incremental — append new orders) "model.demo.fct_orders": createLineageGraphNode( "model.demo.fct_orders", "fct_orders", @@ -563,9 +617,11 @@ export function createCllLineageGraph(): LineageGraph { transformationType: "passthrough", }, ], + undefined, + "incremental", ), - // Mart + // Mart (materialized view — refreshed by the warehouse) "model.demo.mart_customer_orders": createLineageGraphNode( "model.demo.mart_customer_orders", "mart_customer_orders", @@ -598,6 +654,8 @@ export function createCllLineageGraph(): LineageGraph { transformationType: "derived", }, ], + undefined, + "materialized_view", ), }; diff --git a/js/packages/ui/src/api/info.ts b/js/packages/ui/src/api/info.ts index 1df602e72..55894eedd 100644 --- a/js/packages/ui/src/api/info.ts +++ b/js/packages/ui/src/api/info.ts @@ -29,6 +29,9 @@ export interface NodeData { package_name?: string; columns?: Record; primary_key?: string; + config?: { + materialized?: string; + }; } /** diff --git a/js/packages/ui/src/components/lineage/GraphNodeOss.tsx b/js/packages/ui/src/components/lineage/GraphNodeOss.tsx index b9e4f4aea..e52d85189 100644 --- a/js/packages/ui/src/components/lineage/GraphNodeOss.tsx +++ b/js/packages/ui/src/components/lineage/GraphNodeOss.tsx @@ -362,6 +362,9 @@ function GraphNodeComponent(nodeProps: GraphNodeProps) { label: name, changeStatus: nodeChangeStatus, resourceType, + materialized: + data.data?.current?.config?.materialized ?? + data.data?.base?.config?.materialized, }} // Interactive props interactive={interactive} diff --git a/js/packages/ui/src/components/lineage/NodeViewOss.tsx b/js/packages/ui/src/components/lineage/NodeViewOss.tsx index 6150fb218..eda85ab99 100644 --- a/js/packages/ui/src/components/lineage/NodeViewOss.tsx +++ b/js/packages/ui/src/components/lineage/NodeViewOss.tsx @@ -46,7 +46,7 @@ import { type RunTypeIconMap, } from "./NodeView"; import { SandboxViewOss } from "./SandboxViewOss"; -import { ResourceTypeTag as ResourceTypeTagBase } from "./tags"; +import { NodeTag } from "./tags"; // ============================================================================= // TYPES @@ -57,9 +57,18 @@ interface NodeViewProps { onCloseNode: () => void; } -const ResourceTypeTag = ({ node }: { node: LineageGraphNode }) => ( - -); +const ResourceTypeTag = ({ node }: { node: LineageGraphNode }) => { + const materialized = + node.data.data.current?.config?.materialized ?? + node.data.data.base?.config?.materialized; + + return ( + + ); +}; // ============================================================================= // OSS-SPECIFIC WRAPPER COMPONENTS diff --git a/js/packages/ui/src/components/lineage/nodes/LineageNode.tsx b/js/packages/ui/src/components/lineage/nodes/LineageNode.tsx index fdb8eaa52..ebf584e4e 100644 --- a/js/packages/ui/src/components/lineage/nodes/LineageNode.tsx +++ b/js/packages/ui/src/components/lineage/nodes/LineageNode.tsx @@ -26,7 +26,11 @@ import Tooltip from "@mui/material/Tooltip"; import Typography from "@mui/material/Typography"; import { Handle, Position } from "@xyflow/react"; import { type MouseEvent, memo, type ReactNode, useState } from "react"; -import { getIconForChangeStatus, getIconForResourceType } from "../styles"; +import { + getIconForChangeStatus, + getIconForMaterialization, + getIconForResourceType, +} from "../styles"; // ============================================================================= // TYPES @@ -65,6 +69,8 @@ export interface LineageNodeData extends Record { isSelected?: boolean; /** Resource type for icon display */ resourceType?: string; + /** Materialization strategy (table, view, incremental, etc.) */ + materialized?: string; /** Package name */ packageName?: string; /** Whether to show column-level details */ @@ -310,6 +316,7 @@ function LineageNodeComponent({ changeStatus = "unchanged", isSelected: dataIsSelected, resourceType, + materialized, } = data; // Use isNodeSelected prop, fall back to data.isSelected, then to selected @@ -323,7 +330,10 @@ function LineageNodeComponent({ color: colorChangeStatus, backgroundColor: backgroundColorChangeStatus, } = getIconForChangeStatus(changeStatus, isDark); - const { icon: ResourceIcon } = getIconForResourceType(resourceType); + const { icon: ResourceIcon } = + resourceType === "model" && materialized + ? getIconForMaterialization(materialized) + : getIconForResourceType(resourceType); // Calculate styles based on state const borderWidth = "2px"; @@ -549,12 +559,12 @@ function LineageNodeComponent({ <> {ResourceIcon && ( - + )} {changeStatus && IconChangeStatus && ( - + )} diff --git a/js/packages/ui/src/components/lineage/styles.tsx b/js/packages/ui/src/components/lineage/styles.tsx index f286421cb..39efbb607 100644 --- a/js/packages/ui/src/components/lineage/styles.tsx +++ b/js/packages/ui/src/components/lineage/styles.tsx @@ -11,7 +11,7 @@ * Source: Ported from OSS js/src/components/lineage/styles.tsx */ -import type { ComponentType, SVGProps } from "react"; +import { type ComponentType, type SVGProps, useId } from "react"; import { colors } from "../../theme/colors"; // ============================================================================= @@ -315,6 +315,123 @@ export const IconSemanticModel: IconComponent = (props) => ( ); +// ============================================================================= +// MATERIALIZATION ICONS +// ============================================================================= + +/** + * Eye icon for "view" materialization type + */ +export const IconViewMat: IconComponent = (props) => ( + + + +); + +/** + * Cube icon with dashed top portion for "incremental" materialization type + * Bottom 2/3 is solid fill, top 1/3 is dashed strokes + */ +export const IconIncremental: IconComponent = (props) => { + const id = useId(); + const bottomId = `inc-bottom-${id}`; + const topId = `inc-top-${id}`; + + return ( + + + + + + + + + + {/* Bottom 2/3: solid fill */} + + {/* Top 1/3: dashed strokes */} + + + ); +}; + +/** + * Fully dashed cube icon for "ephemeral" materialization type + */ +export const IconEphemeral: IconComponent = (props) => ( + + + +); + +/** + * Solid cube with small eye overlay for "materialized_view" materialization type + */ +export const IconMaterializedView: IconComponent = (props) => ( + + {/* Scaled-down cube at ~75% */} + + + + {/* Eye icon overlay in bottom-right (~50% size) */} + + + + +); + // ============================================================================= // STYLING FUNCTIONS // ============================================================================= @@ -435,6 +552,39 @@ export function getIconForResourceType( } } +/** + * Get icon and color for a materialization type + * + * @param materialization - The materialization type (table, view, incremental, etc.) + * @returns Object containing color and icon component + * + * @example + * ```tsx + * const { color, icon: Icon } = getIconForMaterialization("view"); + * return Icon ? : null; + * ``` + */ +export function getIconForMaterialization( + materialization?: string, +): ResourceTypeStyle { + switch (materialization) { + case "table": + return { color: colors.cyan[200], icon: IconModel }; + case "view": + return { color: colors.fuchsia[300], icon: IconViewMat }; + case "incremental": + return { color: colors.iochmara[300], icon: IconIncremental }; + case "ephemeral": + return { color: colors.neutral[400], icon: IconEphemeral }; + case "materialized_view": + case "dynamic_table": + case "streaming_table": + return { color: colors.fuchsia[200], icon: IconMaterializedView }; + default: + return { color: "inherit", icon: undefined }; + } +} + // ============================================================================= // STYLE CONSTANTS // ============================================================================= diff --git a/js/packages/ui/src/components/lineage/tags/NodeTag.tsx b/js/packages/ui/src/components/lineage/tags/NodeTag.tsx new file mode 100644 index 000000000..d0c991816 --- /dev/null +++ b/js/packages/ui/src/components/lineage/tags/NodeTag.tsx @@ -0,0 +1,66 @@ +"use client"; + +import Box from "@mui/material/Box"; +import Tooltip from "@mui/material/Tooltip"; +import { memo } from "react"; +import { useIsDark } from "../../../hooks/useIsDark"; +import { getIconForMaterialization, getIconForResourceType } from "../styles"; +import { getTagRootSx, tagStartElementSx } from "./tagStyles"; + +export interface NodeTagProps { + resourceType?: string; + materialized?: string; + "data-testid"?: string; +} + +const materializationLabels: Record = { + table: "table", + view: "view", + incremental: "incremental", + ephemeral: "ephemeral", + materialized_view: "mat. view", + dynamic_table: "dyn. table", + streaming_table: "stream. table", +}; + +function getMaterializationLabel(materialized: string): string { + return materializationLabels[materialized] ?? materialized; +} + +function NodeTagComponent({ + resourceType, + materialized, + "data-testid": testId, +}: NodeTagProps) { + const isDark = useIsDark(); + + const showMaterialization = resourceType === "model" && materialized != null; + + const { icon: Icon } = showMaterialization + ? getIconForMaterialization(materialized) + : getIconForResourceType(resourceType); + + const label = showMaterialization + ? getMaterializationLabel(materialized) + : resourceType; + + const tooltip = showMaterialization + ? "Materialization strategy" + : "Type of resource"; + + return ( + + + {Icon && ( + + + )} + {label} + + + ); +} + +export const NodeTag = memo(NodeTagComponent); +NodeTag.displayName = "NodeTag"; diff --git a/js/packages/ui/src/components/lineage/tags/ResourceTypeTag.tsx b/js/packages/ui/src/components/lineage/tags/ResourceTypeTag.tsx deleted file mode 100644 index f4103827f..000000000 --- a/js/packages/ui/src/components/lineage/tags/ResourceTypeTag.tsx +++ /dev/null @@ -1,83 +0,0 @@ -"use client"; - -/** - * @file ResourceTypeTag.tsx - * @description Pure presentation component for displaying resource type with icon - * - * Shows the resource type (model, source, seed, etc.) as a tag with an icon. - * Uses theme-aware styling for light/dark mode support. - * - * Source: Migrated from OSS js/src/components/lineage/NodeTag.tsx - */ - -import Box from "@mui/material/Box"; -import Tooltip from "@mui/material/Tooltip"; -import { memo } from "react"; -import { useIsDark } from "../../../hooks/useIsDark"; -import { getIconForResourceType } from "../styles"; -import { getTagRootSx, tagStartElementSx } from "./tagStyles"; - -// ============================================================================= -// TYPES -// ============================================================================= - -/** - * Data required for ResourceTypeTag - */ -export interface ResourceTypeTagData { - /** The resource type to display (model, source, seed, snapshot, etc.) */ - resourceType?: string; -} - -/** - * Props for ResourceTypeTag component - */ -export interface ResourceTypeTagProps { - /** Node data containing the resource type */ - data: ResourceTypeTagData; - /** Test ID for testing */ - "data-testid"?: string; -} - -// ============================================================================= -// COMPONENT -// ============================================================================= - -/** - * ResourceTypeTag - Displays the resource type with an icon - * - * Shows a pill-shaped tag with an icon representing the resource type - * (model, source, seed, snapshot, metric, exposure, semantic_model). - * - * @example - * ```tsx - * // Basic usage - * - * - * // With snapshot type - * - * ``` - */ -function ResourceTypeTagComponent({ - data, - "data-testid": testId, -}: ResourceTypeTagProps) { - const isDark = useIsDark(); - const { icon: ResourceTypeIcon } = getIconForResourceType(data.resourceType); - - return ( - - - {ResourceTypeIcon && ( - - - - )} - {data.resourceType} - - - ); -} - -export const ResourceTypeTag = memo(ResourceTypeTagComponent); -ResourceTypeTag.displayName = "ResourceTypeTag"; diff --git a/js/packages/ui/src/components/lineage/tags/__tests__/NodeTag.test.tsx b/js/packages/ui/src/components/lineage/tags/__tests__/NodeTag.test.tsx new file mode 100644 index 000000000..ea058357f --- /dev/null +++ b/js/packages/ui/src/components/lineage/tags/__tests__/NodeTag.test.tsx @@ -0,0 +1,116 @@ +import { render, screen } from "@testing-library/react"; +import { vi } from "vitest"; +import { NodeTag } from "../NodeTag"; + +const mockIsDark = vi.fn(() => false); +vi.mock("../../../../hooks/useIsDark", () => ({ + useIsDark: () => mockIsDark(), +})); + +describe("NodeTag", () => { + beforeEach(() => { + mockIsDark.mockReturnValue(false); + }); + + describe("resource type display", () => { + it("renders resource type text", () => { + render(); + expect(screen.getByText("source")).toBeInTheDocument(); + }); + + it("renders different resource types", () => { + const { rerender } = render(); + expect(screen.getByText("source")).toBeInTheDocument(); + + rerender(); + expect(screen.getByText("seed")).toBeInTheDocument(); + + rerender(); + expect(screen.getByText("snapshot")).toBeInTheDocument(); + + rerender(); + expect(screen.getByText("metric")).toBeInTheDocument(); + + rerender(); + expect(screen.getByText("exposure")).toBeInTheDocument(); + + rerender(); + expect(screen.getByText("semantic_model")).toBeInTheDocument(); + }); + + it("renders model as resource type when no materialization", () => { + render(); + expect(screen.getByText("model")).toBeInTheDocument(); + }); + + it("handles undefined resource type", () => { + render(); + expect(screen.queryByRole("tooltip")).not.toBeInTheDocument(); + }); + }); + + describe("materialization display", () => { + it("shows materialization for models", () => { + render(); + expect(screen.getByText("table")).toBeInTheDocument(); + }); + + it("shows display labels for known materializations", () => { + render(); + expect(screen.getByText("mat. view")).toBeInTheDocument(); + }); + + it("ignores materialization for non-model resources", () => { + render(); + expect(screen.getByText("source")).toBeInTheDocument(); + expect(screen.queryByText("table")).not.toBeInTheDocument(); + }); + }); + + describe("data-testid", () => { + it("renders with data-testid", () => { + render(); + expect(screen.getByTestId("test-tag")).toBeInTheDocument(); + }); + }); + + describe("styling", () => { + it("calls useIsDark and renders in light mode", () => { + mockIsDark.mockReturnValue(false); + const { container } = render(); + expect(container.firstChild).toBeInTheDocument(); + expect(mockIsDark).toHaveBeenCalled(); + }); + + it("calls useIsDark and renders in dark mode", () => { + mockIsDark.mockReturnValue(true); + const { container } = render(); + expect(container.firstChild).toBeInTheDocument(); + expect(mockIsDark).toHaveBeenCalled(); + }); + }); + + describe("icon display", () => { + it("renders icon for model type", () => { + const { container } = render(); + expect(container.querySelector("svg")).toBeInTheDocument(); + }); + + it("renders icon for source type", () => { + const { container } = render(); + expect(container.querySelector("svg")).toBeInTheDocument(); + }); + + it("does not render icon for unknown resource type", () => { + const { container } = render(); + expect(container.querySelector("svg")).not.toBeInTheDocument(); + }); + + it("renders materialization icon for model with materialization", () => { + const { container } = render( + , + ); + expect(container.querySelector("svg")).toBeInTheDocument(); + }); + }); +}); diff --git a/js/packages/ui/src/components/lineage/tags/__tests__/ResourceTypeTag.test.tsx b/js/packages/ui/src/components/lineage/tags/__tests__/ResourceTypeTag.test.tsx deleted file mode 100644 index a98ec89be..000000000 --- a/js/packages/ui/src/components/lineage/tags/__tests__/ResourceTypeTag.test.tsx +++ /dev/null @@ -1,122 +0,0 @@ -/** - * @file ResourceTypeTag.test.tsx - * @description Tests for ResourceTypeTag component - * - * Tests verify: - * - Component renders resource type text - * - Icon is displayed for known resource types - * - Tooltip is present - * - Dark mode styling works correctly - */ - -import { render, screen } from "@testing-library/react"; -import { vi } from "vitest"; -import { ResourceTypeTag } from "../ResourceTypeTag"; - -// Mock useIsDark hook -const mockIsDark = vi.fn(() => false); -vi.mock("../../../../hooks/useIsDark", () => ({ - useIsDark: () => mockIsDark(), -})); - -describe("ResourceTypeTag", () => { - beforeEach(() => { - mockIsDark.mockReturnValue(false); - }); - - describe("rendering", () => { - it("renders resource type text", () => { - render(); - expect(screen.getByText("model")).toBeInTheDocument(); - }); - - it("renders different resource types", () => { - const { rerender } = render( - , - ); - expect(screen.getByText("source")).toBeInTheDocument(); - - rerender(); - expect(screen.getByText("seed")).toBeInTheDocument(); - - rerender(); - expect(screen.getByText("snapshot")).toBeInTheDocument(); - - rerender(); - expect(screen.getByText("metric")).toBeInTheDocument(); - - rerender(); - expect(screen.getByText("exposure")).toBeInTheDocument(); - - rerender(); - expect(screen.getByText("semantic_model")).toBeInTheDocument(); - }); - - it("handles undefined resource type", () => { - render(); - // Should not crash, will just render empty text - expect(screen.queryByRole("tooltip")).not.toBeInTheDocument(); - }); - - it("renders with data-testid", () => { - render( - , - ); - expect(screen.getByTestId("test-tag")).toBeInTheDocument(); - }); - }); - - describe("styling", () => { - it("applies light mode styling", () => { - mockIsDark.mockReturnValue(false); - render( - , - ); - // Component renders - styling is applied via MUI sx prop - expect(screen.getByTestId("light-tag")).toBeInTheDocument(); - }); - - it("applies dark mode styling", () => { - mockIsDark.mockReturnValue(true); - render( - , - ); - // Component renders - styling is applied via MUI sx prop - expect(screen.getByTestId("dark-tag")).toBeInTheDocument(); - }); - }); - - describe("icon display", () => { - it("renders icon for model type", () => { - const { container } = render( - , - ); - // Icon is rendered as SVG - expect(container.querySelector("svg")).toBeInTheDocument(); - }); - - it("renders icon for source type", () => { - const { container } = render( - , - ); - expect(container.querySelector("svg")).toBeInTheDocument(); - }); - - it("does not render icon for unknown resource type", () => { - const { container } = render( - , - ); - // Unknown types don't have icons - expect(container.querySelector("svg")).not.toBeInTheDocument(); - }); - }); -}); diff --git a/js/packages/ui/src/components/lineage/tags/index.ts b/js/packages/ui/src/components/lineage/tags/index.ts index 78d0caef8..9d58f5803 100644 --- a/js/packages/ui/src/components/lineage/tags/index.ts +++ b/js/packages/ui/src/components/lineage/tags/index.ts @@ -1,14 +1,2 @@ -/** - * @file index.ts - * @description Exports for lineage tag components - * - * Tag components are small visual indicators used within lineage graph nodes - * to show metadata like resource type, row counts, etc. - */ - -export { - ResourceTypeTag, - type ResourceTypeTagData, - type ResourceTypeTagProps, -} from "./ResourceTypeTag"; +export { NodeTag, type NodeTagProps } from "./NodeTag"; export { getTagRootSx, tagStartElementSx } from "./tagStyles"; diff --git a/js/packages/ui/src/components/summary/SchemaSummary.tsx b/js/packages/ui/src/components/summary/SchemaSummary.tsx index 820645e36..2ea79359d 100644 --- a/js/packages/ui/src/components/summary/SchemaSummary.tsx +++ b/js/packages/ui/src/components/summary/SchemaSummary.tsx @@ -9,7 +9,7 @@ import Typography from "@mui/material/Typography"; import { useEffect, useState } from "react"; import type { LineageGraph, LineageGraphNode } from "../../contexts"; import { mergeKeysWithStatus } from "../../utils"; -import { ResourceTypeTag, RowCountDiffTag } from "../lineage"; +import { NodeTag, RowCountDiffTag } from "../lineage"; import { SchemaView } from "../schema"; interface SchemaDiffCardProps { @@ -28,7 +28,13 @@ function SchemaDiffCard({ node, ...props }: SchemaDiffCardProps) { } subheader={ - + {node.data.resourceType === "model" && ( )} diff --git a/js/packages/ui/src/primitives.ts b/js/packages/ui/src/primitives.ts index e1bd4f9c6..b8ce41d71 100644 --- a/js/packages/ui/src/primitives.ts +++ b/js/packages/ui/src/primitives.ts @@ -64,6 +64,7 @@ export { type LineageNodeProps, type NodeChangeStatus, } from "./components/lineage/nodes"; +export { NodeTag, type NodeTagProps } from "./components/lineage/tags"; // ============================================================================= // CHECK PRIMITIVES diff --git a/js/src/components/summary/__tests__/SchemaSummary.test.tsx b/js/src/components/summary/__tests__/SchemaSummary.test.tsx index 424318ed3..7852fba55 100644 --- a/js/src/components/summary/__tests__/SchemaSummary.test.tsx +++ b/js/src/components/summary/__tests__/SchemaSummary.test.tsx @@ -62,8 +62,8 @@ vi.mock("@datarecce/ui/components/schema", () => ({ })); vi.mock("@datarecce/ui/components/lineage", () => ({ - ResourceTypeTag: ({ data }: { data: { resourceType: string } }) => ( - {data.resourceType} + NodeTag: ({ resourceType }: { resourceType: string }) => ( + {resourceType} ), RowCountDiffTag: () => RowCount, })); @@ -269,7 +269,7 @@ describe("SchemaSummary (Simplified)", () => { }); describe("integration", () => { - it("displays ResourceTypeTag", async () => { + it("displays NodeTag with resource type", async () => { const lineageGraph = createMockLineageGraph([ createMockNode( "node1", @@ -282,7 +282,7 @@ describe("SchemaSummary (Simplified)", () => { render(); await waitFor(() => { - expect(screen.getByTestId("resource-type-tag")).toBeInTheDocument(); + expect(screen.getByText("model")).toBeInTheDocument(); }); });