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>
252 lines
7.7 KiB
TypeScript
252 lines
7.7 KiB
TypeScript
'use client'
|
|
|
|
import { useMemo } from 'react'
|
|
import { type Node, type Edge, MarkerType } from 'reactflow'
|
|
import {
|
|
ARCH_SERVICES,
|
|
ARCH_EDGES,
|
|
LAYERS,
|
|
type ArchService,
|
|
} from '../architecture-data'
|
|
import {
|
|
NODE_WIDTH,
|
|
LANE_Y_START,
|
|
getServicePosition,
|
|
type LayerFilter,
|
|
} from '../_layout'
|
|
|
|
export function useArchGraph({
|
|
layerFilter,
|
|
showDb,
|
|
showRag,
|
|
showApis,
|
|
selectedService,
|
|
}: {
|
|
layerFilter: LayerFilter
|
|
showDb: boolean
|
|
showRag: boolean
|
|
showApis: boolean
|
|
selectedService: ArchService | null
|
|
}) {
|
|
return useMemo(() => {
|
|
const nodes: Node[] = []
|
|
const edges: Edge[] = []
|
|
|
|
const visibleServices =
|
|
layerFilter === 'alle'
|
|
? ARCH_SERVICES
|
|
: ARCH_SERVICES.filter(s => s.layer === layerFilter)
|
|
|
|
const visibleIds = new Set(visibleServices.map(s => s.id))
|
|
|
|
// ── Service Nodes ──────────────────────────────────────────────────────
|
|
visibleServices.forEach(service => {
|
|
const layer = LAYERS[service.layer]
|
|
const pos = getServicePosition(service)
|
|
const isSelected = selectedService?.id === service.id
|
|
|
|
nodes.push({
|
|
id: service.id,
|
|
type: 'default',
|
|
position: pos,
|
|
data: {
|
|
label: (
|
|
<div className="text-center px-1">
|
|
<div className="font-medium text-xs leading-tight">
|
|
{service.nameShort}
|
|
</div>
|
|
<div className="text-[10px] opacity-70 mt-0.5">
|
|
{service.tech}
|
|
</div>
|
|
{service.port && (
|
|
<div className="text-[9px] opacity-50 mt-0.5">
|
|
:{service.port}
|
|
</div>
|
|
)}
|
|
</div>
|
|
),
|
|
},
|
|
style: {
|
|
background: isSelected ? layer.colorBorder : layer.colorBg,
|
|
color: isSelected ? 'white' : layer.colorText,
|
|
border: `2px solid ${layer.colorBorder}`,
|
|
borderRadius: '10px',
|
|
padding: '8px 4px',
|
|
minWidth: `${NODE_WIDTH}px`,
|
|
maxWidth: `${NODE_WIDTH}px`,
|
|
cursor: 'pointer',
|
|
boxShadow: isSelected
|
|
? `0 0 16px ${layer.colorBorder}`
|
|
: '0 1px 3px rgba(0,0,0,0.08)',
|
|
},
|
|
})
|
|
})
|
|
|
|
// ── Connection Edges ───────────────────────────────────────────────────
|
|
ARCH_EDGES.forEach(archEdge => {
|
|
if (visibleIds.has(archEdge.source) && visibleIds.has(archEdge.target)) {
|
|
const isHighlighted =
|
|
selectedService?.id === archEdge.source ||
|
|
selectedService?.id === archEdge.target
|
|
|
|
edges.push({
|
|
id: `e-${archEdge.source}-${archEdge.target}`,
|
|
source: archEdge.source,
|
|
target: archEdge.target,
|
|
type: 'smoothstep',
|
|
animated: isHighlighted,
|
|
label: archEdge.label,
|
|
labelStyle: { fontSize: 9, fill: isHighlighted ? '#7c3aed' : '#94a3b8' },
|
|
style: {
|
|
stroke: isHighlighted ? '#7c3aed' : '#94a3b8',
|
|
strokeWidth: isHighlighted ? 2.5 : 1.5,
|
|
},
|
|
markerEnd: {
|
|
type: MarkerType.ArrowClosed,
|
|
color: isHighlighted ? '#7c3aed' : '#94a3b8',
|
|
width: 14,
|
|
height: 14,
|
|
},
|
|
})
|
|
}
|
|
})
|
|
|
|
// ── DB Table Nodes ─────────────────────────────────────────────────────
|
|
if (showDb) {
|
|
const dbTablesInUse = new Set<string>()
|
|
visibleServices.forEach(s => s.dbTables.forEach(t => dbTablesInUse.add(t)))
|
|
|
|
let dbIdx = 0
|
|
dbTablesInUse.forEach(table => {
|
|
const nodeId = `db-${table}`
|
|
nodes.push({
|
|
id: nodeId,
|
|
type: 'default',
|
|
position: { x: -250, y: LANE_Y_START + dbIdx * 60 },
|
|
data: {
|
|
label: (
|
|
<div className="text-center">
|
|
<div className="font-medium text-[10px] leading-tight">{table}</div>
|
|
</div>
|
|
),
|
|
},
|
|
style: {
|
|
background: '#f1f5f9',
|
|
color: '#475569',
|
|
border: '1px solid #94a3b8',
|
|
borderRadius: '6px',
|
|
padding: '4px 6px',
|
|
fontSize: '10px',
|
|
minWidth: '140px',
|
|
},
|
|
})
|
|
|
|
visibleServices
|
|
.filter(s => s.dbTables.includes(table))
|
|
.forEach(svc => {
|
|
edges.push({
|
|
id: `e-db-${table}-${svc.id}`,
|
|
source: nodeId,
|
|
target: svc.id,
|
|
type: 'straight',
|
|
style: { stroke: '#94a3b8', strokeWidth: 1, strokeDasharray: '6 3' },
|
|
})
|
|
})
|
|
|
|
dbIdx++
|
|
})
|
|
}
|
|
|
|
// ── RAG Collection Nodes ───────────────────────────────────────────────
|
|
if (showRag) {
|
|
const ragInUse = new Set<string>()
|
|
visibleServices.forEach(s => s.ragCollections.forEach(r => ragInUse.add(r)))
|
|
|
|
let ragIdx = 0
|
|
ragInUse.forEach(collection => {
|
|
const nodeId = `rag-${collection}`
|
|
const rightX = 1200
|
|
|
|
nodes.push({
|
|
id: nodeId,
|
|
type: 'default',
|
|
position: { x: rightX, y: LANE_Y_START + ragIdx * 60 },
|
|
data: {
|
|
label: (
|
|
<div className="text-center">
|
|
<div className="font-medium text-[10px] leading-tight">
|
|
{collection.replace('bp_', '')}
|
|
</div>
|
|
</div>
|
|
),
|
|
},
|
|
style: {
|
|
background: '#dcfce7',
|
|
color: '#166534',
|
|
border: '1px solid #22c55e',
|
|
borderRadius: '6px',
|
|
padding: '4px 6px',
|
|
fontSize: '10px',
|
|
minWidth: '130px',
|
|
},
|
|
})
|
|
|
|
visibleServices
|
|
.filter(s => s.ragCollections.includes(collection))
|
|
.forEach(svc => {
|
|
edges.push({
|
|
id: `e-rag-${collection}-${svc.id}`,
|
|
source: nodeId,
|
|
target: svc.id,
|
|
type: 'straight',
|
|
style: { stroke: '#22c55e', strokeWidth: 1, strokeDasharray: '6 3' },
|
|
})
|
|
})
|
|
|
|
ragIdx++
|
|
})
|
|
}
|
|
|
|
// ── API Endpoint Nodes ─────────────────────────────────────────────────
|
|
if (showApis) {
|
|
visibleServices.forEach(svc => {
|
|
if (svc.apiEndpoints.length === 0) return
|
|
const svcPos = getServicePosition(svc)
|
|
|
|
svc.apiEndpoints.forEach((ep, idx) => {
|
|
const nodeId = `api-${svc.id}-${idx}`
|
|
nodes.push({
|
|
id: nodeId,
|
|
type: 'default',
|
|
position: { x: svcPos.x + NODE_WIDTH + 30, y: svcPos.y + idx * 32 },
|
|
data: {
|
|
label: (
|
|
<div className="text-[9px] font-mono leading-tight truncate">{ep}</div>
|
|
),
|
|
},
|
|
style: {
|
|
background: '#faf5ff',
|
|
color: '#7c3aed',
|
|
border: '1px solid #c4b5fd',
|
|
borderRadius: '4px',
|
|
padding: '2px 6px',
|
|
fontSize: '9px',
|
|
minWidth: '160px',
|
|
},
|
|
})
|
|
|
|
edges.push({
|
|
id: `e-api-${svc.id}-${idx}`,
|
|
source: svc.id,
|
|
target: nodeId,
|
|
type: 'straight',
|
|
style: { stroke: '#c4b5fd', strokeWidth: 1 },
|
|
})
|
|
})
|
|
})
|
|
}
|
|
|
|
return { nodes, edges }
|
|
}, [layerFilter, showDb, showRag, showApis, selectedService])
|
|
}
|