Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions backend/backend/application/file_explorer/file_explorer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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] = {
Expand Down
132 changes: 122 additions & 10 deletions frontend/src/ide/editor/lineage-tab/lineage-tab.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 }) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 selectedModelName prop is never wired up from the call site

The standalone LineageTab now accepts selectedModelName and gates all scoping logic on it, but editor-tabs.jsx only passes nodeData={tabDetails} with no selectedModelName. The prop will always be undefined, so applyScopedStyles is never called and the scoping feature is dead code for the standalone lineage tab path.

If the intent is to scope the standalone lineage tab when opened from a model, the model name needs to be embedded in the setPendingLineageTab call in no-code-model.jsx and forwarded through editor-tabs.jsx to the LazyLoadComponent that renders LineageTab.

Prompt To Fix With AI
This is a comment left during a code review.
Path: frontend/src/ide/editor/lineage-tab/lineage-tab.jsx
Line: 366

Comment:
**`selectedModelName` prop is never wired up from the call site**

The standalone `LineageTab` now accepts `selectedModelName` and gates all scoping logic on it, but `editor-tabs.jsx` only passes `nodeData={tabDetails}` with no `selectedModelName`. The prop will always be `undefined`, so `applyScopedStyles` is never called and the scoping feature is dead code for the standalone lineage tab path.

If the intent is to scope the standalone lineage tab when opened from a model, the model name needs to be embedded in the `setPendingLineageTab` call in `no-code-model.jsx` and forwarded through `editor-tabs.jsx` to the `LazyLoadComponent` that renders `LineageTab`.

How can I resolve this? If you propose a fix, please make it concise.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Intentional — the standalone LineageTab shows the full project lineage by design. The selectedModelName prop is optional and prepared for future use (e.g., when opening lineage from a specific model context). The scoping feature is active in the bottom section lineage panel where modelName is always available from the open model tab.

const axios = useAxiosPrivate();
const { selectedOrgId } = orgStore();
const { projectId } = useProjectStore();
Expand Down Expand Up @@ -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";
Expand All @@ -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(
Expand Down Expand Up @@ -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 <SpinnerLoader />;
Expand Down Expand Up @@ -957,6 +1068,7 @@ function LineageTab({ nodeData }) {

LineageTab.propTypes = {
nodeData: PropTypes.object,
selectedModelName: PropTypes.string,
};

export { LineageTab };
103 changes: 99 additions & 4 deletions frontend/src/ide/editor/no-code-model/no-code-model.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
};

Expand Down Expand Up @@ -740,6 +750,7 @@ function NoCodeModel({ nodeData }) {
setSeqEdges(layoutedEdges);
runTransformation(res?.data?.model_data);
setConfigApply(true);
setRefreshModels(true);
handleModalClose("ok");
})
.catch((error) => {
Expand Down Expand Up @@ -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();
Comment on lines 2170 to 2249
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Duplicated getRelatedNodeIds / applyScopedStyles

getRelatedNodeIds and applyScopedStyles are copied verbatim into both no-code-model.jsx and lineage-tab.jsx. Consider extracting them into a shared utility (e.g., src/ide/editor/lineage-utils.js) so that any future fix or enhancement only needs to be applied once.

Prompt To Fix With AI
This is a comment left during a code review.
Path: frontend/src/ide/editor/no-code-model/no-code-model.jsx
Line: 2170-2249

Comment:
**Duplicated `getRelatedNodeIds` / `applyScopedStyles`**

`getRelatedNodeIds` and `applyScopedStyles` are copied verbatim into both `no-code-model.jsx` and `lineage-tab.jsx`. Consider extracting them into a shared utility (e.g., `src/ide/editor/lineage-utils.js`) so that any future fix or enhancement only needs to be applied once.

How can I resolve this? If you propose a fix, please make it concise.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed — extracting to a shared lineage-utils.js is a good idea. Will refactor in a follow-up. Keeping it as-is for now to keep this PR scoped.

Expand All @@ -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);
Expand Down
Loading
Loading