feat(ui): Neo4j graph explorer with interactive relationship visualization

Add Graph sub-tab in Data Sources UI for Neo4j: force-directed SVG view of
Product-Supplier nodes and SUPPLIES/RELATED_TO/PART_OF/COMPATIBLE_WITH edges.
Backend GET /api/sql/graph/neo4j with rel_type filter and edge limit.
This commit is contained in:
mo
2026-06-27 15:48:56 +00:00
parent a4c9b60079
commit 5828113f53
4 changed files with 495 additions and 3 deletions
+16 -2
View File
@@ -5,6 +5,7 @@ import {
Database,
FolderTree,
Loader2,
Network,
RefreshCw,
Server,
Table2,
@@ -12,6 +13,7 @@ import {
} from 'lucide-react'
import { Badge } from '../ui/Badge'
import { DbShell } from './DbShell'
import { Neo4jGraphView } from './Neo4jGraphView'
import { SqlWorkbench } from './SqlWorkbench'
import {
getSourceMeta,
@@ -43,8 +45,9 @@ type Props = {
focusEngine?: SourceEngine | null
}
const SUB_TABS: { id: SourceSubTab; label: string; icon: typeof FolderTree }[] = [
const SUB_TABS: { id: SourceSubTab; label: string; icon: typeof FolderTree; neo4jOnly?: boolean }[] = [
{ id: 'browser', label: 'Browser', icon: FolderTree },
{ id: 'graph', label: 'Graph', icon: Network, neo4jOnly: true },
{ id: 'console', label: 'Query Console', icon: Database },
{ id: 'shell', label: 'Shell', icon: TerminalSquare },
]
@@ -72,6 +75,10 @@ export function DataSourcesView({ focusEngine }: Props) {
if (focusEngine) setActive(focusEngine)
}, [focusEngine])
useEffect(() => {
if (active !== 'neo4j' && subTab === 'graph') setSubTab('browser')
}, [active, subTab])
const loadHealth = useCallback(async () => {
try {
const r = await fetch('/api/sql/health')
@@ -123,9 +130,12 @@ export function DataSourcesView({ focusEngine }: Props) {
const refreshAll = () => {
loadHealth()
if (subTab === 'browser') loadCatalog(active)
else if (subTab === 'graph' && active === 'neo4j') { /* Neo4jGraphView reloads itself */ }
else if (selectedObject) loadSample(active, selectedObject)
}
const visibleSubTabs = SUB_TABS.filter((t) => !t.neo4jOnly || active === 'neo4j')
return (
<div className="flex h-full min-h-0 flex-col gap-2 p-3">
{/* Header */}
@@ -200,7 +210,7 @@ export function DataSourcesView({ focusEngine }: Props) {
</p>
</div>
<div className="flex gap-1">
{SUB_TABS.map(({ id, label, icon: Icon }) => (
{visibleSubTabs.map(({ id, label, icon: Icon }) => (
<button
key={id}
type="button"
@@ -296,6 +306,10 @@ export function DataSourcesView({ focusEngine }: Props) {
</div>
)}
{subTab === 'graph' && active === 'neo4j' && (
<Neo4jGraphView />
)}
{subTab === 'console' && (
<SqlWorkbench engine={active} />
)}