Skip to content

Commit d9b9f27

Browse files
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
1 parent 4bdec24 commit d9b9f27

6 files changed

Lines changed: 331 additions & 14 deletions

File tree

backend/backend/application/file_explorer/file_explorer.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,9 @@ def load_models(self, session: Session):
9393
# Sort models by execution order (DAG order)
9494
sorted_model_names = topological_sort_models(models_with_refs)
9595

96+
# Build lookup for references by model name
97+
refs_by_name = {m["model_name"]: m["references"] for m in models_with_refs}
98+
9699
# Build the model structure in sorted order
97100
no_code_model_structure = []
98101
for no_code_model_name in sorted_model_names:
@@ -103,6 +106,7 @@ def load_models(self, session: Session):
103106
"key": f"{self.project_name}/models/no_code/{no_code_model_name}",
104107
"is_folder": False,
105108
"type": "NO_CODE_MODEL",
109+
"references": refs_by_name.get(no_code_model_name, []),
106110
}
107111
)
108112
model_structure: dict[str, Any] = {

frontend/src/ide/editor/lineage-tab/lineage-tab.jsx

Lines changed: 122 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -289,7 +289,81 @@ const transformLineageData = (data) => {
289289
return data;
290290
};
291291

292-
function LineageTab({ nodeData }) {
292+
// Find all ancestor and descendant node IDs for a given model
293+
const getRelatedNodeIds = (allEdges, selectedLabel, allNodes) => {
294+
const nodeByLabel = {};
295+
allNodes.forEach((n) => {
296+
nodeByLabel[n.data.originalLabel || n.data.label] = n.id;
297+
});
298+
const selectedId = nodeByLabel[selectedLabel];
299+
if (!selectedId) return null;
300+
301+
const related = new Set([selectedId]);
302+
const findAncestors = (id) => {
303+
allEdges.forEach((e) => {
304+
if (e.target === id && !related.has(e.source)) {
305+
related.add(e.source);
306+
findAncestors(e.source);
307+
}
308+
});
309+
};
310+
const findDescendants = (id) => {
311+
allEdges.forEach((e) => {
312+
if (e.source === id && !related.has(e.target)) {
313+
related.add(e.target);
314+
findDescendants(e.target);
315+
}
316+
});
317+
};
318+
findAncestors(selectedId);
319+
findDescendants(selectedId);
320+
return related;
321+
};
322+
323+
const applyScopedStyles = (layoutedNodes, layoutedEdges, selectedLabel) => {
324+
const rawEdges = layoutedEdges.map((e) => ({
325+
source: e.source,
326+
target: e.target,
327+
}));
328+
const related = getRelatedNodeIds(rawEdges, selectedLabel, layoutedNodes);
329+
if (!related) return { nodes: layoutedNodes, edges: layoutedEdges };
330+
331+
const styledNodes = layoutedNodes.map((node) => {
332+
const nodeLabel = node.data.originalLabel || node.data.label;
333+
const isSelected = nodeLabel === selectedLabel;
334+
const isRelated = related.has(node.id);
335+
return {
336+
...node,
337+
style: {
338+
...node.style,
339+
opacity: isRelated ? 1 : 0.25,
340+
border: isSelected
341+
? "2px dashed #1677ff"
342+
: node.style?.border || "1px solid var(--black)",
343+
},
344+
};
345+
});
346+
347+
const relatedEdgeSet = new Set();
348+
layoutedEdges.forEach((e) => {
349+
if (related.has(e.source) && related.has(e.target)) {
350+
relatedEdgeSet.add(e.id);
351+
}
352+
});
353+
354+
const styledEdges = layoutedEdges.map((edge) => ({
355+
...edge,
356+
style: {
357+
...edge.style,
358+
opacity: relatedEdgeSet.has(edge.id) ? 1 : 0.15,
359+
stroke: relatedEdgeSet.has(edge.id) ? "#1677ff" : undefined,
360+
},
361+
}));
362+
363+
return { nodes: styledNodes, edges: styledEdges };
364+
};
365+
366+
function LineageTab({ nodeData, selectedModelName }) {
293367
const axios = useAxiosPrivate();
294368
const { selectedOrgId } = orgStore();
295369
const { projectId } = useProjectStore();
@@ -486,15 +560,32 @@ function LineageTab({ nodeData }) {
486560
transformedData.edges,
487561
layoutDirection
488562
);
489-
setNodes(layoutedNodes);
490-
setEdges(layoutedEdges);
563+
if (selectedModelName) {
564+
const scoped = applyScopedStyles(
565+
layoutedNodes,
566+
layoutedEdges,
567+
selectedModelName
568+
);
569+
setNodes(scoped.nodes);
570+
setEdges(scoped.edges);
571+
} else {
572+
setNodes(layoutedNodes);
573+
setEdges(layoutedEdges);
574+
}
491575
})
492576
.catch((error) => {
493577
console.error(error);
494578
notify({ error });
495579
setLineageData({});
496580
});
497-
}, [projectId, selectedOrgId, setNodes, setEdges, layoutDirection]);
581+
}, [
582+
projectId,
583+
selectedOrgId,
584+
setNodes,
585+
setEdges,
586+
layoutDirection,
587+
selectedModelName,
588+
]);
498589

