Some checks failed
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-school (push) Successful in 42s
CI / test-go-edu-search (push) Successful in 34s
CI / test-python-klausur (push) Failing after 2m51s
CI / test-python-agent-core (push) Successful in 21s
CI / test-nodejs-website (push) Successful in 29s
sed replacement left orphaned hostname references in story page and empty lines in getApiBase functions. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
279 lines
13 KiB
TypeScript
279 lines
13 KiB
TypeScript
'use client'
|
|
|
|
import type { PipelineStatus, PipelineRun, SystemStats, DockerStats, WoodpeckerStatus, TabType } from '../types'
|
|
import { ProgressBar } from './helpers'
|
|
|
|
interface OverviewTabProps {
|
|
pipelineStatus: PipelineStatus | null
|
|
pipelineHistory: PipelineRun[]
|
|
systemStats: SystemStats | null
|
|
dockerStats: DockerStats | null
|
|
woodpeckerStatus: WoodpeckerStatus | null
|
|
triggeringWoodpecker: boolean
|
|
triggerWoodpeckerPipeline: () => Promise<void>
|
|
setActiveTab: (tab: TabType) => void
|
|
}
|
|
|
|
export function OverviewTab({
|
|
pipelineStatus,
|
|
pipelineHistory,
|
|
systemStats,
|
|
dockerStats,
|
|
woodpeckerStatus,
|
|
triggeringWoodpecker,
|
|
triggerWoodpeckerPipeline,
|
|
setActiveTab,
|
|
}: OverviewTabProps) {
|
|
return (
|
|
<div className="space-y-6">
|
|
{/* Woodpecker CI Status - Prominent */}
|
|
<WoodpeckerOverviewCard
|
|
woodpeckerStatus={woodpeckerStatus}
|
|
triggeringWoodpecker={triggeringWoodpecker}
|
|
triggerWoodpeckerPipeline={triggerWoodpeckerPipeline}
|
|
setActiveTab={setActiveTab}
|
|
/>
|
|
|
|
{/* Status Cards */}
|
|
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
|
<div className={`p-4 rounded-lg ${pipelineStatus?.gitea_connected ? 'bg-green-50' : 'bg-yellow-50'}`}>
|
|
<div className="flex items-center gap-2 mb-2">
|
|
<span className={`w-3 h-3 rounded-full ${pipelineStatus?.gitea_connected ? 'bg-green-500' : 'bg-yellow-500'}`}></span>
|
|
<span className="text-sm font-medium">Gitea Status</span>
|
|
</div>
|
|
<p className={`text-lg font-bold ${pipelineStatus?.gitea_connected ? 'text-green-700' : 'text-yellow-700'}`}>
|
|
{pipelineStatus?.gitea_connected ? 'Verbunden' : 'Nicht verbunden'}
|
|
</p>
|
|
<p className="text-xs text-slate-500">http://macmini:3003</p>
|
|
</div>
|
|
|
|
<div className="bg-blue-50 p-4 rounded-lg">
|
|
<div className="flex items-center gap-2 mb-2">
|
|
<svg className="w-4 h-4 text-blue-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" />
|
|
</svg>
|
|
<span className="text-sm font-medium">Pipeline Runs</span>
|
|
</div>
|
|
<p className="text-lg font-bold text-blue-700">{pipelineStatus?.total_runs || 0}</p>
|
|
<p className="text-xs text-slate-500">{pipelineStatus?.successful_runs || 0} erfolgreich</p>
|
|
</div>
|
|
|
|
<div className="bg-purple-50 p-4 rounded-lg">
|
|
<div className="flex items-center gap-2 mb-2">
|
|
<svg className="w-4 h-4 text-purple-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 12h14M5 12a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v4a2 2 0 01-2 2M5 12a2 2 0 00-2 2v4a2 2 0 002 2h14a2 2 0 002-2v-4a2 2 0 00-2-2" />
|
|
</svg>
|
|
<span className="text-sm font-medium">Container</span>
|
|
</div>
|
|
<p className="text-lg font-bold text-purple-700">{dockerStats?.running_containers || 0}</p>
|
|
<p className="text-xs text-slate-500">von {dockerStats?.total_containers || 0} laufend</p>
|
|
</div>
|
|
|
|
<div className="bg-slate-50 p-4 rounded-lg">
|
|
<div className="flex items-center gap-2 mb-2">
|
|
<svg className="w-4 h-4 text-slate-500" 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>
|
|
<span className="text-sm font-medium">Letztes Update</span>
|
|
</div>
|
|
<p className="text-lg font-bold text-slate-700">
|
|
{pipelineStatus?.last_sbom_update ? new Date(pipelineStatus.last_sbom_update).toLocaleDateString('de-DE') : 'Nie'}
|
|
</p>
|
|
<p className="text-xs text-slate-500">
|
|
{pipelineStatus?.last_sbom_update ? new Date(pipelineStatus.last_sbom_update).toLocaleTimeString('de-DE') : '-'}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
{/* System Resources */}
|
|
{systemStats && (
|
|
<div className="bg-slate-50 rounded-lg p-4">
|
|
<h3 className="font-medium text-slate-800 mb-4 flex items-center gap-2">
|
|
<svg className="w-5 h-5 text-slate-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 3v2m6-2v2M9 19v2m6-2v2M5 9H3m2 6H3m18-6h-2m2 6h-2M7 19h10a2 2 0 002-2V7a2 2 0 00-2-2H7a2 2 0 00-2 2v10a2 2 0 002 2zM9 9h6v6H9V9z" />
|
|
</svg>
|
|
Server Ressourcen ({systemStats.hostname})
|
|
</h3>
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
<div className="bg-white rounded-lg p-3">
|
|
<div className="flex justify-between mb-2">
|
|
<span className="text-sm text-slate-600">CPU</span>
|
|
<span className={`font-bold ${systemStats.cpu.usage_percent > 80 ? 'text-red-600' : 'text-slate-900'}`}>
|
|
{systemStats.cpu.usage_percent.toFixed(1)}%
|
|
</span>
|
|
</div>
|
|
<ProgressBar percent={systemStats.cpu.usage_percent} />
|
|
</div>
|
|
<div className="bg-white rounded-lg p-3">
|
|
<div className="flex justify-between mb-2">
|
|
<span className="text-sm text-slate-600">RAM</span>
|
|
<span className={`font-bold ${systemStats.memory.usage_percent > 80 ? 'text-red-600' : 'text-slate-900'}`}>
|
|
{systemStats.memory.usage_percent.toFixed(1)}%
|
|
</span>
|
|
</div>
|
|
<ProgressBar percent={systemStats.memory.usage_percent} color="purple" />
|
|
</div>
|
|
<div className="bg-white rounded-lg p-3">
|
|
<div className="flex justify-between mb-2">
|
|
<span className="text-sm text-slate-600">Disk</span>
|
|
<span className={`font-bold ${systemStats.disk.usage_percent > 80 ? 'text-red-600' : 'text-slate-900'}`}>
|
|
{systemStats.disk.usage_percent.toFixed(1)}%
|
|
</span>
|
|
</div>
|
|
<ProgressBar percent={systemStats.disk.usage_percent} color="green" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Recent Pipeline Runs */}
|
|
{pipelineHistory.length > 0 && (
|
|
<div className="bg-slate-50 rounded-lg p-4">
|
|
<h3 className="font-medium text-slate-800 mb-3">Letzte Pipeline Runs</h3>
|
|
<div className="space-y-2">
|
|
{pipelineHistory.slice(0, 5).map((run) => (
|
|
<div key={run.id} className="flex items-center justify-between bg-white p-3 rounded-lg">
|
|
<div className="flex items-center gap-3">
|
|
<span className={`w-2 h-2 rounded-full ${
|
|
run.status === 'success' ? 'bg-green-500' :
|
|
run.status === 'failed' ? 'bg-red-500' :
|
|
run.status === 'running' ? 'bg-yellow-500 animate-pulse' : 'bg-slate-400'
|
|
}`}></span>
|
|
<div>
|
|
<p className="text-sm font-medium text-slate-800">{run.workflow || 'SBOM Pipeline'}</p>
|
|
<p className="text-xs text-slate-500">{run.branch} - {run.commit_sha.substring(0, 8)}</p>
|
|
</div>
|
|
</div>
|
|
<div className="text-right">
|
|
<p className={`text-sm font-medium ${
|
|
run.status === 'success' ? 'text-green-600' :
|
|
run.status === 'failed' ? 'text-red-600' :
|
|
run.status === 'running' ? 'text-yellow-600' : 'text-slate-600'
|
|
}`}>
|
|
{run.status === 'success' ? 'Erfolgreich' :
|
|
run.status === 'failed' ? 'Fehlgeschlagen' :
|
|
run.status === 'running' ? 'Laeuft...' : run.status}
|
|
</p>
|
|
<p className="text-xs text-slate-500">
|
|
{new Date(run.started_at).toLocaleString('de-DE')}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
// ============================================================================
|
|
// Woodpecker Overview Card (sub-component)
|
|
// ============================================================================
|
|
|
|
function WoodpeckerOverviewCard({
|
|
woodpeckerStatus,
|
|
triggeringWoodpecker,
|
|
triggerWoodpeckerPipeline,
|
|
setActiveTab,
|
|
}: {
|
|
woodpeckerStatus: WoodpeckerStatus | null
|
|
triggeringWoodpecker: boolean
|
|
triggerWoodpeckerPipeline: () => Promise<void>
|
|
setActiveTab: (tab: TabType) => void
|
|
}) {
|
|
const latestPipeline = woodpeckerStatus?.pipelines?.[0]
|
|
const isOnline = woodpeckerStatus?.status === 'online'
|
|
const latestStatus = latestPipeline?.status
|
|
|
|
const borderClass = isOnline
|
|
? latestStatus === 'success'
|
|
? 'border-green-300 bg-green-50'
|
|
: latestStatus === 'failure' || latestStatus === 'error'
|
|
? 'border-red-300 bg-red-50'
|
|
: latestStatus === 'running'
|
|
? 'border-blue-300 bg-blue-50'
|
|
: 'border-slate-300 bg-slate-50'
|
|
: 'border-red-300 bg-red-50'
|
|
|
|
const iconBgClass = isOnline
|
|
? latestStatus === 'success'
|
|
? 'bg-green-100'
|
|
: latestStatus === 'failure' || latestStatus === 'error'
|
|
? 'bg-red-100'
|
|
: 'bg-blue-100'
|
|
: 'bg-red-100'
|
|
|
|
return (
|
|
<div className={`p-4 rounded-xl border-2 ${borderClass}`}>
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center gap-4">
|
|
<div className={`p-3 rounded-lg ${iconBgClass}`}>
|
|
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
|
</svg>
|
|
</div>
|
|
<div>
|
|
<div className="flex items-center gap-2">
|
|
<h3 className="font-semibold text-slate-900">Woodpecker CI</h3>
|
|
<span className={`px-2 py-0.5 text-xs font-medium rounded-full ${
|
|
isOnline ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'
|
|
}`}>
|
|
{isOnline ? 'Online' : 'Offline'}
|
|
</span>
|
|
</div>
|
|
{latestPipeline && (
|
|
<p className="text-sm text-slate-600 mt-1">
|
|
Pipeline #{latestPipeline.number}: {' '}
|
|
<span className={`font-medium ${
|
|
latestStatus === 'success' ? 'text-green-600' :
|
|
latestStatus === 'failure' || latestStatus === 'error' ? 'text-red-600' :
|
|
latestStatus === 'running' ? 'text-blue-600' : 'text-slate-600'
|
|
}`}>
|
|
{latestStatus}
|
|
</span>
|
|
{' '}auf {latestPipeline.branch}
|
|
</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<button
|
|
onClick={() => setActiveTab('woodpecker')}
|
|
className="px-3 py-1.5 text-sm border border-slate-300 text-slate-700 rounded-lg hover:bg-white"
|
|
>
|
|
Details
|
|
</button>
|
|
<button
|
|
onClick={triggerWoodpeckerPipeline}
|
|
disabled={triggeringWoodpecker}
|
|
className="px-3 py-1.5 text-sm bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50 flex items-center gap-1"
|
|
>
|
|
{triggeringWoodpecker ? (
|
|
<div className="animate-spin rounded-full h-3 w-3 border-b-2 border-white" />
|
|
) : (
|
|
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z" />
|
|
</svg>
|
|
)}
|
|
Starten
|
|
</button>
|
|
</div>
|
|
</div>
|
|
{/* Failed steps preview */}
|
|
{latestPipeline?.steps?.some(s => s.state === 'failure') && (
|
|
<div className="mt-3 pt-3 border-t border-red-200">
|
|
<p className="text-xs font-medium text-red-700 mb-2">Fehlgeschlagene Steps:</p>
|
|
<div className="flex flex-wrap gap-2">
|
|
{latestPipeline.steps.filter(s => s.state === 'failure').map((step, i) => (
|
|
<span key={i} className="px-2 py-1 bg-red-100 text-red-700 text-xs rounded">
|
|
{step.name}
|
|
</span>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|