Files
breakpilot-core/admin-core/components/infrastructure/DevOpsPipelineSidebar.tsx
Benjamin Admin 92c86ec6ba [split-required] [guardrail-change] Enforce 500 LOC budget across all services
Install LOC guardrails (check-loc.sh, architecture.md, pre-commit hook)
and split all 44 files exceeding 500 LOC into domain-focused modules:

- consent-service (Go): models, handlers, services, database splits
- backend-core (Python): security_api, rbac_api, pdf_service, auth splits
- admin-core (TypeScript): 5 page.tsx + sidebar extractions
- pitch-deck (TypeScript): 6 slides, 3 UI components, engine.ts splits
- voice-service (Python): enhanced_task_orchestrator split

Result: 0 violations, 36 exempted (pipeline, tests, pure-data files).
Go build verified clean. No behavior changes — pure structural splits.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-27 00:09:30 +02:00

425 lines
18 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client'
/**
* DevOps Pipeline Sidebar
*
* Kompakte Sidebar-Komponente fuer Cross-Navigation zwischen DevOps-Modulen.
* Zeigt Pipeline-Flow und ermoeglicht schnelle Navigation.
*
* Features:
* - Desktop: Fixierte Sidebar rechts
* - Mobile: Floating Action Button mit Slide-In Drawer
* - Live Pipeline-Status Badge
* - Backlog-Count Badge
* - Security-Findings-Count Badge
* - Quick-Action: Pipeline triggern
*
* Datenfluss: CI/CD -> Tests -> SBOM -> Security
*/
import Link from 'next/link'
import { useState, useEffect } from 'react'
import type {
DevOpsPipelineSidebarProps,
DevOpsPipelineSidebarResponsiveProps,
} from '@/types/infrastructure-modules'
import { DEVOPS_PIPELINE_MODULES } from '@/types/infrastructure-modules'
import {
ToolIcon,
ServerIcon,
PlayIcon,
StatusBadge,
usePipelineLiveStatus,
} from './DevOpsPipelineSidebarParts'
// =============================================================================
// Main Sidebar Component
// =============================================================================
export function DevOpsPipelineSidebar({
currentTool,
compact = false,
className = '',
}: DevOpsPipelineSidebarProps) {
const [isExpanded, setIsExpanded] = useState(!compact)
const liveStatus = usePipelineLiveStatus()
return (
<div className={`bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-slate-200 dark:border-gray-700 overflow-hidden ${className}`}>
{/* Header */}
<div
className="px-4 py-3 bg-gradient-to-r from-orange-50 to-amber-50 dark:from-orange-900/20 dark:to-amber-900/20 border-b border-slate-200 dark:border-gray-700 cursor-pointer"
onClick={() => setIsExpanded(!isExpanded)}
>
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<span className="text-orange-600 dark:text-orange-400">
<ServerIcon />
</span>
<span className="font-semibold text-slate-700 dark:text-slate-200 text-sm">
DevOps Pipeline
</span>
{/* Live status indicator */}
{liveStatus?.isRunning && (
<span className="w-2 h-2 bg-green-500 rounded-full animate-pulse" title="Pipeline laeuft" />
)}
</div>
<svg
className={`w-4 h-4 text-slate-400 transition-transform ${isExpanded ? 'rotate-180' : ''}`}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
</div>
</div>
{/* Content */}
{isExpanded && (
<div className="p-3 space-y-3">
{/* Tool Links */}
<div className="space-y-1">
{DEVOPS_PIPELINE_MODULES.map((tool) => (
<Link
key={tool.id}
href={tool.href}
className={`flex items-center gap-3 px-3 py-2 rounded-lg transition-colors ${
currentTool === tool.id
? 'bg-orange-100 dark:bg-orange-900/30 text-orange-700 dark:text-orange-300 font-medium'
: 'text-slate-600 dark:text-slate-400 hover:bg-slate-100 dark:hover:bg-gray-700'
}`}
>
<ToolIcon id={tool.id} />
<div className="flex-1 min-w-0">
<div className="text-sm font-medium truncate">{tool.name}</div>
<div className="text-xs text-slate-500 dark:text-slate-500 truncate">
{tool.description}
</div>
</div>
{/* Status badges */}
{tool.id === 'tests' && liveStatus && (
<StatusBadge count={liveStatus.backlogCount} type="backlog" />
)}
{tool.id === 'security' && liveStatus && (
<StatusBadge count={liveStatus.securityFindingsCount} type="security" />
)}
{currentTool === tool.id && (
<span className="flex-shrink-0 w-2 h-2 bg-orange-500 rounded-full" />
)}
</Link>
))}
</div>
{/* Pipeline Flow Visualization */}
<div className="pt-2 border-t border-slate-200 dark:border-gray-700">
<div className="text-xs font-medium text-slate-500 dark:text-slate-400 mb-2 px-1">
Pipeline Flow
</div>
<div className="flex items-center justify-between px-2 py-2 bg-slate-50 dark:bg-gray-900 rounded-lg">
<div className="flex items-center gap-1.5 text-xs">
<span title="Code" className={currentTool === 'ci-cd' ? 'opacity-100' : 'opacity-50'}>📝</span>
<span className="text-slate-400"></span>
<span title="Build" className={currentTool === 'ci-cd' ? 'opacity-100' : 'opacity-50'}>🏗</span>
<span className="text-slate-400"></span>
<span title="Test" className={currentTool === 'tests' ? 'opacity-100' : 'opacity-50'}></span>
<span className="text-slate-400"></span>
<span title="SBOM" className={currentTool === 'sbom' ? 'opacity-100' : 'opacity-50'}>📦</span>
<span className="text-slate-400"></span>
<span title="Security" className={currentTool === 'security' ? 'opacity-100' : 'opacity-50'}>🛡</span>
<span className="text-slate-400"></span>
<span title="Deploy" className="opacity-50">🚀</span>
</div>
</div>
</div>
{/* Quick Info zum aktuellen Tool */}
<div className="pt-2 border-t border-slate-200 dark:border-gray-700">
<div className="text-xs text-slate-500 dark:text-slate-400 px-1">
{currentTool === 'ci-cd' && (
<span>Verwalten Sie Gitea Actions Pipelines und Deployments</span>
)}
{currentTool === 'tests' && (
<span>Ueberwachen Sie 280+ Tests ueber alle Services</span>
)}
{currentTool === 'sbom' && (
<span>Pruefen Sie Abhaengigkeiten und Lizenzen</span>
)}
{currentTool === 'security' && (
<span>Analysieren Sie Vulnerabilities und Security-Scans</span>
)}
</div>
</div>
{/* Quick Action: Pipeline triggern */}
<div className="pt-2 border-t border-slate-200 dark:border-gray-700">
<button
onClick={() => {
// TODO: Implement pipeline trigger
alert('Pipeline wird getriggert...')
}}
className="w-full flex items-center justify-center gap-2 px-3 py-2 text-sm text-orange-600 dark:text-orange-400 bg-orange-50 dark:bg-orange-900/20 hover:bg-orange-100 dark:hover:bg-orange-900/30 rounded-lg transition-colors"
>
<PlayIcon />
<span>Pipeline triggern</span>
</button>
</div>
</div>
)}
</div>
)
}
// =============================================================================
// Responsive Version with Mobile FAB + Drawer
// =============================================================================
/**
* Responsive DevOps Sidebar mit Mobile FAB + Drawer
*
* Desktop (xl+): Fixierte Sidebar rechts
* Mobile/Tablet: Floating Action Button unten rechts, oeffnet Drawer
*/
export function DevOpsPipelineSidebarResponsive({
currentTool,
compact = false,
className = '',
fabPosition = 'bottom-right',
}: DevOpsPipelineSidebarResponsiveProps) {
const [isMobileOpen, setIsMobileOpen] = useState(false)
const liveStatus = usePipelineLiveStatus()
// Close drawer on escape key
useEffect(() => {
const handleEscape = (e: KeyboardEvent) => {
if (e.key === 'Escape') setIsMobileOpen(false)
}
window.addEventListener('keydown', handleEscape)
return () => window.removeEventListener('keydown', handleEscape)
}, [])
// Prevent body scroll when drawer is open
useEffect(() => {
if (isMobileOpen) {
document.body.style.overflow = 'hidden'
} else {
document.body.style.overflow = ''
}
return () => {
document.body.style.overflow = ''
}
}, [isMobileOpen])
const fabPositionClasses = fabPosition === 'bottom-right'
? 'right-4 bottom-20'
: 'left-4 bottom-20'
// Calculate total badge count for FAB
const totalBadgeCount = liveStatus
? liveStatus.backlogCount + liveStatus.securityFindingsCount
: 0
return (
<>
{/* Desktop: Fixed Sidebar */}
<div className={`hidden xl:block fixed right-6 top-24 w-64 z-10 ${className}`}>
<DevOpsPipelineSidebar currentTool={currentTool} compact={compact} />
</div>
{/* Mobile/Tablet: FAB */}
<button
onClick={() => setIsMobileOpen(true)}
className={`xl:hidden fixed ${fabPositionClasses} z-40 w-14 h-14 bg-gradient-to-r from-orange-500 to-amber-500 text-white rounded-full shadow-lg hover:shadow-xl transition-all flex items-center justify-center group`}
aria-label="DevOps Pipeline Navigation oeffnen"
>
<ServerIcon />
{/* Badge indicator */}
{totalBadgeCount > 0 && (
<span className="absolute -top-1 -right-1 w-5 h-5 bg-red-500 text-white text-xs font-bold rounded-full flex items-center justify-center">
{totalBadgeCount > 9 ? '9+' : totalBadgeCount}
</span>
)}
{/* Pulse indicator when pipeline is running */}
{liveStatus?.isRunning && (
<span className="absolute -top-1 -right-1 w-3 h-3 bg-green-400 rounded-full animate-pulse" />
)}
</button>
{/* Mobile/Tablet: Drawer Overlay */}
{isMobileOpen && (
<div className="xl:hidden fixed inset-0 z-50">
{/* Backdrop */}
<div
className="absolute inset-0 bg-black/50 backdrop-blur-sm transition-opacity"
onClick={() => setIsMobileOpen(false)}
/>
{/* Drawer */}
<div className="absolute right-0 top-0 bottom-0 w-80 max-w-[85vw] bg-white dark:bg-gray-900 shadow-2xl transform transition-transform animate-slide-in-right">
{/* Drawer Header */}
<div className="flex items-center justify-between px-4 py-4 border-b border-slate-200 dark:border-gray-700 bg-gradient-to-r from-orange-50 to-amber-50 dark:from-orange-900/20 dark:to-amber-900/20">
<div className="flex items-center gap-2">
<span className="text-orange-600 dark:text-orange-400">
<ServerIcon />
</span>
<span className="font-semibold text-slate-700 dark:text-slate-200">
DevOps Pipeline
</span>
{liveStatus?.isRunning && (
<span className="w-2 h-2 bg-green-500 rounded-full animate-pulse" />
)}
</div>
<button
onClick={() => setIsMobileOpen(false)}
className="p-2 text-slate-400 hover:text-slate-600 dark:hover:text-slate-300 rounded-lg hover:bg-slate-100 dark:hover:bg-gray-800 transition-colors"
aria-label="Schliessen"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
{/* Drawer Content */}
<div className="p-4 space-y-4 overflow-y-auto max-h-[calc(100vh-80px)]">
{/* Tool Links */}
<div className="space-y-2">
{DEVOPS_PIPELINE_MODULES.map((tool) => (
<Link
key={tool.id}
href={tool.href}
onClick={() => setIsMobileOpen(false)}
className={`flex items-center gap-3 px-4 py-3 rounded-xl transition-all ${
currentTool === tool.id
? 'bg-orange-100 dark:bg-orange-900/30 text-orange-700 dark:text-orange-300 font-medium shadow-sm'
: 'text-slate-600 dark:text-slate-400 hover:bg-slate-100 dark:hover:bg-gray-800'
}`}
>
<ToolIcon id={tool.id} />
<div className="flex-1">
<div className="text-base font-medium">{tool.name}</div>
<div className="text-sm text-slate-500 dark:text-slate-500">
{tool.description}
</div>
</div>
{/* Status badges */}
{tool.id === 'tests' && liveStatus && (
<StatusBadge count={liveStatus.backlogCount} type="backlog" />
)}
{tool.id === 'security' && liveStatus && (
<StatusBadge count={liveStatus.securityFindingsCount} type="security" />
)}
{currentTool === tool.id && (
<span className="flex-shrink-0 w-2.5 h-2.5 bg-orange-500 rounded-full" />
)}
</Link>
))}
</div>
{/* Pipeline Flow Visualization */}
<div className="pt-4 border-t border-slate-200 dark:border-gray-700">
<div className="text-sm font-medium text-slate-500 dark:text-slate-400 mb-3">
Pipeline Flow
</div>
<div className="flex items-center justify-center gap-2 p-4 bg-slate-50 dark:bg-gray-800 rounded-xl">
<div className="flex flex-col items-center">
<span className="text-2xl">📝</span>
<span className="text-xs text-slate-500 mt-1">Code</span>
</div>
<span className="text-slate-400"></span>
<div className="flex flex-col items-center">
<span className="text-2xl">🏗</span>
<span className="text-xs text-slate-500 mt-1">Build</span>
</div>
<span className="text-slate-400"></span>
<div className="flex flex-col items-center">
<span className="text-2xl"></span>
<span className="text-xs text-slate-500 mt-1">Test</span>
</div>
<span className="text-slate-400"></span>
<div className="flex flex-col items-center">
<span className="text-2xl">🚀</span>
<span className="text-xs text-slate-500 mt-1">Deploy</span>
</div>
</div>
</div>
{/* Quick Info */}
<div className="pt-4 border-t border-slate-200 dark:border-gray-700">
<div className="text-sm text-slate-600 dark:text-slate-400 p-3 bg-slate-50 dark:bg-gray-800 rounded-xl">
{currentTool === 'ci-cd' && (
<>
<strong className="text-slate-700 dark:text-slate-300">Aktuell:</strong> Gitea Actions Pipelines und Deployments verwalten
</>
)}
{currentTool === 'tests' && (
<>
<strong className="text-slate-700 dark:text-slate-300">Aktuell:</strong> 280+ Tests ueber alle Services ueberwachen
</>
)}
{currentTool === 'sbom' && (
<>
<strong className="text-slate-700 dark:text-slate-300">Aktuell:</strong> Abhaengigkeiten und Lizenzen pruefen
</>
)}
{currentTool === 'security' && (
<>
<strong className="text-slate-700 dark:text-slate-300">Aktuell:</strong> Vulnerabilities und Security-Scans analysieren
</>
)}
</div>
</div>
{/* Quick Action: Pipeline triggern */}
<div className="pt-4 border-t border-slate-200 dark:border-gray-700">
<button
onClick={() => {
// TODO: Implement pipeline trigger
alert('Pipeline wird getriggert...')
setIsMobileOpen(false)
}}
className="w-full flex items-center justify-center gap-2 px-4 py-3 text-base text-white bg-gradient-to-r from-orange-500 to-amber-500 hover:from-orange-600 hover:to-amber-600 rounded-xl transition-colors font-medium"
>
<PlayIcon />
<span>Pipeline triggern</span>
</button>
</div>
{/* Link to Infrastructure Overview */}
<div className="pt-4 border-t border-slate-200 dark:border-gray-700">
<Link
href="/infrastructure"
onClick={() => setIsMobileOpen(false)}
className="flex items-center gap-2 px-3 py-2 text-sm text-orange-600 dark:text-orange-400 hover:bg-orange-50 dark:hover:bg-orange-900/20 rounded-lg transition-colors"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" />
</svg>
<span>Zur Infrastructure-Uebersicht</span>
</Link>
</div>
</div>
</div>
</div>
)}
{/* CSS for slide-in animation */}
<style jsx>{`
@keyframes slide-in-right {
from {
transform: translateX(100%);
}
to {
transform: translateX(0);
}
}
.animate-slide-in-right {
animation: slide-in-right 0.2s ease-out;
}
`}</style>
</>
)
}
export default DevOpsPipelineSidebar