Skip to content

Commit 6abea7c

Browse files
author
Omri Hopson
committed
fixed explorer
1 parent 0b1bd33 commit 6abea7c

2 files changed

Lines changed: 147 additions & 2 deletions

File tree

dashboard/src/pages/Explorer/Explorer.tsx

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ export default function Explorer() {
6767
const [refreshTrigger, setRefreshTrigger] = useState(0);
6868
const [rawData, setRawData] = useState<GraphData | null>(null);
6969
const [highlightedNode, setHighlightedNode] = useState<string | null>(null);
70+
const [nodeError, setNodeError] = useState<string | null>(null);
7071

7172
useEffect(() => {
7273
fetch('/api/repositories')
@@ -96,14 +97,20 @@ export default function Explorer() {
9697

9798
const loadNodeDetails = useCallback(async (nodeId: string) => {
9899
if (!selectedRepo) return;
100+
setNodeError(null);
99101
try {
100102
const response = await fetch(`/api/graph/${selectedRepo}/node/${nodeId}`);
101-
if (!response.ok) return;
103+
if (!response.ok) {
104+
const errorData = await response.json().catch(() => null);
105+
setNodeError(errorData?.error || `Failed to load node (${response.status})`);
106+
return;
107+
}
102108
const data: NodeDetails = await response.json();
103109
setSelectedNode(data);
104110
setHighlightedNode(nodeId);
105111
} catch (error) {
106112
console.error('Failed to load node details:', error);
113+
setNodeError('Network error loading node details');
107114
}
108115
}, [selectedRepo]);
109116

@@ -502,7 +509,18 @@ export default function Explorer() {
502509
)}
503510

504511
{/* Details Inspector (both modes) */}
505-
<div className="absolute right-6 top-6 bottom-6 w-80 glass-panel rounded-xl border border-outline-variant/20 p-6 flex flex-col gap-6 shadow-2xl details-panel overflow-y-auto">
512+
<div className="absolute right-6 top-6 bottom-6 w-80 glass-panel rounded-xl border border-outline-variant/20 p-6 flex flex-col gap-6 shadow-2xl details-panel overflow-y-auto z-20">
513+
{nodeError && (
514+
<div className="px-3 py-2 bg-error/10 border border-error/20 rounded-lg">
515+
<p className="text-sm text-error font-medium">{nodeError}</p>
516+
<button
517+
onClick={() => setNodeError(null)}
518+
className="text-xs text-error/70 mt-1 underline"
519+
>
520+
Dismiss
521+
</button>
522+
</div>
523+
)}
506524
{selectedNode ? (
507525
<>
508526
<div>

src/core/indexer.ts

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,13 @@ export interface IndexerEvents {
6262

6363
export class Indexer extends EventEmitter {
6464
private contextIgnore: ContextIgnore;
65+
private pendingReferences: Array<{
66+
calls: Array<{ callerNodeId: string; calleeName: string }>;
67+
imports: import('./parser.js').ParsedImport[];
68+
inheritance: import('./parser.js').ParsedInheritance[];
69+
repositoryId: string;
70+
filePath: string;
71+
}> = [];
6572

6673
constructor(
6774
public storage: StorageProvider,
@@ -116,6 +123,8 @@ export class Indexer extends EventEmitter {
116123
const files = await this.discoverFiles(absolutePath, options.respectIgnore);
117124
job.filesTotal = files.length;
118125

126+
this.pendingReferences = [];
127+
119128
const BATCH_SIZE = 50;
120129
for (let i = 0; i < files.length; i += BATCH_SIZE) {
121130
const batch = files.slice(i, i + BATCH_SIZE);
@@ -134,6 +143,12 @@ export class Indexer extends EventEmitter {
134143
}
135144
}
136145

146+
const forwardEdges = this.resolveForwardEdges(repositoryId);
147+
if (forwardEdges > 0) {
148+
console.log(`Resolved ${forwardEdges} forward-reference edges`);
149+
}
150+
this.pendingReferences = [];
151+
137152
const stats = this.graph.getStats();
138153
job.nodesCreated = stats.nodeCount;
139154
job.edgesCreated = stats.edgeCount;
@@ -217,6 +232,14 @@ export class Indexer extends EventEmitter {
217232
}
218233
this.storage.upsertEdges(edges);
219234

235+
this.pendingReferences.push({
236+
calls: parsed.calls,
237+
imports: parsed.imports,
238+
inheritance: parsed.inheritance,
239+
repositoryId,
240+
filePath: parsed.filePath,
241+
});
242+
220243
fileMetadata.hash = parsed.hash;
221244
fileMetadata.language = parsed.language;
222245
fileMetadata.nodeCount = parsed.nodes.length;
@@ -278,6 +301,110 @@ export class Indexer extends EventEmitter {
278301
}
279302
}
280303

304+
private resolveForwardEdges(repositoryId: string): number {
305+
let created = 0;
306+
const now = new Date();
307+
308+
for (const ref of this.pendingReferences) {
309+
if (ref.repositoryId !== repositoryId) continue;
310+
311+
for (const call of ref.calls) {
312+
const targets = this.graph.findByName(call.calleeName);
313+
for (const target of targets) {
314+
if (target.repositoryId !== repositoryId) continue;
315+
const edgeId = this.generateEdgeId(call.callerNodeId, target.id, 'calls');
316+
try {
317+
if (!this.graph.getNode(call.callerNodeId)) continue;
318+
this.graph.addEdge({
319+
id: edgeId,
320+
sourceId: call.callerNodeId,
321+
targetId: target.id,
322+
kind: 'calls',
323+
confidence: 0.9,
324+
repositoryId,
325+
createdAt: now,
326+
updatedAt: now,
327+
});
328+
created++;
329+
} catch {
330+
// Already exists or source/target missing
331+
}
332+
}
333+
}
334+
335+
for (const inh of ref.inheritance) {
336+
const parents = this.graph.findByName(inh.parentName);
337+
for (const parent of parents) {
338+
if (parent.repositoryId !== repositoryId) continue;
339+
const edgeId = this.generateEdgeId(inh.childNodeId, parent.id, inh.kind);
340+
try {
341+
if (!this.graph.getNode(inh.childNodeId)) continue;
342+
this.graph.addEdge({
343+
id: edgeId,
344+
sourceId: inh.childNodeId,
345+
targetId: parent.id,
346+
kind: inh.kind,
347+
confidence: 1.0,
348+
repositoryId,
349+
createdAt: now,
350+
updatedAt: now,
351+
});
352+
created++;
353+
} catch {
354+
// Already exists or source/target missing
355+
}
356+
}
357+
}
358+
359+
for (const imp of ref.imports) {
360+
if (!imp.source || !imp.source.startsWith('.')) continue;
361+
const importDir = dirname(ref.filePath);
362+
const resolvedBase = join(importDir, imp.source).replace(/\\/g, '/');
363+
const extensions = ['', '.ts', '.tsx', '.js', '.jsx', '/index.ts', '/index.tsx', '/index.js'];
364+
365+
for (const importedName of imp.imported) {
366+
const targets = this.graph.findByName(importedName);
367+
for (const target of targets) {
368+
if (target.repositoryId !== repositoryId) continue;
369+
if (target.filePath === ref.filePath) continue;
370+
371+
const targetFileBase = target.filePath.replace(/\.(ts|tsx|js|jsx)$/, '').replace(/\/index$/, '');
372+
const matchesPath = extensions.some(ext => {
373+
const candidate = resolvedBase + ext;
374+
return target.filePath === candidate || targetFileBase === resolvedBase;
375+
});
376+
if (!matchesPath) continue;
377+
378+
const nodesInFile = this.graph.getNodesInFile(ref.filePath);
379+
const sourceNode = nodesInFile.find(n =>
380+
n.kind === 'function' || n.kind === 'class' || n.kind === 'method'
381+
) || nodesInFile[0];
382+
if (!sourceNode) continue;
383+
384+
const edgeId = this.generateEdgeId(sourceNode.id, target.id, 'imports');
385+
try {
386+
this.graph.addEdge({
387+
id: edgeId,
388+
sourceId: sourceNode.id,
389+
targetId: target.id,
390+
kind: 'imports',
391+
confidence: 1.0,
392+
repositoryId,
393+
createdAt: now,
394+
updatedAt: now,
395+
});
396+
created++;
397+
} catch {
398+
// Already exists
399+
}
400+
}
401+
}
402+
}
403+
}
404+
405+
return created;
406+
}
407+
281408
private static readonly SKIP_DIRS = new Set([
282409
'node_modules', '.git', '.svn', '.hg', 'vendor', '__pycache__',
283410
'.venv', 'venv', 'dist', 'build', 'out', 'target', '.next',

0 commit comments

Comments
 (0)