Skip to content

Commit ba58c85

Browse files
committed
feat(ui): add tag-based node color coding for lineage DAG
Signed-off-by: CTC97 <connor.curnin@translucent.co>
1 parent fa46264 commit ba58c85

8 files changed

Lines changed: 57 additions & 3 deletions

File tree

sqlmesh/core/config/ui.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
from __future__ import annotations
22

3+
import typing as t
4+
35
from sqlmesh.core.config.base import BaseConfig
46

57

@@ -8,6 +10,9 @@ class UIConfig(BaseConfig):
810
911
Args:
1012
format_on_save: Whether to format the SQL code on save or not.
13+
node_colors: A mapping of model tags to hex color strings used
14+
to color-code nodes in the lineage DAG visualization.
1115
"""
1216

1317
format_on_save: bool = True
18+
node_colors: t.Dict[str, str] = {}

web/client/openapi.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1371,6 +1371,12 @@
13711371
"type": "boolean",
13721372
"title": "Has Running Task",
13731373
"default": false
1374+
},
1375+
"node_colors": {
1376+
"additionalProperties": { "type": "string" },
1377+
"type": "object",
1378+
"title": "Node Colors",
1379+
"default": {}
13741380
}
13751381
},
13761382
"additionalProperties": false,

web/client/src/context/context.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { isNil, isStringEmptyOrNil, isTrue } from '~/utils'
1212

1313
interface ContextStore {
1414
version?: string
15+
nodeColors: Record<string, string>
1516
showConfirmation: boolean
1617
confirmations: Confirmation[]
1718
environment: ModelEnvironment
@@ -23,6 +24,7 @@ interface ContextStore {
2324
setLastSelectedModel: (model?: ModelSQLMeshModel) => void
2425
setSplitPaneSizes: (splitPaneSizes: number[]) => void
2526
setModules: (modules: ModelModuleController) => void
27+
setNodeColors: (nodeColors: Record<string, string>) => void
2628
setVersion: (version?: string) => void
2729
setShowConfirmation: (showConfirmation: boolean) => void
2830
addConfirmation: (confirmation: Confirmation) => void
@@ -55,6 +57,7 @@ const environment =
5557

5658
export const useStoreContext = create<ContextStore>((set, get) => ({
5759
version: undefined,
60+
nodeColors: {},
5861
modules: new ModelModuleController(),
5962
splitPaneSizes: [20, 80],
6063
showConfirmation: false,
@@ -83,6 +86,11 @@ export const useStoreContext = create<ContextStore>((set, get) => ({
8386
splitPaneSizes,
8487
}))
8588
},
89+
setNodeColors(nodeColors) {
90+
set(() => ({
91+
nodeColors,
92+
}))
93+
},
8694
setVersion(version) {
8795
set(() => ({
8896
version,

web/client/src/library/components/graph/ModelNode.tsx

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { isNil, isArrayNotEmpty, isNotNil, toID, isFalse } from '@utils/index'
22
import clsx from 'clsx'
33
import { useMemo, useCallback, useState, useRef } from 'react'
44
import { ModelType } from '@api/client'
5+
import { useStoreContext } from '@context/context'
56
import { useLineageFlow } from './context'
67
import { type GraphNodeData } from './help'
78
import { Position, type NodeProps, NodeResizeControl } from 'reactflow'
@@ -44,6 +45,7 @@ export default function ModelNode({
4445
highlightedNodes,
4546
activeNodes,
4647
} = useLineageFlow()
48+
const nodeColors = useStoreContext(s => s.nodeColors)
4749

4850
const columns: Column[] = useMemo(() => {
4951
const model = models.get(id)
@@ -113,6 +115,16 @@ export default function ModelNode({
113115
[setSelectedNodes, highlightedNodeModels],
114116
)
115117

118+
const tagColor = useMemo(() => {
119+
const tags = nodeData.tags
120+
if (isNil(tags) || Object.keys(nodeColors).length === 0) return undefined
121+
for (const tag of tags) {
122+
const color = nodeColors[tag]
123+
if (color) return color
124+
}
125+
return undefined
126+
}, [nodeData.tags, nodeColors])
127+
116128
const splat = highlightedNodes['*']
117129
const hasSelectedColumns = columns.some(({ name }) =>
118130
connections.get(toID(id, name)),
@@ -183,7 +195,12 @@ export default function ModelNode({
183195
? 'ring-8 ring-neutral-50'
184196
: isSelected && 'ring-8 ring-secondary-50 dark:ring-primary-50',
185197
)}
186-
style={{ width: '100%' }}
198+
style={{
199+
width: '100%',
200+
...(tagColor != null
201+
? { borderColor: tagColor, backgroundColor: tagColor, color: tagColor }
202+
: {}),
203+
}}
187204
>
188205
<NodeResizeControl
189206
minWidth={150}

web/client/src/library/components/graph/help.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ export interface GraphNodeData {
2323
label: string
2424
type: LineageNodeModelType
2525
withColumns: boolean
26+
tags?: string[]
2627
[key: string]: any
2728
}
2829

@@ -172,9 +173,14 @@ function getNodeMap({
172173

173174
return modelNames.reduce((acc: Record<string, Node>, modelName: string) => {
174175
const model = models.get(modelName)
176+
const tagsStr = model?.details?.tags
177+
const tags = tagsStr
178+
? tagsStr.split(',').map(t => t.trim()).filter(Boolean)
179+
: undefined
175180
const node = createGraphNode(modelName, {
176181
label: model?.displayName ?? modelName,
177182
withColumns,
183+
tags,
178184
type: isNotNil(model)
179185
? (model.type as LineageNodeModelType)
180186
: // If model name present in lineage but not in global models

web/client/src/library/pages/root/Root.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ export default function Root({
106106
const closeTab = useStoreEditor(s => s.closeTab)
107107
const inTabs = useStoreEditor(s => s.inTabs)
108108
const setVersion = useStoreContext(s => s.setVersion)
109+
const setNodeColors = useStoreContext(s => s.setNodeColors)
109110

110111
const { refetch: getMeta, cancel: cancelRequestMeta } = useApiMeta()
111112
const { refetch: getModels, cancel: cancelRequestModels } = useApiModels()
@@ -313,6 +314,7 @@ export default function Root({
313314
useEffect(() => {
314315
void getMeta().then(({ data }) => {
315316
setVersion(data?.version)
317+
setNodeColors(data?.node_colors ?? {})
316318

317319
if (isTrue(data?.has_running_task)) {
318320
setPlanAction(

web/server/api/endpoints/meta.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from sqlmesh.cli.main import _sqlmesh_version
66
from web.server import models
77
from web.server.console import api_console
8-
from web.server.settings import Settings, get_settings
8+
from web.server.settings import Settings, get_context, get_settings
99

1010
router = APIRouter()
1111

@@ -32,4 +32,13 @@ def get_api_meta(
3232
if not has_running_task and api_console.is_cancelling_plan():
3333
api_console.finish_plan_cancellation()
3434

35-
return models.Meta(version=_sqlmesh_version(), has_running_task=has_running_task)
35+
node_colors: dict[str, str] = {}
36+
context = get_context(settings)
37+
if context:
38+
node_colors = context.config.ui.node_colors
39+
40+
return models.Meta(
41+
version=_sqlmesh_version(),
42+
has_running_task=has_running_task,
43+
node_colors=node_colors,
44+
)

web/server/models.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,7 @@ class Directory(PydanticModel):
133133
class Meta(PydanticModel):
134134
version: str
135135
has_running_task: bool = False
136+
node_colors: t.Dict[str, str] = {}
136137

137138

138139
class Reference(PydanticModel):

0 commit comments

Comments
 (0)