Files
breakpilot-compliance/admin-compliance/app/sdk/iace/[projectId]/knowledge-graph/page.tsx
T
Benjamin Admin df15f6f098
Build + Deploy / build-admin-compliance (push) Successful in 10s
Build + Deploy / build-backend-compliance (push) Successful in 10s
Build + Deploy / build-ai-sdk (push) Successful in 9s
Build + Deploy / build-developer-portal (push) Successful in 9s
Build + Deploy / build-tts (push) Successful in 10s
Build + Deploy / build-document-crawler (push) Successful in 9s
Build + Deploy / build-dsms-gateway (push) Successful in 10s
Build + Deploy / build-dsms-node (push) Successful in 11s
CI / branch-name (push) Has been skipped
CI / guardrail-integrity (push) Has been skipped
CI / loc-budget (push) Failing after 14s
CI / secret-scan (push) Has been skipped
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / nodejs-build (push) Successful in 2m23s
CI / dep-audit (push) Has been skipped
CI / sbom-scan (push) Has been skipped
CI / test-go (push) Successful in 40s
CI / test-python-backend (push) Successful in 35s
CI / test-python-document-crawler (push) Successful in 25s
CI / test-python-dsms-gateway (push) Successful in 20s
CI / validate-canonical-controls (push) Successful in 14s
Build + Deploy / trigger-orca (push) Successful in 2m13s
feat(iace): Erweiterung 5 — Safety Knowledge Graph (React Flow)
Interaktiver Graph: Komponente → Gefaehrdung → Massnahme
- 3-Spalten-Layout: Indigo (Komponenten), Rot (Hazards), Gruen (Massnahmen)
- Animierte Kanten mit Pfeilmarkern
- Zoom, Pan, MiniMap, Controls
- Dependency: @xyflow/react v12 (MIT-Lizenz)

Alle 5 IACE Phase-5 Erweiterungen jetzt abgeschlossen:
1. Betriebszustand-UI
2. FMEA-Worksheet
3. Delta-Impact-Preview Modal
4. Textil + Landmaschinen Patterns
5. Safety Knowledge Graph

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-12 07:20:38 +02:00

192 lines
5.8 KiB
TypeScript

