feat: lineage scoping and explorer dependency chain sort#56
feat: lineage scoping and explorer dependency chain sort#56wicky-zipstack wants to merge 2 commits intomainfrom
Conversation
- 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
|
| Filename | Overview |
|---|---|
| backend/backend/application/file_explorer/file_explorer.py | Adds references field to each no-code model in the API response using an efficient lookup dict; non-breaking, backward compatible. |
| frontend/src/ide/editor/lineage-tab/lineage-tab.jsx | Adds getRelatedNodeIds / applyScopedStyles for lineage scoping; correctly guards on selectedModelName, handles both initial fetch and layout-direction toggle. |
| frontend/src/ide/editor/no-code-model/no-code-model.jsx | Duplicates getRelatedNodeIds/applyScopedStyles (refactor deferred per reply), adds setRefreshModels(true) after config save to trigger explorer re-fetch. |
| frontend/src/ide/explorer/explorer-component.jsx | Adds sortModels, applyModelDecorations, handleModelSort, and sort dropdown; exec_order case is undocumented. The stale-closure sort bug was flagged in a previous thread. |
| frontend/src/ide/ide-layout.css | Adds .explorerTree .explorer-child-model padding rule to indent child models; scoped and non-breaking. |
| frontend/src/store/project-store.js | Adds schemaList state and setSchemaList setter; correctly excluded from partialize so it is not persisted to localStorage. |
Flowchart
%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[Config Save\nno-code-model.jsx] -->|setRefreshModels true| B[useRefreshModelsStore]
B -->|refreshModels=true| C[explorer useEffect\nexplorer-component.jsx]
B -->|refreshModels=true| D[model useEffect\nno-code-model.jsx]
C --> E[onRefreshDebounce\ngetExplorer API]
D --> F[reloadAndRollbackData\ngetSampleData]
F -->|setRefreshModels false| B
E --> G[rawTreeDataRef updated]
G --> H[sortModels\ndep_chain / exec_order / A-Z / Z-A]
H --> I[applyModelDecorations\nset _isChild flag]
I --> J[transformTree\nset className]
J --> K[Explorer Tree Rendered\nwith indented children]
L[Lineage Tab Mount\nor layoutDirection toggle] --> M[getLineageData API]
M --> N{selectedModelName?}
N -->|yes| O[getRelatedNodeIds\nDFS ancestors + descendants]
O --> P[applyScopedStyles\nopacity + dashed border]
P --> Q[React Flow Nodes/Edges updated]
N -->|no| Q
Prompt To Fix All With AI
This is a comment left during a code review.
Path: frontend/src/ide/explorer/explorer-component.jsx
Line: 201-234
Comment:
**`exec_order` case is undocumented silent fallthrough**
`sortModels` has no explicit branch for the `"exec_order"` key, so it silently falls through to `return [...models]`. This preserves the API-returned order, which happens to be the topological sort from `topological_sort_models` in `file_explorer.py` — making it correct by coincidence. A future refactor that reorders the default return, or anyone reading this function, won't understand the intent without a comment or an explicit case.
```suggestion
if (sortBy === "exec_order") {
// API already returns models in topological (execution) order; preserve it
return [...models];
}
return [...models];
```
How can I resolve this? If you propose a fix, please make it concise.Reviews (2): Last reviewed commit: "fix: address PR review — stale closure i..." | Re-trigger Greptile
| return { nodes: styledNodes, edges: styledEdges }; | ||
| }; | ||
|
|
||
| function LineageTab({ nodeData, selectedModelName }) { |
There was a problem hiding this 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.
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.There was a problem hiding this comment.
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.
| ); | ||
| }; | ||
|
|
||
| // 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(); |
There was a problem hiding this 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.
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!
There was a problem hiding this comment.
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.
…r external refs - 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
What
Why
How
getRelatedNodeIds()traverses edges to find ancestors/descendants,applyScopedStyles()applies opacity + dashed borderreferencesfield from model_data to API responsesortModels()with dep_chain grouping,applyModelDecorations()for child indent, sort dropdown with 4 optionssetRefreshModels(true)after config save triggers explorer re-fetchCan this PR break any existing features. If yes, please list possible items. If no, please explain why. (PS: Admins do not merge the PR without this section filled)
Database Migrations
Env Config
Relevant Docs
Related Issues or PRs
Dependencies Versions
Notes on Testing
Screenshots
N/A
Checklist