import { useState, useEffect } from 'react'; import { useTranslation } from 'react-i18next'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Plus, Search, GitBranch, Clock, CheckCircle, Loader2, Rocket, Container, ArrowRight, Layers, ShieldCheck, Globe, Code, Workflow, Server, Network, } from 'lucide-react'; import { pipelineApi } from '@/api'; import { usePipelines, usePipelineMutations } from '@/hooks/api'; import { useConfirm } from '@/components/ui/confirm-dialog'; import { toast } from 'sonner'; import { PipelineCard } from './pipeline-manager/PipelineCard'; import { PipelineCreateDialog } from './pipeline-manager/PipelineCreateDialog'; // 流水线列表页编排器 // Logan.Li export function PipelineManager() { const { t } = useTranslation(); const [searchQuery, setSearchQuery] = useState(''); const { data: pipelines = [], isLoading: loading, refetch: refetchPipelines } = usePipelines(); const { triggerMutation, deleteMutation, stopRunMutation } = usePipelineMutations(); const [expanded, setExpanded] = useState>({}); const [runs, setRuns] = useState[]>>({}); const [loadingRuns, setLoadingRuns] = useState>({}); const [envFilter, setEnvFilter] = useState('all'); const [runningRunMap, setRunningRunMap] = useState>>({}); const [newOpen, setNewOpen] = useState(false); const confirm = useConfirm(); const ENV_TYPES = [ { key: 'all', label: t('deploy.env.all'), icon: Layers, color: '#6B7280' }, { key: 'production', label: t('deploy.env.production'), icon: ShieldCheck, color: '#EF4444' }, { key: 'testing', label: t('deploy.env.testing'), icon: CheckCircle, color: '#F59E0B' }, { key: 'staging', label: t('deploy.env.staging'), icon: Globe, color: '#8B5CF6' }, { key: 'development', label: t('deploy.env.development'), icon: Code, color: '#3B82F6' }, ]; // 同步运行中的任务 ID // Logan.Li useEffect(() => { pipelines.forEach((p) => { const id = p.id as number; if (p.last_status === 'running' && !runningRunMap[id]) { pipelineApi.getRuns(p.code as string).then((res: Record) => { const runsList = (res?.list as Record[]) || []; const running = runsList.find((r) => r.status === 'running' || r.status === 'pending'); if (running) setRunningRunMap((prev) => ({ ...prev, [id]: running })); }).catch(() => {}); } }); }, [pipelines, runningRunMap]); const loadRuns = async (pipelineId: number, code: string) => { setLoadingRuns((p) => ({ ...p, [pipelineId]: true })); try { const res = await pipelineApi.getRuns(code); setRuns((p) => ({ ...p, [pipelineId]: (res as Record)?.list as Record[] || [] })); } catch { setRuns((p) => ({ ...p, [pipelineId]: [] })); } finally { setLoadingRuns((p) => ({ ...p, [pipelineId]: false })); } }; const toggleExpand = (pipelineId: number, code: string) => { if (!expanded[pipelineId]) { loadRuns(pipelineId, code); setExpanded((p) => ({ ...p, [pipelineId]: true })); } else { setExpanded((p) => ({ ...p, [pipelineId]: false })); } }; const handleTrigger = async (pipelineId: number, code: string) => { try { await triggerMutation.mutateAsync(code); toast.success(t('deploy.toast.triggered')); loadRuns(pipelineId, code); refetchPipelines(); } catch { toast.error(t('deploy.toast.triggerFailed')); } }; const handleStop = async (pipelineId: number, code: string, runId: number | string) => { const ok = await confirm({ title: t('deploy.run.stopRun'), description: t('deploy.run.stopConfirm'), variant: 'destructive' }); if (!ok) return; try { await stopRunMutation.mutateAsync({ code, runId }); toast.success(t('deploy.toast.stopped')); loadRuns(pipelineId, code); setRunningRunMap((prev) => { const n = { ...prev }; delete n[pipelineId]; return n; }); } catch { toast.error(t('deploy.toast.stopFailed')); } }; const handleStopRunning = async (pipelineId: number, code: string) => { const runningRun = runningRunMap[pipelineId]; if (!runningRun) { try { const res: Record = await pipelineApi.getRuns(code); const running = ((res?.list as Record[]) || []).find( (r) => r.status === 'running' || r.status === 'pending', ); if (running) await handleStop(pipelineId, code, running.id as number | string); else toast.error(t('deploy.pipeline.noRunningTask')); } catch { toast.error(t('deploy.toast.stopFailed')); } return; } await handleStop(pipelineId, code, runningRun.id as number | string); }; const handleDelete = async (pipeline: Record) => { const ok = await confirm({ title: t('deploy.pipeline.deletePipeline'), description: t('deploy.pipeline.deleteConfirm', { name: pipeline.name }), variant: 'destructive', }); if (!ok) return; try { await deleteMutation.mutateAsync(pipeline.id as number | string); toast.success(t('deploy.toast.deleted')); refetchPipelines(); } catch { toast.error(t('deploy.toast.deleteFailed')); } }; const filtered = pipelines.filter((p) => { const matchSearch = String(p.name || '').toLowerCase().includes(searchQuery.toLowerCase()) || String(p.description || '').toLowerCase().includes(searchQuery.toLowerCase()); const env = p.environment as Record | undefined; const matchEnv = envFilter === 'all' || (env?.type || '') === envFilter; return matchSearch && matchEnv; }); return (

{t('deploy.pipeline.title')}

{t('deploy.pipeline.subtitle')}

{/* 全流程可视化横幅 — 双路径 // Logan.Li */}
{t('deploy.banner.build')} {t('deploy.banner.deploy')}
{t('deploy.banner.sshHost')}

{t('deploy.banner.sshDesc')}

{t('deploy.banner.build')} {t('deploy.banner.imageRegistry')} {t('deploy.banner.deploy')}
{t('deploy.banner.kubernetes')}

{t('deploy.banner.k8sDesc')}

{t('deploy.banner.fullFlow')}
setSearchQuery(e.target.value)} className="pl-10 bg-[var(--dg-bg-secondary)] border-[var(--dg-border)] text-[var(--dg-text-primary)]" />
{ENV_TYPES.map((et) => { const active = envFilter === et.key; const Icon = et.icon; return ( ); })}
{loading ? (
) : filtered.length === 0 ? (

{t('deploy.pipeline.noData')}

) : (
{filtered.map((pipeline) => { const id = pipeline.id as number; const code = (pipeline.code || pipeline.id) as string; return ( toggleExpand(id, code)} onTrigger={() => handleTrigger(id, code)} onStopRunning={() => handleStopRunning(id, code)} onDelete={() => handleDelete(pipeline)} onStopRun={(runId) => handleStop(id, code, runId)} /> ); })}
)}
); }