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>
191 lines
7.5 KiB
TypeScript
191 lines
7.5 KiB
TypeScript
'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>
|
|
)
|
|
}
|