499590
const handleToggleLayout = useCallback(() => {
500591
const newDirection = layoutDirection === "TB" ? "LR" : "TB";
@@ -504,10 +595,20 @@ function LineageTab({ nodeData }) {
504595
if (lineageData && lineageData.nodes && lineageData.edges) {
505596
const { nodes: layoutedNodes, edges: layoutedEdges } =
506597
getLayoutedElements(lineageData.nodes, lineageData.edges, newDirection);
507-
setNodes(layoutedNodes);
508-
setEdges(layoutedEdges);
598+
if (selectedModelName) {
599+
const scoped = applyScopedStyles(
600+
layoutedNodes,
601+
layoutedEdges,
602+
selectedModelName
603+
);
604+
setNodes(scoped.nodes);
605+
setEdges(scoped.edges);
606+
} else {
607+
setNodes(layoutedNodes);
608+
setEdges(layoutedEdges);
609+
}
509610
}
510-
}, [layoutDirection, lineageData, setNodes, setEdges]);
611+
}, [layoutDirection, lineageData, setNodes, setEdges, selectedModelName]);
511612

512613
// Fetch sequence data for a model
513614
const fetchSequenceData = useCallback(
@@ -674,15 +775,25 @@ function LineageTab({ nodeData }) {
674775
transformedData.edges,
675776
"TB"
676777
);
677-
setNodes(layoutedNodes);
678-
setEdges(layoutedEdges);
778+
if (selectedModelName) {
779+
const scoped = applyScopedStyles(
780+
layoutedNodes,
781+
layoutedEdges,
782+
selectedModelName
783+
);
784+
setNodes(scoped.nodes);
785+
setEdges(scoped.edges);
786+
} else {
787+
setNodes(layoutedNodes);
788+
setEdges(layoutedEdges);
789+
}
679790
})
680791
.catch((error) => {
681792
console.error(error);
682793
notify({ error });
683794
setLineageData({});
684795
});
685-
}, [projectId, selectedOrgId, setNodes, setEdges]);
796+
}, [projectId, selectedOrgId, setNodes, setEdges, selectedModelName]);
686797

