From d2486c859c90e5410241d499d20de829502cc417 Mon Sep 17 00:00:00 2001 From: Max Mykal Date: Fri, 3 Oct 2025 13:42:43 -0700 Subject: [PATCH 01/20] set up lineage for vscode ext --- vscode/react/package.json | 10 +- vscode/react/postcss.config.js | 6 + vscode/react/src/App.css | 159 ++++++- vscode/react/src/main.tsx | 2 + vscode/react/src/pages/ModelLineage.tsx | 409 ++++++++++++++++++ vscode/react/src/pages/ModelLineageContext.ts | 95 ++++ vscode/react/src/pages/ModelNode.tsx | 365 ++++++++++++++++ vscode/react/src/pages/ModelNodeColumn.tsx | 78 ++++ vscode/react/src/pages/help.ts | 37 ++ vscode/react/src/pages/lineage.tsx | 96 +++- vscode/react/tailwind.config.cjs | 36 +- vscode/react/tsconfig.json | 2 +- vscode/react/vite.config.js | 7 +- 13 files changed, 1260 insertions(+), 42 deletions(-) create mode 100644 vscode/react/postcss.config.js create mode 100644 vscode/react/src/pages/ModelLineage.tsx create mode 100644 vscode/react/src/pages/ModelLineageContext.ts create mode 100644 vscode/react/src/pages/ModelNode.tsx create mode 100644 vscode/react/src/pages/ModelNodeColumn.tsx create mode 100644 vscode/react/src/pages/help.ts diff --git a/vscode/react/package.json b/vscode/react/package.json index e12dd12179..dcaada3166 100644 --- a/vscode/react/package.json +++ b/vscode/react/package.json @@ -18,22 +18,25 @@ "@headlessui/react": "^2.2.5", "@heroicons/react": "^2.2.0", "@radix-ui/react-select": "^2.2.5", - "@tailwindcss/postcss": "^4.1.11", - "@tailwindcss/vite": "^4.1.11", "@tanstack/react-query": "^5.83.0", "@tanstack/react-router": "^1.129.8", "@tanstack/react-router-devtools": "^1.131.26", "@tanstack/react-virtual": "^3.13.12", "@tanstack/router-plugin": "^1.129.8", + "@tobikodata/sqlmesh-common": "0.0.17", + "@xyflow/react": "12.8.4", "apache-arrow": "^19.0.1", "clsx": "^2.1.1", + "cronstrue": "3.3.0", "elkjs": "^0.8.2", + "lodash": "4.17.21", + "lucide-react": "0.542.0", "orval": "^7.10.0", "react": "^18.3.1", "react-dom": "^18.3.1", "react-router": "^7.7.0", "reactflow": "^11.11.4", - "tailwindcss": "^4.1.11", + "tailwindcss": "3.4.17", "vscode-uri": "^3.1.0" }, "devDependencies": { @@ -45,6 +48,7 @@ "@storybook/react-vite": "^9.0.18", "@testing-library/dom": "^10.4.1", "@testing-library/react": "^16.3.0", + "@types/lodash": "4.17.20", "@types/react": "^18.3.23", "@types/react-dom": "^18.3.7", "@vitejs/plugin-react": "^4.7.0", diff --git a/vscode/react/postcss.config.js b/vscode/react/postcss.config.js new file mode 100644 index 0000000000..2e7af2b7f1 --- /dev/null +++ b/vscode/react/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/vscode/react/src/App.css b/vscode/react/src/App.css index c49ccd115b..bcd5054a27 100644 --- a/vscode/react/src/App.css +++ b/vscode/react/src/App.css @@ -1,5 +1,153 @@ -@import 'tailwindcss'; -@config "../tailwind.config.cjs"; +@import url('@tobikodata/sqlmesh-common/design/index.css'); +@import url('@tobikodata/sqlmesh-common/styles/sqlmesh-common.min.css'); + +:root { + /* + Keep commented for reference. + + --color-metadata-label: rgba(100, 100, 100, 1); + --color-metadata-value: rgba(10, 10, 10, 1); + + --color-tooltip-background: rgba(150, 150, 150, 1); + --color-tooltip-foreground: rgba(255, 255, 255, 1); + + --color-model-name-grayscale-link-underline: rgb(24, 13, 13); + --color-model-name-grayscale-link-underline-hover: rgba(125, 125, 125, 1); + --color-model-name-link-underline: rgba(0, 0, 0, 1); + --color-model-name-link-underline-hover: rgba(0, 0, 0, 1); + --color-model-name-catalog: rgba(0, 0, 0, 1); + --color-model-name-schema: rgba(0, 0, 0, 1); + --color-model-name-model: rgba(0, 0, 0, 1); + --color-model-name-grayscale-catalog: rgba(100, 100, 100, 1); + --color-model-name-grayscale-schema: rgba(50, 50, 50, 1); + */ + + --color-model-name-grayscale-model: var(--vscode-foreground); + --color-model-name-copy-icon: var(--vscode-foreground); + --color-model-name-copy-icon-hover: rgba(0, 0, 0, 0.1); + + --color-filterable-list-counter-background: var(--vscode-input-background); + --color-filterable-list-counter-foreground: var(--vscode-input-foreground); + + --color-filterable-list-input-background: var(--vscode-input-background); + --color-filterable-list-input-foreground: var(--vscode-input-foreground); + --color-filterable-list-input-placeholder: var( + --vscode-input-placeholderForeground + ); + --color-filterable-list-input-border: var(--vscode-input-border); + + --color-lineage-background: transparent; + --color-lineage-foreground: var(--vscode-foreground); + --color-lineage-border: var(--vscode-editor-background); + --color-lineage-divider: var(--vscode-text-separatorForeground); + + --color-lineage-grid-dot: rgba(0, 0, 0, 0.1); + + --color-lineage-control-background: var(--vscode-editor-background); + --color-lineage-control-background-hover: var(--vscode-editor-background); + --color-lineage-control-icon-background: var(--vscode-foreground); + --color-lineage-control-icon-foreground: var(--vscode-editor-background); + --color-lineage-control-button-tooltip-background: var( + --vscode-editor-background + ); + --color-lineage-control-button-tooltip-foreground: var(--vscode-foreground); + + --xy-controls-button-background-color: var(--vscode-editor-background); + --xy-controls-button-background-color-hover-default: var( + --vscode-editor-background + ); + --xy-controls-button-background-color-hover-props: var( + --vscode-editor-background + ); + --xy-controls-button-color-default: var(--vscode-foreground); + --xy-attribution-background-color: var(--vscode-editor-background); + --xy-controls-button-border-color-default: var(--vscode-editor-background); + --xy-controls-button-border-color-props: var(--vscode-editor-background); + --xy-controls-button-border-color: var(--vscode-editor-background); + + --color-lineage-node-background: var(--vscode-editor-background); + --color-lineage-node-foreground: var(--vscode-foreground); + --color-lineage-node-border: var(--vscode-text-separatorForeground); + --color-lineage-node-border-hover: var(--vscode-text-separatorForeground); + + --color-lineage-node-selected-border: var( + --vscode-editorLightBulb-foreground + ); + + --color-lineage-node-badge-background: var(--vscode-badge-background); + --color-lineage-node-badge-foreground: var(--vscode-badge-foreground); + + --color-lineage-node-appendix-background: transparent; + + --color-lineage-node-type-background-sql: var(--vscode-errorForeground); + --color-lineage-node-type-foreground-sql: var(--vscode-errorForeground); + --color-lineage-node-type-border-sql: var(--vscode-errorForeground); + + --color-lineage-node-type-background-python: var( + --vscode-list-warningForeground + ); + --color-lineage-node-type-foreground-python: var( + --vscode-list-warningForeground + ); + --color-lineage-node-type-border-python: var(--vscode-list-warningForeground); + + --color-lineage-node-type-background-source: var( + --vscode-minimap-infoHighlight + ); + --color-lineage-node-type-foreground-source: var( + --vscode-minimap-infoHighlight + ); + --color-lineage-node-type-border-source: var(--vscode-minimap-infoHighlight); + + --color-lineage-node-type-background-cte-subquery: var( + --vscode-minimap-selectionOccurrenceHighlight + ); + --color-lineage-node-type-foreground-cte-subquery: var( + --vscode-minimap-selectionOccurrenceHighlight + ); + --color-lineage-node-type-border-cte-subquery: var( + --vscode-minimap-selectionOccurrenceHighlight + ); + + --color-lineage-node-type-handle-icon-background: var( + --vscode-editor-background + ); + --color-lineage-node-type-handle-icon-foreground: var(--vscode-foreground); + + --color-lineage-edge: rgba(0, 0, 0, 0.1); + + --color-lineage-node-port-background: rgba(0, 0, 0, 0.1); + --color-lineage-node-port-handle-source: var(--vscode-progressBar-background); + --color-lineage-node-port-handle-target: var(--vscode-progressBar-background); + --color-lineage-node-port-edge-source: var(--vscode-progressBar-background); + --color-lineage-node-port-edge-target: var(--vscode-progressBar-background); + + --color-lineage-model-column-error-background: var( + --vscode-editor-background + ); + --color-lineage-model-column-source-background: var( + --vscode-editor-background + ); + --color-lineage-model-column-expression-background: var( + --vscode-editor-background + ); + --color-lineage-model-column-error-icon: var( + --vscode-activityErrorBadge-background + ); + --color-lineage-model-column-active: rgba(0, 0, 0, 0.1); + --color-lineage-model-column-icon: var(--vscode-text-separatorForeground); + --color-lineage-model-column-icon-active: var(--vscode-foreground); + + --color-lineage-model-column-badge-background: var(--vscode-input-background); + --color-lineage-model-column-badge-foreground: var(--vscode-input-foreground); + + --color-lineage-model-column-metadata-label: var( + --vscode-input-placeholderForeground + ); + --color-lineage-model-column-metadata-value: var(--vscode-input-foreground); + + --color-lineage-model-column-information-info: var(--vscode-input-foreground); +} @tailwind base; @tailwind components; @@ -65,10 +213,3 @@ @apply outline-none ring-offset-2 ring-4; } } - -:root { - --color-graph-edge-secondary: var(--vscode-disabledForeground); - --color-graph-edge-main: var(--vscode-disabledForeground); - --color-graph-edge-selected: var(--vscode-textLink-foreground); - --color-graph-edge-direct: var(--vscode-disabledForeground); -} diff --git a/vscode/react/src/main.tsx b/vscode/react/src/main.tsx index 5e24fc648f..de3f29bfd1 100644 --- a/vscode/react/src/main.tsx +++ b/vscode/react/src/main.tsx @@ -5,6 +5,8 @@ import { EventBusProvider } from './hooks/eventBus.tsx' import { TableDiffPage } from './pages/tablediff.tsx' import { LineagePage } from './pages/lineage.tsx' +import './App.css' + // Detect panel type declare global { interface Window { diff --git a/vscode/react/src/pages/ModelLineage.tsx b/vscode/react/src/pages/ModelLineage.tsx new file mode 100644 index 0000000000..dfc33fb89b --- /dev/null +++ b/vscode/react/src/pages/ModelLineage.tsx @@ -0,0 +1,409 @@ +import React from 'react' + +import { debounce } from 'lodash' +import { Focus, Rows2, Rows3 } from 'lucide-react' +import { + FactoryEdgeWithGradient, + EdgeWithGradient, + type LineageAdjacencyList, + type LineageDetails, + ZOOM_THRESHOLD, + type LineageEdge, + type LineageNodesMap, + type ColumnLevelLineageAdjacencyList, + useColumnLevelLineage, + createNode, + toPortID, + calculateNodeBaseHeight, + calculateNodeDetailsHeight, + calculateSelectedColumnsHeight, + buildLayout, + calculateColumnsHeight, + calculateNodeColumnsCount, + createEdge, + getEdgesFromColumnLineage, + getOnlySelectedNodes, + getTransformedModelEdgesSourceTargets, + getTransformedNodes, + MAX_COLUMNS_TO_DISPLAY, + toNodeID, + LineageLayout, + type LineageNode, + LineageControlButton, + LineageControlIcon, +} from '@tobikodata/sqlmesh-common/lineage' + +import { + type ColumnName, + type EdgeData, + type ModelColumnID, + type ModelEdgeId, + type ModelLineageNodeDetails, + type ModelNodeId, + type NodeData, + type NodeType, + ModelLineageContext, +} from './ModelLineageContext' +import { ModelNode } from './ModelNode' +import { useModelLineage } from './ModelLineageContext' +import type { ModelName } from '@/domain/models' +import { getNodeTypeColorVar } from './help' + +const nodeTypes = { + node: ModelNode, +} +const edgeTypes = { + edge: FactoryEdgeWithGradient(useModelLineage), + port: EdgeWithGradient, +} + +export const ModelLineage = ({ + selectedModelName, + adjacencyList, + lineageDetails, + className, + onNodeClick, +}: { + adjacencyList: LineageAdjacencyList + lineageDetails: LineageDetails + selectedModelName?: ModelName + className?: string + onNodeClick?: ( + event: React.MouseEvent, + node: LineageNode, + ) => void +}) => { + const [zoom, setZoom] = React.useState(ZOOM_THRESHOLD) + const [isBuildingLayout, setIsBuildingLayout] = React.useState(false) + const [edges, setEdges] = React.useState< + LineageEdge[] + >([]) + const [nodesMap, setNodesMap] = React.useState< + LineageNodesMap + >({}) + const [showOnlySelectedNodes, setShowOnlySelectedNodes] = + React.useState(false) + const [selectedNodes, setSelectedNodes] = React.useState>( + new Set(), + ) + const [selectedEdges, setSelectedEdges] = React.useState>( + new Set(), + ) + const [selectedNodeId, setSelectedNodeId] = + React.useState(null) + + const [showColumns, setShowColumns] = React.useState(false) + const [columnLevelLineage, setColumnLevelLineage] = React.useState< + Map> + >(new Map()) + const [fetchingColumns, setFetchingColumns] = React.useState< + Set + >(new Set()) + + const { + adjacencyListColumnLevel, + selectedColumns, + adjacencyListKeysColumnLevel, + } = useColumnLevelLineage( + columnLevelLineage, + ) + + const adjacencyListKeys = React.useMemo(() => { + let keys: ModelName[] = [] + + if (adjacencyListKeysColumnLevel.length > 0) { + keys = adjacencyListKeysColumnLevel + } else { + keys = Object.keys(adjacencyList) as ModelName[] + } + + return keys + }, [adjacencyListKeysColumnLevel, adjacencyList]) + + const transformNode = React.useCallback( + (nodeId: ModelNodeId, detail: ModelLineageNodeDetails) => { + const columns = detail.columns + + const node = createNode('node', nodeId, { + name: detail.name, + displayName: detail.display_name, + model_type: detail.model_type as NodeType, + identifier: detail.identifier, + kind: detail.kind, + cron: detail.cron, + owner: detail.owner, + dialect: detail.dialect, + version: detail.version, + tags: detail.tags || [], + columns, + }) + const selectedColumnsCount = new Set( + Object.keys(columns ?? {}).map(k => toPortID(detail.name, k)), + ).intersection(selectedColumns).size + // We are trying to project the node hight so we are including the ceiling and floor heights + const nodeBaseHeight = calculateNodeBaseHeight({ + includeNodeFooterHeight: false, + includeCeilingHeight: true, + includeFloorHeight: true, + }) + const nodeDetailsHeight = calculateNodeDetailsHeight({ + nodeDetailsCount: 0, + }) + const selectedColumnsHeight = + calculateSelectedColumnsHeight(selectedColumnsCount) + + const columnsHeight = calculateColumnsHeight({ + columnsCount: calculateNodeColumnsCount( + Object.keys(columns ?? {}).length, + ), + hasColumnsFilter: + Object.keys(columns ?? {}).length > MAX_COLUMNS_TO_DISPLAY, + }) + + node.height = + nodeBaseHeight + + nodeDetailsHeight + + selectedColumnsHeight + + columnsHeight + + return node + }, + [selectedColumns], + ) + + const transformedNodesMap = React.useMemo(() => { + return getTransformedNodes< + ModelName, + ModelLineageNodeDetails, + NodeData, + ModelNodeId + >(adjacencyListKeys, lineageDetails, transformNode) + }, [adjacencyListKeys, lineageDetails, transformNode]) + + const transformEdge = React.useCallback( + ( + edgeType: string, + edgeId: ModelEdgeId, + sourceId: ModelNodeId, + targetId: ModelNodeId, + sourceHandleId?: ModelColumnID, + targetHandleId?: ModelColumnID, + ) => { + const sourceNode = transformedNodesMap[sourceId] + const targetNode = transformedNodesMap[targetId] + const data: EdgeData = {} + + if (sourceHandleId) { + data.startColor = 'var(--color-lineage-node-port-edge-source)' + } else { + if (sourceNode?.data?.model_type) { + data.startColor = getNodeTypeColorVar( + sourceNode.data.model_type as NodeType, + ) + } + } + + if (targetHandleId) { + data.endColor = 'var(--color-lineage-node-port-edge-target)' + } else { + if (targetNode?.data?.model_type) { + data.endColor = getNodeTypeColorVar( + targetNode.data.model_type as NodeType, + ) + } + } + + if (sourceHandleId && targetHandleId) { + data.strokeWidth = 2 + } + + return createEdge( + edgeType, + edgeId, + sourceId, + targetId, + sourceHandleId, + targetHandleId, + data, + ) + }, + [transformedNodesMap], + ) + + const edgesColumnLevel = React.useMemo( + () => + getEdgesFromColumnLineage< + ModelName, + ColumnName, + EdgeData, + ModelEdgeId, + ModelNodeId, + ModelColumnID + >({ + columnLineage: adjacencyListColumnLevel, + transformEdge, + }), + [adjacencyListColumnLevel, transformEdge], + ) + + const transformedEdges = React.useMemo(() => { + return edgesColumnLevel.length > 0 + ? edgesColumnLevel + : getTransformedModelEdgesSourceTargets< + ModelName, + EdgeData, + ModelNodeId, + ModelEdgeId, + ModelColumnID + >(adjacencyListKeys, adjacencyList, transformEdge) + }, [adjacencyListKeys, adjacencyList, transformEdge, edgesColumnLevel]) + + const calculateLayout = React.useMemo(() => { + return debounce( + ( + eds: LineageEdge[], + nds: LineageNodesMap, + ) => { + const { edges, nodesMap } = buildLayout< + NodeData, + EdgeData, + ModelNodeId, + ModelEdgeId, + ModelColumnID + >({ edges: eds, nodesMap: nds }) + setEdges(edges) + setNodesMap(nodesMap) + setIsBuildingLayout(false) + }, + 200, + ) + }, []) + + const nodes = React.useMemo(() => { + return Object.values(nodesMap) + }, [nodesMap]) + + const currentNode = React.useMemo(() => { + return selectedModelName + ? nodesMap[toNodeID(selectedModelName)] + : null + }, [selectedModelName, nodesMap]) + + const handleReset = React.useCallback(() => { + setShowColumns(false) + setEdges([]) + setNodesMap({}) + setShowOnlySelectedNodes(false) + setSelectedNodes(new Set()) + setSelectedEdges(new Set()) + setSelectedNodeId(null) + setColumnLevelLineage(new Map()) + }, []) + + React.useEffect(() => { + setIsBuildingLayout(true) + + if (showOnlySelectedNodes) { + const onlySelectedNodesMap = getOnlySelectedNodes( + transformedNodesMap, + selectedNodes, + ) + const onlySelectedEdges = transformedEdges.filter(edge => + selectedEdges.has(edge.id as ModelEdgeId), + ) + calculateLayout(onlySelectedEdges, onlySelectedNodesMap) + } else { + calculateLayout(transformedEdges, transformedNodesMap) + } + }, [ + calculateLayout, + showOnlySelectedNodes, + transformedEdges, + transformedNodesMap, + ]) + + React.useEffect(() => { + const currentNodeId = selectedModelName + ? toNodeID(selectedModelName) + : undefined + + if (currentNodeId && currentNodeId in nodesMap) { + setSelectedNodeId(currentNodeId) + } else { + handleReset() + } + }, [handleReset, selectedModelName]) + + function toggleColumns() { + setShowColumns(prev => !prev) + } + + return ( + + + useLineage={useModelLineage} + nodeTypes={nodeTypes} + edgeTypes={edgeTypes} + className={className} + onNodeClick={onNodeClick} + controls={ + <> + toggleColumns()} + disabled={isBuildingLayout} + > + {showColumns ? ( + + ) : ( + + )} + + handleReset()} + disabled={isBuildingLayout} + > + + + + } + /> + + ) +} diff --git a/vscode/react/src/pages/ModelLineageContext.ts b/vscode/react/src/pages/ModelLineageContext.ts new file mode 100644 index 0000000000..4140d43740 --- /dev/null +++ b/vscode/react/src/pages/ModelLineageContext.ts @@ -0,0 +1,95 @@ +import type { ModelName } from '@/domain/models' +import type { Branded } from '@tobikodata/sqlmesh-common' +import { + type Column, + type ColumnLevelLineageAdjacencyList, + type ColumnLevelLineageContextValue, + type LineageContextValue, + type PathType, + getInitial as getLineageContextInitial, + getColumnLevelLineageContextInitial, + createLineageContext, +} from '@tobikodata/sqlmesh-common/lineage' + +export type ColumnName = Branded +export type ModelColumnID = Branded +export type ModelNodeId = Branded +export type ModelEdgeId = Branded +export type ModelColumn = Column & { + id: ModelColumnID + name: ColumnName + columnLineageData?: ColumnLevelLineageAdjacencyList +} + +export type NodeType = 'sql' | 'python' +export type ModelLineageNodeDetails = { + name: ModelName + display_name: string + model_type: string + identifier?: string | null + version?: string | null + dialect?: string | null + cron?: string | null + owner?: string | null + kind?: string | null + tags?: string[] + columns?: Record +} + +export type NodeData = { + name: ModelName + displayName: string + model_type: NodeType + identifier?: string | null + version?: string | null + kind?: string | null + cron?: string | null + owner?: string | null + dialect?: string | null + tags?: string[] + columns?: Record +} + +export type EdgeData = { + pathType?: PathType + startColor?: string + endColor?: string + strokeWidth?: number +} + +export type ModelLineageContextValue = ColumnLevelLineageContextValue< + ModelName, + ColumnName, + ModelColumnID +> & + LineageContextValue< + NodeData, + EdgeData, + ModelNodeId, + ModelEdgeId, + ModelColumnID + > + +export const initial = { + ...getLineageContextInitial(), + ...getColumnLevelLineageContextInitial< + ModelName, + ColumnName, + ModelColumnID + >(), +} + +export const { Provider, useLineage } = createLineageContext< + NodeData, + EdgeData, + ModelNodeId, + ModelEdgeId, + ModelColumnID, + ModelLineageContextValue +>(initial) + +export const ModelLineageContext = { + Provider, +} + +export const useModelLineage = useLineage diff --git a/vscode/react/src/pages/ModelNode.tsx b/vscode/react/src/pages/ModelNode.tsx new file mode 100644 index 0000000000..d2fe14b50d --- /dev/null +++ b/vscode/react/src/pages/ModelNode.tsx @@ -0,0 +1,365 @@ +import { type Node, type NodeProps } from '@xyflow/react' +import cronstrue from 'cronstrue' +import React from 'react' + +import { + useModelLineage, + type ModelNodeId, + type NodeData, + type ModelColumn, + type ModelColumnID, + type ColumnName, + type NodeType, +} from './ModelLineageContext' +import { + calculateColumnsHeight, + calculateNodeBaseHeight, + calculateNodeColumnsCount, + calculateNodeDetailsHeight, + calculateSelectedColumnsHeight, + MAX_COLUMNS_TO_DISPLAY, + useColumns, + useNodeMetadata, + ZOOM_THRESHOLD, + NodeContainer, + NodeBase, + NodeDivider, + NodeHandleIcon, + NodeHandles, + NodeHeader, + type Column, + NodeAppendix, + NodeBadge, + type ColumnLevelLineageAdjacencyList, + NodePorts, +} from '@tobikodata/sqlmesh-common/lineage' +import { + getNodeTypeBorderColor, + getNodeTypeColor, + getNodeTypeTextColor, +} from './help' +import { + Badge, + cn, + HorizontalContainer, + Metadata, + ModelName, + Tooltip, + VerticalContainer, +} from '@tobikodata/sqlmesh-common' +import { ModelNodeColumn } from './ModelNodeColumn' +import type { ModelName as ModelNameType } from '@/domain/models' + +export const ModelNode = React.memo(function ModelNode({ + id, + data, + ...props +}: NodeProps>) { + const { + selectedColumns, + zoom, + currentNode, + selectedNodeId, + selectedNodes, + showColumns, + fetchingColumns, + setSelectedNodeId, + } = useModelLineage() + + const [showNodeColumns, setShowNodeColumns] = React.useState(showColumns) + const [isHovered, setIsHovered] = React.useState(false) + + const nodeId = id as ModelNodeId + + const { + leftId, + rightId, + isSelected, // if selected from inside the lineage and node is selcted + isActive, // if selected from inside the lineage and node is not selected but in path + } = useNodeMetadata(nodeId, currentNode, selectedNodeId, selectedNodes) + + const { + columns, + selectedColumns: modelSelectedColumns, + columnNames, + } = useColumns( + selectedColumns, + data.name, + data.columns, + ) + + const hasSelectedColumns = selectedColumns.intersection(columnNames).size > 0 + const hasFetchingColumns = fetchingColumns.intersection(columnNames).size > 0 + + React.useEffect(() => { + setShowNodeColumns(showColumns || isSelected) + }, [columnNames, isSelected, showColumns]) + + function toggleSelectedNode() { + setSelectedNodeId(prev => (prev === nodeId ? null : nodeId)) + } + + const shouldShowColumns = + showNodeColumns || hasSelectedColumns || hasFetchingColumns || isHovered + const modelType = data.model_type?.toLowerCase() as NodeType + const hasColumnsFilter = + shouldShowColumns && columns.length > MAX_COLUMNS_TO_DISPLAY + // We are not including the footer, because we need actual height to dynamically adjust node container height + const nodeBaseHeight = calculateNodeBaseHeight({ + includeNodeFooterHeight: false, + includeCeilingHeight: false, + includeFloorHeight: false, + }) + const nodeDetailsHeight = + zoom > ZOOM_THRESHOLD + ? calculateNodeDetailsHeight({ + nodeDetailsCount: 0, + }) + : 0 + const selectedColumnsHeight = calculateSelectedColumnsHeight( + modelSelectedColumns.length, + ) + const columnsHeight = + zoom > ZOOM_THRESHOLD && shouldShowColumns + ? calculateColumnsHeight({ + columnsCount: calculateNodeColumnsCount(columns.length), + hasColumnsFilter, + }) + : 0 + + // If zoom is less than ZOOM_THRESHOLD, we are making node looks bigger + const nodeHeight = + (zoom > ZOOM_THRESHOLD ? nodeBaseHeight : nodeBaseHeight * 2) + + nodeDetailsHeight + + selectedColumnsHeight + + columnsHeight + + return ( + + + + {zoom > ZOOM_THRESHOLD && ( + <> + {data.kind && {data.kind.toUpperCase()}} + {data.cron && ( + + {data.cron} + + } + className="text-xs p-2 rounded-md font-semibold" + > + + UTC Time + {cronstrue.toString(data.cron, { + dayOfWeekStartIndexZero: true, + use24HourTimeFormat: true, + verbose: true, + })} + + + )} + + )} + + + + setIsHovered(true)} + onMouseLeave={() => setIsHovered(false)} + > + ZOOM_THRESHOLD ? 'shrink-0 h-7' : 'h-full')} + onClick={toggleSelectedNode} + > + + } + rightIcon={ + + } + handleClassName="top-4" + > + + ZOOM_THRESHOLD + ? ' text-xs' + : 'text-2xl justify-center', + )} + /> + + + + {shouldShowColumns && ( + <> + {modelSelectedColumns.length > 0 && ( + + {modelSelectedColumns.map(column => ( + + } + ).columnLineageData + } + /> + ))} + + )} + {columns.length > 0 && zoom > ZOOM_THRESHOLD && ( + + ports={columns} + estimatedListItemHeight={24} + isFilterable={hasColumnsFilter} + filterOptions={{ + keys: ['name', 'description'], + threshold: 0.3, + }} + renderPort={column => ( + + } + ).columnLineageData + } + /> + )} + className="border-t border-lineage-divider" + /> + )} + + )} + + + {modelType && ( + + ZOOM_THRESHOLD ? 'h-5' : 'h-8', + )} + > + ZOOM_THRESHOLD ? '2xs' : 'm'} + className={cn( + 'text-[white] font-black', + getNodeTypeColor(modelType), + )} + > + {modelType.toUpperCase()} + + + + )} + + ) +}) + +export function NodeDetail({ + label, + value, + hasDivider = true, + className, +}: { + label: string + value: string + hasDivider?: boolean + className?: string +}) { + return ( + <> + {hasDivider && } + + + ) +} diff --git a/vscode/react/src/pages/ModelNodeColumn.tsx b/vscode/react/src/pages/ModelNodeColumn.tsx new file mode 100644 index 0000000000..e3349238c8 --- /dev/null +++ b/vscode/react/src/pages/ModelNodeColumn.tsx @@ -0,0 +1,78 @@ +import React from 'react' + +import { + FactoryColumn, + type ColumnLevelLineageAdjacencyList, +} from '@tobikodata/sqlmesh-common/lineage' + +import { + useModelLineage, + type ModelColumnID, + type ModelNodeId, + type ColumnName, +} from './ModelLineageContext' +import type { ModelName } from '@/domain/models' + +const ModelColumn = FactoryColumn< + ModelName, + ColumnName, + ModelNodeId, + ModelColumnID +>(useModelLineage) + +export const ModelNodeColumn = React.memo(function ModelNodeColumn({ + id, + nodeId, + modelName, + name, + description, + type, + className, + columnLineageData, +}: { + id: ModelColumnID + nodeId: ModelNodeId + modelName: ModelName + name: ColumnName + type: string + description?: string | null + className?: string + columnLineageData?: ColumnLevelLineageAdjacencyList +}) { + const { selectedColumns, setColumnLevelLineage } = useModelLineage() + + const isSelectedColumn = selectedColumns.has(id) + + async function toggleSelectedColumn() { + if (isSelectedColumn) { + setColumnLevelLineage(prev => { + prev.delete(id) + return new Map(prev) + }) + } else { + if (columnLineageData != null) { + setColumnLevelLineage(prev => new Map(prev).set(id, columnLineageData)) + } + } + } + + return ( + console.log('cancel')} + renderError={error =>
Error: {error.message}
} + renderExpression={expression =>
{expression}
} + renderSource={source =>
{source}
} + /> + ) +}) diff --git a/vscode/react/src/pages/help.ts b/vscode/react/src/pages/help.ts new file mode 100644 index 0000000000..110a8ebac6 --- /dev/null +++ b/vscode/react/src/pages/help.ts @@ -0,0 +1,37 @@ +import { type NodeType } from './ModelLineageContext' + +export function getNodeTypeColorVar(nodeType: NodeType) { + return { + sql: 'var(--color-lineage-node-type-background-sql)', + python: 'var(--color-lineage-node-type-background-python)', + 'cte/subquery': 'var(--color-lineage-node-type-background-cte-subquery)', + source: 'var(--color-lineage-node-type-background-source)', + }[nodeType] +} + +export function getNodeTypeColor(nodeType: NodeType) { + return { + sql: 'bg-lineage-node-type-background-sql', + python: 'bg-lineage-node-type-background-python', + 'cte/subquery': 'bg-lineage-node-type-background-cte-subquery', + source: 'bg-lineage-node-type-background-source', + }[nodeType] +} + +export function getNodeTypeTextColor(nodeType: NodeType) { + return { + sql: 'text-lineage-node-type-foreground-sql', + python: 'text-lineage-node-type-foreground-python', + 'cte/subquery': 'text-lineage-node-type-foreground-cte-subquery', + source: 'text-lineage-node-type-foreground-source', + }[nodeType] +} + +export function getNodeTypeBorderColor(nodeType: NodeType) { + return { + sql: 'border-lineage-node-type-border-sql', + python: 'border-lineage-node-type-border-python', + 'cte/subquery': 'border-lineage-node-type-border-cte-subquery', + source: 'border-lineage-node-type-border-source', + }[nodeType] +} diff --git a/vscode/react/src/pages/lineage.tsx b/vscode/react/src/pages/lineage.tsx index 18925f28da..f200dd8a12 100644 --- a/vscode/react/src/pages/lineage.tsx +++ b/vscode/react/src/pages/lineage.tsx @@ -1,20 +1,16 @@ -import '../App.css' import { QueryCache, QueryClient, QueryClientProvider, useQueryClient, } from '@tanstack/react-query' -import { useApiModels } from '@/api' -import LineageFlowProvider from '@/components/graph/context' -import { ModelLineage } from '@/components/graph/ModelLineage' -import { useVSCode } from '@/hooks/vscode' +import { useApiModelLineage, useApiModels } from '@/api' import React, { useState } from 'react' import { ModelSQLMeshModel } from '@/domain/sqlmesh-model' import { useEventBus } from '@/hooks/eventBus' import type { VSCodeEvent } from '@bus/callbacks' import { URI } from 'vscode-uri' -import type { Model } from '@/api/client' +import type { Model, ModelLineageApiLineageModelNameGet200 } from '@/api/client' import { useRpc } from '@/utils/rpc' import { type ModelPath, @@ -23,6 +19,15 @@ import { type ModelEncodedFQN, } from '@/domain/models' +import { ModelLineage } from './ModelLineage' +import type { ModelLineageNodeDetails, ColumnName } from './ModelLineageContext' +import type { + Column, + LineageAdjacencyList, + LineageDetails, +} from '@tobikodata/sqlmesh-common/lineage' +import { useVSCode } from '@/hooks/vscode' + export function LineagePage() { const { emit } = useEventBus() @@ -95,7 +100,7 @@ function Lineage() { const activeFile = await rpc('get_active_file', {}) // @ts-ignore if (!activeFile.fileUri) { - return models[0].name + return models[0].fqn } // @ts-ignore const fileUri: string = activeFile.fileUri @@ -107,7 +112,7 @@ function Lineage() { return URI.file(m.full_path).path === filePath }) if (model) { - return model.name + return model.fqn } return undefined } @@ -116,7 +121,7 @@ function Lineage() { if (modelName && selectedModel === undefined) { setSelectedModel(modelName) } else { - setSelectedModel(models[0].name) + setSelectedModel(models[0].fqn) } }) } @@ -126,7 +131,7 @@ function Lineage() { Array.isArray(models) && models.reduce( (acc, model) => { - acc[model.name] = model + acc[model.fqn] = model return acc }, {} as Record, @@ -139,7 +144,7 @@ function Lineage() { m => URI.file(m.full_path).path === full_path, ) if (model) { - setSelectedModel(model.name) + setSelectedModel(model.fqn) } } @@ -222,17 +227,64 @@ export function LineageComponentFromWeb({ full_path: model.full_path as ModelFullPath, }) + const { refetch: getModelLineage } = useApiModelLineage(model?.name ?? '') + + const [modelLineage, setModelLineage] = useState< + ModelLineageApiLineageModelNameGet200 | undefined + >(undefined) + + React.useEffect(() => { + if (model === undefined) return + + getModelLineage() + .then(({ data }) => { + setModelLineage(data) + }) + .catch(handleError) + }, [model?.name, model?.hash]) + + const adjacencyList = modelLineage as LineageAdjacencyList + const lineageDetails = Object.values(models).reduce( + (acc, model) => { + const modelName = model.fqn as ModelName + acc[modelName] = { + name: modelName, + display_name: model.name, + model_type: model.type, + identifier: undefined, + version: undefined, + dialect: model.dialect, + cron: model.details?.cron, + owner: model.details?.owner, + kind: model.details?.kind, + tags: [], + columns: model.columns.reduce( + (acc, column) => { + const columnName = decodeURI(column.name) as ColumnName + acc[columnName] = { + data_type: column.type, + description: column.description, + } + return acc + }, + {} as Record, + ), + } + return acc + }, + {} as LineageDetails, + ) + return ( -
- - - -
+ adjacencyList && + lineageDetails && ( + handleClickModel(node.id)} + selectedModelName={model.fqn as ModelName} + adjacencyList={adjacencyList} + lineageDetails={lineageDetails} + /> + ) ) } diff --git a/vscode/react/tailwind.config.cjs b/vscode/react/tailwind.config.cjs index c31bd9f9ec..1a60a0928b 100644 --- a/vscode/react/tailwind.config.cjs +++ b/vscode/react/tailwind.config.cjs @@ -1,6 +1,13 @@ /** @type {import('tailwindcss').Config} */ module.exports = { - content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'], + presets: [ + require('@tobikodata/sqlmesh-common/configs/tailwind.base.config.js'), + ], + content: [ + './index.html', + './src/**/*.{js,ts,jsx,tsx}', + './node_modules/@tobikodata/sqlmesh-common/**/*.{js,ts,jsx,tsx}', + ], darkMode: ['class', '[mode="dark"]'], theme: { colors: { @@ -177,6 +184,33 @@ module.exports = { 800: 'var(--color-warning-800)', 900: 'var(--color-warning-900)', }, + lineage: { + node: { + type: { + background: { + sql: 'var(--color-lineage-node-type-background-sql)', + python: 'var(--color-lineage-node-type-background-python)', + 'cte-subquery': + 'var(--color-lineage-node-type-background-cte-subquery)', + source: 'var(--color-lineage-node-type-background-source)', + }, + foreground: { + sql: 'var(--color-lineage-node-type-foreground-sql)', + python: 'var(--color-lineage-node-type-foreground-python)', + 'cte-subquery': + 'var(--color-lineage-node-type-foreground-cte-subquery)', + source: 'var(--color-lineage-node-type-foreground-source)', + }, + border: { + sql: 'var(--color-lineage-node-type-border-sql)', + python: 'var(--color-lineage-node-type-border-python)', + 'cte-subquery': + 'var(--color-lineage-node-type-border-cte-subquery)', + source: 'var(--color-lineage-node-type-border-source)', + }, + }, + }, + }, }, fontFamily: { mono: ['JetBrains Mono', 'monospace'], diff --git a/vscode/react/tsconfig.json b/vscode/react/tsconfig.json index b57d3316b0..2d2b2b64dc 100644 --- a/vscode/react/tsconfig.json +++ b/vscode/react/tsconfig.json @@ -4,7 +4,7 @@ "target": "ES2022", "jsx": "react-jsx", "module": "ESNext", - "lib": ["ES2022", "DOM", "DOM.Iterable"], + "lib": ["ESNext", "DOM", "DOM.Iterable"], "types": ["vite/client", "react", "react-dom"], /* Bundler mode */ diff --git a/vscode/react/vite.config.js b/vscode/react/vite.config.js index 84568f6cdd..9adc2fed4b 100644 --- a/vscode/react/vite.config.js +++ b/vscode/react/vite.config.js @@ -2,15 +2,10 @@ import { defineConfig } from 'vite' import viteReact from '@vitejs/plugin-react' import { TanStackRouterVite } from '@tanstack/router-plugin/vite' import { resolve } from 'node:path' -import tailwindcss from '@tailwindcss/vite' // https://vitejs.dev/config/ export default defineConfig({ - plugins: [ - TanStackRouterVite({ autoCodeSplitting: false }), - viteReact(), - tailwindcss(), - ], + plugins: [TanStackRouterVite({ autoCodeSplitting: false }), viteReact()], test: { globals: true, environment: 'jsdom', From cd4e894adc755f4928b90c9ac44cbe2467f8eeac Mon Sep 17 00:00:00 2001 From: Max Mykal Date: Fri, 3 Oct 2025 13:43:26 -0700 Subject: [PATCH 02/20] update packages --- pnpm-lock.yaml | 313 +++++++++++-------------------------------------- 1 file changed, 69 insertions(+), 244 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index aeacb362d0..5439739cdc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -108,12 +108,6 @@ importers: '@radix-ui/react-select': specifier: ^2.2.5 version: 2.2.5(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@tailwindcss/postcss': - specifier: ^4.1.11 - version: 4.1.11 - '@tailwindcss/vite': - specifier: ^4.1.11 - version: 4.1.11(vite@6.3.5(@types/node@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0)) '@tanstack/react-query': specifier: ^5.83.0 version: 5.83.0(react@18.3.1) @@ -129,15 +123,30 @@ importers: '@tanstack/router-plugin': specifier: ^1.129.8 version: 1.129.8(@tanstack/react-router@1.129.8(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(vite@6.3.5(@types/node@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0))(webpack@5.99.8(esbuild@0.25.8)) + '@tobikodata/sqlmesh-common': + specifier: 0.0.17 + version: 0.0.17(@radix-ui/react-slot@1.2.3(@types/react@18.3.23)(react@18.3.1))(@radix-ui/react-tooltip@1.2.8(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@tailwindcss/typography@0.5.16(tailwindcss@3.4.17))(@tanstack/react-virtual@3.13.12(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@xyflow/react@12.8.4(@types/react@18.3.23)(immer@9.0.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(class-variance-authority@0.7.1)(clsx@2.1.1)(cronstrue@3.3.0)(dagre@0.8.5)(deepmerge@4.3.1)(fuse.js@7.1.0)(lodash@4.17.21)(lucide-react@0.542.0(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(tailwind-merge@3.3.1)(tailwind-scrollbar@3.1.0(tailwindcss@3.4.17))(tailwindcss@3.4.17) + '@xyflow/react': + specifier: 12.8.4 + version: 12.8.4(@types/react@18.3.23)(immer@9.0.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) apache-arrow: specifier: ^19.0.1 version: 19.0.1 clsx: specifier: ^2.1.1 version: 2.1.1 + cronstrue: + specifier: 3.3.0 + version: 3.3.0 elkjs: specifier: ^0.8.2 version: 0.8.2 + lodash: + specifier: 4.17.21 + version: 4.17.21 + lucide-react: + specifier: 0.542.0 + version: 0.542.0(react@18.3.1) orval: specifier: ^7.10.0 version: 7.10.0(openapi-types@12.1.3) @@ -154,8 +163,8 @@ importers: specifier: ^11.11.4 version: 11.11.4(@types/react@18.3.23)(immer@9.0.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) tailwindcss: - specifier: ^4.1.11 - version: 4.1.11 + specifier: 3.4.17 + version: 3.4.17 vscode-uri: specifier: ^3.1.0 version: 3.1.0 @@ -184,6 +193,9 @@ importers: '@testing-library/react': specifier: ^16.3.0 version: 16.3.0(@testing-library/dom@10.4.1)(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@types/lodash': + specifier: 4.17.20 + version: 4.17.20 '@types/react': specifier: ^18.3.23 version: 18.3.23 @@ -1158,10 +1170,6 @@ packages: resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} engines: {node: '>=12'} - '@isaacs/fs-minipass@4.0.1': - resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==} - engines: {node: '>=18.0.0'} - '@istanbuljs/schema@0.1.3': resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==} engines: {node: '>=8'} @@ -1197,9 +1205,6 @@ packages: '@jridgewell/trace-mapping@0.3.29': resolution: {integrity: sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==} - '@jridgewell/trace-mapping@0.3.30': - resolution: {integrity: sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==} - '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} @@ -2244,104 +2249,11 @@ packages: peerDependencies: tailwindcss: '>=3.2.0' - '@tailwindcss/node@4.1.11': - resolution: {integrity: sha512-yzhzuGRmv5QyU9qLNg4GTlYI6STedBWRE7NjxP45CsFYYq9taI0zJXZBMqIC/c8fViNLhmrbpSFS57EoxUmD6Q==} - - '@tailwindcss/oxide-android-arm64@4.1.11': - resolution: {integrity: sha512-3IfFuATVRUMZZprEIx9OGDjG3Ou3jG4xQzNTvjDoKmU9JdmoCohQJ83MYd0GPnQIu89YoJqvMM0G3uqLRFtetg==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [android] - - '@tailwindcss/oxide-darwin-arm64@4.1.11': - resolution: {integrity: sha512-ESgStEOEsyg8J5YcMb1xl8WFOXfeBmrhAwGsFxxB2CxY9evy63+AtpbDLAyRkJnxLy2WsD1qF13E97uQyP1lfQ==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [darwin] - - '@tailwindcss/oxide-darwin-x64@4.1.11': - resolution: {integrity: sha512-EgnK8kRchgmgzG6jE10UQNaH9Mwi2n+yw1jWmof9Vyg2lpKNX2ioe7CJdf9M5f8V9uaQxInenZkOxnTVL3fhAw==} - engines: {node: '>= 10'} - cpu: [x64] - os: [darwin] - - '@tailwindcss/oxide-freebsd-x64@4.1.11': - resolution: {integrity: sha512-xdqKtbpHs7pQhIKmqVpxStnY1skuNh4CtbcyOHeX1YBE0hArj2romsFGb6yUmzkq/6M24nkxDqU8GYrKrz+UcA==} - engines: {node: '>= 10'} - cpu: [x64] - os: [freebsd] - - '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.11': - resolution: {integrity: sha512-ryHQK2eyDYYMwB5wZL46uoxz2zzDZsFBwfjssgB7pzytAeCCa6glsiJGjhTEddq/4OsIjsLNMAiMlHNYnkEEeg==} - engines: {node: '>= 10'} - cpu: [arm] - os: [linux] - - '@tailwindcss/oxide-linux-arm64-gnu@4.1.11': - resolution: {integrity: sha512-mYwqheq4BXF83j/w75ewkPJmPZIqqP1nhoghS9D57CLjsh3Nfq0m4ftTotRYtGnZd3eCztgbSPJ9QhfC91gDZQ==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [linux] - - '@tailwindcss/oxide-linux-arm64-musl@4.1.11': - resolution: {integrity: sha512-m/NVRFNGlEHJrNVk3O6I9ggVuNjXHIPoD6bqay/pubtYC9QIdAMpS+cswZQPBLvVvEF6GtSNONbDkZrjWZXYNQ==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [linux] - - '@tailwindcss/oxide-linux-x64-gnu@4.1.11': - resolution: {integrity: sha512-YW6sblI7xukSD2TdbbaeQVDysIm/UPJtObHJHKxDEcW2exAtY47j52f8jZXkqE1krdnkhCMGqP3dbniu1Te2Fg==} - engines: {node: '>= 10'} - cpu: [x64] - os: [linux] - - '@tailwindcss/oxide-linux-x64-musl@4.1.11': - resolution: {integrity: sha512-e3C/RRhGunWYNC3aSF7exsQkdXzQ/M+aYuZHKnw4U7KQwTJotnWsGOIVih0s2qQzmEzOFIJ3+xt7iq67K/p56Q==} - engines: {node: '>= 10'} - cpu: [x64] - os: [linux] - - '@tailwindcss/oxide-wasm32-wasi@4.1.11': - resolution: {integrity: sha512-Xo1+/GU0JEN/C/dvcammKHzeM6NqKovG+6921MR6oadee5XPBaKOumrJCXvopJ/Qb5TH7LX/UAywbqrP4lax0g==} - engines: {node: '>=14.0.0'} - cpu: [wasm32] - bundledDependencies: - - '@napi-rs/wasm-runtime' - - '@emnapi/core' - - '@emnapi/runtime' - - '@tybys/wasm-util' - - '@emnapi/wasi-threads' - - tslib - - '@tailwindcss/oxide-win32-arm64-msvc@4.1.11': - resolution: {integrity: sha512-UgKYx5PwEKrac3GPNPf6HVMNhUIGuUh4wlDFR2jYYdkX6pL/rn73zTq/4pzUm8fOjAn5L8zDeHp9iXmUGOXZ+w==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [win32] - - '@tailwindcss/oxide-win32-x64-msvc@4.1.11': - resolution: {integrity: sha512-YfHoggn1j0LK7wR82TOucWc5LDCguHnoS879idHekmmiR7g9HUtMw9MI0NHatS28u/Xlkfi9w5RJWgz2Dl+5Qg==} - engines: {node: '>= 10'} - cpu: [x64] - os: [win32] - - '@tailwindcss/oxide@4.1.11': - resolution: {integrity: sha512-Q69XzrtAhuyfHo+5/HMgr1lAiPP/G40OMFAnws7xcFEYqcypZmdW8eGXaOUIeOl1dzPJBPENXgbjsOyhg2nkrg==} - engines: {node: '>= 10'} - - '@tailwindcss/postcss@4.1.11': - resolution: {integrity: sha512-q/EAIIpF6WpLhKEuQSEVMZNMIY8KhWoAemZ9eylNAih9jxMGAYPPWBn3I9QL/2jZ+e7OEz/tZkX5HwbBR4HohA==} - '@tailwindcss/typography@0.5.16': resolution: {integrity: sha512-0wDLwCVF5V3x3b1SGXPCDcdsbDHMBe+lkFzBRaHeLvNi+nrrnZ1lA18u+OTWO8iSWU2GxUOCvlXtDuqftc1oiA==} peerDependencies: tailwindcss: '>=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1' - '@tailwindcss/vite@4.1.11': - resolution: {integrity: sha512-RHYhrR3hku0MJFRV+fN2gNbDNEh3dwKvY8XJvTxCSXeMOsCRSr+uKvDWQcbizrHgjML6ZmTE5OwMrl5wKcujCw==} - peerDependencies: - vite: ^5.2.0 || ^6 || ^7 - '@tanstack/history@1.129.7': resolution: {integrity: sha512-I3YTkbe4RZQN54Qw4+IUhOjqG2DdbG2+EBWuQfew4MEk0eddLYAQVa50BZVww4/D2eh5I9vEk2Fd1Y0Wty7pug==} engines: {node: '>=12'} @@ -2491,6 +2403,28 @@ packages: '@textlint/types@15.2.0': resolution: {integrity: sha512-wpF+xjGJgJK2JiwUdYjuNZrbuas3KfC9VDnHKac6aBLFyrI1iXuXtuxKXQDFi5/hebACactSJOuVVbuQbdJZ1Q==} + '@tobikodata/sqlmesh-common@0.0.17': + resolution: {integrity: sha512-64zQd5E2bpxxhee5hWnE0MuAt+la+ZTEBRQHJ17wzDaCHfz1JVfOkyistIEhHzIlDYWQr0JmPSt+zC0kdn3PYQ==} + peerDependencies: + '@radix-ui/react-slot': 1.2.3 + '@radix-ui/react-tooltip': 1.2.8 + '@tailwindcss/typography': 0.5.16 + '@tanstack/react-virtual': 3.13.12 + '@xyflow/react': 12.8.4 + class-variance-authority: 0.7.1 + clsx: 2.1.1 + cronstrue: 3.3.0 + dagre: 0.8.5 + deepmerge: 4.3.1 + fuse.js: 7.1.0 + lodash: 4.17.21 + lucide-react: 0.542.0 + react: 18.3.1 + react-dom: 18.3.1 + tailwind-merge: 3.3.1 + tailwind-scrollbar: 3.1.0 + tailwindcss: 3.4.17 + '@types/argparse@1.0.38': resolution: {integrity: sha512-ebDJ9b0e702Yr7pWgB0jzm+CX4Srzz8RcXtLJDJB+BSccqMa36uyH/zUsSYao5+BD1ytv3k3rPYCq4mAE1hsXA==} @@ -3430,10 +3364,6 @@ packages: chownr@1.1.4: resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==} - chownr@3.0.0: - resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==} - engines: {node: '>=18'} - chromatic@12.2.0: resolution: {integrity: sha512-GswmBW9ZptAoTns1BMyjbm55Z7EsIJnUvYKdQqXIBZIKbGErmpA+p4c0BYA+nzw5B0M+rb3Iqp1IaH8TFwIQew==} hasBin: true @@ -5096,18 +5026,9 @@ packages: resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} engines: {node: '>=16 || 14 >=14.17'} - minizlib@3.0.2: - resolution: {integrity: sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==} - engines: {node: '>= 18'} - mkdirp-classic@0.5.3: resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==} - mkdirp@3.0.1: - resolution: {integrity: sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==} - engines: {node: '>=10'} - hasBin: true - mlly@1.7.4: resolution: {integrity: sha512-qmdSIPC4bDJXgZTCR7XosJiNKySV7O215tsPtDN9iEO/7q/76b/ijtgRu/+epFXSJhijtTCCGp3DWS549P3xKw==} @@ -6159,9 +6080,6 @@ packages: engines: {node: '>=14.0.0'} hasBin: true - tailwindcss@4.1.11: - resolution: {integrity: sha512-2E9TBm6MDD/xKYe+dvJZAmg3yxIEDNRc0jwlNyDg/4Fil2QcSLjFKGVff0lAf1jjeaArlG/M75Ey/EYr/OJtBA==} - tapable@2.2.2: resolution: {integrity: sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg==} engines: {node: '>=6'} @@ -6177,10 +6095,6 @@ packages: resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} engines: {node: '>=6'} - tar@7.4.3: - resolution: {integrity: sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==} - engines: {node: '>=18'} - terminal-link@4.0.0: resolution: {integrity: sha512-lk+vH+MccxNqgVqSnkMVKx4VLJfnLjDBGzH16JVZjKE2DoxP57s6/vt6JmXV5I3jBcfGrxNrYtC+mPtU7WJztA==} engines: {node: '>=18'} @@ -6815,10 +6729,6 @@ packages: yallist@4.0.0: resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} - yallist@5.0.0: - resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==} - engines: {node: '>=18'} - yaml@1.10.2: resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==} engines: {node: '>= 6'} @@ -7609,10 +7519,6 @@ snapshots: wrap-ansi: 8.1.0 wrap-ansi-cjs: wrap-ansi@7.0.0 - '@isaacs/fs-minipass@4.0.1': - dependencies: - minipass: 7.1.2 - '@istanbuljs/schema@0.1.3': {} '@joshwooding/vite-plugin-react-docgen-typescript@0.6.1(typescript@5.8.3)(vite@6.3.5(@types/node@20.11.25)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0))': @@ -7641,7 +7547,7 @@ snapshots: '@jridgewell/gen-mapping@0.3.13': dependencies: '@jridgewell/sourcemap-codec': 1.5.5 - '@jridgewell/trace-mapping': 0.3.30 + '@jridgewell/trace-mapping': 0.3.31 '@jridgewell/resolve-uri@3.1.2': {} @@ -7659,11 +7565,6 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.4 - '@jridgewell/trace-mapping@0.3.30': - dependencies: - '@jridgewell/resolve-uri': 3.1.2 - '@jridgewell/sourcemap-codec': 1.5.5 - '@jridgewell/trace-mapping@0.3.31': dependencies: '@jridgewell/resolve-uri': 3.1.2 @@ -8940,78 +8841,6 @@ snapshots: dependencies: tailwindcss: 3.4.17 - '@tailwindcss/node@4.1.11': - dependencies: - '@ampproject/remapping': 2.3.0 - enhanced-resolve: 5.18.2 - jiti: 2.4.2 - lightningcss: 1.30.1 - magic-string: 0.30.17 - source-map-js: 1.2.1 - tailwindcss: 4.1.11 - - '@tailwindcss/oxide-android-arm64@4.1.11': - optional: true - - '@tailwindcss/oxide-darwin-arm64@4.1.11': - optional: true - - '@tailwindcss/oxide-darwin-x64@4.1.11': - optional: true - - '@tailwindcss/oxide-freebsd-x64@4.1.11': - optional: true - - '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.11': - optional: true - - '@tailwindcss/oxide-linux-arm64-gnu@4.1.11': - optional: true - - '@tailwindcss/oxide-linux-arm64-musl@4.1.11': - optional: true - - '@tailwindcss/oxide-linux-x64-gnu@4.1.11': - optional: true - - '@tailwindcss/oxide-linux-x64-musl@4.1.11': - optional: true - - '@tailwindcss/oxide-wasm32-wasi@4.1.11': - optional: true - - '@tailwindcss/oxide-win32-arm64-msvc@4.1.11': - optional: true - - '@tailwindcss/oxide-win32-x64-msvc@4.1.11': - optional: true - - '@tailwindcss/oxide@4.1.11': - dependencies: - detect-libc: 2.0.4 - tar: 7.4.3 - optionalDependencies: - '@tailwindcss/oxide-android-arm64': 4.1.11 - '@tailwindcss/oxide-darwin-arm64': 4.1.11 - '@tailwindcss/oxide-darwin-x64': 4.1.11 - '@tailwindcss/oxide-freebsd-x64': 4.1.11 - '@tailwindcss/oxide-linux-arm-gnueabihf': 4.1.11 - '@tailwindcss/oxide-linux-arm64-gnu': 4.1.11 - '@tailwindcss/oxide-linux-arm64-musl': 4.1.11 - '@tailwindcss/oxide-linux-x64-gnu': 4.1.11 - '@tailwindcss/oxide-linux-x64-musl': 4.1.11 - '@tailwindcss/oxide-wasm32-wasi': 4.1.11 - '@tailwindcss/oxide-win32-arm64-msvc': 4.1.11 - '@tailwindcss/oxide-win32-x64-msvc': 4.1.11 - - '@tailwindcss/postcss@4.1.11': - dependencies: - '@alloc/quick-lru': 5.2.0 - '@tailwindcss/node': 4.1.11 - '@tailwindcss/oxide': 4.1.11 - postcss: 8.5.6 - tailwindcss: 4.1.11 - '@tailwindcss/typography@0.5.16(tailwindcss@3.4.17)': dependencies: lodash.castarray: 4.4.0 @@ -9020,13 +8849,6 @@ snapshots: postcss-selector-parser: 6.0.10 tailwindcss: 3.4.17 - '@tailwindcss/vite@4.1.11(vite@6.3.5(@types/node@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0))': - dependencies: - '@tailwindcss/node': 4.1.11 - '@tailwindcss/oxide': 4.1.11 - tailwindcss: 4.1.11 - vite: 6.3.5(@types/node@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0) - '@tanstack/history@1.129.7': {} '@tanstack/query-core@5.83.0': {} @@ -9217,6 +9039,27 @@ snapshots: dependencies: '@textlint/ast-node-types': 15.2.0 + '@tobikodata/sqlmesh-common@0.0.17(@radix-ui/react-slot@1.2.3(@types/react@18.3.23)(react@18.3.1))(@radix-ui/react-tooltip@1.2.8(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@tailwindcss/typography@0.5.16(tailwindcss@3.4.17))(@tanstack/react-virtual@3.13.12(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@xyflow/react@12.8.4(@types/react@18.3.23)(immer@9.0.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(class-variance-authority@0.7.1)(clsx@2.1.1)(cronstrue@3.3.0)(dagre@0.8.5)(deepmerge@4.3.1)(fuse.js@7.1.0)(lodash@4.17.21)(lucide-react@0.542.0(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(tailwind-merge@3.3.1)(tailwind-scrollbar@3.1.0(tailwindcss@3.4.17))(tailwindcss@3.4.17)': + dependencies: + '@radix-ui/react-slot': 1.2.3(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-tooltip': 1.2.8(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@tailwindcss/typography': 0.5.16(tailwindcss@3.4.17) + '@tanstack/react-virtual': 3.13.12(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@xyflow/react': 12.8.4(@types/react@18.3.23)(immer@9.0.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + class-variance-authority: 0.7.1 + clsx: 2.1.1 + cronstrue: 3.3.0 + dagre: 0.8.5 + deepmerge: 4.3.1 + fuse.js: 7.1.0 + lodash: 4.17.21 + lucide-react: 0.542.0(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + tailwind-merge: 3.3.1 + tailwind-scrollbar: 3.1.0(tailwindcss@3.4.17) + tailwindcss: 3.4.17 + '@types/argparse@1.0.38': {} '@types/aria-query@5.0.4': {} @@ -10433,8 +10276,6 @@ snapshots: chownr@1.1.4: optional: true - chownr@3.0.0: {} - chromatic@12.2.0: {} chrome-trace-event@1.0.4: {} @@ -10694,7 +10535,8 @@ snapshots: dequal@2.0.3: {} - detect-libc@2.0.4: {} + detect-libc@2.0.4: + optional: true detect-node-es@1.1.0: {} @@ -11675,7 +11517,8 @@ snapshots: jiti@1.21.7: {} - jiti@2.4.2: {} + jiti@2.4.2: + optional: true jju@1.4.0: {} @@ -11857,6 +11700,7 @@ snapshots: lightningcss-linux-x64-musl: 1.30.1 lightningcss-win32-arm64-msvc: 1.30.1 lightningcss-win32-x64-msvc: 1.30.1 + optional: true lilconfig@3.1.3: {} @@ -12259,15 +12103,9 @@ snapshots: minipass@7.1.2: {} - minizlib@3.0.2: - dependencies: - minipass: 7.1.2 - mkdirp-classic@0.5.3: optional: true - mkdirp@3.0.1: {} - mlly@1.7.4: dependencies: acorn: 8.15.0 @@ -13585,8 +13423,6 @@ snapshots: transitivePeerDependencies: - ts-node - tailwindcss@4.1.11: {} - tapable@2.2.2: {} tapable@2.2.3: {} @@ -13608,15 +13444,6 @@ snapshots: readable-stream: 3.6.2 optional: true - tar@7.4.3: - dependencies: - '@isaacs/fs-minipass': 4.0.1 - chownr: 3.0.0 - minipass: 7.1.2 - minizlib: 3.0.2 - mkdirp: 3.0.1 - yallist: 5.0.0 - terminal-link@4.0.0: dependencies: ansi-escapes: 7.0.0 @@ -14405,8 +14232,6 @@ snapshots: yallist@4.0.0: {} - yallist@5.0.0: {} - yaml@1.10.2: {} yaml@2.8.0: {} From f16fb2c37c92bd3b93dab32aa21946a050ca4482 Mon Sep 17 00:00:00 2001 From: Max Mykal Date: Fri, 3 Oct 2025 15:15:34 -0700 Subject: [PATCH 03/20] remove unused --- pnpm-lock.yaml | 21 ++++++++------------- vscode/react/package.json | 1 - vscode/react/src/pages/ModelNode.tsx | 4 ++-- 3 files changed, 10 insertions(+), 16 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5439739cdc..b0883de41c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -126,9 +126,6 @@ importers: '@tobikodata/sqlmesh-common': specifier: 0.0.17 version: 0.0.17(@radix-ui/react-slot@1.2.3(@types/react@18.3.23)(react@18.3.1))(@radix-ui/react-tooltip@1.2.8(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@tailwindcss/typography@0.5.16(tailwindcss@3.4.17))(@tanstack/react-virtual@3.13.12(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@xyflow/react@12.8.4(@types/react@18.3.23)(immer@9.0.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(class-variance-authority@0.7.1)(clsx@2.1.1)(cronstrue@3.3.0)(dagre@0.8.5)(deepmerge@4.3.1)(fuse.js@7.1.0)(lodash@4.17.21)(lucide-react@0.542.0(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(tailwind-merge@3.3.1)(tailwind-scrollbar@3.1.0(tailwindcss@3.4.17))(tailwindcss@3.4.17) - '@xyflow/react': - specifier: 12.8.4 - version: 12.8.4(@types/react@18.3.23)(immer@9.0.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) apache-arrow: specifier: ^19.0.1 version: 19.0.1 @@ -7685,7 +7682,7 @@ snapshots: ajv: 8.17.1 chalk: 4.1.2 compare-versions: 6.1.1 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1 esbuild: 0.25.8 esutils: 2.0.3 fs-extra: 11.3.0 @@ -9280,7 +9277,6 @@ snapshots: '@types/node@24.1.0': dependencies: undici-types: 7.8.0 - optional: true '@types/normalize-package-data@2.4.4': {} @@ -9541,7 +9537,7 @@ snapshots: '@ampproject/remapping': 2.3.0 '@bcoe/v8-coverage': 1.0.2 ast-v8-to-istanbul: 0.3.3 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1 istanbul-lib-coverage: 3.2.2 istanbul-lib-report: 3.0.1 istanbul-lib-source-maps: 5.0.6 @@ -9626,7 +9622,7 @@ snapshots: sirv: 3.0.1 tinyglobby: 0.2.14 tinyrainbow: 2.0.0 - vitest: 3.2.4(@types/debug@4.1.12)(@types/node@20.11.25)(@vitest/browser@3.2.4)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0) + vitest: 3.2.4(@types/debug@4.1.12)(@types/node@24.1.0)(@vitest/browser@3.2.3)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0) '@vitest/utils@3.2.3': dependencies: @@ -11483,7 +11479,7 @@ snapshots: istanbul-lib-source-maps@5.0.6: dependencies: '@jridgewell/trace-mapping': 0.3.29 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1 istanbul-lib-coverage: 3.2.2 transitivePeerDependencies: - supports-color @@ -11511,7 +11507,7 @@ snapshots: jest-worker@27.5.1: dependencies: - '@types/node': 20.11.25 + '@types/node': 24.1.0 merge-stream: 2.0.0 supports-color: 8.1.1 @@ -13688,8 +13684,7 @@ snapshots: undici-types@6.21.0: {} - undici-types@7.8.0: - optional: true + undici-types@7.8.0: {} undici@7.12.0: {} @@ -13833,7 +13828,7 @@ snapshots: vite-node@3.2.4(@types/node@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0): dependencies: cac: 6.7.14 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1 es-module-lexer: 1.7.0 pathe: 2.0.3 vite: 6.3.5(@types/node@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0) @@ -13973,7 +13968,7 @@ snapshots: '@vitest/spy': 3.2.4 '@vitest/utils': 3.2.4 chai: 5.2.1 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1 expect-type: 1.2.2 magic-string: 0.30.17 pathe: 2.0.3 diff --git a/vscode/react/package.json b/vscode/react/package.json index dcaada3166..a6b4747bb0 100644 --- a/vscode/react/package.json +++ b/vscode/react/package.json @@ -24,7 +24,6 @@ "@tanstack/react-virtual": "^3.13.12", "@tanstack/router-plugin": "^1.129.8", "@tobikodata/sqlmesh-common": "0.0.17", - "@xyflow/react": "12.8.4", "apache-arrow": "^19.0.1", "clsx": "^2.1.1", "cronstrue": "3.3.0", diff --git a/vscode/react/src/pages/ModelNode.tsx b/vscode/react/src/pages/ModelNode.tsx index d2fe14b50d..3f689d7aa4 100644 --- a/vscode/react/src/pages/ModelNode.tsx +++ b/vscode/react/src/pages/ModelNode.tsx @@ -1,4 +1,3 @@ -import { type Node, type NodeProps } from '@xyflow/react' import cronstrue from 'cronstrue' import React from 'react' @@ -32,6 +31,7 @@ import { NodeBadge, type ColumnLevelLineageAdjacencyList, NodePorts, + type NodeProps, } from '@tobikodata/sqlmesh-common/lineage' import { getNodeTypeBorderColor, @@ -54,7 +54,7 @@ export const ModelNode = React.memo(function ModelNode({ id, data, ...props -}: NodeProps>) { +}: NodeProps) { const { selectedColumns, zoom, From 3abd3ff46c752b27b55cffbd5e0e8f870f688f64 Mon Sep 17 00:00:00 2001 From: Max Mykal Date: Fri, 3 Oct 2025 15:33:19 -0700 Subject: [PATCH 04/20] update css --- pnpm-lock.yaml | 79 ++++++------------------- vscode/react/package.json | 2 +- vscode/react/src/App.css | 4 +- vscode/react/src/pages/ModelLineage.tsx | 4 +- 4 files changed, 23 insertions(+), 66 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b0883de41c..871b54034b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -124,8 +124,8 @@ importers: specifier: ^1.129.8 version: 1.129.8(@tanstack/react-router@1.129.8(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(vite@6.3.5(@types/node@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0))(webpack@5.99.8(esbuild@0.25.8)) '@tobikodata/sqlmesh-common': - specifier: 0.0.17 - version: 0.0.17(@radix-ui/react-slot@1.2.3(@types/react@18.3.23)(react@18.3.1))(@radix-ui/react-tooltip@1.2.8(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@tailwindcss/typography@0.5.16(tailwindcss@3.4.17))(@tanstack/react-virtual@3.13.12(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@xyflow/react@12.8.4(@types/react@18.3.23)(immer@9.0.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(class-variance-authority@0.7.1)(clsx@2.1.1)(cronstrue@3.3.0)(dagre@0.8.5)(deepmerge@4.3.1)(fuse.js@7.1.0)(lodash@4.17.21)(lucide-react@0.542.0(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(tailwind-merge@3.3.1)(tailwind-scrollbar@3.1.0(tailwindcss@3.4.17))(tailwindcss@3.4.17) + specifier: file:../../web/common + version: link:../../web/common apache-arrow: specifier: ^19.0.1 version: 19.0.1 @@ -2400,28 +2400,6 @@ packages: '@textlint/types@15.2.0': resolution: {integrity: sha512-wpF+xjGJgJK2JiwUdYjuNZrbuas3KfC9VDnHKac6aBLFyrI1iXuXtuxKXQDFi5/hebACactSJOuVVbuQbdJZ1Q==} - '@tobikodata/sqlmesh-common@0.0.17': - resolution: {integrity: sha512-64zQd5E2bpxxhee5hWnE0MuAt+la+ZTEBRQHJ17wzDaCHfz1JVfOkyistIEhHzIlDYWQr0JmPSt+zC0kdn3PYQ==} - peerDependencies: - '@radix-ui/react-slot': 1.2.3 - '@radix-ui/react-tooltip': 1.2.8 - '@tailwindcss/typography': 0.5.16 - '@tanstack/react-virtual': 3.13.12 - '@xyflow/react': 12.8.4 - class-variance-authority: 0.7.1 - clsx: 2.1.1 - cronstrue: 3.3.0 - dagre: 0.8.5 - deepmerge: 4.3.1 - fuse.js: 7.1.0 - lodash: 4.17.21 - lucide-react: 0.542.0 - react: 18.3.1 - react-dom: 18.3.1 - tailwind-merge: 3.3.1 - tailwind-scrollbar: 3.1.0 - tailwindcss: 3.4.17 - '@types/argparse@1.0.38': resolution: {integrity: sha512-ebDJ9b0e702Yr7pWgB0jzm+CX4Srzz8RcXtLJDJB+BSccqMa36uyH/zUsSYao5+BD1ytv3k3rPYCq4mAE1hsXA==} @@ -6962,7 +6940,7 @@ snapshots: '@babel/traverse': 7.28.0 '@babel/types': 7.28.1 convert-source-map: 2.0.0 - debug: 4.4.1 + debug: 4.4.1(supports-color@8.1.1) gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 @@ -7130,7 +7108,7 @@ snapshots: '@babel/parser': 7.28.0 '@babel/template': 7.27.2 '@babel/types': 7.28.1 - debug: 4.4.1 + debug: 4.4.1(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -7682,7 +7660,7 @@ snapshots: ajv: 8.17.1 chalk: 4.1.2 compare-versions: 6.1.1 - debug: 4.4.1 + debug: 4.4.1(supports-color@8.1.1) esbuild: 0.25.8 esutils: 2.0.3 fs-extra: 11.3.0 @@ -9036,27 +9014,6 @@ snapshots: dependencies: '@textlint/ast-node-types': 15.2.0 - '@tobikodata/sqlmesh-common@0.0.17(@radix-ui/react-slot@1.2.3(@types/react@18.3.23)(react@18.3.1))(@radix-ui/react-tooltip@1.2.8(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@tailwindcss/typography@0.5.16(tailwindcss@3.4.17))(@tanstack/react-virtual@3.13.12(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@xyflow/react@12.8.4(@types/react@18.3.23)(immer@9.0.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(class-variance-authority@0.7.1)(clsx@2.1.1)(cronstrue@3.3.0)(dagre@0.8.5)(deepmerge@4.3.1)(fuse.js@7.1.0)(lodash@4.17.21)(lucide-react@0.542.0(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(tailwind-merge@3.3.1)(tailwind-scrollbar@3.1.0(tailwindcss@3.4.17))(tailwindcss@3.4.17)': - dependencies: - '@radix-ui/react-slot': 1.2.3(@types/react@18.3.23)(react@18.3.1) - '@radix-ui/react-tooltip': 1.2.8(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@tailwindcss/typography': 0.5.16(tailwindcss@3.4.17) - '@tanstack/react-virtual': 3.13.12(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@xyflow/react': 12.8.4(@types/react@18.3.23)(immer@9.0.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - class-variance-authority: 0.7.1 - clsx: 2.1.1 - cronstrue: 3.3.0 - dagre: 0.8.5 - deepmerge: 4.3.1 - fuse.js: 7.1.0 - lodash: 4.17.21 - lucide-react: 0.542.0(react@18.3.1) - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - tailwind-merge: 3.3.1 - tailwind-scrollbar: 3.1.0(tailwindcss@3.4.17) - tailwindcss: 3.4.17 - '@types/argparse@1.0.38': {} '@types/aria-query@5.0.4': {} @@ -9277,6 +9234,7 @@ snapshots: '@types/node@24.1.0': dependencies: undici-types: 7.8.0 + optional: true '@types/normalize-package-data@2.4.4': {} @@ -9537,7 +9495,7 @@ snapshots: '@ampproject/remapping': 2.3.0 '@bcoe/v8-coverage': 1.0.2 ast-v8-to-istanbul: 0.3.3 - debug: 4.4.1 + debug: 4.4.1(supports-color@8.1.1) istanbul-lib-coverage: 3.2.2 istanbul-lib-report: 3.0.1 istanbul-lib-source-maps: 5.0.6 @@ -9622,7 +9580,7 @@ snapshots: sirv: 3.0.1 tinyglobby: 0.2.14 tinyrainbow: 2.0.0 - vitest: 3.2.4(@types/debug@4.1.12)(@types/node@24.1.0)(@vitest/browser@3.2.3)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0) + vitest: 3.2.4(@types/debug@4.1.12)(@types/node@20.11.25)(@vitest/browser@3.2.4)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0) '@vitest/utils@3.2.3': dependencies: @@ -10470,10 +10428,6 @@ snapshots: de-indent@1.0.2: {} - debug@4.4.1: - dependencies: - ms: 2.1.3 - debug@4.4.1(supports-color@8.1.1): dependencies: ms: 2.1.3 @@ -10747,7 +10701,7 @@ snapshots: esbuild-register@3.6.0(esbuild@0.25.8): dependencies: - debug: 4.4.1 + debug: 4.4.1(supports-color@8.1.1) esbuild: 0.25.8 transitivePeerDependencies: - supports-color @@ -11241,7 +11195,7 @@ snapshots: http-proxy-agent@7.0.2: dependencies: agent-base: 7.1.4 - debug: 4.4.1 + debug: 4.4.1(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -11250,7 +11204,7 @@ snapshots: https-proxy-agent@7.0.6: dependencies: agent-base: 7.1.4 - debug: 4.4.1 + debug: 4.4.1(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -11479,7 +11433,7 @@ snapshots: istanbul-lib-source-maps@5.0.6: dependencies: '@jridgewell/trace-mapping': 0.3.29 - debug: 4.4.1 + debug: 4.4.1(supports-color@8.1.1) istanbul-lib-coverage: 3.2.2 transitivePeerDependencies: - supports-color @@ -11507,7 +11461,7 @@ snapshots: jest-worker@27.5.1: dependencies: - '@types/node': 24.1.0 + '@types/node': 20.11.25 merge-stream: 2.0.0 supports-color: 8.1.1 @@ -13684,7 +13638,8 @@ snapshots: undici-types@6.21.0: {} - undici-types@7.8.0: {} + undici-types@7.8.0: + optional: true undici@7.12.0: {} @@ -13828,7 +13783,7 @@ snapshots: vite-node@3.2.4(@types/node@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0): dependencies: cac: 6.7.14 - debug: 4.4.1 + debug: 4.4.1(supports-color@8.1.1) es-module-lexer: 1.7.0 pathe: 2.0.3 vite: 6.3.5(@types/node@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0) @@ -13968,7 +13923,7 @@ snapshots: '@vitest/spy': 3.2.4 '@vitest/utils': 3.2.4 chai: 5.2.1 - debug: 4.4.1 + debug: 4.4.1(supports-color@8.1.1) expect-type: 1.2.2 magic-string: 0.30.17 pathe: 2.0.3 diff --git a/vscode/react/package.json b/vscode/react/package.json index a6b4747bb0..e5f41baa9a 100644 --- a/vscode/react/package.json +++ b/vscode/react/package.json @@ -23,7 +23,7 @@ "@tanstack/react-router-devtools": "^1.131.26", "@tanstack/react-virtual": "^3.13.12", "@tanstack/router-plugin": "^1.129.8", - "@tobikodata/sqlmesh-common": "0.0.17", + "@tobikodata/sqlmesh-common": "file:../../web/common", "apache-arrow": "^19.0.1", "clsx": "^2.1.1", "cronstrue": "3.3.0", diff --git a/vscode/react/src/App.css b/vscode/react/src/App.css index bcd5054a27..7a966c5478 100644 --- a/vscode/react/src/App.css +++ b/vscode/react/src/App.css @@ -24,7 +24,9 @@ --color-model-name-grayscale-model: var(--vscode-foreground); --color-model-name-copy-icon: var(--vscode-foreground); - --color-model-name-copy-icon-hover: rgba(0, 0, 0, 0.1); + --color-model-name-copy-icon-hover: var(--vscode-foreground); + --color-model-name-copy-icon-background: transparent; + --color-model-name-copy-icon-background-hover: rgba(255, 255, 255, 0.1); --color-filterable-list-counter-background: var(--vscode-input-background); --color-filterable-list-counter-foreground: var(--vscode-input-foreground); diff --git a/vscode/react/src/pages/ModelLineage.tsx b/vscode/react/src/pages/ModelLineage.tsx index dfc33fb89b..4f94dc2c56 100644 --- a/vscode/react/src/pages/ModelLineage.tsx +++ b/vscode/react/src/pages/ModelLineage.tsx @@ -23,7 +23,7 @@ import { createEdge, getEdgesFromColumnLineage, getOnlySelectedNodes, - getTransformedModelEdgesSourceTargets, + getTransformedModelEdgesTargetSources, getTransformedNodes, MAX_COLUMNS_TO_DISPLAY, toNodeID, @@ -249,7 +249,7 @@ export const ModelLineage = ({ const transformedEdges = React.useMemo(() => { return edgesColumnLevel.length > 0 ? edgesColumnLevel - : getTransformedModelEdgesSourceTargets< + : getTransformedModelEdgesTargetSources< ModelName, EdgeData, ModelNodeId, From 840e4d4336ebe2715842db60761b4666a99c1686 Mon Sep 17 00:00:00 2001 From: Max Mykal Date: Mon, 6 Oct 2025 08:39:09 -0700 Subject: [PATCH 05/20] simplify colors --- vscode/react/src/App.css | 18 ++++++--- vscode/react/src/pages/ModelNode.tsx | 59 ++-------------------------- 2 files changed, 16 insertions(+), 61 deletions(-) diff --git a/vscode/react/src/App.css b/vscode/react/src/App.css index 7a966c5478..76e85db005 100644 --- a/vscode/react/src/App.css +++ b/vscode/react/src/App.css @@ -81,17 +81,23 @@ --color-lineage-node-appendix-background: transparent; - --color-lineage-node-type-background-sql: var(--vscode-errorForeground); - --color-lineage-node-type-foreground-sql: var(--vscode-errorForeground); - --color-lineage-node-type-border-sql: var(--vscode-errorForeground); + --color-lineage-node-type-background-sql: var( + --vscode-minimap-errorHighlight + ); + --color-lineage-node-type-foreground-sql: var( + --vscode-minimap-errorHighlight + ); + --color-lineage-node-type-border-sql: var(--vscode-minimap-errorHighlight); --color-lineage-node-type-background-python: var( - --vscode-list-warningForeground + --vscode-editorWarning-foreground ); --color-lineage-node-type-foreground-python: var( - --vscode-list-warningForeground + --vscode-editorWarning-foreground + ); + --color-lineage-node-type-border-python: var( + --vscode-editorWarning-foreground ); - --color-lineage-node-type-border-python: var(--vscode-list-warningForeground); --color-lineage-node-type-background-source: var( --vscode-minimap-infoHighlight diff --git a/vscode/react/src/pages/ModelNode.tsx b/vscode/react/src/pages/ModelNode.tsx index 3f689d7aa4..afb786b8f6 100644 --- a/vscode/react/src/pages/ModelNode.tsx +++ b/vscode/react/src/pages/ModelNode.tsx @@ -22,7 +22,6 @@ import { ZOOM_THRESHOLD, NodeContainer, NodeBase, - NodeDivider, NodeHandleIcon, NodeHandles, NodeHeader, @@ -33,16 +32,11 @@ import { NodePorts, type NodeProps, } from '@tobikodata/sqlmesh-common/lineage' -import { - getNodeTypeBorderColor, - getNodeTypeColor, - getNodeTypeTextColor, -} from './help' +import { getNodeTypeColor } from './help' import { Badge, cn, HorizontalContainer, - Metadata, ModelName, Tooltip, VerticalContainer, @@ -189,7 +183,6 @@ export const ModelNode = React.memo(function ModelNode({ isSelected ? 'ring-2 ring-lineage-node-selected-border ring-offset-lineage-node-background' : 'hover:ring-2 hover:ring-lineage-node-border-hover', - getNodeTypeBorderColor(modelType), )} > - } - rightIcon={ - - } + leftIcon={} + rightIcon={} handleClassName="top-4" > - + ) }) - -export function NodeDetail({ - label, - value, - hasDivider = true, - className, -}: { - label: string - value: string - hasDivider?: boolean - className?: string -}) { - return ( - <> - {hasDivider && } - - - ) -} From 76e2b72d529319a526dda2b145c303c355dced96 Mon Sep 17 00:00:00 2001 From: Max Mykal Date: Mon, 6 Oct 2025 09:17:36 -0700 Subject: [PATCH 06/20] clean up --- vscode/react/src/pages/ModelNode.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/vscode/react/src/pages/ModelNode.tsx b/vscode/react/src/pages/ModelNode.tsx index afb786b8f6..bf6f3d35da 100644 --- a/vscode/react/src/pages/ModelNode.tsx +++ b/vscode/react/src/pages/ModelNode.tsx @@ -192,7 +192,6 @@ export const ModelNode = React.memo(function ModelNode({ > ZOOM_THRESHOLD ? 'shrink-0 h-7' : 'h-full')} - onClick={toggleSelectedNode} > ZOOM_THRESHOLD ? ' text-xs' : 'text-2xl justify-center', @@ -279,7 +278,7 @@ export const ModelNode = React.memo(function ModelNode({ } /> )} - className="border-t border-lineage-divider" + className="border-t border-lineage-divider cursor-default" /> )} From 9786176a82993e8502fa30e545cc0624666f184c Mon Sep 17 00:00:00 2001 From: Max Mykal Date: Mon, 6 Oct 2025 10:47:41 -0700 Subject: [PATCH 07/20] adjust colors --- vscode/react/src/App.css | 16 ++++++---------- vscode/react/src/pages/ModelLineage.tsx | 3 +-- 2 files changed, 7 insertions(+), 12 deletions(-) diff --git a/vscode/react/src/App.css b/vscode/react/src/App.css index 76e85db005..a2029bde73 100644 --- a/vscode/react/src/App.css +++ b/vscode/react/src/App.css @@ -81,13 +81,9 @@ --color-lineage-node-appendix-background: transparent; - --color-lineage-node-type-background-sql: var( - --vscode-minimap-errorHighlight - ); - --color-lineage-node-type-foreground-sql: var( - --vscode-minimap-errorHighlight - ); - --color-lineage-node-type-border-sql: var(--vscode-minimap-errorHighlight); + --color-lineage-node-type-background-sql: var(--vscode-minimap-infoHighlight); + --color-lineage-node-type-foreground-sql: var(--vscode-minimap-infoHighlight); + --color-lineage-node-type-border-sql: var(--vscode-minimap-infoHighlight); --color-lineage-node-type-background-python: var( --vscode-editorWarning-foreground @@ -100,12 +96,12 @@ ); --color-lineage-node-type-background-source: var( - --vscode-minimap-infoHighlight + --vscode-minimap-errorHighlight ); --color-lineage-node-type-foreground-source: var( - --vscode-minimap-infoHighlight + --vscode-minimap-errorHighlight ); - --color-lineage-node-type-border-source: var(--vscode-minimap-infoHighlight); + --color-lineage-node-type-border-source: var(--vscode-minimap-errorHighlight); --color-lineage-node-type-background-cte-subquery: var( --vscode-minimap-selectionOccurrenceHighlight diff --git a/vscode/react/src/pages/ModelLineage.tsx b/vscode/react/src/pages/ModelLineage.tsx index 4f94dc2c56..8982186bee 100644 --- a/vscode/react/src/pages/ModelLineage.tsx +++ b/vscode/react/src/pages/ModelLineage.tsx @@ -350,7 +350,6 @@ export const ModelLineage = ({ selectedNodes, selectedEdges, selectedNodeId, - isBuildingLayout, zoom, edges, nodes, @@ -363,7 +362,6 @@ export const ModelLineage = ({ setSelectedNodes, setSelectedEdges, setSelectedNodeId, - setIsBuildingLayout, setZoom, setEdges, setNodesMap, @@ -381,6 +379,7 @@ export const ModelLineage = ({ edgeTypes={edgeTypes} className={className} onNodeClick={onNodeClick} + isBuildingLayout={isBuildingLayout} controls={ <> Date: Mon, 6 Oct 2025 17:10:39 -0700 Subject: [PATCH 08/20] remove grpah component from vscode ext --- vscode/react/src/App.css | 14 +- vscode/react/src/components/graph/CogIcon.tsx | 27 - vscode/react/src/components/graph/Graph.css | 49 -- .../src/components/graph/ModelColumns.tsx | 585 -------------- .../src/components/graph/ModelLineage.tsx | 413 ---------- .../react/src/components/graph/ModelNode.tsx | 234 ------ .../graph/ModelNodeHeaderHandles.tsx | 111 --- .../src/components/graph/SettingsControl.tsx | 50 -- .../react/src/components/graph/constants.ts | 16 - vscode/react/src/components/graph/context.tsx | 330 -------- vscode/react/src/components/graph/help.ts | 739 ------------------ vscode/react/src/components/graph/types.ts | 101 --- .../src/components/graph/workers/index.ts | 3 - .../src/components/graph/workers/lineage.ts | 129 --- vscode/react/src/pages/ModelLineage.tsx | 28 +- vscode/react/src/pages/ModelNode.tsx | 36 +- vscode/react/src/pages/ModelNodeColumn.tsx | 68 +- vscode/react/tailwind.config.cjs | 4 + 18 files changed, 96 insertions(+), 2841 deletions(-) delete mode 100644 vscode/react/src/components/graph/CogIcon.tsx delete mode 100644 vscode/react/src/components/graph/Graph.css delete mode 100644 vscode/react/src/components/graph/ModelColumns.tsx delete mode 100644 vscode/react/src/components/graph/ModelLineage.tsx delete mode 100644 vscode/react/src/components/graph/ModelNode.tsx delete mode 100644 vscode/react/src/components/graph/ModelNodeHeaderHandles.tsx delete mode 100644 vscode/react/src/components/graph/SettingsControl.tsx delete mode 100644 vscode/react/src/components/graph/constants.ts delete mode 100644 vscode/react/src/components/graph/context.tsx delete mode 100644 vscode/react/src/components/graph/help.ts delete mode 100644 vscode/react/src/components/graph/types.ts delete mode 100644 vscode/react/src/components/graph/workers/index.ts delete mode 100644 vscode/react/src/components/graph/workers/lineage.ts diff --git a/vscode/react/src/App.css b/vscode/react/src/App.css index a2029bde73..a7dce951b3 100644 --- a/vscode/react/src/App.css +++ b/vscode/react/src/App.css @@ -46,6 +46,7 @@ --color-lineage-grid-dot: rgba(0, 0, 0, 0.1); --color-lineage-control-background: var(--vscode-editor-background); + --color-lineage-control-border: var(--vscode-foreground); --color-lineage-control-background-hover: var(--vscode-editor-background); --color-lineage-control-icon-background: var(--vscode-foreground); --color-lineage-control-icon-foreground: var(--vscode-editor-background); @@ -53,6 +54,7 @@ --vscode-editor-background ); --color-lineage-control-button-tooltip-foreground: var(--vscode-foreground); + --color-lineage-control-button-tooltip-border: var(--vscode-foreground); --xy-controls-button-background-color: var(--vscode-editor-background); --xy-controls-button-background-color-hover-default: var( @@ -72,6 +74,11 @@ --color-lineage-node-border: var(--vscode-text-separatorForeground); --color-lineage-node-border-hover: var(--vscode-text-separatorForeground); + --color-lineage-node-current-background: var( + --vscode-editorLightBulb-foreground + ); + --color-lineage-node-current-foreground: var(--vscode-editor-background); + --color-lineage-node-selected-border: var( --vscode-editorLightBulb-foreground ); @@ -138,7 +145,12 @@ --color-lineage-model-column-error-icon: var( --vscode-activityErrorBadge-background ); - --color-lineage-model-column-active: rgba(0, 0, 0, 0.1); + --color-lineage-model-column-active-background: var( + --vscode-selection-background + ); + --color-lineage-model-column-active-foreground: var( + --vscode-editorLightBulb-foreground + ); --color-lineage-model-column-icon: var(--vscode-text-separatorForeground); --color-lineage-model-column-icon-active: var(--vscode-foreground); diff --git a/vscode/react/src/components/graph/CogIcon.tsx b/vscode/react/src/components/graph/CogIcon.tsx deleted file mode 100644 index 75d3e952bd..0000000000 --- a/vscode/react/src/components/graph/CogIcon.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import * as React from 'react' - -/** - * CogIcon as taken from https://heroicons.com/. Slightly modified to remove fill color. - * - * @param props - SVG props - * @returns SVG element - */ -export function CogIcon(props: React.SVGProps): JSX.Element { - return ( - - ) -} diff --git a/vscode/react/src/components/graph/Graph.css b/vscode/react/src/components/graph/Graph.css deleted file mode 100644 index bef07d33b8..0000000000 --- a/vscode/react/src/components/graph/Graph.css +++ /dev/null @@ -1,49 +0,0 @@ -.react-flow__node { - z-index: 10 !important; - background-color: var(--vscode-editor-background, #ffffff); -} -.react-flow__handle { - background-color: currentColor; -} -.react-flow__node.react-flow__node-model:hover, -.react-flow__node.react-flow__node-model:active { - z-index: 20 !important; -} -react-flow__attribution { - background: transparent; -} -.lineage__column-source b { - font-weight: 900; - /* color: var(--color-primary); */ - color: black; -} -.react-flow__edge { - pointer-events: none !important; - z-index: -1 !important; -} -.react-flow__background path { - stroke: inherit; -} - -.react-flow__controls-button { - box-shadow: none; - border: var(--vscode-button-border); - background: var(--vscode-button-background); - color: var(--vscode-foreground); -} -.react-flow__controls-button:hover { - background: var(--vscode-button-hoverBackground); - border: var(--vscode-button-hoverBorder); -} -.react-flow__controls-button:active { - background: var(--vscode-button-activeBackground); - border: var(--vscode-button-activeBorder); -} -.react-flow__controls { - box-shadow: none; -} -.react-flow__panel { - box-shadow: none !important; - border: none !important; - padding: 0 !important; -} diff --git a/vscode/react/src/components/graph/ModelColumns.tsx b/vscode/react/src/components/graph/ModelColumns.tsx deleted file mode 100644 index e0e180de51..0000000000 --- a/vscode/react/src/components/graph/ModelColumns.tsx +++ /dev/null @@ -1,585 +0,0 @@ -import React, { useEffect, useMemo, useCallback } from 'react' -import { Handle, Position, useUpdateNodeInternals } from 'reactflow' -import 'reactflow/dist/base.css' -import { mergeLineageWithColumns, mergeConnections } from './help' -import { - debounceSync, - isArrayNotEmpty, - isFalse, - isNil, - isNotNil, - truncate, -} from '@/utils/index' -import { toID, type PartialColumnHandleId, type Side } from './types' -import { NoSymbolIcon } from '@heroicons/react/24/solid' -import { ClockIcon, ExclamationCircleIcon } from '@heroicons/react/24/outline' -import clsx from 'clsx' -import { - type ColumnDescription, - type ColumnLineageApiLineageModelNameColumnNameGet200, - type LineageColumn, -} from '@/api/client' -import Loading from '@/components/loading/Loading' -import Spinner from '@/components/logo/Spinner' -import './Graph.css' -import { - type InitialSQLMeshModel, - type ModelSQLMeshModel, -} from '@/domain/sqlmesh-model' -import { useLineageFlow } from './context' -import { useApiColumnLineage } from '@/api/index' -import SourceList from '@/components/sourceList/SourceList' -import type { Lineage } from '@/domain/lineage' -import type { Column, ColumnName } from '@/domain/column' -import type { ModelEncodedFQN } from '@/domain/models' - -export function ModelColumns({ - nodeId, - columns, - disabled, - className, - limit, - withHandles = false, - withDescription = true, - maxHeight = '50vh', -}: { - nodeId: ModelEncodedFQN - columns: Column[] - disabled?: boolean - className?: string - limit: number - withHandles?: boolean - withDescription?: boolean - maxHeight?: string -}): JSX.Element { - const { - mainNode, - connections, - isActiveColumn, - setConnections, - manuallySelectedColumn, - setManuallySelectedColumn, - setLineage, - removeActiveEdges, - addActiveEdges, - lineage, - lineageCache, - setLineageCache, - } = useLineageFlow() - - const [columnsSelected = [], columnsRest = []] = useMemo(() => { - const active: Column[] = [] - const rest: Column[] = [] - - columns.forEach(column => { - if (isActiveColumn(nodeId, column.name)) { - active.push(column) - } else { - rest.push(column) - } - }) - - return [active, rest] - }, [nodeId, columns, isActiveColumn]) - - function updateColumnLineage( - columnLineage: Record> = {}, - ): void { - let newLineageCache = lineageCache - let currentConnections - let currentLineage - - if (isNil(lineageCache)) { - const mainNodeLineage = isNil(mainNode) - ? undefined - : (lineage[mainNode] ?? lineageCache?.[mainNode]) - - newLineageCache = lineage - currentConnections = new Map() - currentLineage = - isNil(mainNode) || isNil(mainNodeLineage) - ? {} - : { [mainNode]: { models: [] } } - } else { - currentConnections = connections - currentLineage = structuredClone(lineage) - } - - const { connections: newConnections, activeEdges } = mergeConnections( - currentConnections, - columnLineage, - ) - - if (newConnections.size === 0 && activeEdges.length === 0) { - currentLineage = structuredClone(lineage) - newLineageCache = undefined - } else { - setConnections(newConnections) - addActiveEdges(activeEdges) - } - - const mergedLineage = mergeLineageWithColumns(currentLineage, columnLineage) - - setLineageCache(newLineageCache) - setLineage(mergedLineage) - } - - const isSelectManually = useCallback( - function isSelectManually(columnName: ColumnName): boolean { - if (isNil(manuallySelectedColumn)) return false - - const [selectedModel, selectedColumn] = manuallySelectedColumn - - if (isNil(selectedModel) || isNil(selectedColumn)) return false - - return selectedModel.fqn === nodeId && selectedColumn.name === columnName - }, - [nodeId, manuallySelectedColumn], - ) - - const removeEdges = useCallback( - function removeEdges(columnId: PartialColumnHandleId): void { - const visited = new Set() - - removeActiveEdges(walk(columnId, 'left').concat(walk(columnId, 'right'))) - - if (connections.size === 0 && isNotNil(lineageCache)) { - setLineage(lineageCache) - setLineageCache(undefined) - } - - setConnections(connections) - - function walk(id: string, side: Side): Array<[string, string]> { - if (visited.has(id)) return [] - - const edges = connections.get(id)?.[side] ?? [] - - connections.delete(id) - - visited.add(id) - - return edges - .map(edge => - [ - side === 'left' - ? [toID('left', id), toID('right', edge)] - : [toID('left', edge), toID('right', id)], - ].concat(walk(edge, side)), - ) - .flat() as Array<[PartialColumnHandleId, PartialColumnHandleId]> - } - }, - [removeActiveEdges, connections], - ) - - return ( -
- {isArrayNotEmpty(columnsSelected) && ( -
- {columnsSelected.map(column => ( - - ))} -
- )} - {columnsRest.length <= limit && ( -
- {columnsRest.map(column => ( - - ))} -
- )} - {columnsRest.length > limit && ( -
- - keyId="name" - keyName="name" - items={columnsRest} - withCounter={false} - withFilter={columnsRest.length > limit} - disabled={disabled} - listItem={({ disabled, item }) => ( - - )} - /> -
- )} -
- ) -} - -function ModelColumn({ - id, - nodeId, - column, - className, - disabled = false, - isActive = false, - hasLeft = false, - hasRight = false, - isEmpty = false, - updateColumnLineage, - removeEdges, - selectManually, - withHandles = false, - withDescription = true, -}: { - id: PartialColumnHandleId - nodeId: ModelEncodedFQN - column: Column - disabled?: boolean - isActive?: boolean - hasLeft?: boolean - hasRight?: boolean - isEmpty?: boolean - withHandles?: boolean - withDescription?: boolean - updateColumnLineage: ( - lineage: ColumnLineageApiLineageModelNameColumnNameGet200, - ) => void - removeEdges: (columnId: PartialColumnHandleId) => void - selectManually?: React.Dispatch< - React.SetStateAction< - [ModelSQLMeshModel, Column] | undefined - > - > - className?: string -}): JSX.Element { - const { - refetch: getColumnLineage, - isFetching, - isError, - } = useApiColumnLineage(nodeId, column.name, { models_only: true }) - - useEffect(() => { - if (isNil(selectManually)) return - - toggleColumnLineage() - selectManually(undefined) - }, [selectManually]) - - function toggleColumnLineage(): void { - if (disabled) return - - if (isActive) { - removeEdges(id) - } else { - void getColumnLineage().then(({ data }) => - updateColumnLineage(data ?? {}), - ) - } - } - - const showHandles = withHandles && (hasLeft || hasRight) - - return ( -
-
- {showHandles ? ( - - - - - ) : ( - <> - - - - )} -
-
- ) -} - -function ColumnHandles({ - nodeId, - id, - hasLeft = false, - hasRight = false, - disabled = false, - children, - className, -}: { - nodeId: ModelEncodedFQN - id: PartialColumnHandleId - children: React.ReactNode - className?: string - hasLeft?: boolean - hasRight?: boolean - disabled?: boolean -}): JSX.Element { - const updateNodeInternals = useUpdateNodeInternals() - - useEffect(() => { - // TODO: This is a hack to fix the issue where the handles are not rendered yet - setTimeout(() => { - updateNodeInternals(nodeId) - }, 100) - }, [hasLeft, hasRight]) - - return ( -
- {hasLeft && ( - - )} - {children} - {hasRight && ( - - )} -
- ) -} - -function ColumnDisplay({ - columnName, - columnType, - columnDescription, - className, - disabled = false, - withDescription = true, -}: { - columnName: ColumnName - columnType: string - columnDescription?: ColumnDescription - disabled?: boolean - withDescription?: boolean - className?: string -}): JSX.Element { - return ( -
-
- - {disabled && ( - - )} - {truncate(columnName, 50, 20)} - - - {truncate(columnType, 20, 10)} - -
- {isNotNil(columnDescription) && withDescription && ( -

{columnDescription}

- )} -
- ) -} - -function ColumnStatus({ - isFetching = false, - isError = false, - isTimeout = false, -}: { - isFetching: boolean - isError: boolean - isTimeout: boolean -}): JSX.Element { - return ( - <> - {isFetching && ( - - - - )} - {isTimeout && isFalse(isFetching) && ( - - )} - {isError && isFalse(isFetching) && ( - - )} - - ) -} - -function getColumnFromLineage( - lineage: Record, - nodeId: string, - columnName: string, -): LineageColumn | undefined { - return lineage?.[nodeId]?.columns?.[columnName as ColumnName] -} diff --git a/vscode/react/src/components/graph/ModelLineage.tsx b/vscode/react/src/components/graph/ModelLineage.tsx deleted file mode 100644 index 3d157d3869..0000000000 --- a/vscode/react/src/components/graph/ModelLineage.tsx +++ /dev/null @@ -1,413 +0,0 @@ -import { useApiModelLineage, useApiModels } from '@/api/index' -import { useEffect, useMemo, useState } from 'react' -import { type ModelSQLMeshModel } from '@/domain/sqlmesh-model' -import { type HighlightedNodes, useLineageFlow } from './context' -import ReactFlow, { - Controls, - Background, - BackgroundVariant, - type EdgeChange, - applyEdgeChanges, - applyNodeChanges, - type NodeChange, - useReactFlow, - type Edge, - type Node, - ReactFlowProvider, -} from 'reactflow' -import Loading from '@/components/loading/Loading' -import Spinner from '@/components/logo/Spinner' -import { createLineageWorker } from '@/components/graph/workers/index' -import { isArrayEmpty, isFalse, isNil, isNotNil } from '@/utils/index' -import clsx from 'clsx' -import ModelNode from './ModelNode' -import { - getEdges, - getLineageIndex, - getActiveNodes, - getUpdatedNodes, - getUpdatedEdges, - createGraphLayout, -} from './help' -import { SettingsControl } from '@/components/graph/SettingsControl' -import { - toModelLineage, - type ModelLineage as ModelLineageType, -} from '@/domain/lineage' -import './Graph.css' -import { - toKeys, - type LineageWorkerMessage, - type LineageWorkerRequestMessage, - type LineageWorkerResponseMessage, - type LineageWorkerErrorMessage, -} from './types' -import { encode } from '@/domain/models' - -const WITH_COLUMNS_LIMIT = 30 - -export function ModelLineage({ - model, - highlightedNodes, -}: { - model: ModelSQLMeshModel - highlightedNodes?: HighlightedNodes -}): JSX.Element { - const { - setActiveNodes, - setActiveEdges, - setConnections, - setLineage, - handleError, - setSelectedNodes, - setMainNode, - setWithColumns, - setHighlightedNodes, - setNodeConnections, - setLineageCache, - setUnknownModels, - models, - unknownModels, - setWithSecondary, - setWithConnected, - setWithImpacted, - } = useLineageFlow() - - useEffect(() => { - setWithColumns(true) - setWithSecondary(true) - setWithConnected(true) - setWithImpacted(true) - }, [setWithSecondary]) - - const { refetch: getModelLineage, isFetching: isFetchingModelLineage } = - useApiModelLineage(model.name) - const { isFetching: isFetchingModels } = useApiModels() - - const [isMergingModels, setIsMergingModels] = useState(false) - const [modelLineage, setModelLineage] = useState< - ModelLineageType | undefined - >(undefined) - - useEffect(() => { - const lineageWorker = new createLineageWorker() - - lineageWorker.addEventListener('message', handleLineageWorkerMessage) - - getModelLineage() - .then(({ data }) => { - setModelLineage(data ? toModelLineage(data) : undefined) - if (isNil(data)) return - - setIsMergingModels(true) - - const message: LineageWorkerRequestMessage = { - topic: 'lineage', - payload: { - currentLineage: {}, - newLineage: data, - mainNode: model.fqn, - }, - } - lineageWorker.postMessage(message) - }) - .catch(error => { - handleError?.(error) - }) - .finally(() => { - setActiveNodes(new Set()) - setActiveEdges(new Map()) - setConnections(new Map()) - setSelectedNodes(new Set()) - setLineageCache(undefined) - setMainNode(model.fqn) - }) - - return () => { - lineageWorker.removeEventListener('message', handleLineageWorkerMessage) - lineageWorker.terminate() - - setLineage({}) - setNodeConnections({}) - setMainNode(undefined) - setHighlightedNodes({}) - } - }, [model.name, model.hash]) - - useEffect(() => { - const modelNames = toKeys(modelLineage ?? {}) - for (const modelName of modelNames) { - const encodedModelName = encode(modelName) - if ( - isFalse(encodedModelName in models) && - isFalse(encodedModelName in unknownModels) - ) { - unknownModels.add(encodedModelName) - } - } - - setUnknownModels(new Set(unknownModels)) - }, [modelLineage, models]) - - useEffect(() => { - setHighlightedNodes(highlightedNodes ?? {}) - }, [highlightedNodes]) - - function handleLineageWorkerMessage( - e: MessageEvent, - ): void { - if (e.data.topic === 'lineage') { - const message = e.data as LineageWorkerResponseMessage - setIsMergingModels(false) - setNodeConnections(message.payload.nodesConnections) - setLineage(message.payload.lineage) - - if ( - Object.values(message.payload.lineage ?? {}).length > WITH_COLUMNS_LIMIT - ) { - setWithColumns(false) - } - } - - if (e.data.topic === 'error') { - const message = e.data as LineageWorkerErrorMessage - handleError?.(message.error) - setIsMergingModels(false) - } - } - - const isFetching = - isFetchingModelLineage || isFetchingModels || isMergingModels - - return ( -
- {isFetching && ( -
- - - -

- {isFetching ? "Loading Model's Lineage..." : "Merging Model's..."} -

-
-
- )} - - - -
- ) -} - -function ModelColumnLineage(): JSX.Element { - const { - withColumns, - lineage, - mainNode, - selectedEdges, - selectedNodes, - withConnected, - withImpacted, - withSecondary, - hasBackground, - activeEdges, - connectedNodes, - connections, - nodesMap, - handleError, - setActiveNodes, - setWithColumns, - } = useLineageFlow() - - const { setCenter } = useReactFlow() - - const [isBuildingLayout, setIsBuildingLayout] = useState(false) - - const nodeTypes = useMemo(() => ({ model: ModelNode }), []) - - const allEdges = useMemo(() => getEdges(lineage), [lineage]) - const lineageIndex = useMemo(() => getLineageIndex(lineage), [lineage]) - - const [nodes, setNodes] = useState([]) - const [edges, setEdges] = useState([]) - - useEffect(() => { - if (isArrayEmpty(allEdges) || isNil(mainNode)) return - - setIsBuildingLayout(true) - - const newActiveNodes = getActiveNodes( - allEdges, - activeEdges, - selectedEdges, - nodesMap, - ) - const newNodes = getUpdatedNodes( - Object.values(nodesMap), - newActiveNodes, - mainNode, - connectedNodes, - selectedNodes, - connections, - withConnected, - withImpacted, - withSecondary, - ) - const newEdges = getUpdatedEdges( - allEdges, - connections, - activeEdges, - newActiveNodes, - selectedEdges, - selectedNodes, - connectedNodes, - withConnected, - withImpacted, - withSecondary, - ) - const createLayout = createGraphLayout({ - nodesMap, - nodes: newNodes, - edges: newEdges, - }) - - createLayout - .create() - .then(layout => { - setEdges(layout.edges) - setNodes(layout.nodes) - }) - .catch(error => { - handleError?.(error) - setEdges([]) - setNodes([]) - }) - .finally(() => { - const node = isNil(mainNode) ? undefined : nodesMap[mainNode] - - if (isNotNil(node)) { - setCenter(node.position.x, node.position.y, { - zoom: 0.5, - duration: 0, - }) - } - - setTimeout(() => { - setIsBuildingLayout(false) - }, 100) - }) - - return () => { - createLayout.terminate() - - setEdges([]) - setNodes([]) - } - }, [activeEdges, nodesMap, lineageIndex]) - - useEffect(() => { - if (isNil(mainNode) || isArrayEmpty(nodes)) return - - const newActiveNodes = getActiveNodes( - allEdges, - activeEdges, - selectedEdges, - nodesMap, - ) - const newNodes = getUpdatedNodes( - nodes, - newActiveNodes, - mainNode, - connectedNodes, - selectedNodes, - connections, - withConnected, - withImpacted, - withSecondary, - ) - - const newEdges = getUpdatedEdges( - allEdges, - connections, - activeEdges, - newActiveNodes, - selectedEdges, - selectedNodes, - connectedNodes, - withConnected, - withImpacted, - withSecondary, - ) - - setEdges(newEdges) - setNodes(newNodes) - setActiveNodes(newActiveNodes) - }, [ - connections, - nodesMap, - allEdges, - activeEdges, - selectedNodes, - selectedEdges, - connectedNodes, - withConnected, - withImpacted, - withSecondary, - withColumns, - mainNode, - ]) - - function onNodesChange(changes: NodeChange[]): void { - setNodes(applyNodeChanges(changes, nodes)) - } - - function onEdgesChange(changes: EdgeChange[]): void { - setEdges(applyEdgeChanges(changes, edges)) - } - - return ( - <> - {isBuildingLayout && ( -
- - - -

Building Lineage...

-
-
- )} - - - - - - - - ) -} diff --git a/vscode/react/src/components/graph/ModelNode.tsx b/vscode/react/src/components/graph/ModelNode.tsx deleted file mode 100644 index 864b1437fa..0000000000 --- a/vscode/react/src/components/graph/ModelNode.tsx +++ /dev/null @@ -1,234 +0,0 @@ -import { isNil, isArrayNotEmpty, isNotNil, isFalse } from '@/utils/index' -import clsx from 'clsx' -import { useMemo, useCallback, useState } from 'react' -import { ModelType, type Model } from '@/api/client' -import { useLineageFlow } from './context' -import { type GraphNodeData } from './help' -import { Position, type NodeProps } from 'reactflow' -import { ModelNodeHeaderHandles } from './ModelNodeHeaderHandles' -import { ModelColumns } from './ModelColumns' -import { fromAPIColumn, type Column } from '@/domain/column' -import { decode, type ModelEncodedFQN } from '@/domain/models' -import { toKeys } from './types' -import { MAX_VISIBLE_COLUMNS } from './constants' - -export const EnumLineageNodeModelType = { - ...ModelType, - cte: 'cte', - unknown: 'unknown', -} as const - -export const EnumColumnType = { - UNKNOWN: 'UNKNOWN', - STRUCT: 'STRUCT', -} as const - -export type LineageNodeModelType = keyof typeof EnumLineageNodeModelType -export type ColumnType = keyof typeof EnumColumnType - -export default function ModelNode({ - id: idProp, - data, - sourcePosition, - targetPosition, -}: NodeProps): JSX.Element { - const id = idProp as ModelEncodedFQN - const nodeData: GraphNodeData = data ?? { - label: '', - type: EnumLineageNodeModelType.unknown, - withColumns: false, - } - const { - // connections, - models, - handleClickModel, - lineage, - lineageCache, - selectedNodes, - setSelectedNodes, - mainNode, - withConnected, - connectedNodes, - highlightedNodes, - activeNodes, - } = useLineageFlow() - - const columns: Column[] = useMemo(() => { - const modelsArray = Object.values(models) - const decodedId = decode(id) - const model = modelsArray.find((m: Model) => m.fqn === decodedId) - const modelColumns = model?.columns?.map(fromAPIColumn) ?? [] - - toKeys(lineage[decodedId]?.columns ?? {}).forEach(column => { - const found = modelColumns.find(({ name }) => name === column) - if (isNil(found)) { - modelColumns.push( - fromAPIColumn({ name: column, type: EnumColumnType.UNKNOWN }), - ) - } - }) - return modelColumns.map(column => { - let columnType = column.type ?? EnumColumnType.UNKNOWN - if (columnType.startsWith(EnumColumnType.STRUCT)) { - columnType = EnumColumnType.STRUCT - } - return { - ...column, - type: columnType, - } - }) - }, [id, models, lineage]) - - const highlightedNodeModels = useMemo( - () => Object.values(highlightedNodes).flat(), - [highlightedNodes], - ) - - const [isMouseOver, setIsMouseOver] = useState(false) - - const handleClick = useCallback( - (e: React.MouseEvent) => { - e.stopPropagation() - if (handleClickModel) { - handleClickModel(id) - } - }, - [handleClickModel, id, data.isInteractive], - ) - - const handleSelect = useCallback( - (e: React.MouseEvent) => { - e.stopPropagation() - - if (highlightedNodeModels.includes(id) || mainNode === id) return - - setSelectedNodes(current => { - if (current.has(id)) { - current.delete(id) - } else { - current.add(id) - } - - return new Set(current) - }) - }, - [setSelectedNodes, highlightedNodeModels], - ) - - const splat = highlightedNodes['*'] - // const hasSelectedColumns = columns.some(({ name }) => - // connections.get(toID(id, name)), - // ) - const hasHighlightedNodes = Object.keys(highlightedNodes).length > 0 - const highlighted = Object.keys(highlightedNodes).find(key => - highlightedNodes[key]!.includes(id), - ) - const isMainNode = mainNode === id - const isHighlightedNode = highlightedNodeModels.includes(id) - const isSelected = selectedNodes.has(id) - // Ensure nodeData.type is a valid LineageNodeModelType - const nodeType: LineageNodeModelType = Object.values( - EnumLineageNodeModelType, - ).includes(nodeData.type) - ? (nodeData.type as LineageNodeModelType) - : EnumLineageNodeModelType.unknown - - const isModelSQL = nodeType === EnumLineageNodeModelType.sql - const isCTE = nodeType === EnumLineageNodeModelType.cte - const isModelExternal = nodeType === EnumLineageNodeModelType.external - const isModelSeed = nodeType === EnumLineageNodeModelType.seed - const isModelUnknown = nodeType === EnumLineageNodeModelType.unknown - const showColumns = - nodeData.withColumns && - isArrayNotEmpty(columns) && - isFalse(hasHighlightedNodes) - const isActiveNode = - selectedNodes.size > 0 || activeNodes.size > 0 || withConnected - ? isSelected || - activeNodes.has(id as ModelEncodedFQN) || - (withConnected && connectedNodes.has(id)) - : connectedNodes.has(id) - const isInteractive = true - // mainNode !== id && - // isNotNil(handleClickModel) && - // isFalse(isCTE) && - // isFalse(isModelUnknown) - const shouldDisableColumns = isFalse(isModelSQL) - - return ( -
setIsMouseOver(true)} - onMouseLeave={() => setIsMouseOver(false)} - className={clsx( - 'text-xs font-semibold border-4', - isMouseOver ? 'z-50' : 'z-1', - showColumns ? 'rounded-xl' : 'rounded-2xl', - (hasHighlightedNodes ? isHighlightedNode : isActiveNode) || isMainNode - ? 'opacity-100' - : 'opacity-40 hover:opacity-100', - isNil(highlighted) - ? hasHighlightedNodes - ? splat - : [ - isCTE - ? 'border-accent-500 bg-accent-500 text-accent-500 dark:border-accent-300 dark:bg-accent-300 dark:text-accent-300' - : isModelUnknown - ? 'border-neutral-500 bg-neutral-500 text-neutral-500 dark:border-neutral-300 dark:bg-neutral-300 dark:text-neutral-300' - : 'border-secondary-500 bg-secondary-500 text-secondary-500 dark:bg-primary-500 dark:border-primary-500 dark:text-primary-500', - isMainNode - ? 'ring-8 ring-brand-50' - : isModelExternal || isModelSeed - ? 'ring-8 ring-accent-50' - : '', - ] - : highlighted, - isSelected && isCTE - ? 'ring-8 ring-accent-50' - : isSelected && isModelUnknown - ? 'ring-8 ring-neutral-50' - : isSelected && 'ring-8 ring-secondary-50 dark:ring-primary-50', - )} - style={{ - maxWidth: isNil(nodeData.width) - ? 'auto' - : `${nodeData.width as number}px`, - }} - > - - {showColumns && ( - - )} -
- ) -} diff --git a/vscode/react/src/components/graph/ModelNodeHeaderHandles.tsx b/vscode/react/src/components/graph/ModelNodeHeaderHandles.tsx deleted file mode 100644 index a23d6af5c4..0000000000 --- a/vscode/react/src/components/graph/ModelNodeHeaderHandles.tsx +++ /dev/null @@ -1,111 +0,0 @@ -import { type MouseEvent } from 'react' -import { Handle, Position } from 'reactflow' -import 'reactflow/dist/base.css' -import { getModelNodeTypeTitle } from './help' -import { isNotNil, truncate } from '@/utils/index' -import { toID } from './types' -import { ArrowRightCircleIcon } from '@heroicons/react/24/solid' -import clsx from 'clsx' -import { type LineageNodeModelType } from './ModelNode' -import type { ModelEncodedFQN } from '@/domain/models' - -export function ModelNodeHeaderHandles({ - id, - className, - hasLeft = false, - hasRight = false, - isSelected = false, - isDraggable = false, - label, - type, - numberOfColumns, - handleClick, - handleSelect, -}: { - id: ModelEncodedFQN - label: string - type?: LineageNodeModelType - hasLeft?: boolean - hasRight?: boolean - numberOfColumns?: number - className?: string - isSelected?: boolean - isDraggable?: boolean - handleClick?: (e: MouseEvent) => void - handleSelect?: (e: MouseEvent) => void -}): JSX.Element { - return ( -
- {hasLeft && ( - - - - )} -
- {isNotNil(handleSelect) && ( - - - - )} - - {isNotNil(type) && ( - - {getModelNodeTypeTitle(type)} - - )} - - {truncate(decodeURI(label), 50, 20)} - - {isNotNil(numberOfColumns) && ( - - {numberOfColumns} - - )} - -
- {hasRight && ( - - - - )} -
- ) -} diff --git a/vscode/react/src/components/graph/SettingsControl.tsx b/vscode/react/src/components/graph/SettingsControl.tsx deleted file mode 100644 index 3016a96ee7..0000000000 --- a/vscode/react/src/components/graph/SettingsControl.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import { Menu, MenuButton, MenuItem, MenuItems } from '@headlessui/react' -import { CheckIcon } from '@heroicons/react/24/outline' -import { CogIcon } from '@/components/graph/CogIcon' -import clsx from 'clsx' - -interface SettingsControlProps { - showColumns: boolean - onWithColumnsChange: (value: boolean) => void -} - -export function SettingsControl({ - showColumns, - onWithColumnsChange, -}: SettingsControlProps): JSX.Element { - return ( - - - - - onWithColumnsChange(!showColumns)} - > - Show Columns - {showColumns && ( - - - - ) -} diff --git a/vscode/react/src/components/graph/constants.ts b/vscode/react/src/components/graph/constants.ts deleted file mode 100644 index 0927ac027e..0000000000 --- a/vscode/react/src/components/graph/constants.ts +++ /dev/null @@ -1,16 +0,0 @@ -/** - * Space between nodes. - */ -export const NODE_BALANCE_SPACE = 64 -/** - * Height of a column line. - */ -export const COLUMN_LINE_HEIGHT = 24 -/** - * Assumed width of a character. - */ -export const CHAR_WIDTH = 8 -/** - * Maximum number of columns that can be visible in a node. - */ -export const MAX_VISIBLE_COLUMNS = 5 diff --git a/vscode/react/src/components/graph/context.tsx b/vscode/react/src/components/graph/context.tsx deleted file mode 100644 index 9ab4f0722e..0000000000 --- a/vscode/react/src/components/graph/context.tsx +++ /dev/null @@ -1,330 +0,0 @@ -import { - createContext, - useState, - useContext, - useCallback, - useMemo, -} from 'react' -import { getNodeMap, hasActiveEdge, hasActiveEdgeConnector } from './help' -import { type Node } from 'reactflow' -import type { Lineage } from '@/domain/lineage' -import type { ModelSQLMeshModel } from '@/domain/sqlmesh-model' -import type { Column } from '@/domain/column' -import type { ModelEncodedFQN, ModelName } from '@/domain/models' -import type { ColumnName } from '@/domain/column' -import type { Model } from '@/api/client' -import { toID, toKeys } from './types' -import type { ConnectedNode } from '@/components/graph/types' - -export interface Connections { - left: string[] - right: string[] -} -export type ActiveColumns = Map -export type ActiveEdges = Map> -export type ActiveNodes = Set -export type SelectedNodes = Set -export type HighlightedNodes = Record - -interface LineageFlow { - lineage: Record - lineageCache?: Record - mainNode?: ModelEncodedFQN - connectedNodes: Set - activeEdges: ActiveEdges - activeNodes: ActiveNodes - selectedNodes: SelectedNodes - selectedEdges: ConnectedNode[] - models: Record - unknownModels: Set - connections: Map - withConnected: boolean - withColumns: boolean - hasBackground: boolean - withImpacted: boolean - withSecondary: boolean - manuallySelectedColumn?: [ModelSQLMeshModel, Column] - highlightedNodes: HighlightedNodes - nodesMap: Record - setHighlightedNodes: React.Dispatch> - setActiveNodes: React.Dispatch> - setWithConnected: React.Dispatch> - setMainNode: React.Dispatch> - setSelectedNodes: React.Dispatch> - setWithColumns: React.Dispatch> - setHasBackground: React.Dispatch> - setWithImpacted: React.Dispatch> - setWithSecondary: React.Dispatch> - setConnections: React.Dispatch>> - hasActiveEdge: (edge: [string | undefined, string | undefined]) => boolean - addActiveEdges: (edges: Array<[string, string]>) => void - removeActiveEdges: (edges: Array<[string, string]>) => void - setActiveEdges: React.Dispatch> - setUnknownModels: React.Dispatch>> - setLineage: React.Dispatch>> - setLineageCache: React.Dispatch< - React.SetStateAction | undefined> - > - handleClickModel?: (modelName: ModelEncodedFQN) => void - handleError?: (error: any) => void - setManuallySelectedColumn: React.Dispatch< - React.SetStateAction<[ModelSQLMeshModel, Column] | undefined> - > - setNodeConnections: React.Dispatch> - isActiveColumn: ( - modelName: ModelEncodedFQN, - columnName: ColumnName, - ) => boolean -} - -export const LineageFlowContext = createContext({ - selectedEdges: [], - lineage: {}, - lineageCache: undefined, - withColumns: false, - withConnected: false, - withImpacted: true, - withSecondary: false, - hasBackground: true, - mainNode: undefined, - activeEdges: new Map(), - activeNodes: new Set(), - models: {}, - unknownModels: new Set(), - manuallySelectedColumn: undefined, - connections: new Map(), - selectedNodes: new Set(), - connectedNodes: new Set(), - highlightedNodes: {}, - nodesMap: {}, - setHighlightedNodes: () => {}, - setWithColumns: () => false, - setHasBackground: () => false, - setWithImpacted: () => false, - setWithSecondary: () => false, - setWithConnected: () => false, - hasActiveEdge: () => false, - addActiveEdges: () => {}, - removeActiveEdges: () => {}, - setActiveEdges: () => {}, - handleClickModel: () => {}, - setManuallySelectedColumn: () => {}, - handleError: error => console.error(error), - setLineage: () => {}, - setLineageCache: () => {}, - isActiveColumn: () => false, - setConnections: () => {}, - setSelectedNodes: () => {}, - setMainNode: () => {}, - setActiveNodes: () => {}, - setNodeConnections: () => {}, - setUnknownModels: () => {}, -}) - -export default function LineageFlowProvider({ - handleError, - handleClickModel, - children, - showColumns = true, - showConnected = false, - showControls = true, - models, -}: { - children: React.ReactNode - handleClickModel?: (modelName: ModelEncodedFQN) => void - handleError?: (error: any) => void - showColumns?: boolean - showConnected?: boolean - showControls?: boolean - models: Record -}): JSX.Element { - const [lineage, setLineage] = useState>({}) - const [unknownModels, setUnknownModels] = useState(new Set()) - const [lineageCache, setLineageCache] = useState< - Record | undefined - >(undefined) - const [nodesConnections, setNodeConnections] = useState< - Record - >({}) - const [withColumns, setWithColumns] = useState(showColumns) - const [mainNode, setMainNode] = useState() - const [manuallySelectedColumn, setManuallySelectedColumn] = - useState<[ModelSQLMeshModel, Column]>() - const [activeEdges, setActiveEdges] = useState(new Map()) - const [connections, setConnections] = useState>( - new Map(), - ) - const [withConnected, setWithConnected] = useState(showConnected) - const [selectedNodes, setSelectedNodes] = useState(new Set()) - const [activeNodes, setActiveNodes] = useState(new Set()) - const [highlightedNodes, setHighlightedNodes] = useState({}) - const [hasBackground, setHasBackground] = useState(true) - const [withImpacted, setWithImpacted] = useState(true) - const [withSecondary, setWithSecondary] = useState(false) - - const nodesMap = useMemo( - () => - getNodeMap({ - lineage, - // @ts-expect-error TODO: fix this, should move to internal representation - models, - unknownModels, - withColumns, - }), - [lineage, models, withColumns, unknownModels], - ) - - const checkActiveEdge = useCallback( - function checkActiveEdge( - edge: [string | undefined, string | undefined], - ): boolean { - return hasActiveEdge(activeEdges, edge) - }, - [activeEdges], - ) - - const addActiveEdges = useCallback( - function addActiveEdges(edges: Array<[string, string]>): void { - setActiveEdges(activeEdges => { - edges.forEach(([leftConnect, rightConnect]) => { - const left = activeEdges.get(leftConnect) ?? [] - const right = activeEdges.get(rightConnect) ?? [] - const hasDuplicateLeft = left.some( - ([left, right]) => left === leftConnect && right === rightConnect, - ) - const hasDuplicateRight = right.some( - ([left, right]) => left === leftConnect && right === rightConnect, - ) - - if (!hasDuplicateLeft) { - left.push([leftConnect, rightConnect]) - } - - if (!hasDuplicateRight) { - right.push([leftConnect, rightConnect]) - } - - activeEdges.set(leftConnect, left) - activeEdges.set(rightConnect, right) - }) - - return new Map(activeEdges) - }) - }, - [setActiveEdges], - ) - - const removeActiveEdges = useCallback( - function removeActiveEdges(edges: Array<[string, string]>): void { - setActiveEdges(activeEdges => { - edges.forEach(([left, right]) => { - const edgesLeft = (activeEdges.get(left) ?? []).filter( - e => e[0] !== left && e[1] !== right, - ) - const edgesRight = (activeEdges.get(right) ?? []).filter( - e => e[0] !== left && e[1] !== right, - ) - - activeEdges.set(left, edgesLeft) - activeEdges.set(right, edgesRight) - }) - - return new Map(activeEdges) - }) - - setConnections(connections => { - edges.forEach(([left, right]) => { - connections.delete(left) - connections.delete(right) - }) - - return new Map(connections) - }) - }, - [setActiveEdges, setConnections], - ) - - const isActiveColumn = useCallback( - function isActive( - modelName: ModelEncodedFQN, - columnName: ColumnName, - ): boolean { - const leftConnector = toID('left', modelName, columnName) - const rightConnector = toID('right', modelName, columnName) - return ( - hasActiveEdgeConnector(activeEdges, leftConnector) || - hasActiveEdgeConnector(activeEdges, rightConnector) - ) - }, - [checkActiveEdge, activeEdges], - ) - - const connectedNodes = useMemo( - () => new Set(toKeys(nodesConnections)), - [nodesConnections], - ) - - const selectedEdges = useMemo( - () => - Array.from(selectedNodes) - .flatMap(id => nodesConnections[id]) - .filter(Boolean) as any[], - [nodesConnections, selectedNodes], - ) - - return ( - - {children} - - ) -} - -export function useLineageFlow(): LineageFlow { - return useContext(LineageFlowContext) -} diff --git a/vscode/react/src/components/graph/help.ts b/vscode/react/src/components/graph/help.ts deleted file mode 100644 index 93e5c4db45..0000000000 --- a/vscode/react/src/components/graph/help.ts +++ /dev/null @@ -1,739 +0,0 @@ -import ELK, { type ElkNode } from 'elkjs/lib/elk.bundled.js' -import { - isArrayNotEmpty, - isFalse, - isNil, - isNotNil, - isObjectEmpty, -} from '@/utils/index' -import { type LineageColumn } from '@/api/client' -import { Position, type Edge, type Node, type XYPosition } from 'reactflow' -import { type ActiveEdges, type Connections } from './context' -import { toID, toKeys } from './types' -import { - EnumLineageNodeModelType, - type LineageNodeModelType, -} from './ModelNode' -import type { Lineage } from '@/domain/lineage' -import type { ConnectedNode } from '@/components/graph/types' -import { encode, type ModelEncodedFQN, type ModelURI } from '@/domain/models' -import type { Column, ColumnName } from '@/domain/column' -import type { ModelSQLMeshModel } from '@/domain/sqlmesh-model' -import { - CHAR_WIDTH, - COLUMN_LINE_HEIGHT, - MAX_VISIBLE_COLUMNS, - NODE_BALANCE_SPACE, -} from './constants' - -export interface GraphNodeData { - label: string - type: LineageNodeModelType - withColumns: boolean - width?: number - height?: number - [key: string]: any -} - -export function createGraphLayout({ - nodesMap, - nodes = [], - edges = [], -}: { - nodesMap: Record - nodes: Node[] - edges: Edge[] -}): { - create: () => Promise<{ nodes: Node[]; edges: Edge[] }> - terminate: () => void -} { - // https://eclipse.dev/elk/reference/options.html - const elk: any = new ELK() - - return { - terminate: () => elk.worker.terminate(), - create: async () => - new Promise((resolve, reject) => { - elk - .layout({ - id: 'root', - layoutOptions: { - 'elk.algorithm': 'layered', - 'elk.layered.layering.strategy': 'NETWORK_SIMPLEX', - 'elk.layered.crossingMinimization.strategy': 'INTERACTIVE', - 'elk.direction': 'RIGHT', - // https://eclipse.dev/elk/reference/options/org-eclipse-elk-layered-considerModelOrder-strategy.html - 'elk.layered.considerModelOrder.strategy': 'PREFER_NODES', - 'elk.layered.nodePlacement.strategy': 'SIMPLE', - }, - children: nodes.map(node => ({ - id: node.id, - width: node.data.width, - height: node.data.height, - })), - edges: edges.map(edge => ({ - id: edge.id, - sources: [edge.source], - targets: [edge.target], - })), - }) - .then((layout: any) => - resolve({ - edges, - nodes: repositionNodes(layout.children, nodesMap), - }), - ) - .catch(reject) - }), - } -} - -export function getEdges( - lineage: Record = {}, -): Edge[] { - const modelNames = toKeys(lineage) - const outputEdges: Edge[] = [] - - for (const targetModelName of modelNames) { - const targetModel = lineage[targetModelName]! - - targetModel.models.forEach(sourceModelName => { - outputEdges.push(createGraphEdge(sourceModelName, targetModelName)) - }) - - const targetColumnNames = toKeys(targetModel.columns ?? {}) - for (const targetColumnName of targetColumnNames) { - const sourceModel = targetModel.columns?.[targetColumnName] - - if (isNil(sourceModel) || isNil(sourceModel.models)) continue - - const sourceModelNames = toKeys(sourceModel.models) - for (const sourceModelName of sourceModelNames) { - const sourceColumns = sourceModel.models[sourceModelName] - - if (isNil(sourceColumns)) continue - - for (const sourceColumnName of sourceColumns) { - const sourceHandler = toID('right', sourceModelName, sourceColumnName) - const targetHandler = toID('left', targetModelName, targetColumnName) - outputEdges.push( - createGraphEdge( - sourceModelName, - targetModelName, - sourceHandler, - targetHandler, - true, - { - columnSource: sourceColumnName, - columnTarget: targetColumnName, - }, - ), - ) - } - } - } - } - - return outputEdges -} - -export function getNodeMap({ - lineage, - models, - unknownModels, - withColumns, -}: { - models: Record - withColumns: boolean - unknownModels: Set - lineage?: Record -}): Record { - if (isNil(lineage)) return {} - - const sources = new Set(Object.values(lineage).flatMap(l => l.models)) - const modelNames = Object.keys(lineage) - - return modelNames.reduce((acc: Record, modelName: string) => { - const decodedModelName = modelName.includes('%') - ? decodeURI(modelName) - : modelName - const model = Object.values(models).find(m => m.fqn === decodedModelName) - const nodeType: LineageNodeModelType = isNotNil(model) - ? (model.type as LineageNodeModelType) - : // If model name present in lineage but not in global models - // it means either this is a CTE or model is UNKNOWN - // CTEs only have connections between columns - // where UNKNOWN model has connection only from another model - unknownModels.has(modelName) - ? EnumLineageNodeModelType.unknown - : EnumLineageNodeModelType.cte - - const node = createGraphNode(modelName, { - label: model?.name ?? modelName, - withColumns, - type: nodeType, - }) - const columnsCount = withColumns - ? (models[modelName]?.columns?.length ?? 0) - : 0 - - const maxWidth = Math.min( - getNodeMaxWidth(modelName, columnsCount === 0, models), - 320, - ) - const maxHeight = getNodeMaxHeight(columnsCount) - - node.data.width = maxWidth + NODE_BALANCE_SPACE * 3 - node.data.height = withColumns - ? maxHeight + NODE_BALANCE_SPACE * 2 - : NODE_BALANCE_SPACE - - if (isArrayNotEmpty(lineage[node.id]?.models)) { - node.targetPosition = Position.Left - } - - if (sources.has(node.id as ModelEncodedFQN)) { - node.sourcePosition = Position.Right - } - - acc[modelName] = node - - return acc - }, {}) -} - -function getNodeMaxWidth( - label: string, - hasColumns: boolean = false, - models: Record = {}, -): number { - const defaultWidth = label.length * CHAR_WIDTH - const columns = models[label]?.columns ?? [] - - return hasColumns - ? Math.max(...columns.map(getColumnWidth), defaultWidth) - : defaultWidth -} - -function getColumnWidth(column: Column): number { - return ( - (column.name.length + column.type.length) * CHAR_WIDTH + NODE_BALANCE_SPACE - ) -} - -function getNodeMaxHeight(columnsCount: number): number { - return ( - COLUMN_LINE_HEIGHT * Math.min(columnsCount, MAX_VISIBLE_COLUMNS) + - NODE_BALANCE_SPACE - ) -} - -function repositionNodes( - elkNodes: ElkNode[] = [], - nodesMap: Record, -): Node[] { - const nodes: Node[] = [] - - elkNodes.forEach(node => { - const output = nodesMap[node.id] - - if (isNil(output)) return - - if (isNotNil(node.x) && output.position.x === 0) { - output.position.x = node.x - } - - if (isNotNil(node.y) && output.position.y === 0) { - output.position.y = node.y - } - - nodes.push(output) - }) - - return nodes -} - -function createGraphNode( - id: string, - data: GraphNodeData, - position: XYPosition = { x: 0, y: 0 }, - hidden: boolean = false, -): Node { - return { - id, - dragHandle: '.drag-handle', - type: 'model', - position, - hidden, - data, - connectable: false, - selectable: false, - deletable: false, - focusable: false, - zIndex: -1, - } -} - -function createGraphEdge( - source: string, - target: string, - sourceHandle?: string, - targetHandle?: string, - hidden: boolean = false, - data?: Data, -): Edge { - const output: Edge = { - id: toID(source, target, sourceHandle, targetHandle), - source, - target, - hidden, - data, - type: 'smoothstep', - style: { - strokeWidth: isNil(sourceHandle) || isNil(targetHandle) ? 2 : 4, - }, - } - - if (sourceHandle != null) { - output.sourceHandle = sourceHandle - } - - if (targetHandle != null) { - output.targetHandle = targetHandle - } - - return output -} - -export function mergeLineageWithColumns( - currentLineage: Record = {}, - newLineage: Record> = {}, -): Record { - for (const targetModelName in newLineage) { - const targetModelNameEncoded = encodeURI(targetModelName) - - if (isNil(currentLineage[targetModelNameEncoded])) { - currentLineage[targetModelNameEncoded] = { columns: {}, models: [] } - } - - const currentLineageModel = currentLineage[targetModelNameEncoded]! - const newLineageModel = newLineage[targetModelName]! - - for (const targetColumnName in newLineageModel) { - const targetColumnNameEncoded = encodeURI(targetColumnName) - const newLineageModelColumn = newLineageModel[targetColumnName]! - - if (isNil(currentLineageModel.columns)) { - currentLineageModel.columns = {} - } - - // New Column Lineage delivers fresh data, so we can just assign it - currentLineageModel.columns[targetColumnNameEncoded as ColumnName] = { - expression: newLineageModelColumn.expression ?? undefined, - source: newLineageModelColumn.source ?? undefined, - models: {}, - } - - // If there are no models in new lineage, skip - if (isObjectEmpty(newLineageModelColumn.models)) continue - - const currentLineageModelColumn = - currentLineageModel.columns[targetColumnNameEncoded as ColumnName]! - const currentLineageModelColumnModels = currentLineageModelColumn.models - - for (const sourceColumnName in newLineageModelColumn.models) { - const sourceColumnNameEncoded = encodeURI(sourceColumnName) - const currentLineageModelColumnModel = - currentLineageModelColumnModels[ - sourceColumnNameEncoded as ModelEncodedFQN - ]! - const newLineageModelColumnModel = - newLineageModelColumn.models[sourceColumnName]! - - // @ts-expect-error TODO: fix this - currentLineageModelColumnModels[ - sourceColumnNameEncoded as ModelEncodedFQN - ] = Array.from( - new Set( - isNil(currentLineageModelColumnModel) - ? newLineageModelColumnModel - : currentLineageModelColumnModel.concat( - newLineageModelColumnModel as ColumnName[], - ), - ), - ).map(uri => encode(uri as ModelURI)) - } - } - } - - return currentLineage -} - -export function mergeConnections( - connections: Map, - lineage: Record> = {}, -): { - connections: Map - activeEdges: Array<[string, string]> -} { - const activeEdges: Array<[string, string]> = [] - - // We are getting lineage in format of target -> source - for (const targetModelName in lineage) { - const targetModelNameEncoded = encodeURI(targetModelName) - const model = lineage[targetModelName]! - - for (const targetColumnName in model) { - const targetColumnNameEncoded = encodeURI(targetColumnName) - const column = model[targetColumnName] - - // We don't have any connectins so we skip - if (isNil(column?.models)) continue - - // At this point our Node is model -> {modelName} and column -> {columnName} - // It is a target (left handler) - // but it can also be a source (right handler) for other connections - const modelColumnIdTarget = toID( - targetModelNameEncoded, - targetColumnNameEncoded, - ) - - // We need to check if {modelColumnIdTarget} is already a source/target for other connections - // Left and Right coresponds to node's handlers for {columnName} column - const connectionsModelTarget = connections.get(modelColumnIdTarget) ?? { - left: [], - right: [], - } - - Object.entries(column.models).forEach(([sourceModelName, columns]) => { - const sourceModelNameEncoded = encodeURI(sourceModelName) - columns.forEach(sourceColumnName => { - const sourceColumnNameEncoded = encodeURI(sourceColumnName) - // It is a source (right handler) - // but it can also be a target (left handler) for other connections - const modelColumnIdSource = toID( - sourceModelNameEncoded, - sourceColumnNameEncoded, - ) - - // We need to check if {modelColumnIdSource} is already a source/target for other connections - // Left and Right coresponds to node's handlers for {column} column - const connectionsModelSource = connections.get( - modelColumnIdSource, - ) ?? { left: [], right: [] } - - // we need to add {modelColumnIdTarget} to {connectionsModelSource}'s right handlers - connectionsModelSource.right = Array.from( - new Set(connectionsModelSource.right.concat(modelColumnIdTarget)), - ) - - // We need to add {modelColumnIdSource} to {connectionsModelTarget}'s right handlers - connectionsModelTarget.left = Array.from( - new Set(connectionsModelTarget.left.concat(modelColumnIdSource)), - ) - - connections.set(modelColumnIdSource, connectionsModelSource) - connections.set(modelColumnIdTarget, connectionsModelTarget) - - // Now we need to update active edges from connections - // Left bucket contains references to all sources (right handlers) - // And right bucket contains references to all targets (left handlers) - connectionsModelSource.left.forEach(id => { - activeEdges.push([ - toID('left', modelColumnIdSource), - toID('right', id), - ]) - }) - connectionsModelSource.right.forEach(id => { - activeEdges.push([ - toID('left', id), - toID('right', modelColumnIdSource), - ]) - }) - }) - }) - } - } - - return { - connections, - activeEdges, - } -} - -export function getLineageIndex(lineage: Record = {}): string { - return Object.keys(lineage) - .reduce((acc: string[], key) => { - const { models = [], columns = {} } = lineage[key]! - const allModels = new Set() - - models.forEach(m => allModels.add(m)) - - if (isNotNil(columns)) { - toKeys(columns).forEach(columnName => { - const column = columns[columnName] - if (isNotNil(column) && isNotNil(column.models)) { - toKeys(column.models).forEach(m => allModels.add(m)) - } - }) - } - - return acc.concat(Array.from(allModels)) - }, []) - .sort() - .join('') -} - -export function getModelAncestors( - lineage: Record = {}, - name: string, - output = new Set(), -): Set { - const model = lineage[name] - const models = model?.models ?? [] - - for (const modelName of models) { - if (output.has(modelName)) continue - - getModelAncestors(lineage, modelName, output).add(modelName) - } - - return output -} - -export function getActiveNodes( - edges: Edge[] = [], - activeEdges: ActiveEdges, - selectedEdges: ConnectedNode[], - nodesMap: Record, -): Set { - return new Set( - edges.reduce((acc: ModelEncodedFQN[], edge) => { - const sourceNode = isNil(edge.sourceHandle) - ? undefined - : nodesMap[edge.sourceHandle] - const targetNode = isNil(edge.targetHandle) - ? undefined - : nodesMap[edge.targetHandle] - - if ( - isNotNil(sourceNode) && - isNotNil(edge.sourceHandle) && - sourceNode.data.type === EnumLineageNodeModelType.external && - hasActiveEdgeConnector(activeEdges, edge.sourceHandle) - ) { - acc.push(edge.source as ModelEncodedFQN) - } else if ( - isNotNil(targetNode) && - isNotNil(edge.targetHandle) && - targetNode.data.type === EnumLineageNodeModelType.external && - hasActiveEdgeConnector(activeEdges, edge.targetHandle) - ) { - acc.push(edge.target as ModelEncodedFQN) - } else { - const isActiveEdge = hasActiveEdge(activeEdges, [ - edge.targetHandle, - edge.sourceHandle, - ]) - - if (isActiveEdge || hasEdge(selectedEdges, edge.id)) { - if (isNotNil(edge.source)) { - acc.push(edge.source as ModelEncodedFQN) - } - - if (isNotNil(edge.target)) { - acc.push(edge.target as ModelEncodedFQN) - } - } - } - return acc - }, []), - ) -} - -export function getUpdatedEdges( - edges: Edge[] = [], - connections: Map, - activeEdges: ActiveEdges, - activeNodes: Set, - selectedEdges: ConnectedNode[], - selectedNodes: Set, - connectedNodes: Set, - withConnected: boolean = false, - withImpacted: boolean = false, - withSecondary: boolean = false, -): Edge[] { - const tempEdges = edges.map(edge => { - const isActiveEdge = hasActiveEdge(activeEdges, [ - edge.targetHandle, - edge.sourceHandle, - ]) - - edge.hidden = true - - if (isNil(edge.sourceHandle) && isNil(edge.targetHandle)) { - // Edge between models - const hasSelections = - selectedNodes.size > 0 || connections.size > 0 || activeNodes.size > 0 - const isImpactedEdge = - connectedNodes.has(edge.source) || connectedNodes.has(edge.target) - const isSecondaryEdge = - isFalse(connectedNodes.has(edge.source)) || - isFalse(connectedNodes.has(edge.target)) - const withoutImpactedNodes = - isFalse(withImpacted) && - isFalse(withConnected) && - isFalse(hasSelections) - const withoutSecondaryNodes = - isFalse(withSecondary) && isFalse(hasSelections) - const shouldHideSecondary = isSecondaryEdge && withoutSecondaryNodes - const shouldHideImpacted = isImpactedEdge && withoutImpactedNodes - const isVisibleEdge = - selectedNodes.size > 0 && - hasEdge(selectedEdges, edge.id) && - activeNodes.has(edge.source) && - activeNodes.has(edge.target) - - if ( - isFalse(shouldHideImpacted) && - isFalse(shouldHideSecondary) && - (isFalse(hasSelections) || isVisibleEdge) - ) { - edge.hidden = false - } - } else { - // Edge between columns - if (connections.size > 0 && isActiveEdge) { - edge.hidden = false - } - } - - let stroke = 'var(--color-graph-edge-main)' - let strokeWidth = 2 - - const isConnectedSource = connectedNodes.has(edge.source) - const isConnectedTarget = connectedNodes.has(edge.target) - - if ( - hasEdge(selectedEdges, edge.id) || - (withConnected && isConnectedSource && isConnectedTarget) - ) { - strokeWidth = 4 - stroke = 'var(--color-graph-edge-selected)' - edge.zIndex = 10 - } else { - if (isActiveEdge) { - stroke = 'var(--color-graph-edge-secondary)' - } else if (isConnectedSource && isConnectedTarget) { - strokeWidth = 4 - stroke = 'var(--color-graph-edge-direct)' - edge.zIndex = 10 - } - } - - edge.style = { - ...edge.style, - stroke, - strokeWidth, - } - - return edge - }) - - return tempEdges -} - -export function getUpdatedNodes( - nodes: Node[] = [], - activeNodes: Set, - mainNode: ModelEncodedFQN, - connectedNodes: Set, - selectedNodes: Set, - connections: Map, - withConnected: boolean, - withImpacted: boolean, - withSecondary: boolean, -): Node[] { - return nodes.map(node => { - node.hidden = true - - const hasSelections = selectedNodes.size > 0 || connections.size > 0 - const isActiveNode = activeNodes.size === 0 || activeNodes.has(node.id) - const isImpactedNode = connectedNodes.has(node.id) - const isSecondaryNode = isFalse(connectedNodes.has(node.id)) - const withoutImpactedNodes = - isFalse(withImpacted) && isFalse(withConnected) && isFalse(hasSelections) - const withoutSecondaryNodes = - isFalse(withSecondary) && isFalse(hasSelections) - const shouldHideSecondary = isSecondaryNode && withoutSecondaryNodes - const shouldHideImpacted = isImpactedNode && withoutImpactedNodes - - if (isFalse(shouldHideImpacted) && isFalse(shouldHideSecondary)) { - node.hidden = isFalse(isActiveNode) - } - - if (node.data.type === EnumLineageNodeModelType.cte) { - node.hidden = isFalse(activeNodes.has(node.id)) - } - - if (mainNode === node.id) { - node.hidden = false - } - - return node - }) -} - -export function hasActiveEdge( - activeEdges: ActiveEdges = new Map(), - [leftConnect, rightConnect]: [ - string | undefined | null, - string | undefined | null, - ], -): boolean { - if (isNil(leftConnect) && isNil(rightConnect)) return false - - const left = isNil(leftConnect) ? undefined : activeEdges.get(leftConnect) - const right = isNil(rightConnect) ? undefined : activeEdges.get(rightConnect) - - if (isNil(left) && isNil(right)) return false - - const inLeft = Boolean( - left?.some(([l, r]) => l === leftConnect && r === rightConnect), - ) - const inRight = Boolean( - right?.some(([l, r]) => l === leftConnect && r === rightConnect), - ) - - return inLeft || inRight -} - -export function hasActiveEdgeConnector( - activeEdges: ActiveEdges = new Map(), - connector: string, -): boolean { - return (activeEdges.get(connector) ?? []).length > 0 -} - -export function getModelNodeTypeTitle(type: LineageNodeModelType): string { - switch (type) { - case EnumLineageNodeModelType.python: - return 'PYTHON' - case EnumLineageNodeModelType.sql: - return 'SQL' - case EnumLineageNodeModelType.seed: - return 'SEED' - case EnumLineageNodeModelType.cte: - return 'CTE' - case EnumLineageNodeModelType.external: - return 'EXTERNAL' - case EnumLineageNodeModelType.source: - return 'SOURCE' - default: - return 'UNKNOWN' - } -} - -function hasEdge(nodes: ConnectedNode[], edge: string): boolean { - return nodes.some(node => node.id === edge || hasEdge(node.edges, edge)) -} diff --git a/vscode/react/src/components/graph/types.ts b/vscode/react/src/components/graph/types.ts deleted file mode 100644 index 6e188b31c8..0000000000 --- a/vscode/react/src/components/graph/types.ts +++ /dev/null @@ -1,101 +0,0 @@ -import type { ColumnName } from '@/domain/column' -import type { ModelEncodedFQN } from '@/domain/models' -import type { Branded } from '@bus/brand' -import type { Lineage } from '@/domain/lineage' - -export type Side = 'left' | 'right' - -export type Direction = 'upstream' | 'downstream' - -export type NodeId = string - -export type EdgeId = string - -/** - * Partial column handle id that isn't complete yet as it's missing the left/right side - * definition. - */ -export type PartialColumnHandleId = Branded -export type ColumnHandleId = Branded -export type ModelHandleId = Branded - -/** - * Converts a list of strings to a single string with a double underscore - * Outlines with types, the type of ids that can be created. - * @param args - * @returns - */ -export function toID( - leftOrRight: Side, - modelName: ModelEncodedFQN, - columnName: ColumnName, -): NodeId -export function toID( - modelName: ModelEncodedFQN, - columnName: ColumnName, -): PartialColumnHandleId -export function toID( - leftOrRight: Side, - partialColumnHandleId: PartialColumnHandleId, -): ColumnHandleId -export function toID( - leftOrRight: Side, - modelName: ModelEncodedFQN, -): ModelHandleId -export function toID(source: NodeId, target: NodeId): NodeId -export function toID( - source: NodeId, - target: NodeId, - sourceHandle: string | undefined, - targetHandle: string | undefined, -): EdgeId -export function toID(...args: Array): string { - return args.filter(Boolean).join('__') -} - -export function toKeys(obj: Record): K[] { - return Object.keys(obj) as K[] -} - -export type ModelLineage = Record - -// Worker Message Types -export interface ConnectedNode { - id?: string - edges: ConnectedNode[] -} - -export interface LineageWorkerRequestPayload { - currentLineage: Record - newLineage: Record - mainNode: string -} - -export interface LineageWorkerResponsePayload { - lineage: Record - nodesConnections: Record -} - -export interface LineageWorkerErrorPayload { - error: Error -} - -export interface LineageWorkerRequestMessage { - topic: 'lineage' - payload: LineageWorkerRequestPayload -} - -export interface LineageWorkerResponseMessage { - topic: 'lineage' - payload: LineageWorkerResponsePayload -} - -export interface LineageWorkerErrorMessage { - topic: 'error' - error: Error -} - -export type LineageWorkerMessage = - | LineageWorkerRequestMessage - | LineageWorkerResponseMessage - | LineageWorkerErrorMessage diff --git a/vscode/react/src/components/graph/workers/index.ts b/vscode/react/src/components/graph/workers/index.ts deleted file mode 100644 index 9eb76d5287..0000000000 --- a/vscode/react/src/components/graph/workers/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -import createLineageWorker from './lineage.ts?worker&inline' - -export { createLineageWorker } diff --git a/vscode/react/src/components/graph/workers/lineage.ts b/vscode/react/src/components/graph/workers/lineage.ts deleted file mode 100644 index fe8337b72d..0000000000 --- a/vscode/react/src/components/graph/workers/lineage.ts +++ /dev/null @@ -1,129 +0,0 @@ -import { isFalse, isNil } from '@/utils/index' -import { type Lineage } from '@/domain/lineage' -import type { ModelEncodedFQN } from '@/domain/models' -import { - toID, - type NodeId, - type LineageWorkerMessage, - type LineageWorkerRequestMessage, - type LineageWorkerResponseMessage, - type LineageWorkerErrorMessage, - type ConnectedNode, -} from '@/components/graph/types' -import type { Direction } from '../types' - -interface WorkerScope { - onmessage: ((e: MessageEvent) => void) | null - postMessage: (message: LineageWorkerMessage) => void -} - -const scope = self as unknown as WorkerScope - -scope.onmessage = async (e: MessageEvent) => { - if (e.data.topic === 'lineage') { - try { - const message = e.data as LineageWorkerRequestMessage - const { currentLineage, newLineage, mainNode } = message.payload - const lineage = await mergeLineageWithModels(currentLineage, newLineage) - const nodesConnections = await getNodesConnections(mainNode, lineage) - - const responseMessage: LineageWorkerResponseMessage = { - topic: 'lineage', - payload: { - lineage, - nodesConnections, - }, - } - scope.postMessage(responseMessage) - } catch (error) { - const errorMessage: LineageWorkerErrorMessage = { - topic: 'error', - error: error as Error, - } - scope.postMessage(errorMessage) - } - } -} - -async function mergeLineageWithModels( - currentLineage: Record = {}, - data: Record = {}, -): Promise> { - return Object.entries(data).reduce( - (acc: Record, [key, models = []]) => { - key = encodeURI(key) - - acc[key] = { - models: models.map(encodeURI) as ModelEncodedFQN[], - columns: currentLineage?.[key]?.columns ?? undefined, - } - - return acc - }, - {}, - ) -} - -async function getNodesConnections( - mainNode: string, - lineage: Record = {}, -): Promise> { - return new Promise((resolve, reject) => { - if (isNil(lineage) || isNil(mainNode)) return {} - - const distances: Record = {} - - try { - getConnectedNodes('upstream', mainNode, lineage, distances) - getConnectedNodes('downstream', mainNode, lineage, distances) - } catch (error) { - reject(error) - } - - resolve(distances) - }) -} - -function getConnectedNodes( - direction: Direction = 'downstream', - node: string, - lineage: Record = {}, - result: Record = {}, -): void { - const isDownstream = direction === 'downstream' - let models: string[] = [] - - if (isDownstream) { - models = Object.keys(lineage).filter(key => - lineage[key]!.models.includes(node as ModelEncodedFQN), - ) - } else { - models = lineage[node]?.models ?? [] - } - - if (isFalse(node in result)) { - result[node] = { edges: [] } - } - - for (const model of models) { - const connectedNode = isDownstream - ? createConnectedNode(node, model, [result[node]!]) - : createConnectedNode(model, node, [result[node]!]) - - if (model in result) { - result[model]!.edges.push(connectedNode) - } else { - result[model] = connectedNode - getConnectedNodes(direction, model, lineage, result) - } - } -} - -function createConnectedNode( - source: NodeId, - target: NodeId, - edges: ConnectedNode[] = [], -): ConnectedNode { - const id = toID(source, target) - return { id, edges } -} diff --git a/vscode/react/src/pages/ModelLineage.tsx b/vscode/react/src/pages/ModelLineage.tsx index 8982186bee..d602d0007a 100644 --- a/vscode/react/src/pages/ModelLineage.tsx +++ b/vscode/react/src/pages/ModelLineage.tsx @@ -73,6 +73,10 @@ export const ModelLineage = ({ node: LineageNode, ) => void }) => { + const currentNodeId = selectedModelName + ? toNodeID(selectedModelName) + : null + const [zoom, setZoom] = React.useState(ZOOM_THRESHOLD) const [isBuildingLayout, setIsBuildingLayout] = React.useState(false) const [edges, setEdges] = React.useState< @@ -275,7 +279,7 @@ export const ModelLineage = ({ setNodesMap(nodesMap) setIsBuildingLayout(false) }, - 200, + 0, ) }, []) @@ -284,10 +288,8 @@ export const ModelLineage = ({ }, [nodesMap]) const currentNode = React.useMemo(() => { - return selectedModelName - ? nodesMap[toNodeID(selectedModelName)] - : null - }, [selectedModelName, nodesMap]) + return currentNodeId ? nodesMap[currentNodeId] : null + }, [currentNodeId, nodesMap]) const handleReset = React.useCallback(() => { setShowColumns(false) @@ -323,16 +325,16 @@ export const ModelLineage = ({ ]) React.useEffect(() => { - const currentNodeId = selectedModelName - ? toNodeID(selectedModelName) - : undefined - - if (currentNodeId && currentNodeId in nodesMap) { + if (currentNodeId) { setSelectedNodeId(currentNodeId) - } else { - handleReset() } - }, [handleReset, selectedModelName]) + }, [currentNodeId]) + + React.useEffect(() => { + if (selectedNodeId == null || selectedColumns.size === 0) { + setSelectedNodeId(currentNode?.id || null) + } + }, [selectedNodeId, selectedColumns]) function toggleColumns() { setShowColumns(prev => !prev) diff --git a/vscode/react/src/pages/ModelNode.tsx b/vscode/react/src/pages/ModelNode.tsx index bf6f3d35da..6e147f38a4 100644 --- a/vscode/react/src/pages/ModelNode.tsx +++ b/vscode/react/src/pages/ModelNode.tsx @@ -28,7 +28,6 @@ import { type Column, NodeAppendix, NodeBadge, - type ColumnLevelLineageAdjacencyList, NodePorts, type NodeProps, } from '@tobikodata/sqlmesh-common/lineage' @@ -57,7 +56,6 @@ export const ModelNode = React.memo(function ModelNode({ selectedNodes, showColumns, fetchingColumns, - setSelectedNodeId, } = useModelLineage() const [showNodeColumns, setShowNodeColumns] = React.useState(showColumns) @@ -68,6 +66,7 @@ export const ModelNode = React.memo(function ModelNode({ const { leftId, rightId, + isCurrent, isSelected, // if selected from inside the lineage and node is selcted isActive, // if selected from inside the lineage and node is not selected but in path } = useNodeMetadata(nodeId, currentNode, selectedNodeId, selectedNodes) @@ -89,10 +88,6 @@ export const ModelNode = React.memo(function ModelNode({ setShowNodeColumns(showColumns || isSelected) }, [columnNames, isSelected, showColumns]) - function toggleSelectedNode() { - setSelectedNodeId(prev => (prev === nodeId ? null : nodeId)) - } - const shouldShowColumns = showNodeColumns || hasSelectedColumns || hasFetchingColumns || isHovered const modelType = data.model_type?.toLowerCase() as NodeType @@ -145,6 +140,11 @@ export const ModelNode = React.memo(function ModelNode({ className="bg-lineage-node-appendix-background" > + {isCurrent && ( + + current + + )} {zoom > ZOOM_THRESHOLD && ( <> {data.kind && {data.kind.toUpperCase()}} @@ -232,17 +232,7 @@ export const ModelNode = React.memo(function ModelNode({ name={column.name} description={column.description} type={column.data_type} - className="p-1 first:border-t-0 h-6" - columnLineageData={ - ( - column as Column & { - columnLineageData?: ColumnLevelLineageAdjacencyList< - ModelNameType, - ColumnName - > - } - ).columnLineageData - } + className="py-1 px-3 first:border-t-0 h-6" /> ))}
@@ -265,17 +255,7 @@ export const ModelNode = React.memo(function ModelNode({ name={column.name} description={column.description} type={column.data_type} - className="p-1 border-t border-lineage-divider first:border-t-0 h-6" - columnLineageData={ - ( - column as Column & { - columnLineageData?: ColumnLevelLineageAdjacencyList< - ModelNameType, - ColumnName - > - } - ).columnLineageData - } + className="py-1 px-3 border-t border-lineage-divider first:border-t-0 h-6" /> )} className="border-t border-lineage-divider cursor-default" diff --git a/vscode/react/src/pages/ModelNodeColumn.tsx b/vscode/react/src/pages/ModelNodeColumn.tsx index e3349238c8..cc51db3f48 100644 --- a/vscode/react/src/pages/ModelNodeColumn.tsx +++ b/vscode/react/src/pages/ModelNodeColumn.tsx @@ -11,7 +11,9 @@ import { type ModelNodeId, type ColumnName, } from './ModelLineageContext' +import { cn } from '@tobikodata/sqlmesh-common' import type { ModelName } from '@/domain/models' +import { useApiColumnLineage } from '@/api/index' const ModelColumn = FactoryColumn< ModelName, @@ -28,7 +30,6 @@ export const ModelNodeColumn = React.memo(function ModelNodeColumn({ description, type, className, - columnLineageData, }: { id: ModelColumnID nodeId: ModelNodeId @@ -37,12 +38,23 @@ export const ModelNodeColumn = React.memo(function ModelNodeColumn({ type: string description?: string | null className?: string - columnLineageData?: ColumnLevelLineageAdjacencyList }) { - const { selectedColumns, setColumnLevelLineage } = useModelLineage() + const { + selectedColumns, + setColumnLevelLineage, + setFetchingColumns, + setSelectedNodeId, + } = useModelLineage() const isSelectedColumn = selectedColumns.has(id) + const { + data: columnLineageData, + refetch: getColumnLineage, + isFetching: isColumnLineageFetching, + error: columnLineageError, + } = useApiColumnLineage(nodeId, name, { models_only: true }) + async function toggleSelectedColumn() { if (isSelectedColumn) { setColumnLevelLineage(prev => { @@ -50,8 +62,34 @@ export const ModelNodeColumn = React.memo(function ModelNodeColumn({ return new Map(prev) }) } else { - if (columnLineageData != null) { - setColumnLevelLineage(prev => new Map(prev).set(id, columnLineageData)) + setSelectedNodeId(nodeId) + + let columnLevelLineage = + columnLineageData as ColumnLevelLineageAdjacencyList< + ModelName, + ColumnName + > + + if (columnLineageData == null) { + setTimeout(() => { + setFetchingColumns(prev => new Set(prev.add(id))) + }) + + const { data } = await getColumnLineage() + + columnLevelLineage = data as ColumnLevelLineageAdjacencyList< + ModelName, + ColumnName + > + + setFetchingColumns(prev => { + prev.delete(id) + return new Set(prev) + }) + } + + if (columnLevelLineage != null) { + setColumnLevelLineage(prev => new Map(prev).set(id, columnLevelLineage)) } } } @@ -64,15 +102,21 @@ export const ModelNodeColumn = React.memo(function ModelNodeColumn({ name={name} type={type} description={description} - className={className} - data={columnLineageData} - error={null} - isFetching={false} + className={cn( + 'ModelNodeColumn', + isSelectedColumn && 'bg-lineage-model-column-active-background', + className, + )} + data={ + columnLineageData as ColumnLevelLineageAdjacencyList< + ModelName, + ColumnName + > + } + isFetching={isColumnLineageFetching} + error={columnLineageError as Error | null} onClick={toggleSelectedColumn} - onCancel={() => console.log('cancel')} renderError={error =>
Error: {error.message}
} - renderExpression={expression =>
{expression}
} - renderSource={source =>
{source}
} /> ) }) diff --git a/vscode/react/tailwind.config.cjs b/vscode/react/tailwind.config.cjs index 1a60a0928b..c81ea258ec 100644 --- a/vscode/react/tailwind.config.cjs +++ b/vscode/react/tailwind.config.cjs @@ -186,6 +186,10 @@ module.exports = { }, lineage: { node: { + current: { + background: 'var(--color-lineage-node-current-background)', + foreground: 'var(--color-lineage-node-current-foreground)', + }, type: { background: { sql: 'var(--color-lineage-node-type-background-sql)', From 21ef75cd7e89d366735ccb567b8e6f3b75b6a195 Mon Sep 17 00:00:00 2001 From: Max Mykal Date: Mon, 6 Oct 2025 17:24:31 -0700 Subject: [PATCH 09/20] fix node selection --- vscode/react/src/pages/ModelLineage.tsx | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/vscode/react/src/pages/ModelLineage.tsx b/vscode/react/src/pages/ModelLineage.tsx index d602d0007a..e002c3f973 100644 --- a/vscode/react/src/pages/ModelLineage.tsx +++ b/vscode/react/src/pages/ModelLineage.tsx @@ -279,7 +279,7 @@ export const ModelLineage = ({ setNodesMap(nodesMap) setIsBuildingLayout(false) }, - 0, + 200, ) }, []) @@ -331,10 +331,13 @@ export const ModelLineage = ({ }, [currentNodeId]) React.useEffect(() => { - if (selectedNodeId == null || selectedColumns.size === 0) { + if ( + (selectedColumns.size === 0 && currentNode?.id !== selectedNodeId) || + selectedNodeId == null + ) { setSelectedNodeId(currentNode?.id || null) } - }, [selectedNodeId, selectedColumns]) + }, [selectedColumns, currentNode?.id, selectedNodeId]) function toggleColumns() { setShowColumns(prev => !prev) From 544eba2b83d5a33c5dc4fd5a376edebc438db689 Mon Sep 17 00:00:00 2001 From: Max Mykal Date: Tue, 7 Oct 2025 13:46:16 -0700 Subject: [PATCH 10/20] some pr comments --- pnpm-lock.yaml | 280 +++++------------- vscode/bus/src/brand.ts | 23 ++ vscode/react/package.json | 10 +- vscode/react/src/pages/ModelLineage.tsx | 98 +++--- vscode/react/src/pages/ModelLineageContext.ts | 51 ++-- vscode/react/src/pages/ModelNode.tsx | 12 +- vscode/react/src/pages/ModelNodeColumn.tsx | 60 ++-- vscode/react/src/pages/help.ts | 63 ++-- vscode/react/src/pages/lineage.tsx | 89 +++--- 9 files changed, 291 insertions(+), 395 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 871b54034b..ffc9865258 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -112,17 +112,17 @@ importers: specifier: ^5.83.0 version: 5.83.0(react@18.3.1) '@tanstack/react-router': - specifier: ^1.129.8 - version: 1.129.8(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + specifier: ^1.131.26 + version: 1.132.47(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@tanstack/react-router-devtools': specifier: ^1.131.26 - version: 1.131.26(@tanstack/react-router@1.129.8(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@tanstack/router-core@1.129.8)(csstype@3.1.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(solid-js@1.9.7)(tiny-invariant@1.3.3) + version: 1.131.26(@tanstack/react-router@1.132.47(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@tanstack/router-core@1.132.47)(csstype@3.1.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(solid-js@1.9.7)(tiny-invariant@1.3.3) '@tanstack/react-virtual': specifier: ^3.13.12 version: 3.13.12(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@tanstack/router-plugin': - specifier: ^1.129.8 - version: 1.129.8(@tanstack/react-router@1.129.8(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(vite@6.3.5(@types/node@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0))(webpack@5.99.8(esbuild@0.25.8)) + specifier: ^1.131.26 + version: 1.132.47(@tanstack/react-router@1.132.47(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(vite@6.3.5(@types/node@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0))(webpack@5.99.8(esbuild@0.25.8)) '@tobikodata/sqlmesh-common': specifier: file:../../web/common version: link:../../web/common @@ -138,9 +138,6 @@ importers: elkjs: specifier: ^0.8.2 version: 0.8.2 - lodash: - specifier: 4.17.21 - version: 4.17.21 lucide-react: specifier: 0.542.0 version: 0.542.0(react@18.3.1) @@ -180,7 +177,7 @@ importers: version: 9.0.18(storybook@9.0.18(@testing-library/dom@10.4.1)(prettier@3.6.2)) '@storybook/addon-vitest': specifier: ^9.0.18 - version: 9.0.18(@vitest/browser@3.2.3)(@vitest/runner@3.2.4)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@9.0.18(@testing-library/dom@10.4.1)(prettier@3.6.2))(vitest@3.2.4) + version: 9.0.18(@vitest/browser@3.2.4)(@vitest/runner@3.2.4)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@9.0.18(@testing-library/dom@10.4.1)(prettier@3.6.2))(vitest@3.2.4) '@storybook/react-vite': specifier: ^9.0.18 version: 9.0.18(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@4.45.1)(storybook@9.0.18(@testing-library/dom@10.4.1)(prettier@3.6.2))(typescript@5.8.3)(vite@6.3.5(@types/node@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0)) @@ -190,9 +187,6 @@ importers: '@testing-library/react': specifier: ^16.3.0 version: 16.3.0(@testing-library/dom@10.4.1)(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@types/lodash': - specifier: 4.17.20 - version: 4.17.20 '@types/react': specifier: ^18.3.23 version: 18.3.23 @@ -203,11 +197,11 @@ importers: specifier: ^4.7.0 version: 4.7.0(vite@6.3.5(@types/node@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0)) '@vitest/browser': - specifier: 3.2.3 - version: 3.2.3(playwright@1.54.1)(vite@6.3.5(@types/node@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0))(vitest@3.2.4) + specifier: 3.2.4 + version: 3.2.4(playwright@1.54.1)(vite@6.3.5(@types/node@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0))(vitest@3.2.4) '@vitest/coverage-v8': - specifier: 3.2.3 - version: 3.2.3(@vitest/browser@3.2.3)(vitest@3.2.4) + specifier: 3.2.4 + version: 3.2.4(@vitest/browser@3.2.4)(vitest@3.2.4) jsdom: specifier: ^26.1.0 version: 26.1.0 @@ -225,7 +219,7 @@ importers: version: 6.3.5(@types/node@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0) vitest: specifier: ^3.2.4 - version: 3.2.4(@types/debug@4.1.12)(@types/node@24.1.0)(@vitest/browser@3.2.3)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0) + version: 3.2.4(@types/debug@4.1.12)(@types/node@24.1.0)(@vitest/browser@3.2.4)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0) web-vitals: specifier: ^4.2.4 version: 4.2.4 @@ -2251,8 +2245,8 @@ packages: peerDependencies: tailwindcss: '>=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1' - '@tanstack/history@1.129.7': - resolution: {integrity: sha512-I3YTkbe4RZQN54Qw4+IUhOjqG2DdbG2+EBWuQfew4MEk0eddLYAQVa50BZVww4/D2eh5I9vEk2Fd1Y0Wty7pug==} + '@tanstack/history@1.132.31': + resolution: {integrity: sha512-UCHM2uS0t/uSszqPEo+SBSSoQVeQ+LlOWAVBl5SA7+AedeAbKafIPjFn8huZCXNLAYb0WKV2+wETr7lDK9uz7g==} engines: {node: '>=12'} '@tanstack/query-core@5.83.0': @@ -2271,8 +2265,8 @@ packages: react: '>=18.0.0 || >=19.0.0' react-dom: '>=18.0.0 || >=19.0.0' - '@tanstack/react-router@1.129.8': - resolution: {integrity: sha512-d5mfM+67h3wq7aHkLjRKXD1ddbzx1YuxaEbNvW45jjZXMgaikZSVfJrZBiUWXE/nhV1sTdbMQ48JcPagvGPmYQ==} + '@tanstack/react-router@1.132.47': + resolution: {integrity: sha512-mjCN1ueVLHBOK1gqLeacCrUPBZietMKTkr7xZlC32dCGn4e+83zMSlRTS2TrEl7+wEH+bqjnoyx8ALYTSiQ1Cg==} engines: {node: '>=12'} peerDependencies: react: '>=18.0.0 || >=19.0.0' @@ -2297,8 +2291,8 @@ packages: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - '@tanstack/router-core@1.129.8': - resolution: {integrity: sha512-Izqf5q8TzJv0DJURynitJioPJT3dPAefrzHi2wlY/Q5+7nEG41SkjYMotTX2Q9i/Pjl91lW8gERCHpksszRdRw==} + '@tanstack/router-core@1.132.47': + resolution: {integrity: sha512-8YKFHmG6VUqXaWAJzEqjyW6w31dARS2USd2mtI5ZeZcihqMbskK28N4iotBXNn+sSKJnPRjc7A4jTnnEf8Mn8Q==} engines: {node: '>=12'} '@tanstack/router-devtools-core@1.131.26': @@ -2313,18 +2307,18 @@ packages: csstype: optional: true - '@tanstack/router-generator@1.129.8': - resolution: {integrity: sha512-i4QTtJeRq3jdRTuUXHKcmPNm6STS0jLJNTKEdeUCIzuVBiiP53oujMOd84e5ARP83k2IB2XcMHekTSzDlWD2fg==} + '@tanstack/router-generator@1.132.47': + resolution: {integrity: sha512-t3HHDWRQ4CDkm141I7pl1xQf6vehNG54m5h/2DqJGugYkP4C1x0jxqzgCbek2SuuGocS1P+NrWQeyNFmkUIgEA==} engines: {node: '>=12'} - '@tanstack/router-plugin@1.129.8': - resolution: {integrity: sha512-DdO6el2slgBO2mIqIGdGyHCzsbQLsTNxsgbNz9ZY9y324iP4G+p3iEYopHWgzLKM2DKinMs9F7AxjLow4V3klQ==} + '@tanstack/router-plugin@1.132.47': + resolution: {integrity: sha512-E/BDgWavv7t0Szp4daIzSoeNiyJaKnN1gofb/ViLbepgHFQUAxuBwqIf+o+hYDggvENcFrYnai1T03PsSyuZ3Q==} engines: {node: '>=12'} peerDependencies: '@rsbuild/core': '>=1.0.2' - '@tanstack/react-router': ^1.129.8 - vite: '>=5.0.0 || >=6.0.0' - vite-plugin-solid: ^2.11.2 + '@tanstack/react-router': ^1.132.47 + vite: '>=5.0.0 || >=6.0.0 || >=7.0.0' + vite-plugin-solid: ^2.11.8 webpack: '>=5.92.0' peerDependenciesMeta: '@rsbuild/core': @@ -2338,8 +2332,8 @@ packages: webpack: optional: true - '@tanstack/router-utils@1.129.7': - resolution: {integrity: sha512-I2OyQF5U6sxHJApXKCUmCncTHKcpj4681FwyxpYg5QYOatHcn/zVMl7Rj4h36fu8/Lo2ZRLxUMd5kmXgp5Pb/A==} + '@tanstack/router-utils@1.132.31': + resolution: {integrity: sha512-uf8mQ3wV58K8TL5XXBoWhkYxmCV7LLWbbf6AvcxdhnCnBNmXBGlY+T8RdsRnXyI2Iyp2HfHaVZ+8H3CEQedXfw==} engines: {node: '>=12'} '@tanstack/store@0.7.2': @@ -2352,8 +2346,8 @@ packages: '@tanstack/virtual-core@3.13.12': resolution: {integrity: sha512-1YBOJfRHV4sXUmWsFSf5rQor4Ss82G8dQWLRbnk3GA4jeP8hQt1hxXh0tmflpC0dz3VgEv/1+qwPyLeWkQuPFA==} - '@tanstack/virtual-file-routes@1.129.7': - resolution: {integrity: sha512-a+MxoAXG+Sq94Jp67OtveKOp2vQq75AWdVI8DRt6w19B0NEqpfm784FTLbVp/qdR1wmxCOmKAvElGSIiBOx5OQ==} + '@tanstack/virtual-file-routes@1.132.31': + resolution: {integrity: sha512-rxS8Cm2nIXroLqkm9pE/8X2lFNuvcTIIiFi5VH4PwzvKscAuaW3YRMN1WmaGDI2mVEn+GLaoY6Kc3jOczL5i4w==} engines: {node: '>=12'} '@testing-library/dom@10.4.1': @@ -2730,21 +2724,6 @@ packages: peerDependencies: vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 - '@vitest/browser@3.2.3': - resolution: {integrity: sha512-5HpUb0ixGF8JWSAjb/P1x/VPuTYUkL4pL0+YO6DJiuvQgqJN3PREaUEcXwfXjU4nBc37EahfpRbAwdE9pHs9lQ==} - peerDependencies: - playwright: '*' - safaridriver: '*' - vitest: 3.2.3 - webdriverio: ^7.0.0 || ^8.0.0 || ^9.0.0 - peerDependenciesMeta: - playwright: - optional: true - safaridriver: - optional: true - webdriverio: - optional: true - '@vitest/browser@3.2.4': resolution: {integrity: sha512-tJxiPrWmzH8a+w9nLKlQMzAKX/7VjFs50MWgcAj7p9XQ7AQ9/35fByFYptgPELyLw+0aixTnC4pUWV+APcZ/kw==} peerDependencies: @@ -2760,11 +2739,11 @@ packages: webdriverio: optional: true - '@vitest/coverage-v8@3.2.3': - resolution: {integrity: sha512-D1QKzngg8PcDoCE8FHSZhREDuEy+zcKmMiMafYse41RZpBE5EDJyKOTdqK3RQfsV2S2nyKor5KCs8PyPRFqKPg==} + '@vitest/coverage-v8@3.2.4': + resolution: {integrity: sha512-EyF9SXU6kS5Ku/U82E259WSnvg6c8KTjppUncuNdm5QHpe17mwREHnjDzozC8x9MZ0xfBUFSaLkRv4TMA75ALQ==} peerDependencies: - '@vitest/browser': 3.2.3 - vitest: 3.2.3 + '@vitest/browser': 3.2.4 + vitest: 3.2.4 peerDependenciesMeta: '@vitest/browser': optional: true @@ -2772,17 +2751,6 @@ packages: '@vitest/expect@3.2.4': resolution: {integrity: sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==} - '@vitest/mocker@3.2.3': - resolution: {integrity: sha512-cP6fIun+Zx8he4rbWvi+Oya6goKQDZK+Yq4hhlggwQBbrlOQ4qtZ+G4nxB6ZnzI9lyIb+JnvyiJnPC2AGbKSPA==} - peerDependencies: - msw: ^2.4.9 - vite: ^5.0.0 || ^6.0.0 || ^7.0.0-0 - peerDependenciesMeta: - msw: - optional: true - vite: - optional: true - '@vitest/mocker@3.2.4': resolution: {integrity: sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==} peerDependencies: @@ -2794,9 +2762,6 @@ packages: vite: optional: true - '@vitest/pretty-format@3.2.3': - resolution: {integrity: sha512-yFglXGkr9hW/yEXngO+IKMhP0jxyFw2/qys/CK4fFUZnSltD+MU7dVYGrH8rvPcK/O6feXQA+EU33gjaBBbAng==} - '@vitest/pretty-format@3.2.4': resolution: {integrity: sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==} @@ -2806,9 +2771,6 @@ packages: '@vitest/snapshot@3.2.4': resolution: {integrity: sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==} - '@vitest/spy@3.2.3': - resolution: {integrity: sha512-JHu9Wl+7bf6FEejTCREy+DmgWe+rQKbK+y32C/k5f4TBIAlijhJbRBIRIOCEpVevgRsCQR2iHRUH2/qKVM/plw==} - '@vitest/spy@3.2.4': resolution: {integrity: sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==} @@ -2817,9 +2779,6 @@ packages: peerDependencies: vitest: 3.2.4 - '@vitest/utils@3.2.3': - resolution: {integrity: sha512-4zFBCU5Pf+4Z6v+rwnZ1HU1yzOKKvDkMXZrymE2PBlbjKJRlrOxbvpfPSvJTGRIwGoahaOGvp+kbCoxifhzJ1Q==} - '@vitest/utils@3.2.4': resolution: {integrity: sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==} @@ -3444,8 +3403,8 @@ packages: convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} - cookie-es@1.2.2: - resolution: {integrity: sha512-+W7VmiVINB+ywl1HGXJXmrqkOhpKrIiVZV6tQuV54ZyQC7MMuBt81Vc336GMLoHBq5hV/F9eXgt5Mnx0Rha5Fg==} + cookie-es@2.0.0: + resolution: {integrity: sha512-RAj4E421UYRgqokKUmotqAwuplYw15qtdXfY+hGzgCJ/MBjCVZcSoHK/kH9kocfjRjcDME7IiDWR/1WX1TM2Pg==} cookie@1.0.2: resolution: {integrity: sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==} @@ -5715,12 +5674,6 @@ packages: serialize-javascript@6.0.2: resolution: {integrity: sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==} - seroval-plugins@1.3.2: - resolution: {integrity: sha512-0QvCV2lM3aj/U3YozDiVwx9zpH0q8A60CTWIv4Jszj/givcudPb48B+rkU5D51NJ0pTpweGMttHjboPa9/zoIQ==} - engines: {node: '>=10'} - peerDependencies: - seroval: ^1.0 - seroval-plugins@1.3.3: resolution: {integrity: sha512-16OL3NnUBw8JG1jBLUoZJsLnQq0n5Ua6aHalhJK4fMQkz1lqR7Osz1sA30trBtd9VUDc2NgkuRCn8+/pBwqZ+w==} engines: {node: '>=10'} @@ -6794,8 +6747,8 @@ snapshots: '@ampproject/remapping@2.3.0': dependencies: - '@jridgewell/gen-mapping': 0.3.12 - '@jridgewell/trace-mapping': 0.3.29 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 '@apidevtools/json-schema-ref-parser@11.7.2': dependencies: @@ -8637,7 +8590,7 @@ snapshots: dependencies: storybook: 9.0.18(@testing-library/dom@10.4.1)(prettier@3.6.2) - '@storybook/addon-vitest@9.0.18(@vitest/browser@3.2.3)(@vitest/runner@3.2.4)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@9.0.18(@testing-library/dom@10.4.1)(prettier@3.6.2))(vitest@3.2.4)': + '@storybook/addon-vitest@9.0.18(@vitest/browser@3.2.4)(@vitest/runner@3.2.4)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@9.0.18(@testing-library/dom@10.4.1)(prettier@3.6.2))(vitest@3.2.4)': dependencies: '@storybook/global': 5.0.0 '@storybook/icons': 1.4.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -8645,9 +8598,9 @@ snapshots: storybook: 9.0.18(@testing-library/dom@10.4.1)(prettier@3.6.2) ts-dedent: 2.2.0 optionalDependencies: - '@vitest/browser': 3.2.3(playwright@1.54.1)(vite@6.3.5(@types/node@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0))(vitest@3.2.4) + '@vitest/browser': 3.2.4(playwright@1.54.1)(vite@6.3.5(@types/node@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0))(vitest@3.2.4) '@vitest/runner': 3.2.4 - vitest: 3.2.4(@types/debug@4.1.12)(@types/node@24.1.0)(@vitest/browser@3.2.3)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0) + vitest: 3.2.4(@types/debug@4.1.12)(@types/node@24.1.0)(@vitest/browser@3.2.4)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0) transitivePeerDependencies: - react - react-dom @@ -8824,7 +8777,7 @@ snapshots: postcss-selector-parser: 6.0.10 tailwindcss: 3.4.17 - '@tanstack/history@1.129.7': {} + '@tanstack/history@1.132.31': {} '@tanstack/query-core@5.83.0': {} @@ -8833,10 +8786,10 @@ snapshots: '@tanstack/query-core': 5.83.0 react: 18.3.1 - '@tanstack/react-router-devtools@1.131.26(@tanstack/react-router@1.129.8(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@tanstack/router-core@1.129.8)(csstype@3.1.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(solid-js@1.9.7)(tiny-invariant@1.3.3)': + '@tanstack/react-router-devtools@1.131.26(@tanstack/react-router@1.132.47(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@tanstack/router-core@1.132.47)(csstype@3.1.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(solid-js@1.9.7)(tiny-invariant@1.3.3)': dependencies: - '@tanstack/react-router': 1.129.8(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@tanstack/router-devtools-core': 1.131.26(@tanstack/router-core@1.129.8)(csstype@3.1.3)(solid-js@1.9.7)(tiny-invariant@1.3.3) + '@tanstack/react-router': 1.132.47(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@tanstack/router-devtools-core': 1.131.26(@tanstack/router-core@1.132.47)(csstype@3.1.3)(solid-js@1.9.7)(tiny-invariant@1.3.3) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) transitivePeerDependencies: @@ -8845,11 +8798,11 @@ snapshots: - solid-js - tiny-invariant - '@tanstack/react-router@1.129.8(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@tanstack/react-router@1.132.47(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@tanstack/history': 1.129.7 + '@tanstack/history': 1.132.31 '@tanstack/react-store': 0.7.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@tanstack/router-core': 1.129.8 + '@tanstack/router-core': 1.132.47 isbot: 5.1.28 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) @@ -8875,19 +8828,19 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - '@tanstack/router-core@1.129.8': + '@tanstack/router-core@1.132.47': dependencies: - '@tanstack/history': 1.129.7 + '@tanstack/history': 1.132.31 '@tanstack/store': 0.7.2 - cookie-es: 1.2.2 + cookie-es: 2.0.0 seroval: 1.3.2 - seroval-plugins: 1.3.2(seroval@1.3.2) + seroval-plugins: 1.3.3(seroval@1.3.2) tiny-invariant: 1.3.3 tiny-warning: 1.0.3 - '@tanstack/router-devtools-core@1.131.26(@tanstack/router-core@1.129.8)(csstype@3.1.3)(solid-js@1.9.7)(tiny-invariant@1.3.3)': + '@tanstack/router-devtools-core@1.131.26(@tanstack/router-core@1.132.47)(csstype@3.1.3)(solid-js@1.9.7)(tiny-invariant@1.3.3)': dependencies: - '@tanstack/router-core': 1.129.8 + '@tanstack/router-core': 1.132.47 clsx: 2.1.1 goober: 2.1.16(csstype@3.1.3) solid-js: 1.9.7 @@ -8895,11 +8848,11 @@ snapshots: optionalDependencies: csstype: 3.1.3 - '@tanstack/router-generator@1.129.8': + '@tanstack/router-generator@1.132.47': dependencies: - '@tanstack/router-core': 1.129.8 - '@tanstack/router-utils': 1.129.7 - '@tanstack/virtual-file-routes': 1.129.7 + '@tanstack/router-core': 1.132.47 + '@tanstack/router-utils': 1.132.31 + '@tanstack/virtual-file-routes': 1.132.31 prettier: 3.6.2 recast: 0.23.11 source-map: 0.7.4 @@ -8908,7 +8861,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@tanstack/router-plugin@1.129.8(@tanstack/react-router@1.129.8(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(vite@6.3.5(@types/node@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0))(webpack@5.99.8(esbuild@0.25.8))': + '@tanstack/router-plugin@1.132.47(@tanstack/react-router@1.132.47(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(vite@6.3.5(@types/node@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0))(webpack@5.99.8(esbuild@0.25.8))': dependencies: '@babel/core': 7.28.0 '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.28.0) @@ -8916,22 +8869,22 @@ snapshots: '@babel/template': 7.27.2 '@babel/traverse': 7.28.0 '@babel/types': 7.28.1 - '@tanstack/router-core': 1.129.8 - '@tanstack/router-generator': 1.129.8 - '@tanstack/router-utils': 1.129.7 - '@tanstack/virtual-file-routes': 1.129.7 + '@tanstack/router-core': 1.132.47 + '@tanstack/router-generator': 1.132.47 + '@tanstack/router-utils': 1.132.31 + '@tanstack/virtual-file-routes': 1.132.31 babel-dead-code-elimination: 1.0.10 chokidar: 3.6.0 unplugin: 2.3.5 zod: 3.25.76 optionalDependencies: - '@tanstack/react-router': 1.129.8(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@tanstack/react-router': 1.132.47(react-dom@18.3.1(react@18.3.1))(react@18.3.1) vite: 6.3.5(@types/node@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0) webpack: 5.99.8(esbuild@0.25.8) transitivePeerDependencies: - supports-color - '@tanstack/router-utils@1.129.7': + '@tanstack/router-utils@1.132.31': dependencies: '@babel/core': 7.28.0 '@babel/generator': 7.28.0 @@ -8939,6 +8892,8 @@ snapshots: '@babel/preset-typescript': 7.27.1(@babel/core@7.28.0) ansis: 4.1.0 diff: 8.0.2 + fast-glob: 3.3.3 + pathe: 2.0.3 transitivePeerDependencies: - supports-color @@ -8948,7 +8903,7 @@ snapshots: '@tanstack/virtual-core@3.13.12': {} - '@tanstack/virtual-file-routes@1.129.7': {} + '@tanstack/virtual-file-routes@1.132.31': {} '@testing-library/dom@10.4.1': dependencies: @@ -9432,25 +9387,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@vitest/browser@3.2.3(playwright@1.54.1)(vite@6.3.5(@types/node@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0))(vitest@3.2.4)': - dependencies: - '@testing-library/dom': 10.4.1 - '@testing-library/user-event': 14.6.1(@testing-library/dom@10.4.1) - '@vitest/mocker': 3.2.3(vite@6.3.5(@types/node@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0)) - '@vitest/utils': 3.2.3 - magic-string: 0.30.17 - sirv: 3.0.1 - tinyrainbow: 2.0.0 - vitest: 3.2.4(@types/debug@4.1.12)(@types/node@24.1.0)(@vitest/browser@3.2.3)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0) - ws: 8.18.3 - optionalDependencies: - playwright: 1.54.1 - transitivePeerDependencies: - - bufferutil - - msw - - utf-8-validate - - vite - '@vitest/browser@3.2.4(playwright@1.54.1)(vite@6.3.5(@types/node@20.11.25)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0))(vitest@3.2.4)': dependencies: '@testing-library/dom': 10.4.1 @@ -9488,9 +9424,8 @@ snapshots: - msw - utf-8-validate - vite - optional: true - '@vitest/coverage-v8@3.2.3(@vitest/browser@3.2.3)(vitest@3.2.4)': + '@vitest/coverage-v8@3.2.4(@vitest/browser@3.2.4)(vitest@3.2.4)': dependencies: '@ampproject/remapping': 2.3.0 '@bcoe/v8-coverage': 1.0.2 @@ -9505,9 +9440,9 @@ snapshots: std-env: 3.9.0 test-exclude: 7.0.1 tinyrainbow: 2.0.0 - vitest: 3.2.4(@types/debug@4.1.12)(@types/node@24.1.0)(@vitest/browser@3.2.3)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0) + vitest: 3.2.4(@types/debug@4.1.12)(@types/node@24.1.0)(@vitest/browser@3.2.4)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0) optionalDependencies: - '@vitest/browser': 3.2.3(playwright@1.54.1)(vite@6.3.5(@types/node@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0))(vitest@3.2.4) + '@vitest/browser': 3.2.4(playwright@1.54.1)(vite@6.3.5(@types/node@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0))(vitest@3.2.4) transitivePeerDependencies: - supports-color @@ -9519,14 +9454,6 @@ snapshots: chai: 5.2.1 tinyrainbow: 2.0.0 - '@vitest/mocker@3.2.3(vite@6.3.5(@types/node@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0))': - dependencies: - '@vitest/spy': 3.2.3 - estree-walker: 3.0.3 - magic-string: 0.30.17 - optionalDependencies: - vite: 6.3.5(@types/node@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0) - '@vitest/mocker@3.2.4(vite@6.3.5(@types/node@20.11.25)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0))': dependencies: '@vitest/spy': 3.2.4 @@ -9543,10 +9470,6 @@ snapshots: optionalDependencies: vite: 6.3.5(@types/node@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0) - '@vitest/pretty-format@3.2.3': - dependencies: - tinyrainbow: 2.0.0 - '@vitest/pretty-format@3.2.4': dependencies: tinyrainbow: 2.0.0 @@ -9563,10 +9486,6 @@ snapshots: magic-string: 0.30.17 pathe: 2.0.3 - '@vitest/spy@3.2.3': - dependencies: - tinyspy: 4.0.3 - '@vitest/spy@3.2.4': dependencies: tinyspy: 4.0.3 @@ -9582,12 +9501,6 @@ snapshots: tinyrainbow: 2.0.0 vitest: 3.2.4(@types/debug@4.1.12)(@types/node@20.11.25)(@vitest/browser@3.2.4)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0) - '@vitest/utils@3.2.3': - dependencies: - '@vitest/pretty-format': 3.2.3 - loupe: 3.1.4 - tinyrainbow: 2.0.0 - '@vitest/utils@3.2.4': dependencies: '@vitest/pretty-format': 3.2.4 @@ -9994,7 +9907,7 @@ snapshots: ast-v8-to-istanbul@0.3.3: dependencies: - '@jridgewell/trace-mapping': 0.3.29 + '@jridgewell/trace-mapping': 0.3.31 estree-walker: 3.0.3 js-tokens: 9.0.1 @@ -10316,7 +10229,7 @@ snapshots: convert-source-map@2.0.0: {} - cookie-es@1.2.2: {} + cookie-es@2.0.0: {} cookie@1.0.2: {} @@ -11432,7 +11345,7 @@ snapshots: istanbul-lib-source-maps@5.0.6: dependencies: - '@jridgewell/trace-mapping': 0.3.29 + '@jridgewell/trace-mapping': 0.3.31 debug: 4.4.1(supports-color@8.1.1) istanbul-lib-coverage: 3.2.2 transitivePeerDependencies: @@ -12927,10 +12840,6 @@ snapshots: dependencies: randombytes: 2.1.0 - seroval-plugins@1.3.2(seroval@1.3.2): - dependencies: - seroval: 1.3.2 - seroval-plugins@1.3.3(seroval@1.3.2): dependencies: seroval: 1.3.2 @@ -13912,51 +13821,6 @@ snapshots: - tsx - yaml - vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.1.0)(@vitest/browser@3.2.3)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0): - dependencies: - '@types/chai': 5.2.2 - '@vitest/expect': 3.2.4 - '@vitest/mocker': 3.2.4(vite@6.3.5(@types/node@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0)) - '@vitest/pretty-format': 3.2.4 - '@vitest/runner': 3.2.4 - '@vitest/snapshot': 3.2.4 - '@vitest/spy': 3.2.4 - '@vitest/utils': 3.2.4 - chai: 5.2.1 - debug: 4.4.1(supports-color@8.1.1) - expect-type: 1.2.2 - magic-string: 0.30.17 - pathe: 2.0.3 - picomatch: 4.0.3 - std-env: 3.9.0 - tinybench: 2.9.0 - tinyexec: 0.3.2 - tinyglobby: 0.2.14 - tinypool: 1.1.1 - tinyrainbow: 2.0.0 - vite: 6.3.5(@types/node@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0) - vite-node: 3.2.4(@types/node@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0) - why-is-node-running: 2.3.0 - optionalDependencies: - '@types/debug': 4.1.12 - '@types/node': 24.1.0 - '@vitest/browser': 3.2.3(playwright@1.54.1)(vite@6.3.5(@types/node@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0))(vitest@3.2.4) - '@vitest/ui': 3.2.4(vitest@3.2.4) - jsdom: 26.1.0 - transitivePeerDependencies: - - jiti - - less - - lightningcss - - msw - - sass - - sass-embedded - - stylus - - sugarss - - supports-color - - terser - - tsx - - yaml - vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.1.0)(@vitest/browser@3.2.4)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0): dependencies: '@types/chai': 5.2.2 diff --git a/vscode/bus/src/brand.ts b/vscode/bus/src/brand.ts index 2b9c3ca37a..52baae9e90 100644 --- a/vscode/bus/src/brand.ts +++ b/vscode/bus/src/brand.ts @@ -17,3 +17,26 @@ type Brand = { [__brand]: B } * userId == userName -> compile error */ export type Branded = T & Brand + +/** + * Constraint that only accepts branded string types + */ +export type BrandedString = string & Brand + +/** + * BrandedRecord is a type that creates a branded Record type with strict key checking. + * This ensures that Record is NOT assignable to Record + * + * @example + * type ModelFQN = Branded + * type ModelName = Branded + * + * type FQNMap = BrandedRecord + * type NameMap = BrandedRecord + * + * const fqnMap: FQNMap = {} + * const nameMap: NameMap = fqnMap // TypeScript error! + */ +export type BrandedRecord = Record & { + readonly __recordKeyBrand: K +} diff --git a/vscode/react/package.json b/vscode/react/package.json index e5f41baa9a..041aa66467 100644 --- a/vscode/react/package.json +++ b/vscode/react/package.json @@ -19,16 +19,15 @@ "@heroicons/react": "^2.2.0", "@radix-ui/react-select": "^2.2.5", "@tanstack/react-query": "^5.83.0", - "@tanstack/react-router": "^1.129.8", + "@tanstack/react-router": "^1.131.26", "@tanstack/react-router-devtools": "^1.131.26", "@tanstack/react-virtual": "^3.13.12", - "@tanstack/router-plugin": "^1.129.8", + "@tanstack/router-plugin": "^1.131.26", "@tobikodata/sqlmesh-common": "file:../../web/common", "apache-arrow": "^19.0.1", "clsx": "^2.1.1", "cronstrue": "3.3.0", "elkjs": "^0.8.2", - "lodash": "4.17.21", "lucide-react": "0.542.0", "orval": "^7.10.0", "react": "^18.3.1", @@ -47,12 +46,11 @@ "@storybook/react-vite": "^9.0.18", "@testing-library/dom": "^10.4.1", "@testing-library/react": "^16.3.0", - "@types/lodash": "4.17.20", "@types/react": "^18.3.23", "@types/react-dom": "^18.3.7", "@vitejs/plugin-react": "^4.7.0", - "@vitest/browser": "3.2.3", - "@vitest/coverage-v8": "3.2.3", + "@vitest/browser": "3.2.4", + "@vitest/coverage-v8": "3.2.4", "jsdom": "^26.1.0", "playwright": "^1.54.1", "storybook": "^9.0.18", diff --git a/vscode/react/src/pages/ModelLineage.tsx b/vscode/react/src/pages/ModelLineage.tsx index e002c3f973..5501e79fcb 100644 --- a/vscode/react/src/pages/ModelLineage.tsx +++ b/vscode/react/src/pages/ModelLineage.tsx @@ -1,12 +1,9 @@ import React from 'react' -import { debounce } from 'lodash' import { Focus, Rows2, Rows3 } from 'lucide-react' import { FactoryEdgeWithGradient, EdgeWithGradient, - type LineageAdjacencyList, - type LineageDetails, ZOOM_THRESHOLD, type LineageEdge, type LineageNodesMap, @@ -34,6 +31,8 @@ import { } from '@tobikodata/sqlmesh-common/lineage' import { + type BrandedLineageAdjacencyList, + type BrandedLineageDetails, type ColumnName, type EdgeData, type ModelColumnID, @@ -41,13 +40,12 @@ import { type ModelLineageNodeDetails, type ModelNodeId, type NodeData, - type NodeType, ModelLineageContext, } from './ModelLineageContext' import { ModelNode } from './ModelNode' import { useModelLineage } from './ModelLineageContext' -import type { ModelName } from '@/domain/models' -import { getNodeTypeColorVar } from './help' +import type { ModelFQN } from '@/domain/models' +import { NODE_TYPE_COLOR_VAR } from './help' const nodeTypes = { node: ModelNode, @@ -64,9 +62,9 @@ export const ModelLineage = ({ className, onNodeClick, }: { - adjacencyList: LineageAdjacencyList - lineageDetails: LineageDetails - selectedModelName?: ModelName + adjacencyList: BrandedLineageAdjacencyList + lineageDetails: BrandedLineageDetails + selectedModelName?: ModelFQN className?: string onNodeClick?: ( event: React.MouseEvent, @@ -98,7 +96,7 @@ export const ModelLineage = ({ const [showColumns, setShowColumns] = React.useState(false) const [columnLevelLineage, setColumnLevelLineage] = React.useState< - Map> + Map> >(new Map()) const [fetchingColumns, setFetchingColumns] = React.useState< Set @@ -108,17 +106,17 @@ export const ModelLineage = ({ adjacencyListColumnLevel, selectedColumns, adjacencyListKeysColumnLevel, - } = useColumnLevelLineage( + } = useColumnLevelLineage( columnLevelLineage, ) const adjacencyListKeys = React.useMemo(() => { - let keys: ModelName[] = [] + let keys: ModelFQN[] = [] if (adjacencyListKeysColumnLevel.length > 0) { keys = adjacencyListKeysColumnLevel } else { - keys = Object.keys(adjacencyList) as ModelName[] + keys = Object.keys(adjacencyList) as ModelFQN[] } return keys @@ -131,7 +129,7 @@ export const ModelLineage = ({ const node = createNode('node', nodeId, { name: detail.name, displayName: detail.display_name, - model_type: detail.model_type as NodeType, + model_type: detail.model_type, identifier: detail.identifier, kind: detail.kind, cron: detail.cron, @@ -177,7 +175,7 @@ export const ModelLineage = ({ const transformedNodesMap = React.useMemo(() => { return getTransformedNodes< - ModelName, + ModelFQN, ModelLineageNodeDetails, NodeData, ModelNodeId @@ -201,9 +199,7 @@ export const ModelLineage = ({ data.startColor = 'var(--color-lineage-node-port-edge-source)' } else { if (sourceNode?.data?.model_type) { - data.startColor = getNodeTypeColorVar( - sourceNode.data.model_type as NodeType, - ) + data.startColor = NODE_TYPE_COLOR_VAR[sourceNode.data.model_type] } } @@ -211,9 +207,7 @@ export const ModelLineage = ({ data.endColor = 'var(--color-lineage-node-port-edge-target)' } else { if (targetNode?.data?.model_type) { - data.endColor = getNodeTypeColorVar( - targetNode.data.model_type as NodeType, - ) + data.endColor = NODE_TYPE_COLOR_VAR[targetNode.data.model_type] } } @@ -237,7 +231,7 @@ export const ModelLineage = ({ const edgesColumnLevel = React.useMemo( () => getEdgesFromColumnLineage< - ModelName, + ModelFQN, ColumnName, EdgeData, ModelEdgeId, @@ -254,7 +248,7 @@ export const ModelLineage = ({ return edgesColumnLevel.length > 0 ? edgesColumnLevel : getTransformedModelEdgesTargetSources< - ModelName, + ModelFQN, EdgeData, ModelNodeId, ModelEdgeId, @@ -262,26 +256,24 @@ export const ModelLineage = ({ >(adjacencyListKeys, adjacencyList, transformEdge) }, [adjacencyListKeys, adjacencyList, transformEdge, edgesColumnLevel]) - const calculateLayout = React.useMemo(() => { - return debounce( - ( - eds: LineageEdge[], - nds: LineageNodesMap, - ) => { - const { edges, nodesMap } = buildLayout< - NodeData, - EdgeData, - ModelNodeId, - ModelEdgeId, - ModelColumnID - >({ edges: eds, nodesMap: nds }) - setEdges(edges) - setNodesMap(nodesMap) - setIsBuildingLayout(false) - }, - 200, - ) - }, []) + const calculateLayout = React.useCallback( + ( + eds: LineageEdge[], + nds: LineageNodesMap, + ) => { + const { edges, nodesMap } = buildLayout< + NodeData, + EdgeData, + ModelNodeId, + ModelEdgeId, + ModelColumnID + >({ edges: eds, nodesMap: nds }) + setEdges(edges) + setNodesMap(nodesMap) + setIsBuildingLayout(false) + }, + [edges, nodesMap, setEdges, setNodesMap, setIsBuildingLayout], + ) const nodes = React.useMemo(() => { return Object.values(nodesMap) @@ -317,27 +309,25 @@ export const ModelLineage = ({ } else { calculateLayout(transformedEdges, transformedNodesMap) } - }, [ - calculateLayout, - showOnlySelectedNodes, - transformedEdges, - transformedNodesMap, - ]) + }, [showOnlySelectedNodes, transformedEdges, transformedNodesMap]) + // currentNodeId is passed from the parent component + // we it change we need to reset the selectedNodeId React.useEffect(() => { if (currentNodeId) { setSelectedNodeId(currentNodeId) } }, [currentNodeId]) + // When the selectedColumns is empty it measn we dont have any selected columns + // so we need to set the selectedNodeId back to the currentNode.id + // where currentNode derived from currentNodeId if present in nodesMap + // if the currentNode is null we need to set the selectedNodeId to null React.useEffect(() => { - if ( - (selectedColumns.size === 0 && currentNode?.id !== selectedNodeId) || - selectedNodeId == null - ) { + if (selectedColumns.size === 0 && selectedNodeId != currentNode?.id) { setSelectedNodeId(currentNode?.id || null) } - }, [selectedColumns, currentNode?.id, selectedNodeId]) + }, [selectedColumns, currentNode?.id]) function toggleColumns() { setShowColumns(prev => !prev) diff --git a/vscode/react/src/pages/ModelLineageContext.ts b/vscode/react/src/pages/ModelLineageContext.ts index 4140d43740..a6940d0586 100644 --- a/vscode/react/src/pages/ModelLineageContext.ts +++ b/vscode/react/src/pages/ModelLineageContext.ts @@ -1,16 +1,39 @@ -import type { ModelName } from '@/domain/models' -import type { Branded } from '@tobikodata/sqlmesh-common' +import type { ModelType } from '@/api/client' +import type { ModelFQN, ModelName } from '@/domain/models' +import type { Branded, BrandedString } from '@bus/brand' import { type Column, - type ColumnLevelLineageAdjacencyList, type ColumnLevelLineageContextValue, type LineageContextValue, type PathType, getInitial as getLineageContextInitial, getColumnLevelLineageContextInitial, createLineageContext, + type LineageAdjacencyList, + type LineageDetails, + type ColumnLevelLineageAdjacencyList, } from '@tobikodata/sqlmesh-common/lineage' +export type BrandedLineageAdjacencyList = + LineageAdjacencyList & { + readonly __adjacencyListKeyBrand: K + } + +export type BrandedLineageDetails = LineageDetails< + K, + V +> & { + readonly __lineageDetailsKeyBrand: K +} + +export type BrandedColumnLevelLineageAdjacencyList< + K extends BrandedString, + V extends BrandedString, +> = ColumnLevelLineageAdjacencyList & { + readonly __columnLevelLineageAdjacencyListKeyBrand: K + readonly __columnLevelLineageAdjacencyListColumnKeyBrand: V +} + export type ColumnName = Branded export type ModelColumnID = Branded export type ModelNodeId = Branded @@ -18,14 +41,12 @@ export type ModelEdgeId = Branded export type ModelColumn = Column & { id: ModelColumnID name: ColumnName - columnLineageData?: ColumnLevelLineageAdjacencyList } -export type NodeType = 'sql' | 'python' export type ModelLineageNodeDetails = { - name: ModelName - display_name: string - model_type: string + name: ModelFQN + display_name: ModelName + model_type: ModelType identifier?: string | null version?: string | null dialect?: string | null @@ -37,9 +58,9 @@ export type ModelLineageNodeDetails = { } export type NodeData = { - name: ModelName - displayName: string - model_type: NodeType + name: ModelFQN + displayName: ModelName + model_type: ModelType identifier?: string | null version?: string | null kind?: string | null @@ -58,7 +79,7 @@ export type EdgeData = { } export type ModelLineageContextValue = ColumnLevelLineageContextValue< - ModelName, + ModelFQN, ColumnName, ModelColumnID > & @@ -72,11 +93,7 @@ export type ModelLineageContextValue = ColumnLevelLineageContextValue< export const initial = { ...getLineageContextInitial(), - ...getColumnLevelLineageContextInitial< - ModelName, - ColumnName, - ModelColumnID - >(), + ...getColumnLevelLineageContextInitial(), } export const { Provider, useLineage } = createLineageContext< diff --git a/vscode/react/src/pages/ModelNode.tsx b/vscode/react/src/pages/ModelNode.tsx index 6e147f38a4..309009caeb 100644 --- a/vscode/react/src/pages/ModelNode.tsx +++ b/vscode/react/src/pages/ModelNode.tsx @@ -8,7 +8,6 @@ import { type ModelColumn, type ModelColumnID, type ColumnName, - type NodeType, } from './ModelLineageContext' import { calculateColumnsHeight, @@ -31,7 +30,6 @@ import { NodePorts, type NodeProps, } from '@tobikodata/sqlmesh-common/lineage' -import { getNodeTypeColor } from './help' import { Badge, cn, @@ -41,7 +39,9 @@ import { VerticalContainer, } from '@tobikodata/sqlmesh-common' import { ModelNodeColumn } from './ModelNodeColumn' -import type { ModelName as ModelNameType } from '@/domain/models' +import type { ModelFQN } from '@/domain/models' +import { NODE_TYPE_COLOR } from './help' +import type { ModelType } from '@/api/client' export const ModelNode = React.memo(function ModelNode({ id, @@ -75,7 +75,7 @@ export const ModelNode = React.memo(function ModelNode({ columns, selectedColumns: modelSelectedColumns, columnNames, - } = useColumns( + } = useColumns( selectedColumns, data.name, data.columns, @@ -90,7 +90,7 @@ export const ModelNode = React.memo(function ModelNode({ const shouldShowColumns = showNodeColumns || hasSelectedColumns || hasFetchingColumns || isHovered - const modelType = data.model_type?.toLowerCase() as NodeType + const modelType = data.model_type?.toLowerCase() as ModelType const hasColumnsFilter = shouldShowColumns && columns.length > MAX_COLUMNS_TO_DISPLAY // We are not including the footer, because we need actual height to dynamically adjust node container height @@ -280,7 +280,7 @@ export const ModelNode = React.memo(function ModelNode({ size={zoom > ZOOM_THRESHOLD ? '2xs' : 'm'} className={cn( 'text-[white] font-black', - getNodeTypeColor(modelType), + NODE_TYPE_COLOR[modelType], )} > {modelType.toUpperCase()} diff --git a/vscode/react/src/pages/ModelNodeColumn.tsx b/vscode/react/src/pages/ModelNodeColumn.tsx index cc51db3f48..ec013a7e9a 100644 --- a/vscode/react/src/pages/ModelNodeColumn.tsx +++ b/vscode/react/src/pages/ModelNodeColumn.tsx @@ -1,22 +1,20 @@ import React from 'react' -import { - FactoryColumn, - type ColumnLevelLineageAdjacencyList, -} from '@tobikodata/sqlmesh-common/lineage' +import { FactoryColumn } from '@tobikodata/sqlmesh-common/lineage' import { useModelLineage, type ModelColumnID, type ModelNodeId, type ColumnName, + type BrandedColumnLevelLineageAdjacencyList, } from './ModelLineageContext' import { cn } from '@tobikodata/sqlmesh-common' -import type { ModelName } from '@/domain/models' import { useApiColumnLineage } from '@/api/index' +import type { ModelFQN } from '@/domain/models' const ModelColumn = FactoryColumn< - ModelName, + ModelFQN, ColumnName, ModelNodeId, ModelColumnID @@ -33,7 +31,7 @@ export const ModelNodeColumn = React.memo(function ModelNodeColumn({ }: { id: ModelColumnID nodeId: ModelNodeId - modelName: ModelName + modelName: ModelFQN name: ColumnName type: string description?: string | null @@ -49,13 +47,16 @@ export const ModelNodeColumn = React.memo(function ModelNodeColumn({ const isSelectedColumn = selectedColumns.has(id) const { - data: columnLineageData, refetch: getColumnLineage, isFetching: isColumnLineageFetching, error: columnLineageError, } = useApiColumnLineage(nodeId, name, { models_only: true }) - async function toggleSelectedColumn() { + const [columnLineageData, setColumnLineageData] = React.useState< + BrandedColumnLevelLineageAdjacencyList | undefined + >(undefined) + + const toggleSelectedColumn = React.useCallback(async () => { if (isSelectedColumn) { setColumnLevelLineage(prev => { prev.delete(id) @@ -64,35 +65,37 @@ export const ModelNodeColumn = React.memo(function ModelNodeColumn({ } else { setSelectedNodeId(nodeId) - let columnLevelLineage = - columnLineageData as ColumnLevelLineageAdjacencyList< - ModelName, - ColumnName - > - if (columnLineageData == null) { setTimeout(() => { setFetchingColumns(prev => new Set(prev.add(id))) }) - const { data } = await getColumnLineage() + const { data } = (await getColumnLineage()) as { + data: + | BrandedColumnLevelLineageAdjacencyList + | undefined + } - columnLevelLineage = data as ColumnLevelLineageAdjacencyList< - ModelName, - ColumnName - > + setColumnLineageData(data) setFetchingColumns(prev => { prev.delete(id) return new Set(prev) }) - } - if (columnLevelLineage != null) { - setColumnLevelLineage(prev => new Map(prev).set(id, columnLevelLineage)) + if (data != null) { + setColumnLevelLineage(prev => new Map(prev).set(id, data)) + } } } - } + }, [ + isSelectedColumn, + id, + setColumnLevelLineage, + columnLineageData, + setFetchingColumns, + getColumnLineage, + ]) return ( - } + data={columnLineageData} isFetching={isColumnLineageFetching} - error={columnLineageError as Error | null} + error={columnLineageError} onClick={toggleSelectedColumn} renderError={error =>
Error: {error.message}
} /> diff --git a/vscode/react/src/pages/help.ts b/vscode/react/src/pages/help.ts index 110a8ebac6..2831dec849 100644 --- a/vscode/react/src/pages/help.ts +++ b/vscode/react/src/pages/help.ts @@ -1,37 +1,36 @@ -import { type NodeType } from './ModelLineageContext' +import type { ModelType } from '@/api/client' -export function getNodeTypeColorVar(nodeType: NodeType) { - return { - sql: 'var(--color-lineage-node-type-background-sql)', - python: 'var(--color-lineage-node-type-background-python)', - 'cte/subquery': 'var(--color-lineage-node-type-background-cte-subquery)', - source: 'var(--color-lineage-node-type-background-source)', - }[nodeType] -} +type NodeType = ModelType | 'cte/subquery' -export function getNodeTypeColor(nodeType: NodeType) { - return { - sql: 'bg-lineage-node-type-background-sql', - python: 'bg-lineage-node-type-background-python', - 'cte/subquery': 'bg-lineage-node-type-background-cte-subquery', - source: 'bg-lineage-node-type-background-source', - }[nodeType] +export const NODE_TYPE_COLOR_VAR: Record = { + sql: 'var(--color-lineage-node-type-background-sql)', + python: 'var(--color-lineage-node-type-background-python)', + 'cte/subquery': 'var(--color-lineage-node-type-background-cte-subquery)', + source: 'var(--color-lineage-node-type-background-source)', + seed: 'var(--color-lineage-node-type-background-source)', + external: 'var(--color-lineage-node-type-background-source)', } - -export function getNodeTypeTextColor(nodeType: NodeType) { - return { - sql: 'text-lineage-node-type-foreground-sql', - python: 'text-lineage-node-type-foreground-python', - 'cte/subquery': 'text-lineage-node-type-foreground-cte-subquery', - source: 'text-lineage-node-type-foreground-source', - }[nodeType] +export const NODE_TYPE_COLOR: Record = { + sql: 'bg-lineage-node-type-background-sql', + python: 'bg-lineage-node-type-background-python', + 'cte/subquery': 'bg-lineage-node-type-background-cte-subquery', + source: 'bg-lineage-node-type-background-source', + seed: 'bg-lineage-node-type-background-source', + external: 'bg-lineage-node-type-background-source', } - -export function getNodeTypeBorderColor(nodeType: NodeType) { - return { - sql: 'border-lineage-node-type-border-sql', - python: 'border-lineage-node-type-border-python', - 'cte/subquery': 'border-lineage-node-type-border-cte-subquery', - source: 'border-lineage-node-type-border-source', - }[nodeType] +export const NODE_TYPE_TEXT_COLOR: Record = { + sql: 'text-lineage-node-type-foreground-sql', + python: 'text-lineage-node-type-foreground-python', + 'cte/subquery': 'text-lineage-node-type-foreground-cte-subquery', + source: 'text-lineage-node-type-foreground-source', + seed: 'text-lineage-node-type-foreground-source', + external: 'text-lineage-node-type-foreground-source', +} +export const NODE_TYPE_BORDER_COLOR: Record = { + sql: 'border-lineage-node-type-border-sql', + python: 'border-lineage-node-type-border-python', + 'cte/subquery': 'border-lineage-node-type-border-cte-subquery', + source: 'border-lineage-node-type-border-source', + seed: 'border-lineage-node-type-border-source', + external: 'border-lineage-node-type-border-source', } diff --git a/vscode/react/src/pages/lineage.tsx b/vscode/react/src/pages/lineage.tsx index f200dd8a12..807d8d2cb5 100644 --- a/vscode/react/src/pages/lineage.tsx +++ b/vscode/react/src/pages/lineage.tsx @@ -10,23 +10,26 @@ import { ModelSQLMeshModel } from '@/domain/sqlmesh-model' import { useEventBus } from '@/hooks/eventBus' import type { VSCodeEvent } from '@bus/callbacks' import { URI } from 'vscode-uri' -import type { Model, ModelLineageApiLineageModelNameGet200 } from '@/api/client' +import type { Model } from '@/api/client' import { useRpc } from '@/utils/rpc' import { type ModelPath, type ModelFullPath, type ModelName, type ModelEncodedFQN, + type ModelFQN, } from '@/domain/models' import { ModelLineage } from './ModelLineage' -import type { ModelLineageNodeDetails, ColumnName } from './ModelLineageContext' import type { - Column, - LineageAdjacencyList, - LineageDetails, -} from '@tobikodata/sqlmesh-common/lineage' + ModelLineageNodeDetails, + ColumnName, + BrandedLineageAdjacencyList, + BrandedLineageDetails, +} from './ModelLineageContext' +import type { Column } from '@tobikodata/sqlmesh-common/lineage' import { useVSCode } from '@/hooks/vscode' +import type { BrandedRecord, BrandedString } from '@bus/brand' export function LineagePage() { const { emit } = useEventBus() @@ -78,7 +81,7 @@ export function LineagePage() { } function Lineage() { - const [selectedModel, setSelectedModel] = useState( + const [selectedModel, setSelectedModel] = useState( undefined, ) const { on } = useEventBus() @@ -91,16 +94,16 @@ function Lineage() { } = useApiModels() const rpc = useRpc() React.useEffect(() => { - const fetchFirstTimeModelIfNotSet = async ( + const fetchFirstTimeModelIfNotSet = async ( models: Model[], - ): Promise => { + ): Promise => { if (!Array.isArray(models)) { return undefined } const activeFile = await rpc('get_active_file', {}) // @ts-ignore if (!activeFile.fileUri) { - return models[0].fqn + return models[0].fqn as T } // @ts-ignore const fileUri: string = activeFile.fileUri @@ -112,16 +115,16 @@ function Lineage() { return URI.file(m.full_path).path === filePath }) if (model) { - return model.fqn + return model.fqn as T } return undefined } if (selectedModel === undefined && Array.isArray(models)) { - fetchFirstTimeModelIfNotSet(models).then(modelName => { + fetchFirstTimeModelIfNotSet(models).then(modelName => { if (modelName && selectedModel === undefined) { setSelectedModel(modelName) } else { - setSelectedModel(models[0].fqn) + setSelectedModel(models[0].fqn as ModelFQN) } }) } @@ -131,20 +134,20 @@ function Lineage() { Array.isArray(models) && models.reduce( (acc, model) => { - acc[model.fqn] = model + acc[model.fqn as ModelFQN] = model return acc }, - {} as Record, + {} as BrandedRecord, ) React.useEffect(() => { const handleChangeFocusedFile = (fileUri: { fileUri: string }) => { const full_path = URI.parse(fileUri.fileUri).path - const model = Object.values(modelsRecord).find( + const model: Model | undefined = Object.values(modelsRecord).find( m => URI.file(m.full_path).path === full_path, ) if (model) { - setSelectedModel(model.fqn) + setSelectedModel(model.fqn as ModelFQN) } } @@ -193,13 +196,15 @@ export function LineageComponentFromWeb({ selectedModel, models, }: { - selectedModel: string - models: Record -}): JSX.Element { + selectedModel: ModelFQN + models: BrandedRecord +}) { const vscode = useVSCode() function handleClickModel(id: string): void { const decodedId = decodeURIComponent(id) - const model = Object.values(models).find(m => m.fqn === decodedId) + const model = (Object.values(models) as Model[]).find( + m => m.fqn === decodedId, + ) if (!model) { throw new Error('Model not found') } @@ -230,7 +235,7 @@ export function LineageComponentFromWeb({ const { refetch: getModelLineage } = useApiModelLineage(model?.name ?? '') const [modelLineage, setModelLineage] = useState< - ModelLineageApiLineageModelNameGet200 | undefined + BrandedLineageAdjacencyList | undefined >(undefined) React.useEffect(() => { @@ -238,18 +243,19 @@ export function LineageComponentFromWeb({ getModelLineage() .then(({ data }) => { - setModelLineage(data) + setModelLineage( + data as unknown as BrandedLineageAdjacencyList, + ) }) .catch(handleError) }, [model?.name, model?.hash]) - const adjacencyList = modelLineage as LineageAdjacencyList - const lineageDetails = Object.values(models).reduce( + const lineageDetails = (Object.values(models) as Model[]).reduce( (acc, model) => { - const modelName = model.fqn as ModelName - acc[modelName] = { - name: modelName, - display_name: model.name, + const modelFQN = model.fqn as ModelFQN + acc[modelFQN] = { + name: modelFQN, + display_name: model.name as ModelName, model_type: model.type, identifier: undefined, version: undefined, @@ -267,24 +273,25 @@ export function LineageComponentFromWeb({ } return acc }, - {} as Record, + {} as BrandedRecord, ), } return acc }, - {} as LineageDetails, + {} as BrandedLineageDetails, ) + if (!modelLineage || !lineageDetails) { + return null + } + return ( - adjacencyList && - lineageDetails && ( - handleClickModel(node.id)} - selectedModelName={model.fqn as ModelName} - adjacencyList={adjacencyList} - lineageDetails={lineageDetails} - /> - ) + handleClickModel(node.id)} + selectedModelName={model.fqn as ModelFQN} + adjacencyList={modelLineage} + lineageDetails={lineageDetails} + /> ) } From 3f419a6b8ca6b74d1d583d6029d191873040ae9e Mon Sep 17 00:00:00 2001 From: Max Mykal Date: Wed, 8 Oct 2025 17:28:50 -0700 Subject: [PATCH 11/20] package --- pnpm-lock.yaml | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ffc9865258..40737704c7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -7318,7 +7318,7 @@ snapshots: '@eslint/config-array@0.21.0': dependencies: '@eslint/object-schema': 2.1.6 - debug: 4.4.1 + debug: 4.4.1(supports-color@8.1.1) minimatch: 3.1.2 transitivePeerDependencies: - supports-color @@ -7332,7 +7332,7 @@ snapshots: '@eslint/eslintrc@3.3.1': dependencies: ajv: 6.12.6 - debug: 4.4.1 + debug: 4.4.1(supports-color@8.1.1) espree: 10.4.0 globals: 14.0.0 ignore: 5.3.2 @@ -9243,7 +9243,7 @@ snapshots: '@typescript-eslint/types': 8.38.0 '@typescript-eslint/typescript-estree': 8.38.0(typescript@5.8.3) '@typescript-eslint/visitor-keys': 8.38.0 - debug: 4.4.1 + debug: 4.4.1(supports-color@8.1.1) eslint: 9.31.0(jiti@2.4.2) typescript: 5.8.3 transitivePeerDependencies: @@ -9253,7 +9253,7 @@ snapshots: dependencies: '@typescript-eslint/tsconfig-utils': 8.38.0(typescript@5.8.3) '@typescript-eslint/types': 8.38.0 - debug: 4.4.1 + debug: 4.4.1(supports-color@8.1.1) typescript: 5.8.3 transitivePeerDependencies: - supports-color @@ -9272,7 +9272,7 @@ snapshots: '@typescript-eslint/types': 8.38.0 '@typescript-eslint/typescript-estree': 8.38.0(typescript@5.8.3) '@typescript-eslint/utils': 8.38.0(eslint@9.31.0(jiti@2.4.2))(typescript@5.8.3) - debug: 4.4.1 + debug: 4.4.1(supports-color@8.1.1) eslint: 9.31.0(jiti@2.4.2) ts-api-utils: 2.1.0(typescript@5.8.3) typescript: 5.8.3 @@ -9287,7 +9287,7 @@ snapshots: '@typescript-eslint/tsconfig-utils': 8.38.0(typescript@5.8.3) '@typescript-eslint/types': 8.38.0 '@typescript-eslint/visitor-keys': 8.38.0 - debug: 4.4.1 + debug: 4.4.1(supports-color@8.1.1) fast-glob: 3.3.3 is-glob: 4.0.3 minimatch: 9.0.5 @@ -10697,7 +10697,7 @@ snapshots: ajv: 6.12.6 chalk: 4.1.2 cross-spawn: 7.0.6 - debug: 4.4.1 + debug: 4.4.1(supports-color@8.1.1) escape-string-regexp: 4.0.0 eslint-scope: 8.4.0 eslint-visitor-keys: 4.2.1 @@ -13671,7 +13671,7 @@ snapshots: vite-node@3.2.4(@types/node@20.11.25)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0): dependencies: cac: 6.7.14 - debug: 4.4.1 + debug: 4.4.1(supports-color@8.1.1) es-module-lexer: 1.7.0 pathe: 2.0.3 vite: 6.3.5(@types/node@20.11.25)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0) @@ -13721,7 +13721,7 @@ snapshots: '@volar/typescript': 2.4.23 '@vue/language-core': 2.2.0(typescript@5.8.3) compare-versions: 6.1.1 - debug: 4.4.1 + debug: 4.4.1(supports-color@8.1.1) kolorist: 1.8.0 local-pkg: 1.1.1 magic-string: 0.30.17 @@ -13787,7 +13787,7 @@ snapshots: '@vitest/spy': 3.2.4 '@vitest/utils': 3.2.4 chai: 5.2.1 - debug: 4.4.1 + debug: 4.4.1(supports-color@8.1.1) expect-type: 1.2.2 magic-string: 0.30.17 pathe: 2.0.3 From ec70f345adb3538b3ec563eb4f8ddc4de7329bb5 Mon Sep 17 00:00:00 2001 From: Max Mykal Date: Thu, 9 Oct 2025 16:37:35 -0700 Subject: [PATCH 12/20] include common package directly --- pnpm-lock.yaml | 38 +++-- vscode/react/package.json | 2 - vscode/react/src/App.css | 3 - vscode/react/src/main.tsx | 2 + vscode/react/src/pages/ModelLineage.tsx | 131 ++++++++++++------ vscode/react/src/pages/ModelLineageContext.ts | 78 ++++++----- vscode/react/src/pages/ModelNode.tsx | 8 +- vscode/react/src/pages/ModelNodeColumn.tsx | 24 ++-- vscode/react/src/pages/lineage.tsx | 56 ++++---- vscode/react/tailwind.config.cjs | 6 +- vscode/react/tsconfig.json | 3 +- vscode/react/vite.config.js | 1 + 12 files changed, 209 insertions(+), 143 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 40737704c7..878c6fecbb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -123,9 +123,6 @@ importers: '@tanstack/router-plugin': specifier: ^1.131.26 version: 1.132.47(@tanstack/react-router@1.132.47(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(vite@6.3.5(@types/node@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0))(webpack@5.99.8(esbuild@0.25.8)) - '@tobikodata/sqlmesh-common': - specifier: file:../../web/common - version: link:../../web/common apache-arrow: specifier: ^19.0.1 version: 19.0.1 @@ -153,9 +150,6 @@ importers: react-router: specifier: ^7.7.0 version: 7.7.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - reactflow: - specifier: ^11.11.4 - version: 11.11.4(@types/react@18.3.23)(immer@9.0.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) tailwindcss: specifier: 3.4.17 version: 3.4.17 @@ -6893,7 +6887,7 @@ snapshots: '@babel/traverse': 7.28.0 '@babel/types': 7.28.1 convert-source-map: 2.0.0 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1 gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 @@ -7061,7 +7055,7 @@ snapshots: '@babel/parser': 7.28.0 '@babel/template': 7.27.2 '@babel/types': 7.28.1 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1 transitivePeerDependencies: - supports-color @@ -7613,7 +7607,7 @@ snapshots: ajv: 8.17.1 chalk: 4.1.2 compare-versions: 6.1.1 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1 esbuild: 0.25.8 esutils: 2.0.3 fs-extra: 11.3.0 @@ -9189,7 +9183,6 @@ snapshots: '@types/node@24.1.0': dependencies: undici-types: 7.8.0 - optional: true '@types/normalize-package-data@2.4.4': {} @@ -9430,7 +9423,7 @@ snapshots: '@ampproject/remapping': 2.3.0 '@bcoe/v8-coverage': 1.0.2 ast-v8-to-istanbul: 0.3.3 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1 istanbul-lib-coverage: 3.2.2 istanbul-lib-report: 3.0.1 istanbul-lib-source-maps: 5.0.6 @@ -9499,7 +9492,7 @@ snapshots: sirv: 3.0.1 tinyglobby: 0.2.14 tinyrainbow: 2.0.0 - vitest: 3.2.4(@types/debug@4.1.12)(@types/node@20.11.25)(@vitest/browser@3.2.4)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0) + vitest: 3.2.4(@types/debug@4.1.12)(@types/node@24.1.0)(@vitest/browser@3.2.4)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0) '@vitest/utils@3.2.4': dependencies: @@ -10341,6 +10334,10 @@ snapshots: de-indent@1.0.2: {} + debug@4.4.1: + dependencies: + ms: 2.1.3 + debug@4.4.1(supports-color@8.1.1): dependencies: ms: 2.1.3 @@ -10614,7 +10611,7 @@ snapshots: esbuild-register@3.6.0(esbuild@0.25.8): dependencies: - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1 esbuild: 0.25.8 transitivePeerDependencies: - supports-color @@ -11108,7 +11105,7 @@ snapshots: http-proxy-agent@7.0.2: dependencies: agent-base: 7.1.4 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1 transitivePeerDependencies: - supports-color @@ -11117,7 +11114,7 @@ snapshots: https-proxy-agent@7.0.6: dependencies: agent-base: 7.1.4 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1 transitivePeerDependencies: - supports-color @@ -11346,7 +11343,7 @@ snapshots: istanbul-lib-source-maps@5.0.6: dependencies: '@jridgewell/trace-mapping': 0.3.31 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1 istanbul-lib-coverage: 3.2.2 transitivePeerDependencies: - supports-color @@ -11374,7 +11371,7 @@ snapshots: jest-worker@27.5.1: dependencies: - '@types/node': 20.11.25 + '@types/node': 24.1.0 merge-stream: 2.0.0 supports-color: 8.1.1 @@ -13547,8 +13544,7 @@ snapshots: undici-types@6.21.0: {} - undici-types@7.8.0: - optional: true + undici-types@7.8.0: {} undici@7.12.0: {} @@ -13692,7 +13688,7 @@ snapshots: vite-node@3.2.4(@types/node@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0): dependencies: cac: 6.7.14 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1 es-module-lexer: 1.7.0 pathe: 2.0.3 vite: 6.3.5(@types/node@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0) @@ -13832,7 +13828,7 @@ snapshots: '@vitest/spy': 3.2.4 '@vitest/utils': 3.2.4 chai: 5.2.1 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1 expect-type: 1.2.2 magic-string: 0.30.17 pathe: 2.0.3 diff --git a/vscode/react/package.json b/vscode/react/package.json index 041aa66467..d7e302444e 100644 --- a/vscode/react/package.json +++ b/vscode/react/package.json @@ -23,7 +23,6 @@ "@tanstack/react-router-devtools": "^1.131.26", "@tanstack/react-virtual": "^3.13.12", "@tanstack/router-plugin": "^1.131.26", - "@tobikodata/sqlmesh-common": "file:../../web/common", "apache-arrow": "^19.0.1", "clsx": "^2.1.1", "cronstrue": "3.3.0", @@ -33,7 +32,6 @@ "react": "^18.3.1", "react-dom": "^18.3.1", "react-router": "^7.7.0", - "reactflow": "^11.11.4", "tailwindcss": "3.4.17", "vscode-uri": "^3.1.0" }, diff --git a/vscode/react/src/App.css b/vscode/react/src/App.css index a7dce951b3..2588273da7 100644 --- a/vscode/react/src/App.css +++ b/vscode/react/src/App.css @@ -1,6 +1,3 @@ -@import url('@tobikodata/sqlmesh-common/design/index.css'); -@import url('@tobikodata/sqlmesh-common/styles/sqlmesh-common.min.css'); - :root { /* Keep commented for reference. diff --git a/vscode/react/src/main.tsx b/vscode/react/src/main.tsx index de3f29bfd1..477f2ce62f 100644 --- a/vscode/react/src/main.tsx +++ b/vscode/react/src/main.tsx @@ -5,6 +5,8 @@ import { EventBusProvider } from './hooks/eventBus.tsx' import { TableDiffPage } from './pages/tablediff.tsx' import { LineagePage } from './pages/lineage.tsx' +import '@sqlmesh-common/styles/index.css' + import './App.css' // Detect panel type diff --git a/vscode/react/src/pages/ModelLineage.tsx b/vscode/react/src/pages/ModelLineage.tsx index 5501e79fcb..2aa0b52e1b 100644 --- a/vscode/react/src/pages/ModelLineage.tsx +++ b/vscode/react/src/pages/ModelLineage.tsx @@ -1,13 +1,15 @@ import React from 'react' -import { Focus, Rows2, Rows3 } from 'lucide-react' +import { Focus, LockOpen, Lock, Rows2, Rows3 } from 'lucide-react' + import { - FactoryEdgeWithGradient, - EdgeWithGradient, - ZOOM_THRESHOLD, + type LineageNode, type LineageEdge, type LineageNodesMap, - type ColumnLevelLineageAdjacencyList, + MAX_COLUMNS_TO_DISPLAY, + ZOOM_THRESHOLD, + FactoryEdgeWithGradient, + EdgeWithGradient, useColumnLevelLineage, createNode, toPortID, @@ -22,20 +24,21 @@ import { getOnlySelectedNodes, getTransformedModelEdgesTargetSources, getTransformedNodes, - MAX_COLUMNS_TO_DISPLAY, toNodeID, - LineageLayout, - type LineageNode, LineageControlButton, LineageControlIcon, -} from '@tobikodata/sqlmesh-common/lineage' + LineageLayout, +} from '@sqlmesh-common/components/Lineage' import { + type BrandedColumnLevelLineageAdjacencyList, type BrandedLineageAdjacencyList, type BrandedLineageDetails, - type ColumnName, + type ModelColumnName, type EdgeData, type ModelColumnID, + type ModelColumnLeftHandleId, + type ModelColumnRightHandleId, type ModelEdgeId, type ModelLineageNodeDetails, type ModelNodeId, @@ -62,8 +65,8 @@ export const ModelLineage = ({ className, onNodeClick, }: { - adjacencyList: BrandedLineageAdjacencyList - lineageDetails: BrandedLineageDetails + adjacencyList: BrandedLineageAdjacencyList + lineageDetails: BrandedLineageDetails selectedModelName?: ModelFQN className?: string onNodeClick?: ( @@ -77,8 +80,16 @@ export const ModelLineage = ({ const [zoom, setZoom] = React.useState(ZOOM_THRESHOLD) const [isBuildingLayout, setIsBuildingLayout] = React.useState(false) + const [nodesDraggable, setNodesDraggable] = React.useState(false) const [edges, setEdges] = React.useState< - LineageEdge[] + LineageEdge< + EdgeData, + ModelEdgeId, + ModelNodeId, + ModelNodeId, + ModelColumnRightHandleId, + ModelColumnLeftHandleId + >[] >([]) const [nodesMap, setNodesMap] = React.useState< LineageNodesMap @@ -92,11 +103,11 @@ export const ModelLineage = ({ new Set(), ) const [selectedNodeId, setSelectedNodeId] = - React.useState(null) + React.useState(currentNodeId) const [showColumns, setShowColumns] = React.useState(false) const [columnLevelLineage, setColumnLevelLineage] = React.useState< - Map> + Map >(new Map()) const [fetchingColumns, setFetchingColumns] = React.useState< Set @@ -106,9 +117,12 @@ export const ModelLineage = ({ adjacencyListColumnLevel, selectedColumns, adjacencyListKeysColumnLevel, - } = useColumnLevelLineage( - columnLevelLineage, - ) + } = useColumnLevelLineage< + ModelFQN, + ModelColumnName, + ModelColumnID, + BrandedColumnLevelLineageAdjacencyList + >(columnLevelLineage) const adjacencyListKeys = React.useMemo(() => { let keys: ModelFQN[] = [] @@ -126,7 +140,7 @@ export const ModelLineage = ({ (nodeId: ModelNodeId, detail: ModelLineageNodeDetails) => { const columns = detail.columns - const node = createNode('node', nodeId, { + const node = createNode('node', nodeId, { name: detail.name, displayName: detail.display_name, model_type: detail.model_type, @@ -188,8 +202,8 @@ export const ModelLineage = ({ edgeId: ModelEdgeId, sourceId: ModelNodeId, targetId: ModelNodeId, - sourceHandleId?: ModelColumnID, - targetHandleId?: ModelColumnID, + sourceHandleId?: ModelColumnRightHandleId, + targetHandleId?: ModelColumnLeftHandleId, ) => { const sourceNode = transformedNodesMap[sourceId] const targetNode = transformedNodesMap[targetId] @@ -215,7 +229,14 @@ export const ModelLineage = ({ data.strokeWidth = 2 } - return createEdge( + return createEdge< + EdgeData, + ModelEdgeId, + ModelNodeId, + ModelNodeId, + ModelColumnRightHandleId, + ModelColumnLeftHandleId + >( edgeType, edgeId, sourceId, @@ -232,11 +253,14 @@ export const ModelLineage = ({ () => getEdgesFromColumnLineage< ModelFQN, - ColumnName, + ModelColumnName, EdgeData, ModelEdgeId, ModelNodeId, - ModelColumnID + ModelNodeId, + ModelColumnRightHandleId, + ModelColumnLeftHandleId, + BrandedColumnLevelLineageAdjacencyList >({ columnLineage: adjacencyListColumnLevel, transformEdge, @@ -250,38 +274,49 @@ export const ModelLineage = ({ : getTransformedModelEdgesTargetSources< ModelFQN, EdgeData, - ModelNodeId, ModelEdgeId, - ModelColumnID + ModelNodeId, + ModelNodeId, + ModelColumnRightHandleId, + ModelColumnLeftHandleId >(adjacencyListKeys, adjacencyList, transformEdge) }, [adjacencyListKeys, adjacencyList, transformEdge, edgesColumnLevel]) const calculateLayout = React.useCallback( ( - eds: LineageEdge[], - nds: LineageNodesMap, + eds: LineageEdge< + EdgeData, + ModelEdgeId, + ModelNodeId, + ModelNodeId, + ModelColumnRightHandleId, + ModelColumnLeftHandleId + >[], + nds: LineageNodesMap, ) => { - const { edges, nodesMap } = buildLayout< + const layoutNodesMap = buildLayout< NodeData, EdgeData, - ModelNodeId, ModelEdgeId, - ModelColumnID + ModelNodeId, + ModelNodeId, + ModelColumnRightHandleId, + ModelColumnLeftHandleId >({ edges: eds, nodesMap: nds }) - setEdges(edges) - setNodesMap(nodesMap) + + setEdges(eds) + setNodesMap(layoutNodesMap) setIsBuildingLayout(false) }, - [edges, nodesMap, setEdges, setNodesMap, setIsBuildingLayout], + [], ) const nodes = React.useMemo(() => { return Object.values(nodesMap) }, [nodesMap]) - const currentNode = React.useMemo(() => { - return currentNodeId ? nodesMap[currentNodeId] : null - }, [currentNodeId, nodesMap]) + const currentNode = currentNodeId ? nodesMap[currentNodeId] : null + const selectedNode = selectedNodeId ? nodesMap[selectedNodeId] : null const handleReset = React.useCallback(() => { setShowColumns(false) @@ -314,9 +349,7 @@ export const ModelLineage = ({ // currentNodeId is passed from the parent component // we it change we need to reset the selectedNodeId React.useEffect(() => { - if (currentNodeId) { - setSelectedNodeId(currentNodeId) - } + setSelectedNodeId(currentNodeId) }, [currentNodeId]) // When the selectedColumns is empty it measn we dont have any selected columns @@ -345,11 +378,13 @@ export const ModelLineage = ({ selectedNodes, selectedEdges, selectedNodeId, + selectedNode, + currentNodeId, + currentNode, zoom, edges, nodes, nodesMap, - currentNode, setFetchingColumns, setColumnLevelLineage, setShowColumns, @@ -367,7 +402,10 @@ export const ModelLineage = ({ EdgeData, ModelNodeId, ModelEdgeId, - ModelColumnID + ModelNodeId, + ModelNodeId, + ModelColumnRightHandleId, + ModelColumnLeftHandleId > useLineage={useModelLineage} nodeTypes={nodeTypes} @@ -375,6 +413,8 @@ export const ModelLineage = ({ className={className} onNodeClick={onNodeClick} isBuildingLayout={isBuildingLayout} + showControlOnlySelectedNodes={selectedColumns.size === 0} + nodesDraggable={nodesDraggable} controls={ <> + setNodesDraggable(prev => !prev)} + disabled={isBuildingLayout} + > + + } /> diff --git a/vscode/react/src/pages/ModelLineageContext.ts b/vscode/react/src/pages/ModelLineageContext.ts index a6940d0586..5dc310736a 100644 --- a/vscode/react/src/pages/ModelLineageContext.ts +++ b/vscode/react/src/pages/ModelLineageContext.ts @@ -1,6 +1,6 @@ import type { ModelType } from '@/api/client' import type { ModelFQN, ModelName } from '@/domain/models' -import type { Branded, BrandedString } from '@bus/brand' +import type { Branded } from '@bus/brand' import { type Column, type ColumnLevelLineageContextValue, @@ -12,37 +12,41 @@ import { type LineageAdjacencyList, type LineageDetails, type ColumnLevelLineageAdjacencyList, -} from '@tobikodata/sqlmesh-common/lineage' +} from '@sqlmesh-common/components/Lineage' -export type BrandedLineageAdjacencyList = - LineageAdjacencyList & { - readonly __adjacencyListKeyBrand: K - } - -export type BrandedLineageDetails = LineageDetails< - K, - V -> & { - readonly __lineageDetailsKeyBrand: K -} - -export type BrandedColumnLevelLineageAdjacencyList< - K extends BrandedString, - V extends BrandedString, -> = ColumnLevelLineageAdjacencyList & { - readonly __columnLevelLineageAdjacencyListKeyBrand: K - readonly __columnLevelLineageAdjacencyListColumnKeyBrand: V -} - -export type ColumnName = Branded +export type ModelColumnName = Branded export type ModelColumnID = Branded export type ModelNodeId = Branded export type ModelEdgeId = Branded export type ModelColumn = Column & { id: ModelColumnID - name: ColumnName + name: ModelColumnName +} +export type ModelColumnLeftHandleId = Branded +export type ModelColumnRightHandleId = Branded< + string, + 'ModelColumnRightHandleId' +> + +export type BrandedLineageAdjacencyList = LineageAdjacencyList & { + readonly __adjacencyListKeyBrand: ModelFQN +} + +export type BrandedLineageDetails = LineageDetails< + ModelFQN, + ModelLineageNodeDetails +> & { + readonly __lineageDetailsKeyBrand: ModelFQN } +export type BrandedModelColumns = Record + +export type BrandedColumnLevelLineageAdjacencyList = + ColumnLevelLineageAdjacencyList & { + readonly __columnLevelLineageAdjacencyListKeyBrand: ModelFQN + readonly __columnLevelLineageAdjacencyListColumnKeyBrand: ModelColumnName + } + export type ModelLineageNodeDetails = { name: ModelFQN display_name: ModelName @@ -54,7 +58,7 @@ export type ModelLineageNodeDetails = { owner?: string | null kind?: string | null tags?: string[] - columns?: Record + columns?: BrandedModelColumns } export type NodeData = { @@ -68,7 +72,7 @@ export type NodeData = { owner?: string | null dialect?: string | null tags?: string[] - columns?: Record + columns?: BrandedModelColumns } export type EdgeData = { @@ -80,20 +84,29 @@ export type EdgeData = { export type ModelLineageContextValue = ColumnLevelLineageContextValue< ModelFQN, - ColumnName, - ModelColumnID + ModelColumnName, + ModelColumnID, + BrandedColumnLevelLineageAdjacencyList > & LineageContextValue< NodeData, EdgeData, ModelNodeId, ModelEdgeId, - ModelColumnID + ModelNodeId, + ModelNodeId, + ModelColumnRightHandleId, + ModelColumnLeftHandleId > export const initial = { ...getLineageContextInitial(), - ...getColumnLevelLineageContextInitial(), + ...getColumnLevelLineageContextInitial< + ModelFQN, + ModelColumnName, + ModelColumnID, + BrandedColumnLevelLineageAdjacencyList + >(), } export const { Provider, useLineage } = createLineageContext< @@ -101,7 +114,10 @@ export const { Provider, useLineage } = createLineageContext< EdgeData, ModelNodeId, ModelEdgeId, - ModelColumnID, + ModelNodeId, + ModelNodeId, + ModelColumnRightHandleId, + ModelColumnLeftHandleId, ModelLineageContextValue >(initial) diff --git a/vscode/react/src/pages/ModelNode.tsx b/vscode/react/src/pages/ModelNode.tsx index 309009caeb..1d61b7fbf3 100644 --- a/vscode/react/src/pages/ModelNode.tsx +++ b/vscode/react/src/pages/ModelNode.tsx @@ -7,7 +7,7 @@ import { type NodeData, type ModelColumn, type ModelColumnID, - type ColumnName, + type ModelColumnName, } from './ModelLineageContext' import { calculateColumnsHeight, @@ -29,7 +29,7 @@ import { NodeBadge, NodePorts, type NodeProps, -} from '@tobikodata/sqlmesh-common/lineage' +} from '@sqlmesh-common/components/Lineage' import { Badge, cn, @@ -37,7 +37,7 @@ import { ModelName, Tooltip, VerticalContainer, -} from '@tobikodata/sqlmesh-common' +} from '@sqlmesh-common/index' import { ModelNodeColumn } from './ModelNodeColumn' import type { ModelFQN } from '@/domain/models' import { NODE_TYPE_COLOR } from './help' @@ -75,7 +75,7 @@ export const ModelNode = React.memo(function ModelNode({ columns, selectedColumns: modelSelectedColumns, columnNames, - } = useColumns( + } = useColumns( selectedColumns, data.name, data.columns, diff --git a/vscode/react/src/pages/ModelNodeColumn.tsx b/vscode/react/src/pages/ModelNodeColumn.tsx index ec013a7e9a..4b910e2ef5 100644 --- a/vscode/react/src/pages/ModelNodeColumn.tsx +++ b/vscode/react/src/pages/ModelNodeColumn.tsx @@ -1,23 +1,27 @@ import React from 'react' -import { FactoryColumn } from '@tobikodata/sqlmesh-common/lineage' - +import { FactoryColumn } from '@sqlmesh-common/components/Lineage' +import { cn } from '@sqlmesh-common/utils' import { useModelLineage, type ModelColumnID, type ModelNodeId, - type ColumnName, + type ModelColumnName, type BrandedColumnLevelLineageAdjacencyList, + type ModelColumnRightHandleId, + type ModelColumnLeftHandleId, } from './ModelLineageContext' -import { cn } from '@tobikodata/sqlmesh-common' import { useApiColumnLineage } from '@/api/index' import type { ModelFQN } from '@/domain/models' const ModelColumn = FactoryColumn< ModelFQN, - ColumnName, + ModelColumnName, ModelNodeId, - ModelColumnID + ModelColumnID, + ModelColumnLeftHandleId, + ModelColumnRightHandleId, + BrandedColumnLevelLineageAdjacencyList >(useModelLineage) export const ModelNodeColumn = React.memo(function ModelNodeColumn({ @@ -32,7 +36,7 @@ export const ModelNodeColumn = React.memo(function ModelNodeColumn({ id: ModelColumnID nodeId: ModelNodeId modelName: ModelFQN - name: ColumnName + name: ModelColumnName type: string description?: string | null className?: string @@ -53,7 +57,7 @@ export const ModelNodeColumn = React.memo(function ModelNodeColumn({ } = useApiColumnLineage(nodeId, name, { models_only: true }) const [columnLineageData, setColumnLineageData] = React.useState< - BrandedColumnLevelLineageAdjacencyList | undefined + BrandedColumnLevelLineageAdjacencyList | undefined >(undefined) const toggleSelectedColumn = React.useCallback(async () => { @@ -71,9 +75,7 @@ export const ModelNodeColumn = React.memo(function ModelNodeColumn({ }) const { data } = (await getColumnLineage()) as { - data: - | BrandedColumnLevelLineageAdjacencyList - | undefined + data: BrandedColumnLevelLineageAdjacencyList | undefined } setColumnLineageData(data) diff --git a/vscode/react/src/pages/lineage.tsx b/vscode/react/src/pages/lineage.tsx index 807d8d2cb5..8d58f4da62 100644 --- a/vscode/react/src/pages/lineage.tsx +++ b/vscode/react/src/pages/lineage.tsx @@ -22,12 +22,13 @@ import { import { ModelLineage } from './ModelLineage' import type { - ModelLineageNodeDetails, - ColumnName, + ModelColumnName, BrandedLineageAdjacencyList, BrandedLineageDetails, + ModelNodeId, + NodeData, } from './ModelLineageContext' -import type { Column } from '@tobikodata/sqlmesh-common/lineage' +import type { Column, LineageNode } from '@sqlmesh-common/components/Lineage' import { useVSCode } from '@/hooks/vscode' import type { BrandedRecord, BrandedString } from '@bus/brand' @@ -200,19 +201,6 @@ export function LineageComponentFromWeb({ models: BrandedRecord }) { const vscode = useVSCode() - function handleClickModel(id: string): void { - const decodedId = decodeURIComponent(id) - const model = (Object.values(models) as Model[]).find( - m => m.fqn === decodedId, - ) - if (!model) { - throw new Error('Model not found') - } - if (!model.full_path) { - return - } - vscode('openFile', { uri: URI.file(model.full_path).toString() }) - } function handleError(error: any): void { console.error(error) @@ -235,17 +223,37 @@ export function LineageComponentFromWeb({ const { refetch: getModelLineage } = useApiModelLineage(model?.name ?? '') const [modelLineage, setModelLineage] = useState< - BrandedLineageAdjacencyList | undefined + BrandedLineageAdjacencyList | undefined >(undefined) + const handleNodeClick = React.useCallback( + ( + event: React.MouseEvent, + node: LineageNode, + ) => { + event.stopPropagation() + + const decodedId = decodeURIComponent(node.id) + const model = (Object.values(models) as Model[]).find( + m => m.fqn === decodedId, + ) + if (!model) { + throw new Error('Model not found') + } + if (!model.full_path) { + return + } + vscode('openFile', { uri: URI.file(model.full_path).toString() }) + }, + [models, vscode], + ) + React.useEffect(() => { if (model === undefined) return getModelLineage() .then(({ data }) => { - setModelLineage( - data as unknown as BrandedLineageAdjacencyList, - ) + setModelLineage(data as unknown as BrandedLineageAdjacencyList) }) .catch(handleError) }, [model?.name, model?.hash]) @@ -266,19 +274,19 @@ export function LineageComponentFromWeb({ tags: [], columns: model.columns.reduce( (acc, column) => { - const columnName = decodeURI(column.name) as ColumnName + const columnName = decodeURI(column.name) as ModelColumnName acc[columnName] = { data_type: column.type, description: column.description, } return acc }, - {} as BrandedRecord, + {} as BrandedRecord, ), } return acc }, - {} as BrandedLineageDetails, + {} as BrandedLineageDetails, ) if (!modelLineage || !lineageDetails) { @@ -288,7 +296,7 @@ export function LineageComponentFromWeb({ return ( handleClickModel(node.id)} + onNodeClick={handleNodeClick} selectedModelName={model.fqn as ModelFQN} adjacencyList={modelLineage} lineageDetails={lineageDetails} diff --git a/vscode/react/tailwind.config.cjs b/vscode/react/tailwind.config.cjs index c81ea258ec..d336abdd35 100644 --- a/vscode/react/tailwind.config.cjs +++ b/vscode/react/tailwind.config.cjs @@ -1,12 +1,10 @@ /** @type {import('tailwindcss').Config} */ module.exports = { - presets: [ - require('@tobikodata/sqlmesh-common/configs/tailwind.base.config.js'), - ], + presets: [require('../../web/common/tailwind.base.config.js')], content: [ './index.html', './src/**/*.{js,ts,jsx,tsx}', - './node_modules/@tobikodata/sqlmesh-common/**/*.{js,ts,jsx,tsx}', + '../../web/common/src/**/*.{js,ts,jsx,tsx}', ], darkMode: ['class', '[mode="dark"]'], theme: { diff --git a/vscode/react/tsconfig.json b/vscode/react/tsconfig.json index 2d2b2b64dc..0c6439b9c4 100644 --- a/vscode/react/tsconfig.json +++ b/vscode/react/tsconfig.json @@ -23,7 +23,8 @@ "baseUrl": ".", "paths": { "@/*": ["./src/*"], - "@bus/*": ["../bus/src/*"] + "@bus/*": ["../bus/src/*"], + "@sqlmesh-common/*": ["../../web/common/src/*"] } } } diff --git a/vscode/react/vite.config.js b/vscode/react/vite.config.js index 9adc2fed4b..d0d38c66d8 100644 --- a/vscode/react/vite.config.js +++ b/vscode/react/vite.config.js @@ -16,6 +16,7 @@ export default defineConfig({ alias: { '@': resolve(__dirname, './src'), '@bus': resolve(__dirname, '../bus/src'), + '@sqlmesh-common': resolve(__dirname, '../../web/common/src'), }, }, From c3269369a2f64bd4882a55a6b5a04cff3f78848d Mon Sep 17 00:00:00 2001 From: Max Mykal Date: Fri, 10 Oct 2025 16:42:45 -0700 Subject: [PATCH 13/20] pr comments --- vscode/react/src/App.css | 12 +---- vscode/react/src/pages/ModelLineage.tsx | 60 +++++++++------------- vscode/react/src/pages/ModelNode.tsx | 54 +++++++------------ vscode/react/src/pages/ModelNodeColumn.tsx | 12 ++--- vscode/react/src/pages/help.ts | 6 +-- vscode/react/src/pages/lineage.tsx | 12 ++--- 6 files changed, 55 insertions(+), 101 deletions(-) diff --git a/vscode/react/src/App.css b/vscode/react/src/App.css index 2588273da7..24119bf9ef 100644 --- a/vscode/react/src/App.css +++ b/vscode/react/src/App.css @@ -36,8 +36,8 @@ --color-filterable-list-input-border: var(--vscode-input-border); --color-lineage-background: transparent; + --color-lineage-border: transparent; --color-lineage-foreground: var(--vscode-foreground); - --color-lineage-border: var(--vscode-editor-background); --color-lineage-divider: var(--vscode-text-separatorForeground); --color-lineage-grid-dot: rgba(0, 0, 0, 0.1); @@ -107,16 +107,6 @@ ); --color-lineage-node-type-border-source: var(--vscode-minimap-errorHighlight); - --color-lineage-node-type-background-cte-subquery: var( - --vscode-minimap-selectionOccurrenceHighlight - ); - --color-lineage-node-type-foreground-cte-subquery: var( - --vscode-minimap-selectionOccurrenceHighlight - ); - --color-lineage-node-type-border-cte-subquery: var( - --vscode-minimap-selectionOccurrenceHighlight - ); - --color-lineage-node-type-handle-icon-background: var( --vscode-editor-background ); diff --git a/vscode/react/src/pages/ModelLineage.tsx b/vscode/react/src/pages/ModelLineage.tsx index 2aa0b52e1b..ff108217eb 100644 --- a/vscode/react/src/pages/ModelLineage.tsx +++ b/vscode/react/src/pages/ModelLineage.tsx @@ -1,6 +1,6 @@ import React from 'react' -import { Focus, LockOpen, Lock, Rows2, Rows3 } from 'lucide-react' +import { Focus, Rows2, Rows3 } from 'lucide-react' import { type LineageNode, @@ -78,9 +78,11 @@ export const ModelLineage = ({ ? toNodeID(selectedModelName) : null + // Store all nodes to keep track of the position and to reuse nodes + const allNodesMap = React.useRef>({}) + const [zoom, setZoom] = React.useState(ZOOM_THRESHOLD) const [isBuildingLayout, setIsBuildingLayout] = React.useState(false) - const [nodesDraggable, setNodesDraggable] = React.useState(false) const [edges, setEdges] = React.useState< LineageEdge< EdgeData, @@ -153,6 +155,7 @@ export const ModelLineage = ({ tags: detail.tags || [], columns, }) + const selectedColumnsCount = new Set( Object.keys(columns ?? {}).map(k => toPortID(detail.name, k)), ).intersection(selectedColumns).size @@ -160,7 +163,7 @@ export const ModelLineage = ({ const nodeBaseHeight = calculateNodeBaseHeight({ includeNodeFooterHeight: false, includeCeilingHeight: true, - includeFloorHeight: true, + includeFloorHeight: false, }) const nodeDetailsHeight = calculateNodeDetailsHeight({ nodeDetailsCount: 0, @@ -193,7 +196,7 @@ export const ModelLineage = ({ ModelLineageNodeDetails, NodeData, ModelNodeId - >(adjacencyListKeys, lineageDetails, transformNode) + >(adjacencyListKeys, lineageDetails, transformNode, allNodesMap.current) }, [adjacencyListKeys, lineageDetails, transformNode]) const transformEdge = React.useCallback( @@ -293,7 +296,10 @@ export const ModelLineage = ({ ModelColumnLeftHandleId >[], nds: LineageNodesMap, + buildingLayoutId: NodeJS.Timeout, ) => { + clearTimeout(buildingLayoutId) + const layoutNodesMap = buildLayout< NodeData, EdgeData, @@ -304,19 +310,22 @@ export const ModelLineage = ({ ModelColumnLeftHandleId >({ edges: eds, nodesMap: nds }) + allNodesMap.current = { ...allNodesMap.current, ...layoutNodesMap } + setEdges(eds) setNodesMap(layoutNodesMap) setIsBuildingLayout(false) }, - [], + [allNodesMap.current], ) const nodes = React.useMemo(() => { return Object.values(nodesMap) }, [nodesMap]) - const currentNode = currentNodeId ? nodesMap[currentNodeId] : null - const selectedNode = selectedNodeId ? nodesMap[selectedNodeId] : null + const selectedNode = selectedNodeId + ? allNodesMap.current[selectedNodeId] + : null const handleReset = React.useCallback(() => { setShowColumns(false) @@ -325,12 +334,14 @@ export const ModelLineage = ({ setShowOnlySelectedNodes(false) setSelectedNodes(new Set()) setSelectedEdges(new Set()) - setSelectedNodeId(null) + setSelectedNodeId(currentNodeId) setColumnLevelLineage(new Map()) - }, []) + }, [currentNodeId]) React.useEffect(() => { - setIsBuildingLayout(true) + const buildingLayoutId = setTimeout(() => { + setIsBuildingLayout(true) + }, 500) if (showOnlySelectedNodes) { const onlySelectedNodesMap = getOnlySelectedNodes( @@ -340,27 +351,15 @@ export const ModelLineage = ({ const onlySelectedEdges = transformedEdges.filter(edge => selectedEdges.has(edge.id as ModelEdgeId), ) - calculateLayout(onlySelectedEdges, onlySelectedNodesMap) + calculateLayout(onlySelectedEdges, onlySelectedNodesMap, buildingLayoutId) } else { - calculateLayout(transformedEdges, transformedNodesMap) + calculateLayout(transformedEdges, transformedNodesMap, buildingLayoutId) } }, [showOnlySelectedNodes, transformedEdges, transformedNodesMap]) - // currentNodeId is passed from the parent component - // we it change we need to reset the selectedNodeId - React.useEffect(() => { - setSelectedNodeId(currentNodeId) - }, [currentNodeId]) - - // When the selectedColumns is empty it measn we dont have any selected columns - // so we need to set the selectedNodeId back to the currentNode.id - // where currentNode derived from currentNodeId if present in nodesMap - // if the currentNode is null we need to set the selectedNodeId to null React.useEffect(() => { - if (selectedColumns.size === 0 && selectedNodeId != currentNode?.id) { - setSelectedNodeId(currentNode?.id || null) - } - }, [selectedColumns, currentNode?.id]) + handleReset() + }, [handleReset]) function toggleColumns() { setShowColumns(prev => !prev) @@ -380,7 +379,6 @@ export const ModelLineage = ({ selectedNodeId, selectedNode, currentNodeId, - currentNode, zoom, edges, nodes, @@ -414,7 +412,6 @@ export const ModelLineage = ({ onNodeClick={onNodeClick} isBuildingLayout={isBuildingLayout} showControlOnlySelectedNodes={selectedColumns.size === 0} - nodesDraggable={nodesDraggable} controls={ <> - setNodesDraggable(prev => !prev)} - disabled={isBuildingLayout} - > - - } /> diff --git a/vscode/react/src/pages/ModelNode.tsx b/vscode/react/src/pages/ModelNode.tsx index 1d61b7fbf3..eb5fc18588 100644 --- a/vscode/react/src/pages/ModelNode.tsx +++ b/vscode/react/src/pages/ModelNode.tsx @@ -51,7 +51,7 @@ export const ModelNode = React.memo(function ModelNode({ const { selectedColumns, zoom, - currentNode, + currentNodeId, selectedNodeId, selectedNodes, showColumns, @@ -69,7 +69,7 @@ export const ModelNode = React.memo(function ModelNode({ isCurrent, isSelected, // if selected from inside the lineage and node is selcted isActive, // if selected from inside the lineage and node is not selected but in path - } = useNodeMetadata(nodeId, currentNode, selectedNodeId, selectedNodes) + } = useNodeMetadata(nodeId, currentNodeId, selectedNodeId, selectedNodes) const { columns, @@ -93,6 +93,7 @@ export const ModelNode = React.memo(function ModelNode({ const modelType = data.model_type?.toLowerCase() as ModelType const hasColumnsFilter = shouldShowColumns && columns.length > MAX_COLUMNS_TO_DISPLAY + // We are not including the footer, because we need actual height to dynamically adjust node container height const nodeBaseHeight = calculateNodeBaseHeight({ includeNodeFooterHeight: false, @@ -126,8 +127,8 @@ export const ModelNode = React.memo(function ModelNode({ return ( - {isCurrent && ( - - current - + {modelType && ( + ZOOM_THRESHOLD ? '2xs' : 'm'} + className={cn( + 'text-[white] font-black', + NODE_TYPE_COLOR[modelType], + )} + > + {modelType.toUpperCase()} + )} {zoom > ZOOM_THRESHOLD && ( <> @@ -180,7 +187,7 @@ export const ModelNode = React.memo(function ModelNode({ {...props} className={cn( 'ring-offset-2 z-10', - isSelected + isCurrent || isSelected ? 'ring-2 ring-lineage-node-selected-border ring-offset-lineage-node-background' : 'hover:ring-2 hover:ring-lineage-node-border-hover', )} @@ -232,7 +239,7 @@ export const ModelNode = React.memo(function ModelNode({ name={column.name} description={column.description} type={column.data_type} - className="py-1 px-3 first:border-t-0 h-6" + className="px-2 first:border-t-0 h-6" /> ))} @@ -255,39 +262,16 @@ export const ModelNode = React.memo(function ModelNode({ name={column.name} description={column.description} type={column.data_type} - className="py-1 px-3 border-t border-lineage-divider first:border-t-0 h-6" + className="border-t border-lineage-divider first:border-t-0 h-6" /> )} - className="border-t border-lineage-divider cursor-default" + className="border-t border-lineage-divider" /> )} )} - {modelType && ( - - ZOOM_THRESHOLD ? 'h-5' : 'h-8', - )} - > - ZOOM_THRESHOLD ? '2xs' : 'm'} - className={cn( - 'text-[white] font-black', - NODE_TYPE_COLOR[modelType], - )} - > - {modelType.toUpperCase()} - - - - )} ) }) diff --git a/vscode/react/src/pages/ModelNodeColumn.tsx b/vscode/react/src/pages/ModelNodeColumn.tsx index 4b910e2ef5..1791689e7b 100644 --- a/vscode/react/src/pages/ModelNodeColumn.tsx +++ b/vscode/react/src/pages/ModelNodeColumn.tsx @@ -41,12 +41,8 @@ export const ModelNodeColumn = React.memo(function ModelNodeColumn({ description?: string | null className?: string }) { - const { - selectedColumns, - setColumnLevelLineage, - setFetchingColumns, - setSelectedNodeId, - } = useModelLineage() + const { selectedColumns, setColumnLevelLineage, setFetchingColumns } = + useModelLineage() const isSelectedColumn = selectedColumns.has(id) @@ -67,8 +63,6 @@ export const ModelNodeColumn = React.memo(function ModelNodeColumn({ return new Map(prev) }) } else { - setSelectedNodeId(nodeId) - if (columnLineageData == null) { setTimeout(() => { setFetchingColumns(prev => new Set(prev.add(id))) @@ -108,7 +102,7 @@ export const ModelNodeColumn = React.memo(function ModelNodeColumn({ type={type} description={description} className={cn( - 'ModelNodeColumn', + 'ModelNodeColumn cursor-pointer', isSelectedColumn && 'bg-lineage-model-column-active-background', className, )} diff --git a/vscode/react/src/pages/help.ts b/vscode/react/src/pages/help.ts index 2831dec849..410d97ed07 100644 --- a/vscode/react/src/pages/help.ts +++ b/vscode/react/src/pages/help.ts @@ -1,11 +1,10 @@ import type { ModelType } from '@/api/client' -type NodeType = ModelType | 'cte/subquery' +type NodeType = ModelType export const NODE_TYPE_COLOR_VAR: Record = { sql: 'var(--color-lineage-node-type-background-sql)', python: 'var(--color-lineage-node-type-background-python)', - 'cte/subquery': 'var(--color-lineage-node-type-background-cte-subquery)', source: 'var(--color-lineage-node-type-background-source)', seed: 'var(--color-lineage-node-type-background-source)', external: 'var(--color-lineage-node-type-background-source)', @@ -13,7 +12,6 @@ export const NODE_TYPE_COLOR_VAR: Record = { export const NODE_TYPE_COLOR: Record = { sql: 'bg-lineage-node-type-background-sql', python: 'bg-lineage-node-type-background-python', - 'cte/subquery': 'bg-lineage-node-type-background-cte-subquery', source: 'bg-lineage-node-type-background-source', seed: 'bg-lineage-node-type-background-source', external: 'bg-lineage-node-type-background-source', @@ -21,7 +19,6 @@ export const NODE_TYPE_COLOR: Record = { export const NODE_TYPE_TEXT_COLOR: Record = { sql: 'text-lineage-node-type-foreground-sql', python: 'text-lineage-node-type-foreground-python', - 'cte/subquery': 'text-lineage-node-type-foreground-cte-subquery', source: 'text-lineage-node-type-foreground-source', seed: 'text-lineage-node-type-foreground-source', external: 'text-lineage-node-type-foreground-source', @@ -29,7 +26,6 @@ export const NODE_TYPE_TEXT_COLOR: Record = { export const NODE_TYPE_BORDER_COLOR: Record = { sql: 'border-lineage-node-type-border-sql', python: 'border-lineage-node-type-border-python', - 'cte/subquery': 'border-lineage-node-type-border-cte-subquery', source: 'border-lineage-node-type-border-source', seed: 'border-lineage-node-type-border-source', external: 'border-lineage-node-type-border-source', diff --git a/vscode/react/src/pages/lineage.tsx b/vscode/react/src/pages/lineage.tsx index 8d58f4da62..d1ec5d1432 100644 --- a/vscode/react/src/pages/lineage.tsx +++ b/vscode/react/src/pages/lineage.tsx @@ -30,7 +30,7 @@ import type { } from './ModelLineageContext' import type { Column, LineageNode } from '@sqlmesh-common/components/Lineage' import { useVSCode } from '@/hooks/vscode' -import type { BrandedRecord, BrandedString } from '@bus/brand' +import type { BrandedRecord } from '@bus/brand' export function LineagePage() { const { emit } = useEventBus() @@ -95,16 +95,16 @@ function Lineage() { } = useApiModels() const rpc = useRpc() React.useEffect(() => { - const fetchFirstTimeModelIfNotSet = async ( + const fetchFirstTimeModelIfNotSet = async ( models: Model[], - ): Promise => { + ): Promise => { if (!Array.isArray(models)) { return undefined } const activeFile = await rpc('get_active_file', {}) // @ts-ignore if (!activeFile.fileUri) { - return models[0].fqn as T + return models[0].fqn as ModelFQN } // @ts-ignore const fileUri: string = activeFile.fileUri @@ -116,12 +116,12 @@ function Lineage() { return URI.file(m.full_path).path === filePath }) if (model) { - return model.fqn as T + return model.fqn as ModelFQN } return undefined } if (selectedModel === undefined && Array.isArray(models)) { - fetchFirstTimeModelIfNotSet(models).then(modelName => { + fetchFirstTimeModelIfNotSet(models).then(modelName => { if (modelName && selectedModel === undefined) { setSelectedModel(modelName) } else { From 25362424569f690cad2d08cb6137961dc03b2610 Mon Sep 17 00:00:00 2001 From: Max Mykal Date: Fri, 10 Oct 2025 17:49:45 -0700 Subject: [PATCH 14/20] types --- vscode/bus/src/brand.ts | 23 ---------------- vscode/react/src/pages/ModelLineageContext.ts | 27 +++---------------- vscode/react/src/pages/ModelNodeColumn.tsx | 14 ++++++---- vscode/react/src/pages/lineage.tsx | 23 +++++++++------- 4 files changed, 26 insertions(+), 61 deletions(-) diff --git a/vscode/bus/src/brand.ts b/vscode/bus/src/brand.ts index 52baae9e90..2b9c3ca37a 100644 --- a/vscode/bus/src/brand.ts +++ b/vscode/bus/src/brand.ts @@ -17,26 +17,3 @@ type Brand = { [__brand]: B } * userId == userName -> compile error */ export type Branded = T & Brand - -/** - * Constraint that only accepts branded string types - */ -export type BrandedString = string & Brand - -/** - * BrandedRecord is a type that creates a branded Record type with strict key checking. - * This ensures that Record is NOT assignable to Record - * - * @example - * type ModelFQN = Branded - * type ModelName = Branded - * - * type FQNMap = BrandedRecord - * type NameMap = BrandedRecord - * - * const fqnMap: FQNMap = {} - * const nameMap: NameMap = fqnMap // TypeScript error! - */ -export type BrandedRecord = Record & { - readonly __recordKeyBrand: K -} diff --git a/vscode/react/src/pages/ModelLineageContext.ts b/vscode/react/src/pages/ModelLineageContext.ts index 5dc310736a..2814bd924b 100644 --- a/vscode/react/src/pages/ModelLineageContext.ts +++ b/vscode/react/src/pages/ModelLineageContext.ts @@ -28,25 +28,6 @@ export type ModelColumnRightHandleId = Branded< 'ModelColumnRightHandleId' > -export type BrandedLineageAdjacencyList = LineageAdjacencyList & { - readonly __adjacencyListKeyBrand: ModelFQN -} - -export type BrandedLineageDetails = LineageDetails< - ModelFQN, - ModelLineageNodeDetails -> & { - readonly __lineageDetailsKeyBrand: ModelFQN -} - -export type BrandedModelColumns = Record - -export type BrandedColumnLevelLineageAdjacencyList = - ColumnLevelLineageAdjacencyList & { - readonly __columnLevelLineageAdjacencyListKeyBrand: ModelFQN - readonly __columnLevelLineageAdjacencyListColumnKeyBrand: ModelColumnName - } - export type ModelLineageNodeDetails = { name: ModelFQN display_name: ModelName @@ -58,7 +39,7 @@ export type ModelLineageNodeDetails = { owner?: string | null kind?: string | null tags?: string[] - columns?: BrandedModelColumns + columns?: Record } export type NodeData = { @@ -72,7 +53,7 @@ export type NodeData = { owner?: string | null dialect?: string | null tags?: string[] - columns?: BrandedModelColumns + columns?: Record } export type EdgeData = { @@ -86,7 +67,7 @@ export type ModelLineageContextValue = ColumnLevelLineageContextValue< ModelFQN, ModelColumnName, ModelColumnID, - BrandedColumnLevelLineageAdjacencyList + ColumnLevelLineageAdjacencyList > & LineageContextValue< NodeData, @@ -105,7 +86,7 @@ export const initial = { ModelFQN, ModelColumnName, ModelColumnID, - BrandedColumnLevelLineageAdjacencyList + ColumnLevelLineageAdjacencyList >(), } diff --git a/vscode/react/src/pages/ModelNodeColumn.tsx b/vscode/react/src/pages/ModelNodeColumn.tsx index 1791689e7b..256143fd47 100644 --- a/vscode/react/src/pages/ModelNodeColumn.tsx +++ b/vscode/react/src/pages/ModelNodeColumn.tsx @@ -1,13 +1,15 @@ import React from 'react' -import { FactoryColumn } from '@sqlmesh-common/components/Lineage' +import { + FactoryColumn, + type ColumnLevelLineageAdjacencyList, +} from '@sqlmesh-common/components/Lineage' import { cn } from '@sqlmesh-common/utils' import { useModelLineage, type ModelColumnID, type ModelNodeId, type ModelColumnName, - type BrandedColumnLevelLineageAdjacencyList, type ModelColumnRightHandleId, type ModelColumnLeftHandleId, } from './ModelLineageContext' @@ -21,7 +23,7 @@ const ModelColumn = FactoryColumn< ModelColumnID, ModelColumnLeftHandleId, ModelColumnRightHandleId, - BrandedColumnLevelLineageAdjacencyList + ColumnLevelLineageAdjacencyList >(useModelLineage) export const ModelNodeColumn = React.memo(function ModelNodeColumn({ @@ -53,7 +55,7 @@ export const ModelNodeColumn = React.memo(function ModelNodeColumn({ } = useApiColumnLineage(nodeId, name, { models_only: true }) const [columnLineageData, setColumnLineageData] = React.useState< - BrandedColumnLevelLineageAdjacencyList | undefined + ColumnLevelLineageAdjacencyList | undefined >(undefined) const toggleSelectedColumn = React.useCallback(async () => { @@ -69,7 +71,9 @@ export const ModelNodeColumn = React.memo(function ModelNodeColumn({ }) const { data } = (await getColumnLineage()) as { - data: BrandedColumnLevelLineageAdjacencyList | undefined + data: + | ColumnLevelLineageAdjacencyList + | undefined } setColumnLineageData(data) diff --git a/vscode/react/src/pages/lineage.tsx b/vscode/react/src/pages/lineage.tsx index d1ec5d1432..637af22b33 100644 --- a/vscode/react/src/pages/lineage.tsx +++ b/vscode/react/src/pages/lineage.tsx @@ -23,14 +23,17 @@ import { import { ModelLineage } from './ModelLineage' import type { ModelColumnName, - BrandedLineageAdjacencyList, - BrandedLineageDetails, + ModelLineageNodeDetails, ModelNodeId, NodeData, } from './ModelLineageContext' -import type { Column, LineageNode } from '@sqlmesh-common/components/Lineage' +import type { + Column, + LineageAdjacencyList, + LineageDetails, + LineageNode, +} from '@sqlmesh-common/components/Lineage' import { useVSCode } from '@/hooks/vscode' -import type { BrandedRecord } from '@bus/brand' export function LineagePage() { const { emit } = useEventBus() @@ -138,7 +141,7 @@ function Lineage() { acc[model.fqn as ModelFQN] = model return acc }, - {} as BrandedRecord, + {} as Record, ) React.useEffect(() => { @@ -198,7 +201,7 @@ export function LineageComponentFromWeb({ models, }: { selectedModel: ModelFQN - models: BrandedRecord + models: Record }) { const vscode = useVSCode() @@ -223,7 +226,7 @@ export function LineageComponentFromWeb({ const { refetch: getModelLineage } = useApiModelLineage(model?.name ?? '') const [modelLineage, setModelLineage] = useState< - BrandedLineageAdjacencyList | undefined + LineageAdjacencyList | undefined >(undefined) const handleNodeClick = React.useCallback( @@ -253,7 +256,7 @@ export function LineageComponentFromWeb({ getModelLineage() .then(({ data }) => { - setModelLineage(data as unknown as BrandedLineageAdjacencyList) + setModelLineage(data as unknown as LineageAdjacencyList) }) .catch(handleError) }, [model?.name, model?.hash]) @@ -281,12 +284,12 @@ export function LineageComponentFromWeb({ } return acc }, - {} as BrandedRecord, + {} as Record, ), } return acc }, - {} as BrandedLineageDetails, + {} as LineageDetails, ) if (!modelLineage || !lineageDetails) { From 295df1538029683f9eb80140aa73b14a1e12d110 Mon Sep 17 00:00:00 2001 From: Max Mykal Date: Fri, 10 Oct 2025 17:50:08 -0700 Subject: [PATCH 15/20] formatting --- vscode/react/src/pages/ModelLineage.tsx | 32 ++++++++++++------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/vscode/react/src/pages/ModelLineage.tsx b/vscode/react/src/pages/ModelLineage.tsx index ff108217eb..d8170faedb 100644 --- a/vscode/react/src/pages/ModelLineage.tsx +++ b/vscode/react/src/pages/ModelLineage.tsx @@ -28,12 +28,12 @@ import { LineageControlButton, LineageControlIcon, LineageLayout, + type LineageAdjacencyList, + type LineageDetails, + type ColumnLevelLineageAdjacencyList, } from '@sqlmesh-common/components/Lineage' import { - type BrandedColumnLevelLineageAdjacencyList, - type BrandedLineageAdjacencyList, - type BrandedLineageDetails, type ModelColumnName, type EdgeData, type ModelColumnID, @@ -65,8 +65,8 @@ export const ModelLineage = ({ className, onNodeClick, }: { - adjacencyList: BrandedLineageAdjacencyList - lineageDetails: BrandedLineageDetails + adjacencyList: LineageAdjacencyList + lineageDetails: LineageDetails selectedModelName?: ModelFQN className?: string onNodeClick?: ( @@ -109,7 +109,7 @@ export const ModelLineage = ({ const [showColumns, setShowColumns] = React.useState(false) const [columnLevelLineage, setColumnLevelLineage] = React.useState< - Map + Map> >(new Map()) const [fetchingColumns, setFetchingColumns] = React.useState< Set @@ -123,7 +123,7 @@ export const ModelLineage = ({ ModelFQN, ModelColumnName, ModelColumnID, - BrandedColumnLevelLineageAdjacencyList + ColumnLevelLineageAdjacencyList >(columnLevelLineage) const adjacencyListKeys = React.useMemo(() => { @@ -263,7 +263,7 @@ export const ModelLineage = ({ ModelNodeId, ModelColumnRightHandleId, ModelColumnLeftHandleId, - BrandedColumnLevelLineageAdjacencyList + ColumnLevelLineageAdjacencyList >({ columnLineage: adjacencyListColumnLevel, transformEdge, @@ -275,14 +275,14 @@ export const ModelLineage = ({ return edgesColumnLevel.length > 0 ? edgesColumnLevel : getTransformedModelEdgesTargetSources< - ModelFQN, - EdgeData, - ModelEdgeId, - ModelNodeId, - ModelNodeId, - ModelColumnRightHandleId, - ModelColumnLeftHandleId - >(adjacencyListKeys, adjacencyList, transformEdge) + ModelFQN, + EdgeData, + ModelEdgeId, + ModelNodeId, + ModelNodeId, + ModelColumnRightHandleId, + ModelColumnLeftHandleId + >(adjacencyListKeys, adjacencyList, transformEdge) }, [adjacencyListKeys, adjacencyList, transformEdge, edgesColumnLevel]) const calculateLayout = React.useCallback( From 360e301a345e5ad783197a267d2806adb0741171 Mon Sep 17 00:00:00 2001 From: Max Mykal Date: Mon, 13 Oct 2025 08:06:32 -0700 Subject: [PATCH 16/20] formatting --- vscode/react/src/pages/ModelLineage.tsx | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/vscode/react/src/pages/ModelLineage.tsx b/vscode/react/src/pages/ModelLineage.tsx index d8170faedb..e5e13c05a6 100644 --- a/vscode/react/src/pages/ModelLineage.tsx +++ b/vscode/react/src/pages/ModelLineage.tsx @@ -109,7 +109,10 @@ export const ModelLineage = ({ const [showColumns, setShowColumns] = React.useState(false) const [columnLevelLineage, setColumnLevelLineage] = React.useState< - Map> + Map< + ModelColumnID, + ColumnLevelLineageAdjacencyList + > >(new Map()) const [fetchingColumns, setFetchingColumns] = React.useState< Set @@ -275,14 +278,14 @@ export const ModelLineage = ({ return edgesColumnLevel.length > 0 ? edgesColumnLevel : getTransformedModelEdgesTargetSources< - ModelFQN, - EdgeData, - ModelEdgeId, - ModelNodeId, - ModelNodeId, - ModelColumnRightHandleId, - ModelColumnLeftHandleId - >(adjacencyListKeys, adjacencyList, transformEdge) + ModelFQN, + EdgeData, + ModelEdgeId, + ModelNodeId, + ModelNodeId, + ModelColumnRightHandleId, + ModelColumnLeftHandleId + >(adjacencyListKeys, adjacencyList, transformEdge) }, [adjacencyListKeys, adjacencyList, transformEdge, edgesColumnLevel]) const calculateLayout = React.useCallback( From 3b0e2a65b813bdd61502cb36e44f40227a6bd4fe Mon Sep 17 00:00:00 2001 From: Max Mykal Date: Mon, 13 Oct 2025 09:33:47 -0700 Subject: [PATCH 17/20] clean up --- vscode/react/src/pages/ModelLineageContext.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/vscode/react/src/pages/ModelLineageContext.ts b/vscode/react/src/pages/ModelLineageContext.ts index 2814bd924b..850fe42b95 100644 --- a/vscode/react/src/pages/ModelLineageContext.ts +++ b/vscode/react/src/pages/ModelLineageContext.ts @@ -9,8 +9,6 @@ import { getInitial as getLineageContextInitial, getColumnLevelLineageContextInitial, createLineageContext, - type LineageAdjacencyList, - type LineageDetails, type ColumnLevelLineageAdjacencyList, } from '@sqlmesh-common/components/Lineage' From 0df3cec880937bad901b9cb424903d2820447679 Mon Sep 17 00:00:00 2001 From: Max Mykal Date: Mon, 13 Oct 2025 15:58:22 -0700 Subject: [PATCH 18/20] fix test --- vscode/extension/tests/lineage_settings.spec.ts | 2 +- vscode/react/src/pages/ModelLineage.tsx | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/vscode/extension/tests/lineage_settings.spec.ts b/vscode/extension/tests/lineage_settings.spec.ts index c3237f13dc..bc1a379772 100644 --- a/vscode/extension/tests/lineage_settings.spec.ts +++ b/vscode/extension/tests/lineage_settings.spec.ts @@ -48,7 +48,7 @@ test('Settings button is visible in the lineage view', async ({ try { await activeFrame .getByRole('button', { - name: 'Settings', + name: 'Reset', }) .waitFor({ timeout: 1000 }) settingsCount++ diff --git a/vscode/react/src/pages/ModelLineage.tsx b/vscode/react/src/pages/ModelLineage.tsx index e5e13c05a6..0c62ead240 100644 --- a/vscode/react/src/pages/ModelLineage.tsx +++ b/vscode/react/src/pages/ModelLineage.tsx @@ -278,14 +278,14 @@ export const ModelLineage = ({ return edgesColumnLevel.length > 0 ? edgesColumnLevel : getTransformedModelEdgesTargetSources< - ModelFQN, - EdgeData, - ModelEdgeId, - ModelNodeId, - ModelNodeId, - ModelColumnRightHandleId, - ModelColumnLeftHandleId - >(adjacencyListKeys, adjacencyList, transformEdge) + ModelFQN, + EdgeData, + ModelEdgeId, + ModelNodeId, + ModelNodeId, + ModelColumnRightHandleId, + ModelColumnLeftHandleId + >(adjacencyListKeys, adjacencyList, transformEdge) }, [adjacencyListKeys, adjacencyList, transformEdge, edgesColumnLevel]) const calculateLayout = React.useCallback( From 30e9e671627e211e9a4a8676dcf6fce0993bfd22 Mon Sep 17 00:00:00 2001 From: Max Mykal Date: Mon, 13 Oct 2025 15:58:52 -0700 Subject: [PATCH 19/20] formattign --- vscode/react/src/pages/ModelLineage.tsx | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/vscode/react/src/pages/ModelLineage.tsx b/vscode/react/src/pages/ModelLineage.tsx index 0c62ead240..e5e13c05a6 100644 --- a/vscode/react/src/pages/ModelLineage.tsx +++ b/vscode/react/src/pages/ModelLineage.tsx @@ -278,14 +278,14 @@ export const ModelLineage = ({ return edgesColumnLevel.length > 0 ? edgesColumnLevel : getTransformedModelEdgesTargetSources< - ModelFQN, - EdgeData, - ModelEdgeId, - ModelNodeId, - ModelNodeId, - ModelColumnRightHandleId, - ModelColumnLeftHandleId - >(adjacencyListKeys, adjacencyList, transformEdge) + ModelFQN, + EdgeData, + ModelEdgeId, + ModelNodeId, + ModelNodeId, + ModelColumnRightHandleId, + ModelColumnLeftHandleId + >(adjacencyListKeys, adjacencyList, transformEdge) }, [adjacencyListKeys, adjacencyList, transformEdge, edgesColumnLevel]) const calculateLayout = React.useCallback( From 15723fabe189d3709e5b06d221bcfbf158671818 Mon Sep 17 00:00:00 2001 From: Max Mykal Date: Tue, 14 Oct 2025 08:20:36 -0700 Subject: [PATCH 20/20] add lineage controls test --- .../extension/tests/lineage_settings.spec.ts | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/vscode/extension/tests/lineage_settings.spec.ts b/vscode/extension/tests/lineage_settings.spec.ts index bc1a379772..1af9fed8f1 100644 --- a/vscode/extension/tests/lineage_settings.spec.ts +++ b/vscode/extension/tests/lineage_settings.spec.ts @@ -47,9 +47,22 @@ test('Settings button is visible in the lineage view', async ({ if (activeFrame) { try { await activeFrame - .getByRole('button', { - name: 'Reset', - }) + .getByRole('button', { name: 'Zoom In' }) + .waitFor({ timeout: 1000 }) + await activeFrame + .getByRole('button', { name: 'Zoom Out' }) + .waitFor({ timeout: 1000 }) + await activeFrame + .getByRole('button', { name: 'Only selected nodes' }) + .waitFor({ timeout: 1000 }) + await activeFrame + .getByRole('button', { name: 'Zoom to selected node' }) + .waitFor({ timeout: 1000 }) + await activeFrame + .getByRole('button', { name: 'Show columns' }) + .waitFor({ timeout: 1000 }) + await activeFrame + .getByRole('button', { name: 'Reset' }) .waitFor({ timeout: 1000 }) settingsCount++ } catch {