Skip to content

feat: lineage scoping and explorer dependency chain sort#56

Open
wicky-zipstack wants to merge 2 commits intomainfrom
feat/lineage-scoping
Open

feat: lineage scoping and explorer dependency chain sort#56
wicky-zipstack wants to merge 2 commits intomainfrom
feat/lineage-scoping

Conversation

@wicky-zipstack
Copy link
Copy Markdown
Contributor

What

  • Lineage scoping: highlight selected model's parent/child chain, fade unrelated nodes
  • Explorer: dependency chain sort (default), execution order, A-Z, Z-A sorting
  • Child models indented in explorer to show hierarchy
  • Explorer auto-refreshes when model configuration (references) is saved
  • Add references field to explorer API response

Why

  • When a model is opened, the lineage tab showed ALL project models equally — confusing with large model counts
  • No way to understand parent-child relationships from the explorer without opening lineage tab
  • No sorting options for models in the explorer pane

How

  • lineage-tab.jsx & no-code-model.jsx: getRelatedNodeIds() traverses edges to find ancestors/descendants, applyScopedStyles() applies opacity + dashed border
  • file_explorer.py: added references field from model_data to API response
  • explorer-component.jsx: sortModels() with dep_chain grouping, applyModelDecorations() for child indent, sort dropdown with 4 options
  • setRefreshModels(true) after config save triggers explorer re-fetch

Can 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)

  • No. Lineage scoping is purely visual (opacity/border styles). Explorer sort is additive — default "Dependency Chain" maintains all models visible. Backend change only adds a new field to the API response, no existing fields changed.

Database Migrations

  • None required

Env Config

  • None required

Relevant Docs

  • N/A

Related Issues or PRs

Dependencies Versions

  • No dependency changes

Notes on Testing

  • Open a model tab, check lineage: selected model should have dashed blue border, its chain highlighted, unrelated nodes faded
  • Toggle layout direction: scoping should persist
  • Check explorer sort dropdown: Dependency Chain groups parents with children, A-Z/Z-A alphabetical
  • Change model references in Configuration modal: explorer should auto-refresh with updated ordering
  • Child/reference models should appear slightly indented

Screenshots

N/A

Checklist

- 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
@wicky-zipstack wicky-zipstack requested review from a team as code owners April 10, 2026 16:27
@greptile-apps
Copy link
Copy Markdown

greptile-apps bot commented Apr 10, 2026

Greptile Summary

This PR adds lineage scoping (highlight a selected model's ancestor/descendant chain with opacity fading on unrelated nodes) and four sort options in the model explorer (dependency chain, execution order, A–Z, Z–A), plus child-model indentation and an explorer auto-refresh on config save.

The backend change is a clean, non-breaking addition of a references field to the file explorer API. The frontend scoping logic is correct and consistent between the bottom-panel and standalone lineage views. schemaList is correctly excluded from Zustand persistence via partialize. The previously noted stale-closure bug (modelSortBy snapshot inside treeNoCodeModeTitleIcon) and the deferred lineage-utils.js extraction remain open items from prior threads.

Confidence Score: 5/5

Safe to merge; all remaining findings are P2 style/clarity suggestions.

The backend change is non-breaking and correct. Lineage scoping logic is sound in both lineage-tab and no-code-model. The schemaList state addition is correctly non-persisted. The only open functional concern (stale closure in sort) was raised in a prior thread; the code still works for the initial sort-on-load path. The one new finding (undocumented exec_order fallthrough) is P2 only.

frontend/src/ide/explorer/explorer-component.jsx — stale-closure sort issue flagged in prior thread; exec_order fallthrough deserves a comment.

Important Files Changed

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
Loading
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 }) {
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.

Comment on lines 2170 to 2249
);
};

// 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();
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.

…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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant