Extract DetailPanel, ArchHeader, Toolbar, ArchCanvas and ServiceTable into _components/, the ReactFlow node/edge builder into _hooks/useArchGraph, and layout constants/helpers into _layout.ts. page.tsx drops from 950 to 91 LOC, well below the 300 soft target. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
165 lines
5.4 KiB
TypeScript
165 lines
5.4 KiB
TypeScript
'use client'
|
|
|
|
import React, { useCallback, useEffect } from 'react'
|
|
import ReactFlow, {
|
|
Node,
|
|
Controls,
|
|
Background,
|
|
MiniMap,
|
|
useNodesState,
|
|
useEdgesState,
|
|
BackgroundVariant,
|
|
Panel,
|
|
} from 'reactflow'
|
|
import 'reactflow/dist/style.css'
|
|
import { ARCH_SERVICES, LAYERS, type ArchService } from '../architecture-data'
|
|
import { LAYER_ORDER, type LayerFilter } from '../_layout'
|
|
import { useArchGraph } from '../_hooks/useArchGraph'
|
|
import DetailPanel from './DetailPanel'
|
|
|
|
export default function ArchCanvas({
|
|
layerFilter,
|
|
showDb,
|
|
showRag,
|
|
showApis,
|
|
selectedService,
|
|
setSelectedService,
|
|
}: {
|
|
layerFilter: LayerFilter
|
|
showDb: boolean
|
|
showRag: boolean
|
|
showApis: boolean
|
|
selectedService: ArchService | null
|
|
setSelectedService: React.Dispatch<React.SetStateAction<ArchService | null>>
|
|
}) {
|
|
const { nodes: initialNodes, edges: initialEdges } = useArchGraph({
|
|
layerFilter,
|
|
showDb,
|
|
showRag,
|
|
showApis,
|
|
selectedService,
|
|
})
|
|
|
|
const [nodes, setNodes, onNodesChange] = useNodesState([])
|
|
const [edges, setEdges, onEdgesChange] = useEdgesState([])
|
|
|
|
useEffect(() => {
|
|
setNodes(initialNodes)
|
|
setEdges(initialEdges)
|
|
}, [initialNodes, initialEdges, setNodes, setEdges])
|
|
|
|
const onNodeClick = useCallback((_event: React.MouseEvent, node: Node) => {
|
|
const service = ARCH_SERVICES.find(s => s.id === node.id)
|
|
if (service) {
|
|
setSelectedService(prev => (prev?.id === service.id ? null : service))
|
|
}
|
|
}, [setSelectedService])
|
|
|
|
const onPaneClick = useCallback(() => {
|
|
setSelectedService(null)
|
|
}, [setSelectedService])
|
|
|
|
return (
|
|
<div className="flex bg-white rounded-xl border border-slate-200 shadow-sm overflow-hidden" style={{ height: '700px' }}>
|
|
{/* Canvas */}
|
|
<div className="flex-1 relative">
|
|
<ReactFlow
|
|
nodes={nodes}
|
|
edges={edges}
|
|
onNodesChange={onNodesChange}
|
|
onEdgesChange={onEdgesChange}
|
|
onNodeClick={onNodeClick}
|
|
onPaneClick={onPaneClick}
|
|
fitView
|
|
fitViewOptions={{ padding: 0.15 }}
|
|
attributionPosition="bottom-left"
|
|
>
|
|
<Controls />
|
|
<MiniMap
|
|
nodeColor={node => {
|
|
if (node.id.startsWith('db-')) return '#94a3b8'
|
|
if (node.id.startsWith('rag-')) return '#22c55e'
|
|
if (node.id.startsWith('api-')) return '#c4b5fd'
|
|
const svc = ARCH_SERVICES.find(s => s.id === node.id)
|
|
return svc ? LAYERS[svc.layer].colorBorder : '#94a3b8'
|
|
}}
|
|
maskColor="rgba(0,0,0,0.08)"
|
|
/>
|
|
<Background variant={BackgroundVariant.Dots} gap={16} size={1} />
|
|
|
|
{/* Legende */}
|
|
<Panel
|
|
position="bottom-right"
|
|
className="bg-white/95 p-3 rounded-lg shadow-lg text-xs"
|
|
>
|
|
<div className="font-medium text-slate-700 mb-2">Legende</div>
|
|
<div className="space-y-1">
|
|
{LAYER_ORDER.map(layerId => {
|
|
const layer = LAYERS[layerId]
|
|
return (
|
|
<div key={layerId} className="flex items-center gap-2">
|
|
<span
|
|
className="w-3 h-3 rounded"
|
|
style={{
|
|
background: layer.colorBg,
|
|
border: `1px solid ${layer.colorBorder}`,
|
|
}}
|
|
/>
|
|
<span className="text-slate-600">{layer.name}</span>
|
|
</div>
|
|
)
|
|
})}
|
|
<div className="border-t border-slate-200 my-1.5 pt-1.5">
|
|
<div className="flex items-center gap-2">
|
|
<span className="w-3 h-3 rounded bg-slate-200 border border-slate-400" />
|
|
<span className="text-slate-500">DB-Tabelle</span>
|
|
</div>
|
|
<div className="flex items-center gap-2 mt-0.5">
|
|
<span className="w-3 h-3 rounded bg-green-100 border border-green-500" />
|
|
<span className="text-slate-500">RAG-Collection</span>
|
|
</div>
|
|
<div className="flex items-center gap-2 mt-0.5">
|
|
<span className="w-3 h-3 rounded bg-violet-100 border border-violet-400" />
|
|
<span className="text-slate-500">API-Endpunkt</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</Panel>
|
|
|
|
{/* Swim-Lane Labels */}
|
|
{layerFilter === 'alle' && (
|
|
<Panel position="top-left" className="pointer-events-none">
|
|
<div className="space-y-1">
|
|
{LAYER_ORDER.map(layerId => {
|
|
const layer = LAYERS[layerId]
|
|
return (
|
|
<div
|
|
key={layerId}
|
|
className="px-3 py-1 rounded text-xs font-medium opacity-50"
|
|
style={{
|
|
background: layer.colorBg,
|
|
color: layer.colorText,
|
|
border: `1px solid ${layer.colorBorder}`,
|
|
}}
|
|
>
|
|
{layer.name}
|
|
</div>
|
|
)
|
|
})}
|
|
</div>
|
|
</Panel>
|
|
)}
|
|
</ReactFlow>
|
|
</div>
|
|
|
|
{/* Detail Panel */}
|
|
{selectedService && (
|
|
<DetailPanel
|
|
service={selectedService}
|
|
onClose={() => setSelectedService(null)}
|
|
/>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|