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..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,7 +290,7 @@ const transformLineageData = (data) => {
return data;
};
-function LineageTab({ nodeData }) {
+function LineageTab({ nodeData, selectedModelName }) {
const axios = useAxiosPrivate();
const { selectedOrgId } = orgStore();
const { projectId } = useProjectStore();
@@ -486,15 +487,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 +522,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 +702,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 +995,7 @@ function LineageTab({ nodeData }) {
LineageTab.propTypes = {
nodeData: PropTypes.object,
+ selectedModelName: PropTypes.string,
};
export { LineageTab };
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 80304d0..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";
@@ -261,8 +262,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 +751,7 @@ function NoCodeModel({ nodeData }) {
setSeqEdges(layoutedEdges);
runTransformation(res?.data?.model_data);
setConfigApply(true);
+ setRefreshModels(true);
handleModalClose("ok");
})
.catch((error) => {
@@ -2159,6 +2171,7 @@ function NoCodeModel({ nodeData }) {
);
};
+ // Find all ancestor and descendant node IDs for a given model
const getLineageData = (callSample = false) => {
if (!projectId) return;
setLineageData();
@@ -2174,8 +2187,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..20a3aec 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 = () => {},
@@ -82,6 +96,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 +142,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 +204,64 @@ 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;
+ }
+ // "exec_order" and default: keep original backend order (topological/execution)
+ return [...models];
+ };
+
+ 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) => {
+ const refs = (m.references || []).filter((r) => modelNames.has(r));
+ if (refs.length > 0) {
+ m._isChild = true;
+ }
+ });
+ };
+
+ // 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
const mapIconsToTreeData = (data, depth = 0) => {
@@ -308,6 +382,9 @@ const IdeExplorer = ({
setCurrentSchema("");
}
+ // Store plain schema list in shared store
+ setSchemaList(allSchemas);
+
const items = allSchemas.map((el) => ({
label: el,
key: el,
@@ -350,6 +427,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 +535,17 @@ const IdeExplorer = ({
+
+
+
+
+
+
+
>
)}
@@ -980,6 +1071,34 @@ 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);
+ }
+ }, []);
+
+ const modelSortMenu = useMemo(
+ () => ({
+ items: MODEL_SORT_ITEMS,
+ selectedKeys: [modelSortBy],
+ onClick: ({ key }) => handleModelSort(key),
+ }),
+ [modelSortBy, handleModelSort]
+ );
+
useEffect(() => {
if (schemaMenu) {
getExplorer(projectId);
@@ -2446,6 +2565,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..b3fcfec 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 .ant-tree-treenode.explorer-child-model {
+ padding-left: 12px;
+}
+
.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",
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;
}