fix(admin-v2): Restore complete admin-v2 application
The admin-v2 application was incomplete in the repository. This commit restores all missing components: - Admin pages (76 pages): dashboard, ai, compliance, dsgvo, education, infrastructure, communication, development, onboarding, rbac - SDK pages (45 pages): tom, dsfa, vvt, loeschfristen, einwilligungen, vendor-compliance, tom-generator, dsr, and more - Developer portal (25 pages): API docs, SDK guides, frameworks - All components, lib files, hooks, and types - Updated package.json with all dependencies The issue was caused by incomplete initial repository state - the full admin-v2 codebase existed in backend/admin-v2 and docs-src/admin-v2 but was never fully synced to the main admin-v2 directory. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
510
admin-v2/components/common/DataFlowDiagram.tsx
Normal file
510
admin-v2/components/common/DataFlowDiagram.tsx
Normal file
@@ -0,0 +1,510 @@
|
||||
'use client'
|
||||
|
||||
/**
|
||||
* DataFlowDiagram - Visual representation of module dependencies
|
||||
*
|
||||
* Shows how backend services, modules, and frontend pages are connected.
|
||||
* Uses SVG for rendering connections.
|
||||
*/
|
||||
|
||||
import { useState, useRef, useEffect } from 'react'
|
||||
import {
|
||||
MODULE_REGISTRY,
|
||||
type BackendModule
|
||||
} from '@/lib/module-registry'
|
||||
|
||||
interface NodePosition {
|
||||
x: number
|
||||
y: number
|
||||
width: number
|
||||
height: number
|
||||
}
|
||||
|
||||
interface ServiceGroup {
|
||||
name: string
|
||||
port: number
|
||||
modules: BackendModule[]
|
||||
}
|
||||
|
||||
const SERVICE_COLORS: Record<string, string> = {
|
||||
'consent-service': '#8b5cf6', // purple
|
||||
'python-backend': '#f59e0b', // amber
|
||||
'klausur-service': '#10b981', // emerald
|
||||
'voice-service': '#3b82f6', // blue
|
||||
}
|
||||
|
||||
const STATUS_COLORS = {
|
||||
connected: '#22c55e',
|
||||
partial: '#eab308',
|
||||
'not-connected': '#ef4444',
|
||||
deprecated: '#6b7280'
|
||||
}
|
||||
|
||||
export function DataFlowDiagram() {
|
||||
const [selectedModule, setSelectedModule] = useState<string | null>(null)
|
||||
const [hoveredModule, setHoveredModule] = useState<string | null>(null)
|
||||
const [showLegend, setShowLegend] = useState(true)
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
// Group modules by backend service
|
||||
const serviceGroups: ServiceGroup[] = []
|
||||
const seenServices = new Set<string>()
|
||||
|
||||
MODULE_REGISTRY.forEach(module => {
|
||||
if (!seenServices.has(module.backend.service)) {
|
||||
seenServices.add(module.backend.service)
|
||||
serviceGroups.push({
|
||||
name: module.backend.service,
|
||||
port: module.backend.port,
|
||||
modules: MODULE_REGISTRY.filter(m => m.backend.service === module.backend.service)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// Calculate positions
|
||||
const serviceWidth = 280
|
||||
const serviceSpacing = 40
|
||||
const moduleHeight = 60
|
||||
const moduleSpacing = 10
|
||||
const headerHeight = 50
|
||||
const padding = 20
|
||||
|
||||
const totalWidth = serviceGroups.length * serviceWidth + (serviceGroups.length - 1) * serviceSpacing + padding * 2
|
||||
const maxModulesInService = Math.max(...serviceGroups.map(s => s.modules.length))
|
||||
const totalHeight = headerHeight + maxModulesInService * (moduleHeight + moduleSpacing) + padding * 2 + 100
|
||||
|
||||
// Get connections between modules (dependencies)
|
||||
const connections: { from: string; to: string }[] = []
|
||||
MODULE_REGISTRY.forEach(module => {
|
||||
if (module.dependencies) {
|
||||
module.dependencies.forEach(dep => {
|
||||
connections.push({ from: module.id, to: dep })
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// Get module position
|
||||
const getModulePosition = (moduleId: string): NodePosition | null => {
|
||||
let serviceIndex = 0
|
||||
for (const service of serviceGroups) {
|
||||
const moduleIndex = service.modules.findIndex(m => m.id === moduleId)
|
||||
if (moduleIndex !== -1) {
|
||||
return {
|
||||
x: padding + serviceIndex * (serviceWidth + serviceSpacing) + serviceWidth / 2,
|
||||
y: headerHeight + moduleIndex * (moduleHeight + moduleSpacing) + moduleHeight / 2 + 40,
|
||||
width: serviceWidth - 40,
|
||||
height: moduleHeight
|
||||
}
|
||||
}
|
||||
serviceIndex++
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
// Check if module is related to selected/hovered module
|
||||
const isRelated = (moduleId: string): boolean => {
|
||||
const target = selectedModule || hoveredModule
|
||||
if (!target) return false
|
||||
|
||||
// Direct match
|
||||
if (moduleId === target) return true
|
||||
|
||||
// Check dependencies
|
||||
const targetModule = MODULE_REGISTRY.find(m => m.id === target)
|
||||
if (targetModule?.dependencies?.includes(moduleId)) return true
|
||||
|
||||
// Check reverse dependencies
|
||||
const module = MODULE_REGISTRY.find(m => m.id === moduleId)
|
||||
if (module?.dependencies?.includes(target)) return true
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{/* Controls */}
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-lg font-semibold text-slate-900">Datenfluss-Diagramm</h3>
|
||||
<div className="flex items-center gap-4">
|
||||
<button
|
||||
onClick={() => setShowLegend(!showLegend)}
|
||||
className="text-sm text-slate-600 hover:text-slate-900"
|
||||
>
|
||||
{showLegend ? 'Legende ausblenden' : 'Legende anzeigen'}
|
||||
</button>
|
||||
{selectedModule && (
|
||||
<button
|
||||
onClick={() => setSelectedModule(null)}
|
||||
className="px-3 py-1 bg-purple-100 text-purple-700 rounded-full text-sm"
|
||||
>
|
||||
Auswahl aufheben
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Legend */}
|
||||
{showLegend && (
|
||||
<div className="bg-white rounded-xl shadow-sm border p-4 flex flex-wrap items-center gap-6">
|
||||
<span className="text-sm font-medium text-slate-700">Services:</span>
|
||||
{Object.entries(SERVICE_COLORS).map(([service, color]) => (
|
||||
<div key={service} className="flex items-center gap-2">
|
||||
<div className="w-3 h-3 rounded" style={{ backgroundColor: color }} />
|
||||
<span className="text-sm text-slate-600">{service}</span>
|
||||
</div>
|
||||
))}
|
||||
<span className="text-sm font-medium text-slate-700 ml-4">Status:</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-3 h-3 rounded-full bg-green-500" />
|
||||
<span className="text-sm text-slate-600">Verbunden</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-3 h-3 rounded-full bg-yellow-500" />
|
||||
<span className="text-sm text-slate-600">Teilweise</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-3 h-3 rounded-full bg-red-500" />
|
||||
<span className="text-sm text-slate-600">Nicht verbunden</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Diagram */}
|
||||
<div
|
||||
ref={containerRef}
|
||||
className="bg-white rounded-xl shadow-sm border overflow-x-auto"
|
||||
>
|
||||
<svg
|
||||
width={totalWidth}
|
||||
height={totalHeight}
|
||||
className="min-w-full"
|
||||
>
|
||||
<defs>
|
||||
{/* Arrow marker */}
|
||||
<marker
|
||||
id="arrowhead"
|
||||
markerWidth="10"
|
||||
markerHeight="7"
|
||||
refX="9"
|
||||
refY="3.5"
|
||||
orient="auto"
|
||||
>
|
||||
<polygon points="0 0, 10 3.5, 0 7" fill="#a78bfa" />
|
||||
</marker>
|
||||
<marker
|
||||
id="arrowhead-highlight"
|
||||
markerWidth="10"
|
||||
markerHeight="7"
|
||||
refX="9"
|
||||
refY="3.5"
|
||||
orient="auto"
|
||||
>
|
||||
<polygon points="0 0, 10 3.5, 0 7" fill="#8b5cf6" />
|
||||
</marker>
|
||||
</defs>
|
||||
|
||||
{/* Background Grid */}
|
||||
<pattern id="grid" width="20" height="20" patternUnits="userSpaceOnUse">
|
||||
<path d="M 20 0 L 0 0 0 20" fill="none" stroke="#f1f5f9" strokeWidth="1"/>
|
||||
</pattern>
|
||||
<rect width="100%" height="100%" fill="url(#grid)" />
|
||||
|
||||
{/* Service Groups */}
|
||||
{serviceGroups.map((service, serviceIdx) => {
|
||||
const x = padding + serviceIdx * (serviceWidth + serviceSpacing)
|
||||
const serviceColor = SERVICE_COLORS[service.name] || '#6b7280'
|
||||
|
||||
return (
|
||||
<g key={service.name}>
|
||||
{/* Service Container */}
|
||||
<rect
|
||||
x={x}
|
||||
y={padding}
|
||||
width={serviceWidth}
|
||||
height={totalHeight - padding * 2}
|
||||
fill={`${serviceColor}10`}
|
||||
stroke={serviceColor}
|
||||
strokeWidth="2"
|
||||
rx="12"
|
||||
/>
|
||||
|
||||
{/* Service Header */}
|
||||
<rect
|
||||
x={x}
|
||||
y={padding}
|
||||
width={serviceWidth}
|
||||
height={headerHeight}
|
||||
fill={serviceColor}
|
||||
rx="12"
|
||||
/>
|
||||
<rect
|
||||
x={x}
|
||||
y={padding + headerHeight - 12}
|
||||
width={serviceWidth}
|
||||
height={12}
|
||||
fill={serviceColor}
|
||||
/>
|
||||
<text
|
||||
x={x + serviceWidth / 2}
|
||||
y={padding + headerHeight / 2 + 5}
|
||||
textAnchor="middle"
|
||||
fill="white"
|
||||
fontSize="14"
|
||||
fontWeight="600"
|
||||
>
|
||||
{service.name}
|
||||
</text>
|
||||
<text
|
||||
x={x + serviceWidth / 2}
|
||||
y={padding + headerHeight - 8}
|
||||
textAnchor="middle"
|
||||
fill="rgba(255,255,255,0.7)"
|
||||
fontSize="11"
|
||||
>
|
||||
Port {service.port}
|
||||
</text>
|
||||
|
||||
{/* Modules */}
|
||||
{service.modules.map((module, moduleIdx) => {
|
||||
const moduleX = x + 20
|
||||
const moduleY = padding + headerHeight + 20 + moduleIdx * (moduleHeight + moduleSpacing)
|
||||
const isSelected = selectedModule === module.id
|
||||
const isHovered = hoveredModule === module.id
|
||||
const related = isRelated(module.id)
|
||||
const statusColor = STATUS_COLORS[module.frontend.status]
|
||||
|
||||
const opacity = (selectedModule || hoveredModule)
|
||||
? (related ? 1 : 0.3)
|
||||
: 1
|
||||
|
||||
return (
|
||||
<g
|
||||
key={module.id}
|
||||
onClick={() => setSelectedModule(isSelected ? null : module.id)}
|
||||
onMouseEnter={() => setHoveredModule(module.id)}
|
||||
onMouseLeave={() => setHoveredModule(null)}
|
||||
style={{ cursor: 'pointer', opacity }}
|
||||
className="transition-opacity duration-200"
|
||||
>
|
||||
{/* Module Box */}
|
||||
<rect
|
||||
x={moduleX}
|
||||
y={moduleY}
|
||||
width={serviceWidth - 40}
|
||||
height={moduleHeight}
|
||||
fill="white"
|
||||
stroke={isSelected || isHovered ? serviceColor : '#e2e8f0'}
|
||||
strokeWidth={isSelected || isHovered ? 2 : 1}
|
||||
rx="8"
|
||||
filter={isSelected ? 'drop-shadow(0 4px 6px rgba(0,0,0,0.1))' : undefined}
|
||||
/>
|
||||
|
||||
{/* Status Indicator */}
|
||||
<circle
|
||||
cx={moduleX + 16}
|
||||
cy={moduleY + moduleHeight / 2}
|
||||
r="5"
|
||||
fill={statusColor}
|
||||
/>
|
||||
|
||||
{/* Module Name */}
|
||||
<text
|
||||
x={moduleX + 30}
|
||||
y={moduleY + 24}
|
||||
fill="#1e293b"
|
||||
fontSize="12"
|
||||
fontWeight="500"
|
||||
>
|
||||
{module.name.length > 25 ? module.name.slice(0, 25) + '...' : module.name}
|
||||
</text>
|
||||
|
||||
{/* Module ID */}
|
||||
<text
|
||||
x={moduleX + 30}
|
||||
y={moduleY + 42}
|
||||
fill="#94a3b8"
|
||||
fontSize="10"
|
||||
>
|
||||
{module.id}
|
||||
</text>
|
||||
|
||||
{/* Priority Badge */}
|
||||
<rect
|
||||
x={moduleX + serviceWidth - 90}
|
||||
y={moduleY + 20}
|
||||
width={40}
|
||||
height={18}
|
||||
fill={
|
||||
module.priority === 'critical' ? '#fef2f2' :
|
||||
module.priority === 'high' ? '#fff7ed' :
|
||||
module.priority === 'medium' ? '#fefce8' :
|
||||
'#f1f5f9'
|
||||
}
|
||||
rx="4"
|
||||
/>
|
||||
<text
|
||||
x={moduleX + serviceWidth - 70}
|
||||
y={moduleY + 33}
|
||||
textAnchor="middle"
|
||||
fill={
|
||||
module.priority === 'critical' ? '#dc2626' :
|
||||
module.priority === 'high' ? '#ea580c' :
|
||||
module.priority === 'medium' ? '#ca8a04' :
|
||||
'#64748b'
|
||||
}
|
||||
fontSize="9"
|
||||
fontWeight="500"
|
||||
>
|
||||
{module.priority.toUpperCase()}
|
||||
</text>
|
||||
|
||||
{/* Dependency indicator */}
|
||||
{module.dependencies && module.dependencies.length > 0 && (
|
||||
<g>
|
||||
<circle
|
||||
cx={moduleX + serviceWidth - 55}
|
||||
cy={moduleY + moduleHeight - 12}
|
||||
r="8"
|
||||
fill="#f3e8ff"
|
||||
/>
|
||||
<text
|
||||
x={moduleX + serviceWidth - 55}
|
||||
y={moduleY + moduleHeight - 8}
|
||||
textAnchor="middle"
|
||||
fill="#8b5cf6"
|
||||
fontSize="9"
|
||||
fontWeight="600"
|
||||
>
|
||||
{module.dependencies.length}
|
||||
</text>
|
||||
</g>
|
||||
)}
|
||||
</g>
|
||||
)
|
||||
})}
|
||||
</g>
|
||||
)
|
||||
})}
|
||||
|
||||
{/* Connections (Dependencies) */}
|
||||
{connections.map((conn, idx) => {
|
||||
const fromPos = getModulePosition(conn.from)
|
||||
const toPos = getModulePosition(conn.to)
|
||||
|
||||
if (!fromPos || !toPos) return null
|
||||
|
||||
const isHighlighted = (selectedModule || hoveredModule) &&
|
||||
(conn.from === (selectedModule || hoveredModule) || conn.to === (selectedModule || hoveredModule))
|
||||
|
||||
const opacity = (selectedModule || hoveredModule)
|
||||
? (isHighlighted ? 1 : 0.1)
|
||||
: 0.4
|
||||
|
||||
// Calculate curved path
|
||||
const startX = fromPos.x
|
||||
const startY = fromPos.y
|
||||
const endX = toPos.x
|
||||
const endY = toPos.y
|
||||
|
||||
const midX = (startX + endX) / 2
|
||||
const controlOffset = Math.abs(startX - endX) * 0.3
|
||||
|
||||
const path = startX < endX
|
||||
? `M ${startX + fromPos.width / 2} ${startY}
|
||||
C ${startX + fromPos.width / 2 + controlOffset} ${startY},
|
||||
${endX - toPos.width / 2 - controlOffset} ${endY},
|
||||
${endX - toPos.width / 2} ${endY}`
|
||||
: `M ${startX - fromPos.width / 2} ${startY}
|
||||
C ${startX - fromPos.width / 2 - controlOffset} ${startY},
|
||||
${endX + toPos.width / 2 + controlOffset} ${endY},
|
||||
${endX + toPos.width / 2} ${endY}`
|
||||
|
||||
return (
|
||||
<path
|
||||
key={idx}
|
||||
d={path}
|
||||
fill="none"
|
||||
stroke={isHighlighted ? '#8b5cf6' : '#a78bfa'}
|
||||
strokeWidth={isHighlighted ? 2 : 1.5}
|
||||
strokeDasharray={isHighlighted ? undefined : '4 2'}
|
||||
markerEnd={isHighlighted ? 'url(#arrowhead-highlight)' : 'url(#arrowhead)'}
|
||||
opacity={opacity}
|
||||
className="transition-opacity duration-200"
|
||||
/>
|
||||
)
|
||||
})}
|
||||
|
||||
{/* Frontend Layer (Bottom) */}
|
||||
<g>
|
||||
<rect
|
||||
x={padding}
|
||||
y={totalHeight - 80}
|
||||
width={totalWidth - padding * 2}
|
||||
height={60}
|
||||
fill="#f8fafc"
|
||||
stroke="#e2e8f0"
|
||||
strokeWidth="1"
|
||||
rx="8"
|
||||
/>
|
||||
<text
|
||||
x={totalWidth / 2}
|
||||
y={totalHeight - 55}
|
||||
textAnchor="middle"
|
||||
fill="#64748b"
|
||||
fontSize="12"
|
||||
fontWeight="500"
|
||||
>
|
||||
Admin v2 Frontend (Next.js - Port 3002)
|
||||
</text>
|
||||
<text
|
||||
x={totalWidth / 2}
|
||||
y={totalHeight - 35}
|
||||
textAnchor="middle"
|
||||
fill="#94a3b8"
|
||||
fontSize="11"
|
||||
>
|
||||
/compliance | /ai | /infrastructure | /education | /communication | /development
|
||||
</text>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
{/* Selected Module Details */}
|
||||
{selectedModule && (
|
||||
<div className="bg-purple-50 border border-purple-200 rounded-xl p-4">
|
||||
<h4 className="font-medium text-purple-900 mb-2">
|
||||
{MODULE_REGISTRY.find(m => m.id === selectedModule)?.name}
|
||||
</h4>
|
||||
<div className="text-sm text-purple-700 space-y-1">
|
||||
<p>ID: <code className="bg-purple-100 px-1 rounded">{selectedModule}</code></p>
|
||||
{MODULE_REGISTRY.find(m => m.id === selectedModule)?.dependencies && (
|
||||
<p>
|
||||
Abhaengigkeiten:
|
||||
{MODULE_REGISTRY.find(m => m.id === selectedModule)?.dependencies?.map(dep => (
|
||||
<button
|
||||
key={dep}
|
||||
onClick={() => setSelectedModule(dep)}
|
||||
className="ml-2 px-2 py-0.5 bg-purple-200 text-purple-800 rounded hover:bg-purple-300"
|
||||
>
|
||||
{dep}
|
||||
</button>
|
||||
))}
|
||||
</p>
|
||||
)}
|
||||
{MODULE_REGISTRY.find(m => m.id === selectedModule)?.frontend.adminV2Page && (
|
||||
<p>
|
||||
Frontend:
|
||||
<a
|
||||
href={MODULE_REGISTRY.find(m => m.id === selectedModule)?.frontend.adminV2Page}
|
||||
className="ml-2 text-purple-600 hover:underline"
|
||||
>
|
||||
{MODULE_REGISTRY.find(m => m.id === selectedModule)?.frontend.adminV2Page}
|
||||
</a>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user