687798
if (!lineageData) {
688799
return <SpinnerLoader />;
@@ -957,6 +1068,7 @@ function LineageTab({ nodeData }) {
9571068

9581069
LineageTab.propTypes = {
9591070
nodeData: PropTypes.object,
1071+
selectedModelName: PropTypes.string,
9601072
};
9611073

9621074
export { LineageTab };

frontend/src/ide/editor/no-code-model/no-code-model.jsx

Lines changed: 99 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -261,8 +261,18 @@ function NoCodeModel({ nodeData }) {
261261
if (lineageData?.nodes && lineageData?.edges) {
262262
const { nodes: layoutedNodes, edges: layoutedEdges } =
263263
getLayoutedElements(lineageData.nodes, lineageData.edges, newDirection);
264-
setNodes(layoutedNodes);
265-
setEdges(layoutedEdges);
264+
if (modelName) {
265+
const scoped = applyScopedStyles(
266+
layoutedNodes,
267+
layoutedEdges,
268+
modelName
269+
);
270+
setNodes(scoped.nodes);
271+
setEdges(scoped.edges);
272+
} else {
273+
setNodes(layoutedNodes);
274+
setEdges(layoutedEdges);
275+
}
266276
}
267277
};
268278

@@ -740,6 +750,7 @@ function NoCodeModel({ nodeData }) {
740750
setSeqEdges(layoutedEdges);
741751
runTransformation(res?.data?.model_data);
742752
setConfigApply(true);
753+
setRefreshModels(true);
743754
handleModalClose("ok");
744755
})
745756
.catch((error) => {
@@ -2159,6 +2170,80 @@ function NoCodeModel({ nodeData }) {
21592170
);
21602171
};
21612172

2173+
// Find all ancestor and descendant node IDs for a given model
2174+
const getRelatedNodeIds = (allEdges, selectedLabel, allNodes) => {
2175+
const nodeByLabel = {};
2176+
allNodes.forEach((n) => {
2177+
nodeByLabel[n.data.originalLabel || n.data.label] = n.id;
2178+
});
2179+
const selectedId = nodeByLabel[selectedLabel];
2180+
if (!selectedId) return null;
2181+
2182+
const related = new Set([selectedId]);
2183+
const findAncestors = (id) => {
2184+
allEdges.forEach((e) => {
2185+
if (e.target === id && !related.has(e.source)) {
2186+
related.add(e.source);
2187+
findAncestors(e.source);
2188+
}
2189+
});
2190+
};
2191+
const findDescendants = (id) => {
2192+
allEdges.forEach((e) => {
2193+
if (e.source === id && !related.has(e.target)) {
2194+
related.add(e.target);
2195+
findDescendants(e.target);
2196+
}
2197+
});
2198+
};
2199+
findAncestors(selectedId);
2200+
findDescendants(selectedId);
2201+
return related;
2202+
};
2203+
2204+
const applyScopedStyles = (layoutedNodes, layoutedEdges, selectedLabel) => {
2205+
const rawEdges = layoutedEdges.map((e) => ({
2206+
source: e.source,
2207+
target: e.target,
2208+
}));
2209+
const related = getRelatedNodeIds(rawEdges, selectedLabel, layoutedNodes);
2210+
if (!related) return { nodes: layoutedNodes, edges: layoutedEdges };
2211+
2212+
const styledNodes = layoutedNodes.map((node) => {
2213+
const nodeLabel = node.data.originalLabel || node.data.label;
2214+
const isSelected = nodeLabel === selectedLabel;
2215+
const isRelated = related.has(node.id);
2216+
return {
2217+
...node,
2218+
style: {
2219+
...node.style,
2220+
opacity: isRelated ? 1 : 0.25,
2221+
border: isSelected
2222+
? "2px dashed #1677ff"
2223+
: node.style?.border || "1px solid var(--black)",
2224+
},
2225+
};
2226+
});
2227+
2228+
const relatedEdgeSet = new Set();
2229+
layoutedEdges.forEach((e) => {
2230+
if (related.has(e.source) && related.has(e.target)) {
2231+
relatedEdgeSet.add(e.id);
2232+
}
2233+
});
2234+
2235+
const styledEdges = layoutedEdges.map((edge) => ({
2236+
...edge,
2237+
style: {
2238+
...edge.style,
2239+
opacity: relatedEdgeSet.has(edge.id) ? 1 : 0.15,
2240+
stroke: relatedEdgeSet.has(edge.id) ? "#1677ff" : undefined,
2241+
},
2242+
}));
2243+
2244+
return { nodes: styledNodes, edges: styledEdges };
2245+
};
2246+
21622247
const getLineageData = (callSample = false) => {
21632248
if (!projectId) return;
21642249
setLineageData();
@@ -2174,8 +2259,18 @@ function NoCodeModel({ nodeData }) {
21742259
setLineageData(data);
21752260
const { nodes: layoutedNodes, edges: layoutedEdges } =
21762261
getLayoutedElements(data.nodes, data.edges, lineageLayoutDirection);
2177-
setNodes(layoutedNodes);
2178-
setEdges(layoutedEdges);
2262+
if (modelName) {
2263+
const scoped = applyScopedStyles(
2264+
layoutedNodes,
2265+
layoutedEdges,
2266+
modelName
2267+
);
2268+
setNodes(scoped.nodes);
2269+
setEdges(scoped.edges);
2270+
} else {
2271+
setNodes(layoutedNodes);
2272+
setEdges(layoutedEdges);
2273+
}
21792274
})
21802275
.catch((error) => {
21812276
console.error(error);

0 commit comments

Comments
 (0)