Skip to content

Commit d5c9cef

Browse files
committed
Merge master into feature branch
- Resolve conflicts between FileGraph and ERD features - Both visualization panels now available in chart menu - Add erd-canvas option to SettingsPanel for consistency
2 parents e30731e + 052678d commit d5c9cef

33 files changed

Lines changed: 8744 additions & 652 deletions

app/components/SettingsPanel.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ const PANEL_OPTIONS: Record<SlotType, { value: PanelType; label: string }[]> = {
2424
{ value: 'git-graph', label: 'Git Graph' },
2525
{ value: 'timeline', label: 'Timeline' },
2626
{ value: 'tech-tree', label: 'Tech Tree' },
27+
{ value: 'erd-canvas', label: 'ERD' },
2728
{ value: 'file-graph', label: 'Code Map' },
2829
],
2930
}

app/components/canvas/CanvasRenderer.tsx

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import { EditorSlot } from './EditorSlot'
3131
// Import panels
3232
import { PRList, BranchList, WorktreeList, StashList, CommitList, Sidebar, RepoList } from '../panels/list'
3333
import { GitGraph, ContributorChart, TechTreeChart, FileGraph } from '../panels/viz'
34+
import { ERDCanvasPanel } from '../panels/viz/erd'
3435

3536
// ========================================
3637
// Data Interface
@@ -402,6 +403,7 @@ export function CanvasRenderer({
402403
{ id: 'git-graph', label: 'Git Graph', icon: '◉' },
403404
{ id: 'timeline', label: 'Timeline', icon: '◔' },
404405
{ id: 'tech-tree', label: 'Tech Tree', icon: '⬡' },
406+
{ id: 'erd-canvas', label: 'ERD', icon: '◫' },
405407
{ id: 'file-graph', label: 'Code Map', icon: '▦' },
406408
]
407409

@@ -495,10 +497,10 @@ export function CanvasRenderer({
495497
case 'tech-tree':
496498
return (
497499
<div className="viz-panel tech-tree-panel">
498-
<VizHeader
500+
<VizHeader
499501
panel={column.panel}
500-
label={column.label || 'Tech Tree'}
501-
icon={column.icon || '⬡'}
502+
label={column.label || 'Tech Tree'}
503+
icon={column.icon || '⬡'}
502504
/>
503505
<div className="viz-panel-content">
504506
<TechTreeChart
@@ -512,6 +514,20 @@ export function CanvasRenderer({
512514
</div>
513515
)
514516

517+
case 'erd-canvas':
518+
return (
519+
<div className="viz-panel erd-canvas-panel">
520+
<VizHeader
521+
panel={column.panel}
522+
label={column.label || 'ERD'}
523+
icon={column.icon || '◫'}
524+
/>
525+
<div className="viz-panel-content erd-canvas-content">
526+
<ERDCanvasPanel repoPath={data.repoPath} />
527+
</div>
528+
</div>
529+
)
530+
515531
case 'file-graph':
516532
return (
517533
<div className="viz-panel file-graph-panel">
@@ -536,6 +552,7 @@ export function CanvasRenderer({
536552
},
537553
[
538554
data.commits,
555+
data.repoPath,
539556
data.fileGraph,
540557
data.fileGraphLoading,
541558
selection.selectedCommit,

app/components/canvas/Column.tsx

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -110,16 +110,20 @@ export function Column({
110110
data-column-id={column.id}
111111
data-width-mode={column.width === 'flex' ? 'flex' : 'fixed'}
112112
style={style}
113-
draggable={canDrag}
114-
onDragStart={canDrag ? handleDragStart : undefined}
115113
onDragOver={canDrag ? handleDragOver : undefined}
116114
onDrop={canDrag ? handleDrop : undefined}
117115
onDragEnd={canDrag ? handleDragEnd : undefined}
118116
onDragLeave={canDrag ? onDragLeave : undefined}
119117
>
120-
{/* Drag handle - only show if dragging is enabled */}
118+
{/* Drag handle - only the handle is draggable, not the entire column */}
119+
{/* This allows canvas content (like tldraw) to handle its own pointer events */}
121120
{canDrag && (
122-
<div className="canvas-column-drag-handle" title="Drag to reorder">
121+
<div
122+
className="canvas-column-drag-handle"
123+
title="Drag to reorder"
124+
draggable
125+
onDragStart={handleDragStart}
126+
>
123127
⋮⋮
124128
</div>
125129
)}
Lines changed: 239 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,239 @@
1+
/**
2+
* ERD Canvas Panel
3+
*
4+
* Visualizes Entity Relationship Diagrams with multiple renderer options:
5+
* - Canvas (tldraw): Freeform infinite canvas
6+
* - Graph (React Flow): Structured node graph
7+
* - JSON: Raw data inspector
8+
*
9+
* Supports Laravel and Rails schema parsing.
10+
*/
11+
12+
import { useCallback, useEffect, useState, useRef } from 'react'
13+
import { TldrawRenderer, ReactFlowRenderer, JsonRenderer } from './renderers'
14+
import { filterSchemaByRelationshipCount } from './layout/erd-layout'
15+
import type { ERDSchema, ERDFramework } from '@/lib/services/erd/erd-types'
16+
17+
const INITIAL_RELATIONSHIP_FILTER_MIN = 3
18+
19+
// Renderer types
20+
type RendererType = 'tldraw' | 'reactflow' | 'json'
21+
22+
interface RendererOption {
23+
id: RendererType
24+
label: string
25+
icon: string
26+
title: string
27+
}
28+
29+
const RENDERER_OPTIONS: RendererOption[] = [
30+
{ id: 'tldraw', label: 'Canvas', icon: '◫', title: 'Infinite canvas (tldraw)' },
31+
{ id: 'reactflow', label: 'Graph', icon: '◉', title: 'Node graph (React Flow)' },
32+
{ id: 'json', label: 'JSON', icon: '{ }', title: 'Raw data inspector' },
33+
]
34+
35+
interface ERDCanvasPanelProps {
36+
repoPath: string | null
37+
}
38+
39+
type LoadingState = 'idle' | 'loading' | 'success' | 'error' | 'no-schema'
40+
41+
export function ERDCanvasPanel({ repoPath }: ERDCanvasPanelProps) {
42+
const initialFilterApplied = useRef(false)
43+
const loadVersionRef = useRef(0) // Track load version to prevent stale updates
44+
const [schema, setSchema] = useState<ERDSchema | null>(null)
45+
const [framework, setFramework] = useState<ERDFramework | null>(null)
46+
const [loadingState, setLoadingState] = useState<LoadingState>('idle')
47+
const [errorMessage, setErrorMessage] = useState<string | null>(null)
48+
const [renderer, setRenderer] = useState<RendererType>('tldraw')
49+
50+
// Reset initial filter when repo changes
51+
useEffect(() => {
52+
initialFilterApplied.current = false
53+
}, [repoPath])
54+
55+
// Load ERD schema when repo path changes
56+
const loadSchema = useCallback(async () => {
57+
if (!repoPath) {
58+
setLoadingState('idle')
59+
setSchema(null)
60+
return
61+
}
62+
63+
// Increment version to invalidate any in-flight requests
64+
const currentVersion = ++loadVersionRef.current
65+
66+
setLoadingState('loading')
67+
setErrorMessage(null)
68+
69+
try {
70+
// Detect framework first
71+
const frameworkResult = await window.electronAPI.detectERDFramework(repoPath)
72+
73+
// Check if this request is still current (user may have switched repos)
74+
if (loadVersionRef.current !== currentVersion) return
75+
76+
if (frameworkResult.success && frameworkResult.data) {
77+
setFramework(frameworkResult.data as ERDFramework)
78+
}
79+
80+
// Parse schema
81+
const result = await window.electronAPI.getERDSchema(repoPath)
82+
83+
// Check again after async operation
84+
if (loadVersionRef.current !== currentVersion) return
85+
86+
if (result.success && result.data) {
87+
const parsedSchema = result.data as ERDSchema
88+
let schemaToRender = parsedSchema
89+
90+
if (!initialFilterApplied.current) {
91+
const filteredSchema = filterSchemaByRelationshipCount(parsedSchema, INITIAL_RELATIONSHIP_FILTER_MIN)
92+
if (filteredSchema.entities.length > 0) {
93+
schemaToRender = filteredSchema
94+
}
95+
initialFilterApplied.current = true
96+
}
97+
98+
if (schemaToRender.entities.length === 0) {
99+
setLoadingState('no-schema')
100+
setSchema(null)
101+
} else {
102+
setSchema(schemaToRender)
103+
setLoadingState('success')
104+
}
105+
} else {
106+
setErrorMessage(result.message || 'Failed to parse schema')
107+
setLoadingState('error')
108+
}
109+
} catch (err) {
110+
// Only update error state if this request is still current
111+
if (loadVersionRef.current !== currentVersion) return
112+
setErrorMessage(err instanceof Error ? err.message : 'Unknown error')
113+
setLoadingState('error')
114+
}
115+
}, [repoPath])
116+
117+
// Load schema on mount and when repo changes
118+
useEffect(() => {
119+
loadSchema()
120+
}, [loadSchema])
121+
122+
// Refresh handler
123+
const handleRefresh = useCallback(() => {
124+
// Reset filter flag so the improved filter runs again
125+
initialFilterApplied.current = false
126+
loadSchema()
127+
}, [loadSchema])
128+
129+
// Framework badge
130+
const frameworkBadge = framework && framework !== 'generic' && (
131+
<span className={`erd-framework-badge erd-framework-${framework}`}>
132+
{framework === 'laravel' ? '🐘 Laravel' : '💎 Rails'}
133+
</span>
134+
)
135+
136+
// Render the selected renderer
137+
const renderContent = () => {
138+
switch (renderer) {
139+
case 'tldraw':
140+
return <TldrawRenderer schema={schema} />
141+
case 'reactflow':
142+
return <ReactFlowRenderer schema={schema} />
143+
case 'json':
144+
return <JsonRenderer schema={schema} />
145+
default:
146+
return <TldrawRenderer schema={schema} />
147+
}
148+
}
149+
150+
// Render loading/error states
151+
if (loadingState === 'idle' || !repoPath) {
152+
return (
153+
<div className="erd-canvas-container erd-empty-state">
154+
<div className="erd-empty-content">
155+
<span className="erd-empty-icon">📊</span>
156+
<p>Select a repository to visualize its ERD</p>
157+
</div>
158+
</div>
159+
)
160+
}
161+
162+
if (loadingState === 'loading') {
163+
return (
164+
<div className="erd-canvas-container erd-loading-state">
165+
<div className="erd-loading-content">
166+
<span className="erd-loading-spinner" />
167+
<p>Parsing database schema...</p>
168+
</div>
169+
</div>
170+
)
171+
}
172+
173+
if (loadingState === 'error') {
174+
return (
175+
<div className="erd-canvas-container erd-error-state">
176+
<div className="erd-error-content">
177+
<span className="erd-error-icon">⚠️</span>
178+
<p>Failed to parse schema</p>
179+
<p className="erd-error-message">{errorMessage}</p>
180+
<button className="erd-retry-button" onClick={handleRefresh}>
181+
Retry
182+
</button>
183+
</div>
184+
</div>
185+
)
186+
}
187+
188+
if (loadingState === 'no-schema') {
189+
return (
190+
<div className="erd-canvas-container erd-empty-state">
191+
<div className="erd-empty-content">
192+
<span className="erd-empty-icon">🔍</span>
193+
<p>No database schema found</p>
194+
<p className="erd-empty-hint">Supports Laravel migrations, Rails schema.rb, and Mermaid ERD files</p>
195+
<button className="erd-retry-button" onClick={handleRefresh}>
196+
Scan Again
197+
</button>
198+
</div>
199+
</div>
200+
)
201+
}
202+
203+
// Success state - render selected visualization
204+
return (
205+
<div className="erd-canvas-container">
206+
<div className="erd-canvas-header">
207+
<div className="erd-canvas-title">
208+
{frameworkBadge}
209+
<span className="erd-entity-count">
210+
{schema?.entities.length || 0} tables, {schema?.relationships.length || 0} relationships
211+
</span>
212+
</div>
213+
<div className="erd-canvas-actions">
214+
{/* Renderer toggle */}
215+
<div className="erd-renderer-toggle">
216+
{RENDERER_OPTIONS.map((option) => (
217+
<button
218+
key={option.id}
219+
className={`erd-renderer-btn ${renderer === option.id ? 'active' : ''}`}
220+
onClick={() => setRenderer(option.id)}
221+
title={option.title}
222+
>
223+
<span className="erd-renderer-icon">{option.icon}</span>
224+
<span className="erd-renderer-label">{option.label}</span>
225+
</button>
226+
))}
227+
</div>
228+
{/* Refresh button */}
229+
<button className="erd-action-button" onClick={handleRefresh} title="Refresh schema">
230+
231+
</button>
232+
</div>
233+
</div>
234+
<div className="erd-canvas-wrapper">{renderContent()}</div>
235+
</div>
236+
)
237+
}
238+
239+
export default ERDCanvasPanel

0 commit comments

Comments
 (0)