[split-required] Split final 43 files (500-668 LOC) to complete refactoring
klausur-service (11 files): - cv_gutter_repair, ocr_pipeline_regression, upload_api - ocr_pipeline_sessions, smart_spell, nru_worksheet_generator - ocr_pipeline_overlays, mail/aggregator, zeugnis_api - cv_syllable_detect, self_rag backend-lehrer (17 files): - classroom_engine/suggestions, generators/quiz_generator - worksheets_api, llm_gateway/comparison, state_engine_api - classroom/models (→ 4 submodules), services/file_processor - alerts_agent/api/wizard+digests+routes, content_generators/pdf - classroom/routes/sessions, llm_gateway/inference - classroom_engine/analytics, auth/keycloak_auth - alerts_agent/processing/rule_engine, ai_processor/print_versions agent-core (5 files): - brain/memory_store, brain/knowledge_graph, brain/context_manager - orchestrator/supervisor, sessions/session_manager admin-lehrer (5 components): - GridOverlay, StepGridReview, DevOpsPipelineSidebar - DataFlowDiagram, sbom/wizard/page website (2 files): - DependencyMap, lehrer/abitur-archiv Other: nibis_ingestion, grid_detection_service, export-doclayout-onnx Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -10,67 +10,9 @@
|
||||
*/
|
||||
|
||||
import { useState, useMemo } from 'react'
|
||||
import { Language, getTerm } from '@/lib/compliance-i18n'
|
||||
|
||||
interface Requirement {
|
||||
id: string
|
||||
article: string
|
||||
title: string
|
||||
regulation_code: string
|
||||
}
|
||||
|
||||
interface Control {
|
||||
id: string
|
||||
control_id: string
|
||||
title: string
|
||||
domain: string
|
||||
status: string
|
||||
}
|
||||
|
||||
interface Mapping {
|
||||
requirement_id: string
|
||||
control_id: string
|
||||
coverage_level: 'full' | 'partial' | 'planned'
|
||||
}
|
||||
|
||||
interface DependencyMapProps {
|
||||
requirements: Requirement[]
|
||||
controls: Control[]
|
||||
mappings: Mapping[]
|
||||
lang?: Language
|
||||
onControlClick?: (control: Control) => void
|
||||
onRequirementClick?: (requirement: Requirement) => void
|
||||
}
|
||||
|
||||
const DOMAIN_COLORS: Record<string, string> = {
|
||||
gov: '#64748b',
|
||||
priv: '#3b82f6',
|
||||
iam: '#a855f7',
|
||||
crypto: '#eab308',
|
||||
sdlc: '#22c55e',
|
||||
ops: '#f97316',
|
||||
ai: '#ec4899',
|
||||
cra: '#06b6d4',
|
||||
aud: '#6366f1',
|
||||
}
|
||||
|
||||
const DOMAIN_LABELS: Record<string, string> = {
|
||||
gov: 'Governance',
|
||||
priv: 'Datenschutz',
|
||||
iam: 'Identity & Access',
|
||||
crypto: 'Kryptografie',
|
||||
sdlc: 'Secure Dev',
|
||||
ops: 'Operations',
|
||||
ai: 'KI-spezifisch',
|
||||
cra: 'Supply Chain',
|
||||
aud: 'Audit',
|
||||
}
|
||||
|
||||
const COVERAGE_COLORS: Record<string, { bg: string; border: string; text: string }> = {
|
||||
full: { bg: 'bg-green-100', border: 'border-green-500', text: 'text-green-700' },
|
||||
partial: { bg: 'bg-yellow-100', border: 'border-yellow-500', text: 'text-yellow-700' },
|
||||
planned: { bg: 'bg-slate-100', border: 'border-slate-400', text: 'text-slate-600' },
|
||||
}
|
||||
import type { Control, Requirement, DependencyMapProps } from './DependencyMapTypes'
|
||||
import { DOMAIN_COLORS, COVERAGE_COLORS } from './DependencyMapTypes'
|
||||
import { DependencyMapSankey } from './DependencyMapSankey'
|
||||
|
||||
export default function DependencyMap({
|
||||
requirements,
|
||||
@@ -115,7 +57,7 @@ export default function DependencyMap({
|
||||
|
||||
// Build mapping lookup
|
||||
const mappingLookup = useMemo(() => {
|
||||
const lookup: Record<string, Record<string, Mapping>> = {}
|
||||
const lookup: Record<string, Record<string, typeof mappings[0]>> = {}
|
||||
mappings.forEach((m) => {
|
||||
if (!lookup[m.control_id]) lookup[m.control_id] = {}
|
||||
lookup[m.control_id][m.requirement_id] = m
|
||||
@@ -123,11 +65,6 @@ export default function DependencyMap({
|
||||
return lookup
|
||||
}, [mappings])
|
||||
|
||||
// Get connected requirements for a control
|
||||
const getConnectedRequirements = (controlId: string) => {
|
||||
return Object.keys(mappingLookup[controlId] || {})
|
||||
}
|
||||
|
||||
// Get connected controls for a requirement
|
||||
const getConnectedControls = (requirementId: string) => {
|
||||
return Object.keys(mappingLookup)
|
||||
@@ -201,81 +138,42 @@ export default function DependencyMap({
|
||||
{/* Statistics Header */}
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
<div className="bg-white rounded-lg border p-4">
|
||||
<p className="text-sm text-slate-500">
|
||||
{lang === 'de' ? 'Abdeckung' : 'Coverage'}
|
||||
</p>
|
||||
<p className="text-sm text-slate-500">{lang === 'de' ? 'Abdeckung' : 'Coverage'}</p>
|
||||
<p className="text-2xl font-bold text-primary-600">{stats.coveragePercent}%</p>
|
||||
<p className="text-xs text-slate-400">
|
||||
{stats.coveredRequirements}/{stats.totalRequirements} {lang === 'de' ? 'Anforderungen' : 'Requirements'}
|
||||
</p>
|
||||
<p className="text-xs text-slate-400">{stats.coveredRequirements}/{stats.totalRequirements} {lang === 'de' ? 'Anforderungen' : 'Requirements'}</p>
|
||||
</div>
|
||||
<div className="bg-white rounded-lg border p-4">
|
||||
<p className="text-sm text-slate-500">
|
||||
{lang === 'de' ? 'Vollstaendig' : 'Full'}
|
||||
</p>
|
||||
<p className="text-sm text-slate-500">{lang === 'de' ? 'Vollstaendig' : 'Full'}</p>
|
||||
<p className="text-2xl font-bold text-green-600">{stats.fullMappings}</p>
|
||||
<p className="text-xs text-slate-400">{lang === 'de' ? 'Mappings' : 'Mappings'}</p>
|
||||
<p className="text-xs text-slate-400">Mappings</p>
|
||||
</div>
|
||||
<div className="bg-white rounded-lg border p-4">
|
||||
<p className="text-sm text-slate-500">
|
||||
{lang === 'de' ? 'Teilweise' : 'Partial'}
|
||||
</p>
|
||||
<p className="text-sm text-slate-500">{lang === 'de' ? 'Teilweise' : 'Partial'}</p>
|
||||
<p className="text-2xl font-bold text-yellow-600">{stats.partialMappings}</p>
|
||||
<p className="text-xs text-slate-400">{lang === 'de' ? 'Mappings' : 'Mappings'}</p>
|
||||
<p className="text-xs text-slate-400">Mappings</p>
|
||||
</div>
|
||||
<div className="bg-white rounded-lg border p-4">
|
||||
<p className="text-sm text-slate-500">
|
||||
{lang === 'de' ? 'Geplant' : 'Planned'}
|
||||
</p>
|
||||
<p className="text-sm text-slate-500">{lang === 'de' ? 'Geplant' : 'Planned'}</p>
|
||||
<p className="text-2xl font-bold text-slate-600">{stats.plannedMappings}</p>
|
||||
<p className="text-xs text-slate-400">{lang === 'de' ? 'Mappings' : 'Mappings'}</p>
|
||||
<p className="text-xs text-slate-400">Mappings</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Filters */}
|
||||
<div className="bg-white rounded-xl shadow-sm border p-4">
|
||||
<div className="flex flex-wrap items-center gap-4">
|
||||
<select
|
||||
value={filterRegulation}
|
||||
onChange={(e) => setFilterRegulation(e.target.value)}
|
||||
className="px-3 py-2 border rounded-lg focus:ring-2 focus:ring-primary-500"
|
||||
>
|
||||
<select value={filterRegulation} onChange={(e) => setFilterRegulation(e.target.value)} className="px-3 py-2 border rounded-lg focus:ring-2 focus:ring-primary-500">
|
||||
<option value="">{lang === 'de' ? 'Alle Verordnungen' : 'All Regulations'}</option>
|
||||
{regulations.map((reg) => (
|
||||
<option key={reg} value={reg}>{reg}</option>
|
||||
))}
|
||||
{regulations.map((reg) => (<option key={reg} value={reg}>{reg}</option>))}
|
||||
</select>
|
||||
|
||||
<select
|
||||
value={filterDomain}
|
||||
onChange={(e) => setFilterDomain(e.target.value)}
|
||||
className="px-3 py-2 border rounded-lg focus:ring-2 focus:ring-primary-500"
|
||||
>
|
||||
<select value={filterDomain} onChange={(e) => setFilterDomain(e.target.value)} className="px-3 py-2 border rounded-lg focus:ring-2 focus:ring-primary-500">
|
||||
<option value="">{lang === 'de' ? 'Alle Domains' : 'All Domains'}</option>
|
||||
{domains.map((dom) => (
|
||||
<option key={dom} value={dom}>{DOMAIN_LABELS[dom] || dom}</option>
|
||||
))}
|
||||
{domains.map((dom) => (<option key={dom} value={dom}>{dom}</option>))}
|
||||
</select>
|
||||
|
||||
<div className="flex-1" />
|
||||
|
||||
<div className="flex bg-slate-100 rounded-lg p-1">
|
||||
<button
|
||||
onClick={() => setViewMode('matrix')}
|
||||
className={`px-3 py-1.5 text-sm rounded-md transition-colors ${
|
||||
viewMode === 'matrix' ? 'bg-white shadow text-slate-900' : 'text-slate-600'
|
||||
}`}
|
||||
>
|
||||
Matrix
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setViewMode('sankey')}
|
||||
className={`px-3 py-1.5 text-sm rounded-md transition-colors ${
|
||||
viewMode === 'sankey' ? 'bg-white shadow text-slate-900' : 'text-slate-600'
|
||||
}`}
|
||||
>
|
||||
{lang === 'de' ? 'Verbindungen' : 'Connections'}
|
||||
</button>
|
||||
<button onClick={() => setViewMode('matrix')} className={`px-3 py-1.5 text-sm rounded-md transition-colors ${viewMode === 'matrix' ? 'bg-white shadow text-slate-900' : 'text-slate-600'}`}>Matrix</button>
|
||||
<button onClick={() => setViewMode('sankey')} className={`px-3 py-1.5 text-sm rounded-md transition-colors ${viewMode === 'sankey' ? 'bg-white shadow text-slate-900' : 'text-slate-600'}`}>{lang === 'de' ? 'Verbindungen' : 'Connections'}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -284,96 +182,38 @@ export default function DependencyMap({
|
||||
{viewMode === 'matrix' ? (
|
||||
<div className="bg-white rounded-xl shadow-sm border p-4 overflow-auto">
|
||||
<div className="min-w-[800px]">
|
||||
{/* Matrix Header */}
|
||||
<div className="flex">
|
||||
<div className="w-48 flex-shrink-0" />
|
||||
<div className="flex-1 flex">
|
||||
{filteredControls.map((control) => (
|
||||
<div
|
||||
key={control.control_id}
|
||||
onClick={() => handleControlClick(control)}
|
||||
className={`
|
||||
w-20 flex-shrink-0 text-center p-2 cursor-pointer transition-colors
|
||||
${selectedControl === control.control_id ? 'bg-primary-100' : 'hover:bg-slate-50'}
|
||||
`}
|
||||
>
|
||||
<div
|
||||
className="w-3 h-3 rounded-full mx-auto mb-1"
|
||||
style={{ backgroundColor: DOMAIN_COLORS[control.domain] || '#94a3b8' }}
|
||||
/>
|
||||
<p className="text-xs font-mono font-medium truncate" title={control.control_id}>
|
||||
{control.control_id}
|
||||
</p>
|
||||
<div key={control.control_id} onClick={() => handleControlClick(control)} className={`w-20 flex-shrink-0 text-center p-2 cursor-pointer transition-colors ${selectedControl === control.control_id ? 'bg-primary-100' : 'hover:bg-slate-50'}`}>
|
||||
<div className="w-3 h-3 rounded-full mx-auto mb-1" style={{ backgroundColor: DOMAIN_COLORS[control.domain] || '#94a3b8' }} />
|
||||
<p className="text-xs font-mono font-medium truncate" title={control.control_id}>{control.control_id}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Matrix Body */}
|
||||
{filteredRequirements.map((req) => {
|
||||
const connectedControls = getConnectedControls(req.id)
|
||||
const isHighlighted = selectedRequirement === req.id ||
|
||||
(selectedControl && connectedControls.some((c) => c.controlId === selectedControl))
|
||||
|
||||
const isHighlighted = selectedRequirement === req.id || (selectedControl && connectedControls.some((c) => c.controlId === selectedControl))
|
||||
return (
|
||||
<div
|
||||
key={req.id}
|
||||
className={`flex border-t ${isHighlighted ? 'bg-primary-50' : ''}`}
|
||||
>
|
||||
<div
|
||||
onClick={() => handleRequirementClick(req)}
|
||||
className={`
|
||||
w-48 flex-shrink-0 p-2 cursor-pointer transition-colors
|
||||
${selectedRequirement === req.id ? 'bg-primary-100' : 'hover:bg-slate-50'}
|
||||
`}
|
||||
>
|
||||
<p className="text-xs font-medium text-slate-700 truncate" title={req.article}>
|
||||
{req.regulation_code} {req.article}
|
||||
</p>
|
||||
<p className="text-xs text-slate-400 truncate" title={req.title}>
|
||||
{req.title}
|
||||
</p>
|
||||
<div key={req.id} className={`flex border-t ${isHighlighted ? 'bg-primary-50' : ''}`}>
|
||||
<div onClick={() => handleRequirementClick(req)} className={`w-48 flex-shrink-0 p-2 cursor-pointer transition-colors ${selectedRequirement === req.id ? 'bg-primary-100' : 'hover:bg-slate-50'}`}>
|
||||
<p className="text-xs font-medium text-slate-700 truncate" title={req.article}>{req.regulation_code} {req.article}</p>
|
||||
<p className="text-xs text-slate-400 truncate" title={req.title}>{req.title}</p>
|
||||
</div>
|
||||
<div className="flex-1 flex">
|
||||
{filteredControls.map((control) => {
|
||||
const mapping = mappingLookup[control.control_id]?.[req.id]
|
||||
const isControlHighlighted = selectedControl === control.control_id
|
||||
const isConnected = selectedControl && mapping
|
||||
|
||||
return (
|
||||
<div
|
||||
key={`${req.id}-${control.control_id}`}
|
||||
className={`
|
||||
w-20 flex-shrink-0 h-10 flex items-center justify-center
|
||||
${isControlHighlighted ? 'bg-primary-50' : ''}
|
||||
${isConnected ? 'ring-2 ring-primary-400' : ''}
|
||||
`}
|
||||
>
|
||||
<div key={`${req.id}-${control.control_id}`} className={`w-20 flex-shrink-0 h-10 flex items-center justify-center ${isControlHighlighted ? 'bg-primary-50' : ''} ${isConnected ? 'ring-2 ring-primary-400' : ''}`}>
|
||||
{mapping && (
|
||||
<div
|
||||
className={`
|
||||
w-6 h-6 rounded flex items-center justify-center
|
||||
${COVERAGE_COLORS[mapping.coverage_level].bg}
|
||||
${COVERAGE_COLORS[mapping.coverage_level].text}
|
||||
border ${COVERAGE_COLORS[mapping.coverage_level].border}
|
||||
`}
|
||||
title={`${mapping.coverage_level === 'full' ? 'Vollstaendig' : mapping.coverage_level === 'partial' ? 'Teilweise' : 'Geplant'}`}
|
||||
>
|
||||
{mapping.coverage_level === 'full' && (
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
)}
|
||||
{mapping.coverage_level === 'partial' && (
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M20 12H4" />
|
||||
</svg>
|
||||
)}
|
||||
{mapping.coverage_level === 'planned' && (
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
)}
|
||||
<div className={`w-6 h-6 rounded flex items-center justify-center ${COVERAGE_COLORS[mapping.coverage_level].bg} ${COVERAGE_COLORS[mapping.coverage_level].text} border ${COVERAGE_COLORS[mapping.coverage_level].border}`} title={mapping.coverage_level}>
|
||||
{mapping.coverage_level === 'full' && (<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" /></svg>)}
|
||||
{mapping.coverage_level === 'partial' && (<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M20 12H4" /></svg>)}
|
||||
{mapping.coverage_level === 'planned' && (<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -386,179 +226,35 @@ export default function DependencyMap({
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
/* Sankey/Connection View */
|
||||
<div className="bg-white rounded-xl shadow-sm border p-6">
|
||||
<div className="flex gap-8">
|
||||
{/* Controls Column */}
|
||||
<div className="w-1/3 space-y-2">
|
||||
<h4 className="text-sm font-medium text-slate-700 mb-3">
|
||||
Controls ({filteredControls.length})
|
||||
</h4>
|
||||
{filteredControls.map((control) => {
|
||||
const connectedReqs = getConnectedRequirements(control.control_id)
|
||||
const isSelected = selectedControl === control.control_id
|
||||
|
||||
return (
|
||||
<button
|
||||
key={control.control_id}
|
||||
onClick={() => handleControlClick(control)}
|
||||
className={`
|
||||
w-full text-left p-3 rounded-lg border transition-all
|
||||
${isSelected ? 'border-primary-500 bg-primary-50 shadow' : 'border-slate-200 hover:border-slate-300'}
|
||||
`}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<div
|
||||
className="w-3 h-3 rounded-full flex-shrink-0"
|
||||
style={{ backgroundColor: DOMAIN_COLORS[control.domain] || '#94a3b8' }}
|
||||
/>
|
||||
<span className="font-mono text-sm font-medium">{control.control_id}</span>
|
||||
<span className="text-xs text-slate-400 ml-auto">{connectedReqs.length}</span>
|
||||
</div>
|
||||
<p className="text-xs text-slate-500 mt-1 truncate">{control.title}</p>
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Connection Lines (simplified) */}
|
||||
<div className="w-1/3 flex items-center justify-center">
|
||||
<div className="relative w-full h-full min-h-[200px]">
|
||||
{selectedControl && (
|
||||
<div className="absolute inset-0 flex flex-col items-center justify-center gap-2">
|
||||
{getConnectedRequirements(selectedControl).slice(0, 10).map((reqId, idx) => {
|
||||
const req = requirements.find((r) => r.id === reqId)
|
||||
const mapping = mappingLookup[selectedControl][reqId]
|
||||
if (!req) return null
|
||||
|
||||
return (
|
||||
<div
|
||||
key={reqId}
|
||||
className={`
|
||||
px-3 py-1.5 rounded-full text-xs font-medium
|
||||
${COVERAGE_COLORS[mapping.coverage_level].bg}
|
||||
${COVERAGE_COLORS[mapping.coverage_level].text}
|
||||
border ${COVERAGE_COLORS[mapping.coverage_level].border}
|
||||
`}
|
||||
>
|
||||
{req.regulation_code} {req.article}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
{getConnectedRequirements(selectedControl).length > 10 && (
|
||||
<span className="text-xs text-slate-400">
|
||||
+{getConnectedRequirements(selectedControl).length - 10} {lang === 'de' ? 'weitere' : 'more'}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{selectedRequirement && (
|
||||
<div className="absolute inset-0 flex flex-col items-center justify-center gap-2">
|
||||
{getConnectedControls(selectedRequirement).slice(0, 10).map(({ controlId, coverage }) => {
|
||||
const control = controls.find((c) => c.control_id === controlId)
|
||||
if (!control) return null
|
||||
|
||||
return (
|
||||
<div
|
||||
key={controlId}
|
||||
className={`
|
||||
px-3 py-1.5 rounded-full text-xs font-medium
|
||||
${COVERAGE_COLORS[coverage].bg}
|
||||
${COVERAGE_COLORS[coverage].text}
|
||||
border ${COVERAGE_COLORS[coverage].border}
|
||||
`}
|
||||
>
|
||||
{control.control_id}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
{!selectedControl && !selectedRequirement && (
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<p className="text-sm text-slate-400 text-center">
|
||||
{lang === 'de'
|
||||
? 'Waehlen Sie ein Control oder eine Anforderung aus'
|
||||
: 'Select a control or requirement'}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Requirements Column */}
|
||||
<div className="w-1/3 space-y-2">
|
||||
<h4 className="text-sm font-medium text-slate-700 mb-3">
|
||||
{lang === 'de' ? 'Anforderungen' : 'Requirements'} ({filteredRequirements.length})
|
||||
</h4>
|
||||
{filteredRequirements.slice(0, 15).map((req) => {
|
||||
const connectedCtrls = getConnectedControls(req.id)
|
||||
const isSelected = selectedRequirement === req.id
|
||||
const isHighlighted = selectedControl && connectedCtrls.some((c) => c.controlId === selectedControl)
|
||||
|
||||
return (
|
||||
<button
|
||||
key={req.id}
|
||||
onClick={() => handleRequirementClick(req)}
|
||||
className={`
|
||||
w-full text-left p-3 rounded-lg border transition-all
|
||||
${isSelected ? 'border-primary-500 bg-primary-50 shadow' : ''}
|
||||
${isHighlighted && !isSelected ? 'border-primary-300 bg-primary-25' : ''}
|
||||
${!isSelected && !isHighlighted ? 'border-slate-200 hover:border-slate-300' : ''}
|
||||
`}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs font-medium text-slate-600">{req.regulation_code}</span>
|
||||
<span className="font-mono text-sm">{req.article}</span>
|
||||
<span className="text-xs text-slate-400 ml-auto">{connectedCtrls.length}</span>
|
||||
</div>
|
||||
<p className="text-xs text-slate-500 mt-1 truncate">{req.title}</p>
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
{filteredRequirements.length > 15 && (
|
||||
<p className="text-xs text-slate-400 text-center py-2">
|
||||
+{filteredRequirements.length - 15} {lang === 'de' ? 'weitere' : 'more'}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<DependencyMapSankey
|
||||
filteredControls={filteredControls}
|
||||
filteredRequirements={filteredRequirements}
|
||||
requirements={requirements}
|
||||
controls={controls}
|
||||
mappingLookup={mappingLookup}
|
||||
selectedControl={selectedControl}
|
||||
selectedRequirement={selectedRequirement}
|
||||
onControlClick={handleControlClick}
|
||||
onRequirementClick={handleRequirementClick}
|
||||
lang={lang}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Legend */}
|
||||
<div className="bg-white rounded-xl shadow-sm border p-4">
|
||||
<div className="flex flex-wrap gap-6 justify-center">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className={`w-6 h-6 rounded flex items-center justify-center ${COVERAGE_COLORS.full.bg} ${COVERAGE_COLORS.full.text} border ${COVERAGE_COLORS.full.border}`}>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
{(['full', 'partial', 'planned'] as const).map((level) => (
|
||||
<div key={level} className="flex items-center gap-2">
|
||||
<div className={`w-6 h-6 rounded flex items-center justify-center ${COVERAGE_COLORS[level].bg} ${COVERAGE_COLORS[level].text} border ${COVERAGE_COLORS[level].border}`}>
|
||||
{level === 'full' && (<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" /></svg>)}
|
||||
{level === 'partial' && (<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M20 12H4" /></svg>)}
|
||||
{level === 'planned' && (<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>)}
|
||||
</div>
|
||||
<span className="text-sm text-slate-600">
|
||||
{lang === 'de' ? (level === 'full' ? 'Vollstaendig abgedeckt' : level === 'partial' ? 'Teilweise abgedeckt' : 'Geplant') : (level === 'full' ? 'Fully covered' : level === 'partial' ? 'Partially covered' : 'Planned')}
|
||||
</span>
|
||||
</div>
|
||||
<span className="text-sm text-slate-600">
|
||||
{lang === 'de' ? 'Vollstaendig abgedeckt' : 'Fully covered'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className={`w-6 h-6 rounded flex items-center justify-center ${COVERAGE_COLORS.partial.bg} ${COVERAGE_COLORS.partial.text} border ${COVERAGE_COLORS.partial.border}`}>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M20 12H4" />
|
||||
</svg>
|
||||
</div>
|
||||
<span className="text-sm text-slate-600">
|
||||
{lang === 'de' ? 'Teilweise abgedeckt' : 'Partially covered'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className={`w-6 h-6 rounded flex items-center justify-center ${COVERAGE_COLORS.planned.bg} ${COVERAGE_COLORS.planned.text} border ${COVERAGE_COLORS.planned.border}`}>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
<span className="text-sm text-slate-600">
|
||||
{lang === 'de' ? 'Geplant' : 'Planned'}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
190
website/components/compliance/charts/DependencyMapSankey.tsx
Normal file
190
website/components/compliance/charts/DependencyMapSankey.tsx
Normal file
@@ -0,0 +1,190 @@
|
||||
'use client'
|
||||
|
||||
/**
|
||||
* DependencyMap Sankey/Connection View
|
||||
*
|
||||
* Extracted from DependencyMap to keep each file under 500 LOC.
|
||||
*/
|
||||
|
||||
import type { Language } from '@/lib/compliance-i18n'
|
||||
import type { Requirement, Control, Mapping } from './DependencyMapTypes'
|
||||
import { DOMAIN_COLORS, COVERAGE_COLORS } from './DependencyMapTypes'
|
||||
|
||||
interface DependencyMapSankeyProps {
|
||||
filteredControls: Control[]
|
||||
filteredRequirements: Requirement[]
|
||||
requirements: Requirement[]
|
||||
controls: Control[]
|
||||
mappingLookup: Record<string, Record<string, Mapping>>
|
||||
selectedControl: string | null
|
||||
selectedRequirement: string | null
|
||||
onControlClick: (control: Control) => void
|
||||
onRequirementClick: (requirement: Requirement) => void
|
||||
lang: Language
|
||||
}
|
||||
|
||||
export function DependencyMapSankey({
|
||||
filteredControls,
|
||||
filteredRequirements,
|
||||
requirements,
|
||||
controls,
|
||||
mappingLookup,
|
||||
selectedControl,
|
||||
selectedRequirement,
|
||||
onControlClick,
|
||||
onRequirementClick,
|
||||
lang,
|
||||
}: DependencyMapSankeyProps) {
|
||||
const getConnectedRequirements = (controlId: string) => {
|
||||
return Object.keys(mappingLookup[controlId] || {})
|
||||
}
|
||||
|
||||
const getConnectedControls = (requirementId: string) => {
|
||||
return Object.keys(mappingLookup)
|
||||
.filter((controlId) => mappingLookup[controlId][requirementId])
|
||||
.map((controlId) => ({
|
||||
controlId,
|
||||
coverage: mappingLookup[controlId][requirementId].coverage_level,
|
||||
}))
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="bg-white rounded-xl shadow-sm border p-6">
|
||||
<div className="flex gap-8">
|
||||
{/* Controls Column */}
|
||||
<div className="w-1/3 space-y-2">
|
||||
<h4 className="text-sm font-medium text-slate-700 mb-3">
|
||||
Controls ({filteredControls.length})
|
||||
</h4>
|
||||
{filteredControls.map((control) => {
|
||||
const connectedReqs = getConnectedRequirements(control.control_id)
|
||||
const isSelected = selectedControl === control.control_id
|
||||
|
||||
return (
|
||||
<button
|
||||
key={control.control_id}
|
||||
onClick={() => onControlClick(control)}
|
||||
className={`
|
||||
w-full text-left p-3 rounded-lg border transition-all
|
||||
${isSelected ? 'border-primary-500 bg-primary-50 shadow' : 'border-slate-200 hover:border-slate-300'}
|
||||
`}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<div
|
||||
className="w-3 h-3 rounded-full flex-shrink-0"
|
||||
style={{ backgroundColor: DOMAIN_COLORS[control.domain] || '#94a3b8' }}
|
||||
/>
|
||||
<span className="font-mono text-sm font-medium">{control.control_id}</span>
|
||||
<span className="text-xs text-slate-400 ml-auto">{connectedReqs.length}</span>
|
||||
</div>
|
||||
<p className="text-xs text-slate-500 mt-1 truncate">{control.title}</p>
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Connection Lines (simplified) */}
|
||||
<div className="w-1/3 flex items-center justify-center">
|
||||
<div className="relative w-full h-full min-h-[200px]">
|
||||
{selectedControl && (
|
||||
<div className="absolute inset-0 flex flex-col items-center justify-center gap-2">
|
||||
{getConnectedRequirements(selectedControl).slice(0, 10).map((reqId) => {
|
||||
const req = requirements.find((r) => r.id === reqId)
|
||||
const mapping = mappingLookup[selectedControl][reqId]
|
||||
if (!req) return null
|
||||
|
||||
return (
|
||||
<div
|
||||
key={reqId}
|
||||
className={`
|
||||
px-3 py-1.5 rounded-full text-xs font-medium
|
||||
${COVERAGE_COLORS[mapping.coverage_level].bg}
|
||||
${COVERAGE_COLORS[mapping.coverage_level].text}
|
||||
border ${COVERAGE_COLORS[mapping.coverage_level].border}
|
||||
`}
|
||||
>
|
||||
{req.regulation_code} {req.article}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
{getConnectedRequirements(selectedControl).length > 10 && (
|
||||
<span className="text-xs text-slate-400">
|
||||
+{getConnectedRequirements(selectedControl).length - 10} {lang === 'de' ? 'weitere' : 'more'}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{selectedRequirement && (
|
||||
<div className="absolute inset-0 flex flex-col items-center justify-center gap-2">
|
||||
{getConnectedControls(selectedRequirement).slice(0, 10).map(({ controlId, coverage }) => {
|
||||
const control = controls.find((c) => c.control_id === controlId)
|
||||
if (!control) return null
|
||||
|
||||
return (
|
||||
<div
|
||||
key={controlId}
|
||||
className={`
|
||||
px-3 py-1.5 rounded-full text-xs font-medium
|
||||
${COVERAGE_COLORS[coverage].bg}
|
||||
${COVERAGE_COLORS[coverage].text}
|
||||
border ${COVERAGE_COLORS[coverage].border}
|
||||
`}
|
||||
>
|
||||
{control.control_id}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
{!selectedControl && !selectedRequirement && (
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<p className="text-sm text-slate-400 text-center">
|
||||
{lang === 'de'
|
||||
? 'Waehlen Sie ein Control oder eine Anforderung aus'
|
||||
: 'Select a control or requirement'}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Requirements Column */}
|
||||
<div className="w-1/3 space-y-2">
|
||||
<h4 className="text-sm font-medium text-slate-700 mb-3">
|
||||
{lang === 'de' ? 'Anforderungen' : 'Requirements'} ({filteredRequirements.length})
|
||||
</h4>
|
||||
{filteredRequirements.slice(0, 15).map((req) => {
|
||||
const connectedCtrls = getConnectedControls(req.id)
|
||||
const isSelected = selectedRequirement === req.id
|
||||
const isHighlighted = selectedControl && connectedCtrls.some((c) => c.controlId === selectedControl)
|
||||
|
||||
return (
|
||||
<button
|
||||
key={req.id}
|
||||
onClick={() => onRequirementClick(req)}
|
||||
className={`
|
||||
w-full text-left p-3 rounded-lg border transition-all
|
||||
${isSelected ? 'border-primary-500 bg-primary-50 shadow' : ''}
|
||||
${isHighlighted && !isSelected ? 'border-primary-300 bg-primary-25' : ''}
|
||||
${!isSelected && !isHighlighted ? 'border-slate-200 hover:border-slate-300' : ''}
|
||||
`}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs font-medium text-slate-600">{req.regulation_code}</span>
|
||||
<span className="font-mono text-sm">{req.article}</span>
|
||||
<span className="text-xs text-slate-400 ml-auto">{connectedCtrls.length}</span>
|
||||
</div>
|
||||
<p className="text-xs text-slate-500 mt-1 truncate">{req.title}</p>
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
{filteredRequirements.length > 15 && (
|
||||
<p className="text-xs text-slate-400 text-center py-2">
|
||||
+{filteredRequirements.length - 15} {lang === 'de' ? 'weitere' : 'more'}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
65
website/components/compliance/charts/DependencyMapTypes.ts
Normal file
65
website/components/compliance/charts/DependencyMapTypes.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
/**
|
||||
* Types and constants for DependencyMap component.
|
||||
*/
|
||||
|
||||
import type { Language } from '@/lib/compliance-i18n'
|
||||
|
||||
export interface Requirement {
|
||||
id: string
|
||||
article: string
|
||||
title: string
|
||||
regulation_code: string
|
||||
}
|
||||
|
||||
export interface Control {
|
||||
id: string
|
||||
control_id: string
|
||||
title: string
|
||||
domain: string
|
||||
status: string
|
||||
}
|
||||
|
||||
export interface Mapping {
|
||||
requirement_id: string
|
||||
control_id: string
|
||||
coverage_level: 'full' | 'partial' | 'planned'
|
||||
}
|
||||
|
||||
export interface DependencyMapProps {
|
||||
requirements: Requirement[]
|
||||
controls: Control[]
|
||||
mappings: Mapping[]
|
||||
lang?: Language
|
||||
onControlClick?: (control: Control) => void
|
||||
onRequirementClick?: (requirement: Requirement) => void
|
||||
}
|
||||
|
||||
export const DOMAIN_COLORS: Record<string, string> = {
|
||||
gov: '#64748b',
|
||||
priv: '#3b82f6',
|
||||
iam: '#a855f7',
|
||||
crypto: '#eab308',
|
||||
sdlc: '#22c55e',
|
||||
ops: '#f97316',
|
||||
ai: '#ec4899',
|
||||
cra: '#06b6d4',
|
||||
aud: '#6366f1',
|
||||
}
|
||||
|
||||
export const DOMAIN_LABELS: Record<string, string> = {
|
||||
gov: 'Governance',
|
||||
priv: 'Datenschutz',
|
||||
iam: 'Identity & Access',
|
||||
crypto: 'Kryptografie',
|
||||
sdlc: 'Secure Dev',
|
||||
ops: 'Operations',
|
||||
ai: 'KI-spezifisch',
|
||||
cra: 'Supply Chain',
|
||||
aud: 'Audit',
|
||||
}
|
||||
|
||||
export const COVERAGE_COLORS: Record<string, { bg: string; border: string; text: string }> = {
|
||||
full: { bg: 'bg-green-100', border: 'border-green-500', text: 'text-green-700' },
|
||||
partial: { bg: 'bg-yellow-100', border: 'border-yellow-500', text: 'text-yellow-700' },
|
||||
planned: { bg: 'bg-slate-100', border: 'border-slate-400', text: 'text-slate-600' },
|
||||
}
|
||||
Reference in New Issue
Block a user