Files
breakpilot-compliance/admin-compliance/app/sdk/sdk-flow/_components/useFlowGraph.tsx
Sharang Parnerkar 2b818c6fb3 refactor(admin): split sdk-flow page.tsx into colocated components
Extract BetriebOverviewPanel, DetailPanel, FlowCanvas, FlowToolbar,
StepTable, useFlowGraph hook and helpers into _components/ so page.tsx
drops from 1019 to 156 LOC (under the 300 soft target).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 22:49:33 +02:00

259 lines
7.9 KiB
TypeScript

'use client'
import { useMemo } from 'react'
import type { Node, Edge } from 'reactflow'
import { MarkerType } from 'reactflow'
import { SDK_FLOW_STEPS, FLOW_PACKAGES, type SDKFlowStep } from '../flow-data'
import {
NODE_WIDTH,
PACKAGE_X_OFFSET,
STEP_Y_START,
completionColor,
getStepPosition,
type PackageFilter,
} from './helpers'
export function useFlowGraph(
packageFilter: PackageFilter,
showDb: boolean,
showRag: boolean,
selectedStep: SDKFlowStep | null,
): { nodes: Node[]; edges: Edge[] } {
return useMemo(() => {
const nodes: Node[] = []
const edges: Edge[] = []
const visibleSteps =
packageFilter === 'alle'
? SDK_FLOW_STEPS
: SDK_FLOW_STEPS.filter(s => s.package === packageFilter)
const visibleStepIds = new Set(visibleSteps.map(s => s.id))
// Step Nodes
visibleSteps.forEach(step => {
const pkg = FLOW_PACKAGES[step.package]
const pos = getStepPosition(step)
const isSelected = selectedStep?.id === step.id
// Checkpoint badge text
let badge = ''
if (step.checkpointId) {
badge = step.checkpointType === 'REQUIRED' ? ' *' : ' ~'
if (step.checkpointReviewer && step.checkpointReviewer !== 'NONE') {
badge += ` [${step.checkpointReviewer}]`
}
}
nodes.push({
id: step.id,
type: 'default',
position: pos,
data: {
label: (
<div className="text-center px-1">
<div className="font-medium text-xs leading-tight">
{step.nameShort}
</div>
<div className="text-[10px] opacity-70 mt-0.5">
{step.checkpointId || ''}
{badge}
</div>
{step.completion !== undefined && (
<div className="mt-1.5 px-1">
<div
className="flex items-center justify-center gap-1 text-[9px] font-bold mb-0.5"
style={{ color: isSelected ? 'rgba(255,255,255,0.9)' : completionColor(step.completion) }}
>
{step.completion}%
</div>
<div
className="h-1 rounded-full overflow-hidden"
style={{ background: isSelected ? 'rgba(255,255,255,0.25)' : 'rgba(0,0,0,0.1)' }}
>
<div
className="h-full rounded-full"
style={{
width: `${step.completion}%`,
background: isSelected ? 'rgba(255,255,255,0.8)' : completionColor(step.completion),
}}
/>
</div>
</div>
)}
</div>
),
},
style: {
background: isSelected ? pkg.color.border : pkg.color.bg,
color: isSelected ? 'white' : pkg.color.text,
border: `2px solid ${pkg.color.border}`,
borderRadius: '10px',
padding: '8px 4px',
minWidth: `${NODE_WIDTH}px`,
maxWidth: `${NODE_WIDTH}px`,
cursor: 'pointer',
boxShadow: isSelected
? `0 0 16px ${pkg.color.border}`
: '0 1px 3px rgba(0,0,0,0.08)',
opacity: step.isOptional ? 0.85 : 1,
borderStyle: step.isOptional ? 'dashed' : 'solid',
},
})
})
// Prerequisite Edges
visibleSteps.forEach(step => {
step.prerequisiteSteps.forEach(preId => {
if (visibleStepIds.has(preId)) {
edges.push({
id: `e-${preId}-${step.id}`,
source: preId,
target: step.id,
type: 'smoothstep',
animated: selectedStep?.id === preId || selectedStep?.id === step.id,
style: {
stroke:
selectedStep?.id === preId || selectedStep?.id === step.id
? '#7c3aed'
: '#94a3b8',
strokeWidth:
selectedStep?.id === preId || selectedStep?.id === step.id
? 2.5
: 1.5,
},
markerEnd: {
type: MarkerType.ArrowClosed,
color:
selectedStep?.id === preId || selectedStep?.id === step.id
? '#7c3aed'
: '#94a3b8',
width: 14,
height: 14,
},
})
}
})
})
// DB Table Nodes
if (showDb) {
const dbTablesInUse = new Set<string>()
visibleSteps.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: -280, y: STEP_Y_START + dbIdx * 90 },
data: {
label: (
<div className="text-center">
<div className="text-sm mb-0.5">DB</div>
<div className="font-medium text-[10px] leading-tight">{table}</div>
</div>
),
},
style: {
background: '#f1f5f9',
color: '#475569',
border: '2px solid #94a3b8',
borderRadius: '50%',
width: '90px',
height: '90px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
padding: '4px',
},
})
// Connect steps to this DB table
visibleSteps
.filter(s => s.dbTables.includes(table))
.forEach(step => {
edges.push({
id: `e-db-${table}-${step.id}`,
source: nodeId,
target: step.id,
type: 'straight',
style: {
stroke: '#94a3b8',
strokeWidth: 1,
strokeDasharray: '6 3',
},
labelStyle: { fontSize: 9, fill: '#94a3b8' },
label: step.dbMode !== 'none' ? step.dbMode : undefined,
})
})
dbIdx++
})
}
// RAG Collection Nodes
if (showRag) {
const ragInUse = new Set<string>()
visibleSteps.forEach(s => s.ragCollections.forEach(r => ragInUse.add(r)))
let ragIdx = 0
ragInUse.forEach(collection => {
const nodeId = `rag-${collection}`
// Place RAG nodes to the right of all packages
const rightX = (packageFilter === 'alle' ? 1280 : PACKAGE_X_OFFSET[packageFilter] || 0) + NODE_WIDTH + 180
nodes.push({
id: nodeId,
type: 'default',
position: { x: rightX, y: STEP_Y_START + ragIdx * 90 },
data: {
label: (
<div className="text-center">
<div className="text-sm mb-0.5">RAG</div>
<div className="font-medium text-[10px] leading-tight">
{collection.replace('bp_', '')}
</div>
</div>
),
},
style: {
background: '#dcfce7',
color: '#166534',
border: '2px solid #22c55e',
borderRadius: '50%',
width: '90px',
height: '90px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
padding: '4px',
},
})
// Connect steps to this RAG collection
visibleSteps
.filter(s => s.ragCollections.includes(collection))
.forEach(step => {
edges.push({
id: `e-rag-${collection}-${step.id}`,
source: nodeId,
target: step.id,
type: 'straight',
style: {
stroke: '#22c55e',
strokeWidth: 1,
strokeDasharray: '6 3',
},
})
})
ragIdx++
})
}
return { nodes, edges }
}, [packageFilter, showDb, showRag, selectedStep])
}