From d9b9f273f3e379257fc8ca1dddeee186bb2e098f Mon Sep 17 00:00:00 2001 From: wicky <130177258+wicky-zipstack@users.noreply.github.com> Date: Fri, 10 Apr 2026 21:49:52 +0530 Subject: [PATCH 1/6] feat: lineage scoping, explorer dependency chain sort and child indent - Lineage scoping: highlight selected model's parent/child chain, fade unrelated nodes - Selected model shown with dashed blue border, related edges in blue - Explorer: add references to API, dependency chain sort (default), A-Z, Z-A, execution order - Child models indented in explorer to show hierarchy - Explorer auto-refreshes on model config save via setRefreshModels - Applied in both bottom section lineage and standalone LineageTab --- .../file_explorer/file_explorer.py | 4 + .../ide/editor/lineage-tab/lineage-tab.jsx | 132 ++++++++++++++++-- .../editor/no-code-model/no-code-model.jsx | 103 +++++++++++++- .../src/ide/explorer/explorer-component.jsx | 95 +++++++++++++ frontend/src/ide/ide-layout.css | 5 + frontend/src/store/project-store.js | 6 + 6 files changed, 331 insertions(+), 14 deletions(-) diff --git a/backend/backend/application/file_explorer/file_explorer.py b/backend/backend/application/file_explorer/file_explorer.py index 79402ba..2bbf3db 100644 --- a/backend/backend/application/file_explorer/file_explorer.py +++ b/backend/backend/application/file_explorer/file_explorer.py @@ -93,6 +93,9 @@ def load_models(self, session: Session): # Sort models by execution order (DAG order) sorted_model_names = topological_sort_models(models_with_refs) + # Build lookup for references by model name + refs_by_name = {m["model_name"]: m["references"] for m in models_with_refs} + # Build the model structure in sorted order no_code_model_structure = [] for no_code_model_name in sorted_model_names: @@ -103,6 +106,7 @@ def load_models(self, session: Session): "key": f"{self.project_name}/models/no_code/{no_code_model_name}", "is_folder": False, "type": "NO_CODE_MODEL", + "references": refs_by_name.get(no_code_model_name, []), } ) model_structure: dict[str, Any] = { diff --git a/frontend/src/ide/editor/lineage-tab/lineage-tab.jsx b/frontend/src/ide/editor/lineage-tab/lineage-tab.jsx index 624238c..d10ec15 100644 --- a/frontend/src/ide/editor/lineage-tab/lineage-tab.jsx +++ b/frontend/src/ide/editor/lineage-tab/lineage-tab.jsx @@ -289,7 +289,81 @@ const transformLineageData = (data) => { return data; }; -function LineageTab({ nodeData }) { +// Find all ancestor and descendant node IDs for a given model +const getRelatedNodeIds = (allEdges, selectedLabel, allNodes) => { + const nodeByLabel = {}; + allNodes.forEach((n) => { + nodeByLabel[n.data.originalLabel || n.data.label] = n.id; + }); + const selectedId = nodeByLabel[selectedLabel]; + if (!selectedId) return null; + + const related = new Set([selectedId]); + const findAncestors = (id) => { + allEdges.forEach((e) => { + if (e.target === id && !related.has(e.source)) { + related.add(e.source); + findAncestors(e.source); + } + }); + }; + const findDescendants = (id) => { + allEdges.forEach((e) => { + if (e.source === id && !related.has(e.target)) { + related.add(e.target); + findDescendants(e.target); + } + }); + }; + findAncestors(selectedId); + findDescendants(selectedId); + return related; +}; + +const applyScopedStyles = (layoutedNodes, layoutedEdges, selectedLabel) => { + const rawEdges = layoutedEdges.map((e) => ({ + source: e.source, + target: e.target, + })); + const related = getRelatedNodeIds(rawEdges, selectedLabel, layoutedNodes); + if (!related) return { nodes: layoutedNodes, edges: layoutedEdges }; + + const styledNodes = layoutedNodes.map((node) => { + const nodeLabel = node.data.originalLabel || node.data.label; + const isSelected = nodeLabel === selectedLabel; + const isRelated = related.has(node.id); + return { + ...node, + style: { + ...node.style, + opacity: isRelated ? 1 : 0.25, + border: isSelected + ? "2px dashed #1677ff" + : node.style?.border || "1px solid var(--black)", + }, + }; + }); + + const relatedEdgeSet = new Set(); + layoutedEdges.forEach((e) => { + if (related.has(e.source) && related.has(e.target)) { + relatedEdgeSet.add(e.id); + } + }); + + const styledEdges = layoutedEdges.map((edge) => ({ + ...edge, + style: { + ...edge.style, + opacity: relatedEdgeSet.has(edge.id) ? 1 : 0.15, + stroke: relatedEdgeSet.has(edge.id) ? "#1677ff" : undefined, + }, + })); + + return { nodes: styledNodes, edges: styledEdges }; +}; + +function LineageTab({ nodeData, selectedModelName }) { const axios = useAxiosPrivate(); const { selectedOrgId } = orgStore(); const { projectId } = useProjectStore(); @@ -486,15 +560,32 @@ function LineageTab({ nodeData }) { transformedData.edges, layoutDirection ); - setNodes(layoutedNodes); - setEdges(layoutedEdges); + if (selectedModelName) { + const scoped = applyScopedStyles( + layoutedNodes, + layoutedEdges, + selectedModelName + ); + setNodes(scoped.nodes); + setEdges(scoped.edges); + } else { + setNodes(layoutedNodes); + setEdges(layoutedEdges); + } }) .catch((error) => { console.error(error); notify({ error }); setLineageData({}); }); - }, [projectId, selectedOrgId, setNodes, setEdges, layoutDirection]); + }, [ + projectId, + selectedOrgId, + setNodes, + setEdges, + layoutDirection, + selectedModelName, + ]); const handleToggleLayout = useCallback(() => { const newDirection = layoutDirection === "TB" ? "LR" : "TB"; @@ -504,10 +595,20 @@ function LineageTab({ nodeData }) { if (lineageData && lineageData.nodes && lineageData.edges) { const { nodes: layoutedNodes, edges: layoutedEdges } = getLayoutedElements(lineageData.nodes, lineageData.edges, newDirection); - setNodes(layoutedNodes); - setEdges(layoutedEdges); + if (selectedModelName) { + const scoped = applyScopedStyles( + layoutedNodes, + layoutedEdges, + selectedModelName + ); + setNodes(scoped.nodes); + setEdges(scoped.edges); + } else { + setNodes(layoutedNodes); + setEdges(layoutedEdges); + } } - }, [layoutDirection, lineageData, setNodes, setEdges]); + }, [layoutDirection, lineageData, setNodes, setEdges, selectedModelName]); // Fetch sequence data for a model const fetchSequenceData = useCallback( @@ -674,15 +775,25 @@ function LineageTab({ nodeData }) { transformedData.edges, "TB" ); - setNodes(layoutedNodes); - setEdges(layoutedEdges); + if (selectedModelName) { + const scoped = applyScopedStyles( + layoutedNodes, + layoutedEdges, + selectedModelName + ); + setNodes(scoped.nodes); + setEdges(scoped.edges); + } else { + setNodes(layoutedNodes); + setEdges(layoutedEdges); + } }) .catch((error) => { console.error(error); notify({ error }); setLineageData({}); }); - }, [projectId, selectedOrgId, setNodes, setEdges]); + }, [projectId, selectedOrgId, setNodes, setEdges, selectedModelName]); if (!lineageData) { return ; @@ -957,6 +1068,7 @@ function LineageTab({ nodeData }) { LineageTab.propTypes = { nodeData: PropTypes.object, + selectedModelName: PropTypes.string, }; export { LineageTab }; diff --git a/frontend/src/ide/editor/no-code-model/no-code-model.jsx b/frontend/src/ide/editor/no-code-model/no-code-model.jsx index 80304d0..bb83974 100644 --- a/frontend/src/ide/editor/no-code-model/no-code-model.jsx +++ b/frontend/src/ide/editor/no-code-model/no-code-model.jsx @@ -261,8 +261,18 @@ function NoCodeModel({ nodeData }) { if (lineageData?.nodes && lineageData?.edges) { const { nodes: layoutedNodes, edges: layoutedEdges } = getLayoutedElements(lineageData.nodes, lineageData.edges, newDirection); - setNodes(layoutedNodes); - setEdges(layoutedEdges); + if (modelName) { + const scoped = applyScopedStyles( + layoutedNodes, + layoutedEdges, + modelName + ); + setNodes(scoped.nodes); + setEdges(scoped.edges); + } else { + setNodes(layoutedNodes); + setEdges(layoutedEdges); + } } }; @@ -740,6 +750,7 @@ function NoCodeModel({ nodeData }) { setSeqEdges(layoutedEdges); runTransformation(res?.data?.model_data); setConfigApply(true); + setRefreshModels(true); handleModalClose("ok"); }) .catch((error) => { @@ -2159,6 +2170,80 @@ function NoCodeModel({ nodeData }) { ); }; + // Find all ancestor and descendant node IDs for a given model + const getRelatedNodeIds = (allEdges, selectedLabel, allNodes) => { + const nodeByLabel = {}; + allNodes.forEach((n) => { + nodeByLabel[n.data.originalLabel || n.data.label] = n.id; + }); + const selectedId = nodeByLabel[selectedLabel]; + if (!selectedId) return null; + + const related = new Set([selectedId]); + const findAncestors = (id) => { + allEdges.forEach((e) => { + if (e.target === id && !related.has(e.source)) { + related.add(e.source); + findAncestors(e.source); + } + }); + }; + const findDescendants = (id) => { + allEdges.forEach((e) => { + if (e.source === id && !related.has(e.target)) { + related.add(e.target); + findDescendants(e.target); + } + }); + }; + findAncestors(selectedId); + findDescendants(selectedId); + return related; + }; + + const applyScopedStyles = (layoutedNodes, layoutedEdges, selectedLabel) => { + const rawEdges = layoutedEdges.map((e) => ({ + source: e.source, + target: e.target, + })); + const related = getRelatedNodeIds(rawEdges, selectedLabel, layoutedNodes); + if (!related) return { nodes: layoutedNodes, edges: layoutedEdges }; + + const styledNodes = layoutedNodes.map((node) => { + const nodeLabel = node.data.originalLabel || node.data.label; + const isSelected = nodeLabel === selectedLabel; + const isRelated = related.has(node.id); + return { + ...node, + style: { + ...node.style, + opacity: isRelated ? 1 : 0.25, + border: isSelected + ? "2px dashed #1677ff" + : node.style?.border || "1px solid var(--black)", + }, + }; + }); + + const relatedEdgeSet = new Set(); + layoutedEdges.forEach((e) => { + if (related.has(e.source) && related.has(e.target)) { + relatedEdgeSet.add(e.id); + } + }); + + const styledEdges = layoutedEdges.map((edge) => ({ + ...edge, + style: { + ...edge.style, + opacity: relatedEdgeSet.has(edge.id) ? 1 : 0.15, + stroke: relatedEdgeSet.has(edge.id) ? "#1677ff" : undefined, + }, + })); + + return { nodes: styledNodes, edges: styledEdges }; + }; + const getLineageData = (callSample = false) => { if (!projectId) return; setLineageData(); @@ -2174,8 +2259,18 @@ function NoCodeModel({ nodeData }) { setLineageData(data); const { nodes: layoutedNodes, edges: layoutedEdges } = getLayoutedElements(data.nodes, data.edges, lineageLayoutDirection); - setNodes(layoutedNodes); - setEdges(layoutedEdges); + if (modelName) { + const scoped = applyScopedStyles( + layoutedNodes, + layoutedEdges, + modelName + ); + setNodes(scoped.nodes); + setEdges(scoped.edges); + } else { + setNodes(layoutedNodes); + setEdges(layoutedEdges); + } }) .catch((error) => { console.error(error); diff --git a/frontend/src/ide/explorer/explorer-component.jsx b/frontend/src/ide/explorer/explorer-component.jsx index 83ef24a..60f1753 100644 --- a/frontend/src/ide/explorer/explorer-component.jsx +++ b/frontend/src/ide/explorer/explorer-component.jsx @@ -82,6 +82,7 @@ const IdeExplorer = ({ } = useProjectStore(); const currentSchema = useProjectStore((state) => state.currentSchema); const setCurrentSchema = useProjectStore((state) => state.setCurrentSchema); + const setSchemaList = useProjectStore((state) => state.setSchemaList); // Reset currentSchema on unmount to prevent stale data useEffect(() => { @@ -127,6 +128,7 @@ const IdeExplorer = ({ const [isModalVisible, setIsModalVisible] = useState(false); const [uploading, setUploading] = useState(false); const [fileList, setFileList] = useState([]); + const [modelSortBy, setModelSortBy] = useState("dep_chain"); const MAX_FILE_SIZE_MB = 50; const MAX_FILE_SIZE_BYTES = MAX_FILE_SIZE_MB * 1024 * 1024; const { refreshModels, setRefreshModels } = useRefreshModelsStore(); @@ -188,6 +190,62 @@ const IdeExplorer = ({ }, 0); }; + // Sort no_code models based on sort option + const sortModels = (models, sortBy) => { + if (sortBy === "alpha_asc") { + return [...models].sort((a, b) => a.title.localeCompare(b.title)); + } + if (sortBy === "alpha_desc") { + return [...models].sort((a, b) => b.title.localeCompare(a.title)); + } + if (sortBy === "dep_chain") { + const modelSet = new Set(models.map((m) => m.title)); + const childrenOf = {}; + models.forEach((m) => { + (m.references || []).forEach((ref) => { + if (modelSet.has(ref)) { + if (!childrenOf[ref]) childrenOf[ref] = []; + childrenOf[ref].push(m.title); + } + }); + }); + const modelByName = {}; + models.forEach((m) => { + modelByName[m.title] = m; + }); + const visited = new Set(); + const result = []; + const addChain = (name) => { + if (visited.has(name) || !modelByName[name]) return; + visited.add(name); + result.push(modelByName[name]); + (childrenOf[name] || []).forEach(addChain); + }; + models.forEach((m) => { + const refs = (m.references || []).filter((r) => modelSet.has(r)); + if (refs.length === 0) addChain(m.title); + }); + models.forEach((m) => { + if (!visited.has(m.title)) result.push(m); + }); + return result; + } + return [...models]; + }; + + const applyModelDecorations = (models) => { + models.forEach((m) => { + if ((m.references || []).length > 0) { + m._isChild = true; + } + }); + }; + + const handleModelSort = (key) => { + setModelSortBy(key); + rebuildTree(); + }; + // Function to map string icons from API to actual icon components // depth: 0 = root (Database), 1 = schema, 2 = table, 3 = column const mapIconsToTreeData = (data, depth = 0) => { @@ -308,6 +366,9 @@ const IdeExplorer = ({ setCurrentSchema(""); } + // Store plain schema list in shared store + setSchemaList(allSchemas); + const items = allSchemas.map((el) => ({ label: el, key: el, @@ -350,6 +411,9 @@ const IdeExplorer = ({ data.map((item) => { if (item.title === "no_code") { item.children = item.children || []; + // Sort and decorate models + item.children = sortModels(item.children, modelSortBy); + applyModelDecorations(item.children); // Clean up stale selected model keys const currentModelKeys = item.children.map((c) => c.key); const filtered = selectedModelKeysRef.current.filter((k) => @@ -455,6 +519,32 @@ const IdeExplorer = ({ + handleModelSort(key), + }} + trigger={["click"]} + placement="bottomRight" + > + + + + + + )} @@ -2446,6 +2536,11 @@ function transformTree(tree) { // change is_folder to isLeaf key and delete is_folder delete Object.assign(node, { isLeaf: !node.is_folder }).is_folder; + // Indent child/reference models + if (node._isChild) { + node.className = "explorer-child-model"; + } + if (node.children) { transformTree(node.children); } diff --git a/frontend/src/ide/ide-layout.css b/frontend/src/ide/ide-layout.css index 4a58aa7..db14961 100644 --- a/frontend/src/ide/ide-layout.css +++ b/frontend/src/ide/ide-layout.css @@ -79,6 +79,11 @@ width: 8px !important; } +/* Indent child/reference models to show hierarchy */ +.explorerTree .explorer-child-model { + padding-left: 12px !important; +} + .contextMenu { position: fixed; z-index: 1000; diff --git a/frontend/src/store/project-store.js b/frontend/src/store/project-store.js index e99f8d6..67be3b8 100644 --- a/frontend/src/store/project-store.js +++ b/frontend/src/store/project-store.js @@ -9,6 +9,7 @@ const STORE_VARIABLES = { projectId: "", renamedModel: {}, currentSchema: "", + schemaList: [], }; const useProjectStore = create( @@ -71,6 +72,11 @@ const useProjectStore = create( return { currentSchema: schema }; }); }, + setSchemaList: (list) => { + setState(() => { + return { schemaList: list }; + }); + }, }), { name: "project-tab-storage", From 24bb897037069f819f10402d06bc8cb2e53d3022 Mon Sep 17 00:00:00 2001 From: wicky <130177258+wicky-zipstack@users.noreply.github.com> Date: Fri, 10 Apr 2026 22:22:55 +0530 Subject: [PATCH 2/6] =?UTF-8?q?fix:=20address=20PR=20review=20=E2=80=94=20?= =?UTF-8?q?stale=20closure=20in=20sort,=20false=20child=20indent=20for=20e?= =?UTF-8?q?xternal=20refs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - handleModelSort: pass sortBy directly to sortModels instead of relying on async state - applyModelDecorations: filter references to only include sibling model names, ignore external table references --- .../src/ide/explorer/explorer-component.jsx | 25 ++++++++++++++++--- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/frontend/src/ide/explorer/explorer-component.jsx b/frontend/src/ide/explorer/explorer-component.jsx index 60f1753..cf9c39b 100644 --- a/frontend/src/ide/explorer/explorer-component.jsx +++ b/frontend/src/ide/explorer/explorer-component.jsx @@ -234,16 +234,33 @@ const IdeExplorer = ({ }; const applyModelDecorations = (models) => { + // Build set of model names to distinguish from external table references + const modelNames = new Set(models.map((m) => m.title)); models.forEach((m) => { - if ((m.references || []).length > 0) { + const refs = (m.references || []).filter((r) => modelNames.has(r)); + if (refs.length > 0) { m._isChild = true; } }); }; - const handleModelSort = (key) => { - setModelSortBy(key); - rebuildTree(); + const handleModelSort = (sortBy) => { + setModelSortBy(sortBy); + if (rawTreeDataRef.current.length > 0) { + const freshData = JSON.parse(JSON.stringify(rawTreeDataRef.current)); + freshData.forEach((node) => { + if (node.title === "models" && node.children) { + node.children.forEach((child) => { + if (child.title === "no_code" && child.children) { + child.children = sortModels(child.children, sortBy); + applyModelDecorations(child.children); + } + }); + } + }); + transformTree(freshData); + setTreeData(freshData, false); + } }; // Function to map string icons from API to actual icon components From 835752d9365b516fc6718d161fe7cc98a8208d05 Mon Sep 17 00:00:00 2001 From: wicky <130177258+wicky-zipstack@users.noreply.github.com> Date: Mon, 13 Apr 2026 13:59:00 +0530 Subject: [PATCH 3/6] fix: address PR #56 review feedback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove !important from explorer-child-model padding — use higher specificity selector instead - Move static sort dropdown items to MODULE_SORT_ITEMS constant outside component - Memoize sort menu config with useMemo to avoid recreation on every render - Wrap handleModelSort with useCallback --- .../src/ide/explorer/explorer-component.jsx | 81 +++++++++++-------- frontend/src/ide/ide-layout.css | 4 +- 2 files changed, 48 insertions(+), 37 deletions(-) diff --git a/frontend/src/ide/explorer/explorer-component.jsx b/frontend/src/ide/explorer/explorer-component.jsx index cf9c39b..38c0d0b 100644 --- a/frontend/src/ide/explorer/explorer-component.jsx +++ b/frontend/src/ide/explorer/explorer-component.jsx @@ -1,4 +1,10 @@ -import React, { useState, useCallback, useRef, useEffect } from "react"; +import React, { + useState, + useCallback, + useRef, + useEffect, + useMemo, +} from "react"; import { createPortal } from "react-dom"; import PropTypes from "prop-types"; import { @@ -58,6 +64,14 @@ import { SpinnerLoader } from "../../widgets/spinner_loader/index.js"; import { useRefreshModelsStore } from "../../store/refresh-models-store.js"; import { LinearScale } from "../../base/icons"; +// Static sort options for model explorer +const MODEL_SORT_ITEMS = [ + { label: "Dependency Chain", key: "dep_chain" }, + { label: "Execution Order", key: "exec_order" }, + { label: "A \u2192 Z", key: "alpha_asc" }, + { label: "Z \u2192 A", key: "alpha_desc" }, +]; + const IdeExplorer = ({ currentNode, onSelect = () => {}, @@ -244,24 +258,36 @@ const IdeExplorer = ({ }); }; - const handleModelSort = (sortBy) => { - setModelSortBy(sortBy); - if (rawTreeDataRef.current.length > 0) { - const freshData = JSON.parse(JSON.stringify(rawTreeDataRef.current)); - freshData.forEach((node) => { - if (node.title === "models" && node.children) { - node.children.forEach((child) => { - if (child.title === "no_code" && child.children) { - child.children = sortModels(child.children, sortBy); - applyModelDecorations(child.children); - } - }); - } - }); - transformTree(freshData); - setTreeData(freshData, false); - } - }; + const handleModelSort = useCallback( + (sortBy) => { + setModelSortBy(sortBy); + if (rawTreeDataRef.current.length > 0) { + const freshData = JSON.parse(JSON.stringify(rawTreeDataRef.current)); + freshData.forEach((node) => { + if (node.title === "models" && node.children) { + node.children.forEach((child) => { + if (child.title === "no_code" && child.children) { + child.children = sortModels(child.children, sortBy); + applyModelDecorations(child.children); + } + }); + } + }); + transformTree(freshData); + setTreeData(freshData, false); + } + }, + [setTreeData] + ); + + const modelSortMenu = useMemo( + () => ({ + items: MODEL_SORT_ITEMS, + selectedKeys: [modelSortBy], + onClick: ({ key }) => handleModelSort(key), + }), + [modelSortBy, handleModelSort] + ); // Function to map string icons from API to actual icon components // depth: 0 = root (Database), 1 = schema, 2 = table, 3 = column @@ -537,22 +563,7 @@ const IdeExplorer = ({ handleModelSort(key), - }} + menu={modelSortMenu} trigger={["click"]} placement="bottomRight" > diff --git a/frontend/src/ide/ide-layout.css b/frontend/src/ide/ide-layout.css index db14961..b3fcfec 100644 --- a/frontend/src/ide/ide-layout.css +++ b/frontend/src/ide/ide-layout.css @@ -80,8 +80,8 @@ } /* Indent child/reference models to show hierarchy */ -.explorerTree .explorer-child-model { - padding-left: 12px !important; +.explorerTree .ant-tree-treenode.explorer-child-model { + padding-left: 12px; } .contextMenu { From 1a8ef53061a54133a30d6d46652e8d1e77411192 Mon Sep 17 00:00:00 2001 From: wicky <130177258+wicky-zipstack@users.noreply.github.com> Date: Mon, 13 Apr 2026 17:34:30 +0530 Subject: [PATCH 4/6] =?UTF-8?q?fix:=20address=20PR=20#56=20review=20?= =?UTF-8?q?=E2=80=94=20extract=20shared=20lineage=20utils,=20CSS=20variabl?= =?UTF-8?q?e,=20exec=5Forder?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Extract getRelatedNodeIds and applyScopedStyles into shared lineage-utils.js - Remove duplicate functions from lineage-tab.jsx and no-code-model.jsx - Move hardcoded #1677ff to --lineage-selected-border CSS variable (light: #1677ff, dark: #69b1ff) - Add comment clarifying exec_order fallback uses backend's topological order --- .../ide/editor/lineage-tab/lineage-tab.jsx | 75 +------------- frontend/src/ide/editor/lineage-utils.js | 98 +++++++++++++++++++ .../editor/no-code-model/no-code-model.jsx | 74 +------------- .../src/ide/explorer/explorer-component.jsx | 1 + frontend/src/variables.css | 6 ++ 5 files changed, 107 insertions(+), 147 deletions(-) create mode 100644 frontend/src/ide/editor/lineage-utils.js diff --git a/frontend/src/ide/editor/lineage-tab/lineage-tab.jsx b/frontend/src/ide/editor/lineage-tab/lineage-tab.jsx index d10ec15..0905aae 100644 --- a/frontend/src/ide/editor/lineage-tab/lineage-tab.jsx +++ b/frontend/src/ide/editor/lineage-tab/lineage-tab.jsx @@ -40,6 +40,7 @@ import { THEME } from "../../../common/constants.js"; import { SpinnerLoader } from "../../../widgets/spinner_loader/index.js"; import { useNotificationService } from "../../../service/notification-service.js"; import { Tech } from "../../../base/icons/index.js"; +import { applyScopedStyles } from "../lineage-utils.js"; import "reactflow/dist/style.css"; import "./lineage-tab.css"; @@ -289,80 +290,6 @@ const transformLineageData = (data) => { return data; }; -// Find all ancestor and descendant node IDs for a given model -const getRelatedNodeIds = (allEdges, selectedLabel, allNodes) => { - const nodeByLabel = {}; - allNodes.forEach((n) => { - nodeByLabel[n.data.originalLabel || n.data.label] = n.id; - }); - const selectedId = nodeByLabel[selectedLabel]; - if (!selectedId) return null; - - const related = new Set([selectedId]); - const findAncestors = (id) => { - allEdges.forEach((e) => { - if (e.target === id && !related.has(e.source)) { - related.add(e.source); - findAncestors(e.source); - } - }); - }; - const findDescendants = (id) => { - allEdges.forEach((e) => { - if (e.source === id && !related.has(e.target)) { - related.add(e.target); - findDescendants(e.target); - } - }); - }; - findAncestors(selectedId); - findDescendants(selectedId); - return related; -}; - -const applyScopedStyles = (layoutedNodes, layoutedEdges, selectedLabel) => { - const rawEdges = layoutedEdges.map((e) => ({ - source: e.source, - target: e.target, - })); - const related = getRelatedNodeIds(rawEdges, selectedLabel, layoutedNodes); - if (!related) return { nodes: layoutedNodes, edges: layoutedEdges }; - - const styledNodes = layoutedNodes.map((node) => { - const nodeLabel = node.data.originalLabel || node.data.label; - const isSelected = nodeLabel === selectedLabel; - const isRelated = related.has(node.id); - return { - ...node, - style: { - ...node.style, - opacity: isRelated ? 1 : 0.25, - border: isSelected - ? "2px dashed #1677ff" - : node.style?.border || "1px solid var(--black)", - }, - }; - }); - - const relatedEdgeSet = new Set(); - layoutedEdges.forEach((e) => { - if (related.has(e.source) && related.has(e.target)) { - relatedEdgeSet.add(e.id); - } - }); - - const styledEdges = layoutedEdges.map((edge) => ({ - ...edge, - style: { - ...edge.style, - opacity: relatedEdgeSet.has(edge.id) ? 1 : 0.15, - stroke: relatedEdgeSet.has(edge.id) ? "#1677ff" : undefined, - }, - })); - - return { nodes: styledNodes, edges: styledEdges }; -}; - function LineageTab({ nodeData, selectedModelName }) { const axios = useAxiosPrivate(); const { selectedOrgId } = orgStore(); diff --git a/frontend/src/ide/editor/lineage-utils.js b/frontend/src/ide/editor/lineage-utils.js new file mode 100644 index 0000000..37c159b --- /dev/null +++ b/frontend/src/ide/editor/lineage-utils.js @@ -0,0 +1,98 @@ +/** + * Shared utility functions for lineage scoping. + * Used by both lineage-tab.jsx (standalone) and no-code-model.jsx (bottom section). + */ + +/** + * Find all ancestor and descendant node IDs for a given model. + * @param {Array} allEdges - Array of { source, target } edge objects + * @param {string} selectedLabel - The label of the selected model + * @param {Array} allNodes - Array of node objects with data.originalLabel or data.label + * @return {Set|null} Set of related node IDs, or null if selected model not found + */ +export const getRelatedNodeIds = (allEdges, selectedLabel, allNodes) => { + const nodeByLabel = {}; + allNodes.forEach((n) => { + nodeByLabel[n.data.originalLabel || n.data.label] = n.id; + }); + const selectedId = nodeByLabel[selectedLabel]; + if (!selectedId) return null; + + const related = new Set([selectedId]); + const findAncestors = (id) => { + allEdges.forEach((e) => { + if (e.target === id && !related.has(e.source)) { + related.add(e.source); + findAncestors(e.source); + } + }); + }; + const findDescendants = (id) => { + allEdges.forEach((e) => { + if (e.source === id && !related.has(e.target)) { + related.add(e.target); + findDescendants(e.target); + } + }); + }; + findAncestors(selectedId); + findDescendants(selectedId); + return related; +}; + +/** + * Apply scoped styles to nodes and edges based on the selected model's lineage chain. + * Related nodes stay full opacity, unrelated nodes are faded. + * @param {Array} layoutedNodes - Array of positioned node objects + * @param {Array} layoutedEdges - Array of edge objects + * @param {string} selectedLabel - The label of the selected model + * @return {Object} { nodes, edges } with scoped styles applied + */ +export const applyScopedStyles = ( + layoutedNodes, + layoutedEdges, + selectedLabel +) => { + const rawEdges = layoutedEdges.map((e) => ({ + source: e.source, + target: e.target, + })); + const related = getRelatedNodeIds(rawEdges, selectedLabel, layoutedNodes); + if (!related) return { nodes: layoutedNodes, edges: layoutedEdges }; + + const styledNodes = layoutedNodes.map((node) => { + const nodeLabel = node.data.originalLabel || node.data.label; + const isSelected = nodeLabel === selectedLabel; + const isRelated = related.has(node.id); + return { + ...node, + style: { + ...node.style, + opacity: isRelated ? 1 : 0.25, + border: isSelected + ? "2px dashed var(--lineage-selected-border)" + : node.style?.border || "1px solid var(--black)", + }, + }; + }); + + const relatedEdgeSet = new Set(); + layoutedEdges.forEach((e) => { + if (related.has(e.source) && related.has(e.target)) { + relatedEdgeSet.add(e.id); + } + }); + + const styledEdges = layoutedEdges.map((edge) => ({ + ...edge, + style: { + ...edge.style, + opacity: relatedEdgeSet.has(edge.id) ? 1 : 0.15, + stroke: relatedEdgeSet.has(edge.id) + ? "var(--lineage-selected-border)" + : undefined, + }, + })); + + return { nodes: styledNodes, edges: styledEdges }; +}; diff --git a/frontend/src/ide/editor/no-code-model/no-code-model.jsx b/frontend/src/ide/editor/no-code-model/no-code-model.jsx index bb83974..5754c02 100644 --- a/frontend/src/ide/editor/no-code-model/no-code-model.jsx +++ b/frontend/src/ide/editor/no-code-model/no-code-model.jsx @@ -69,6 +69,7 @@ import dagre from "dagre"; import { useAxiosPrivate } from "../../../service/axios-service.js"; import { NoCodeToolbar } from "../no-code-toolbar/no-code-toolbar.jsx"; import { NoCodeTopbar } from "../no-code-topbar/no-code-topbar.jsx"; +import { applyScopedStyles } from "../lineage-utils.js"; import { ConfigureSourceDestination } from "../no-code-configuration/configure-source-destination.jsx"; import { ConfigureJoins } from "../no-code-configuration/configure-joins.jsx"; import { useProjectStore } from "../../../store/project-store.js"; @@ -2171,79 +2172,6 @@ function NoCodeModel({ nodeData }) { }; // Find all ancestor and descendant node IDs for a given model - const getRelatedNodeIds = (allEdges, selectedLabel, allNodes) => { - const nodeByLabel = {}; - allNodes.forEach((n) => { - nodeByLabel[n.data.originalLabel || n.data.label] = n.id; - }); - const selectedId = nodeByLabel[selectedLabel]; - if (!selectedId) return null; - - const related = new Set([selectedId]); - const findAncestors = (id) => { - allEdges.forEach((e) => { - if (e.target === id && !related.has(e.source)) { - related.add(e.source); - findAncestors(e.source); - } - }); - }; - const findDescendants = (id) => { - allEdges.forEach((e) => { - if (e.source === id && !related.has(e.target)) { - related.add(e.target); - findDescendants(e.target); - } - }); - }; - findAncestors(selectedId); - findDescendants(selectedId); - return related; - }; - - const applyScopedStyles = (layoutedNodes, layoutedEdges, selectedLabel) => { - const rawEdges = layoutedEdges.map((e) => ({ - source: e.source, - target: e.target, - })); - const related = getRelatedNodeIds(rawEdges, selectedLabel, layoutedNodes); - if (!related) return { nodes: layoutedNodes, edges: layoutedEdges }; - - const styledNodes = layoutedNodes.map((node) => { - const nodeLabel = node.data.originalLabel || node.data.label; - const isSelected = nodeLabel === selectedLabel; - const isRelated = related.has(node.id); - return { - ...node, - style: { - ...node.style, - opacity: isRelated ? 1 : 0.25, - border: isSelected - ? "2px dashed #1677ff" - : node.style?.border || "1px solid var(--black)", - }, - }; - }); - - const relatedEdgeSet = new Set(); - layoutedEdges.forEach((e) => { - if (related.has(e.source) && related.has(e.target)) { - relatedEdgeSet.add(e.id); - } - }); - - const styledEdges = layoutedEdges.map((edge) => ({ - ...edge, - style: { - ...edge.style, - opacity: relatedEdgeSet.has(edge.id) ? 1 : 0.15, - stroke: relatedEdgeSet.has(edge.id) ? "#1677ff" : undefined, - }, - })); - - return { nodes: styledNodes, edges: styledEdges }; - }; - const getLineageData = (callSample = false) => { if (!projectId) return; setLineageData(); diff --git a/frontend/src/ide/explorer/explorer-component.jsx b/frontend/src/ide/explorer/explorer-component.jsx index 38c0d0b..13e2204 100644 --- a/frontend/src/ide/explorer/explorer-component.jsx +++ b/frontend/src/ide/explorer/explorer-component.jsx @@ -244,6 +244,7 @@ const IdeExplorer = ({ }); return result; } + // "exec_order" and default: keep original backend order (topological/execution) return [...models]; }; diff --git a/frontend/src/variables.css b/frontend/src/variables.css index 036d9c4..f5db37f 100644 --- a/frontend/src/variables.css +++ b/frontend/src/variables.css @@ -81,6 +81,9 @@ --node-color-blue: #b0e3f9; --node-color-yellow: #ffdd8a; --node-color-pink: #ffc8d2; + + /* Lineage scoping */ + --lineage-selected-border: #1677ff; } .dark { @@ -156,4 +159,7 @@ --node-color-blue: #1a5a6e; --node-color-yellow: #6e5a1a; --node-color-pink: #6e3a4a; + + /* Lineage scoping */ + --lineage-selected-border: #69b1ff; } From db58bbf8301ac44cecdbd81ce634264a2875be21 Mon Sep 17 00:00:00 2001 From: wicky <130177258+wicky-zipstack@users.noreply.github.com> Date: Mon, 13 Apr 2026 17:49:00 +0530 Subject: [PATCH 5/6] =?UTF-8?q?fix:=20resolve=20temporal=20dead=20zone=20?= =?UTF-8?q?=E2=80=94=20move=20handleModelSort=20after=20setTreeData?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - handleModelSort and modelSortMenu were defined before setTreeData, causing 'Cannot access setTreeData before initialization' ReferenceError - Moved both after rebuildTree/setTreeData definition to fix initialization order --- .../src/ide/explorer/explorer-component.jsx | 60 +++++++++---------- 1 file changed, 30 insertions(+), 30 deletions(-) diff --git a/frontend/src/ide/explorer/explorer-component.jsx b/frontend/src/ide/explorer/explorer-component.jsx index 13e2204..b825088 100644 --- a/frontend/src/ide/explorer/explorer-component.jsx +++ b/frontend/src/ide/explorer/explorer-component.jsx @@ -259,36 +259,8 @@ const IdeExplorer = ({ }); }; - const handleModelSort = useCallback( - (sortBy) => { - setModelSortBy(sortBy); - if (rawTreeDataRef.current.length > 0) { - const freshData = JSON.parse(JSON.stringify(rawTreeDataRef.current)); - freshData.forEach((node) => { - if (node.title === "models" && node.children) { - node.children.forEach((child) => { - if (child.title === "no_code" && child.children) { - child.children = sortModels(child.children, sortBy); - applyModelDecorations(child.children); - } - }); - } - }); - transformTree(freshData); - setTreeData(freshData, false); - } - }, - [setTreeData] - ); - - const modelSortMenu = useMemo( - () => ({ - items: MODEL_SORT_ITEMS, - selectedKeys: [modelSortBy], - onClick: ({ key }) => handleModelSort(key), - }), - [modelSortBy, handleModelSort] - ); + // handleModelSort and modelSortMenu are defined after setTreeData (line ~816) + // to avoid temporal dead zone — see sortMenuRef below for the Dropdown binding // Function to map string icons from API to actual icon components // depth: 0 = root (Database), 1 = schema, 2 = table, 3 = column @@ -1099,6 +1071,34 @@ const IdeExplorer = ({ } }; + const handleModelSort = (sortBy) => { + setModelSortBy(sortBy); + if (rawTreeDataRef.current.length > 0) { + const freshData = JSON.parse(JSON.stringify(rawTreeDataRef.current)); + freshData.forEach((node) => { + if (node.title === "models" && node.children) { + node.children.forEach((child) => { + if (child.title === "no_code" && child.children) { + child.children = sortModels(child.children, sortBy); + applyModelDecorations(child.children); + } + }); + } + }); + transformTree(freshData); + setTreeData(freshData, false); + } + }; + + const modelSortMenu = useMemo( + () => ({ + items: MODEL_SORT_ITEMS, + selectedKeys: [modelSortBy], + onClick: ({ key }) => handleModelSort(key), + }), + [modelSortBy] + ); + useEffect(() => { if (schemaMenu) { getExplorer(projectId); From 0d13c473c00beeab78ad118880c090ded3b1e7c6 Mon Sep 17 00:00:00 2001 From: wicky <130177258+wicky-zipstack@users.noreply.github.com> Date: Mon, 13 Apr 2026 18:06:52 +0530 Subject: [PATCH 6/6] fix: wrap handleModelSort in useCallback and add to modelSortMenu deps --- frontend/src/ide/explorer/explorer-component.jsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/src/ide/explorer/explorer-component.jsx b/frontend/src/ide/explorer/explorer-component.jsx index b825088..20a3aec 100644 --- a/frontend/src/ide/explorer/explorer-component.jsx +++ b/frontend/src/ide/explorer/explorer-component.jsx @@ -1071,7 +1071,7 @@ const IdeExplorer = ({ } }; - const handleModelSort = (sortBy) => { + const handleModelSort = useCallback((sortBy) => { setModelSortBy(sortBy); if (rawTreeDataRef.current.length > 0) { const freshData = JSON.parse(JSON.stringify(rawTreeDataRef.current)); @@ -1088,7 +1088,7 @@ const IdeExplorer = ({ transformTree(freshData); setTreeData(freshData, false); } - }; + }, []); const modelSortMenu = useMemo( () => ({ @@ -1096,7 +1096,7 @@ const IdeExplorer = ({ selectedKeys: [modelSortBy], onClick: ({ key }) => handleModelSort(key), }), - [modelSortBy] + [modelSortBy, handleModelSort] ); useEffect(() => {