'use client'
import { useCallback, useMemo } from 'react'
import { useParams } from 'next/navigation'
import {
ReactFlow,
Background,
Controls,
MiniMap,
useNodesState,
useEdgesState,
type Node,
type Edge,
MarkerType,
} from '@xyflow/react'
import '@xyflow/react/dist/style.css'
import { useKnowledgeGraph } from './_hooks/useKnowledgeGraph'
const TYPE_STYLES: Record<string, { bg: string; border: string }> = {
component: { bg: '#EEF2FF', border: '#6366F1' },
hazard: { bg: '#FEF2F2', border: '#EF4444' },
mitigation: { bg: '#ECFDF5', border: '#10B981' },
}
const TYPE_LABELS: Record<string, string> = {
component: 'Komponente',
hazard: 'Gefaehrdung',
mitigation: 'Massnahme',
}
export default function KnowledgeGraphPage() {
const { projectId } = useParams<{ projectId: string }>()
const { nodes: graphNodes, edges: graphEdges, loading, stats } = useKnowledgeGraph(projectId)
// Convert to React Flow nodes with layout
const rfNodes = useMemo((): Node[] => {
const compNodes = graphNodes.filter((n) => n.type === 'component')
const hazNodes = graphNodes.filter((n) => n.type === 'hazard')
const mitNodes = graphNodes.filter((n) => n.type === 'mitigation')
const nodes: Node[] = []
const colWidth = 300
const rowHeight = 80
// Column 1: Components
compNodes.forEach((n, i) => {
nodes.push({
id: n.id,
position: { x: 0, y: i * rowHeight },
data: { label: n.label, subLabel: n.subLabel, nodeType: n.type },
style: {
background: TYPE_STYLES.component.bg,
border: `2px solid ${TYPE_STYLES.component.border}`,
borderRadius: '12px',
padding: '8px 12px',
fontSize: '12px',
fontWeight: 500,
width: 200,
},
})
})
// Column 2: Hazards
hazNodes.forEach((n, i) => {
nodes.push({
id: n.id,
position: { x: colWidth, y: i * rowHeight },
data: { label: n.label, subLabel: n.subLabel, nodeType: n.type },
style: {
background: TYPE_STYLES.hazard.bg,
border: `2px solid ${TYPE_STYLES.hazard.border}`,
borderRadius: '12px',
padding: '8px 12px',
fontSize: '12px',
fontWeight: 500,
width: 220,
},
})
})
// Column 3: Mitigations
mitNodes.forEach((n, i) => {
nodes.push({
id: n.id,
position: { x: colWidth * 2, y: i * rowHeight },
data: { label: n.label, subLabel: n.subLabel, nodeType: n.type },
style: {
background: TYPE_STYLES.mitigation.bg,
border: `2px solid ${TYPE_STYLES.mitigation.border}`,
borderRadius: '12px',
padding: '8px 12px',
fontSize: '12px',
fontWeight: 500,
width: 220,
},
})
})
return nodes
}, [graphNodes])
const rfEdges = useMemo((): Edge[] => {
return graphEdges.map((e) => ({
id: e.id,
source: e.source,
target: e.target,
label: e.label,
type: 'smoothstep',
animated: true,
style: { stroke: '#94A3B8', strokeWidth: 1.5 },
labelStyle: { fontSize: 10, fill: '#64748B' },
markerEnd: { type: MarkerType.ArrowClosed, color: '#94A3B8' },
}))
}, [graphEdges])
const [nodes, setNodes, onNodesChange] = useNodesState(rfNodes)
const [edges, setEdges, onEdgesChange] = useEdgesState(rfEdges)
// Update when data loads
const onInit = useCallback(() => {
if (rfNodes.length > 0) {
setNodes(rfNodes)
setEdges(rfEdges)
}
}, [rfNodes, rfEdges, setNodes, setEdges])
if (loading) {
return (
<div className="flex items-center justify-center h-64">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-purple-600" />
</div>
)
}
return (
<div className="space-y-4">
{/* Header */}
<div>
<h1 className="text-xl font-bold text-gray-900 dark:text-white">Safety Knowledge Graph</h1>
<p className="text-sm text-gray-500 dark:text-gray-400 mt-0.5">
Interaktive Visualisierung: Komponente Gefaehrdung Massnahme
</p>
</div>
{/* Legend + Stats */}
<div className="flex items-center gap-6">
{(['component', 'hazard', 'mitigation'] as const).map((t) => (
<div key={t} className="flex items-center gap-2">
<div className="w-3 h-3 rounded-full" style={{ backgroundColor: TYPE_STYLES[t].border }} />
<span className="text-xs text-gray-600">{TYPE_LABELS[t]} ({
t === 'component' ? stats.components : t === 'hazard' ? stats.hazards : stats.mitigations
})</span>
</div>
))}
</div>
{/* Graph */}
{graphNodes.length === 0 ? (
<div className="text-center py-16 text-gray-500">
Keine Daten bitte zuerst Projekt initialisieren.
</div>
) : (
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 overflow-hidden" style={{ height: '600px' }}>
<ReactFlow
nodes={rfNodes}
edges={rfEdges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onInit={onInit}
fitView
fitViewOptions={{ padding: 0.2 }}
minZoom={0.3}
maxZoom={2}
nodesDraggable
nodesConnectable={false}
>
<Background gap={20} size={1} color="#f0f0f0" />
<Controls />
<MiniMap
nodeColor={(node) => {
const t = (node.data as { nodeType?: string })?.nodeType || 'component'
return TYPE_STYLES[t]?.border || '#94A3B8'
}}
maskColor="rgba(0,0,0,0.05)"
/>
</ReactFlow>
</div>
)}
</div>
)
}