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
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>
192 lines
5.8 KiB
TypeScript
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>
|
|
)
|
|
}
|