import { useState, useEffect, useRef, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { useParams } from 'react-router-dom'; import { Play, RefreshCw, Trash2, History, } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; import { toast } from 'sonner'; import { pipelineApi } from '@/api'; import { FileBrowserDialog } from './FileBrowserDialog'; import { PageShell } from '@/components/layout/PageShell'; import { StatStrip } from '@/components/ui/stat-strip'; import type { JsonData } from '@/types/common'; import { type PipelineRun, type LogEntry, getStageLogSummary, } from './pipeline-run-detail/types'; import { StageProgressBar } from './pipeline-run-detail/StageProgressBar'; import { RunsTable } from './pipeline-run-detail/RunsTable'; import { LogConsole } from './pipeline-run-detail/LogConsole'; export default function PipelineRunDetail() { const { t } = useTranslation(); const { code: pipelineCode } = useParams<{ code: string }>(); const [pipeline, setPipeline] = useState(null); const [runs, setRuns] = useState([]); const [selectedRun, setSelectedRun] = useState(null); const [logs, setLogs] = useState([]); const [loading, setLoading] = useState(false); const [selectedStage, setSelectedStage] = useState(null); const [pageSize, setPageSize] = useState(10); const [selectedIds, setSelectedIds] = useState>(new Set()); const [deleting, setDeleting] = useState(false); const logEndRef = useRef(null); const wsRef = useRef(null); // 文件浏览器 dialog 状态 const [browserOpen, setBrowserOpen] = useState(false); const [browserRun, setBrowserRun] = useState<{ id: number; buildNumber: number } | null>(null); // 加载流水线详情 const fetchPipeline = useCallback(async () => { try { const data: JsonData = await pipelineApi.getByCode(pipelineCode); setPipeline(data?.data || data); } catch { /* silent, pipeline info optional */ } }, [pipelineCode]); // 获取执行记录 const fetchRuns = useCallback(async () => { setLoading(true); try { const data: JsonData = await pipelineApi.getRunsByCode(pipelineCode, pageSize); const list = data?.list || []; setRuns(list); if (!selectedRun && list.length > 0) { setSelectedRun(list[0]); } } catch { toast.error(t('deploy.runDetail.fetchRunsFailed')); } finally { setLoading(false); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [pipelineCode, pageSize]); // eslint-disable-next-line react-hooks/exhaustive-deps useEffect(() => { fetchPipeline(); fetchRuns(); }, []); // eslint-disable-next-line react-hooks/exhaustive-deps useEffect(() => { fetchRuns(); }, [pageSize]); // 自动刷新 useEffect(() => { const hasRunning = runs.some(r => r.status === 'running'); if (!hasRunning) return; const timer = setInterval(fetchRuns, 5000); return () => clearInterval(timer); }, [runs, fetchRuns]); // WebSocket 连接(运行中实时日志) const connectWS = useCallback((runId: number) => { if (wsRef.current) wsRef.current.close(); const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; const token = localStorage.getItem('token'); const ws = new WebSocket(`${protocol}//${window.location.host}/ws/pipeline/${runId}/log?token=${token}`); ws.onmessage = (event) => { try { const data = JSON.parse(event.data); if (data.type === 'init' && data.logs) { const parsed = typeof data.logs === 'string' ? JSON.parse(data.logs) : data.logs; setLogs(Array.isArray(parsed) ? parsed : []); } else if (data.type === 'done') { ws.close(); fetchRuns(); } else if (data.time && data.level) { setLogs(prev => [...prev, data]); } } catch { /* binary frame */ } }; ws.onclose = () => { wsRef.current = null; }; ws.onerror = () => { wsRef.current = null; }; wsRef.current = ws; }, [fetchRuns]); // 加载日志 const fetchLogs = useCallback(async (run: PipelineRun) => { try { const data: JsonData = await pipelineApi.getRunLogStream(run.id); if (data?.logs) { const parsed = typeof data.logs === 'string' ? JSON.parse(data.logs) : data.logs; setLogs(Array.isArray(parsed) ? parsed : []); } else { setLogs([]); } if (run.status === 'running') connectWS(run.id); } catch { setLogs([]); } }, [connectWS]); useEffect(() => { if (selectedRun) { fetchLogs(selectedRun); setSelectedStage(null); // reset stage filter on run change } return () => { if (wsRef.current) wsRef.current.close(); }; }, [selectedRun, fetchLogs]); // 自动滚动到底部 useEffect(() => { logEndRef.current?.scrollIntoView({ behavior: 'smooth' }); }, [logs]); // 触发新运行 const triggerRun = async () => { try { await pipelineApi.triggerByCode(pipelineCode); toast.success(t('deploy.toast.triggered')); fetchRuns(); } catch { toast.error(t('deploy.toast.triggerFailed')); } }; // 停止运行 const stopRun = async (runId: number) => { try { await pipelineApi.stopRunByCode(pipelineCode, runId); toast.success(t('deploy.toast.stopped')); fetchRuns(); } catch { toast.error(t('deploy.toast.stopFailed')); } }; // 批量删除 const batchDelete = async () => { if (selectedIds.size === 0) return; setDeleting(true); try { await pipelineApi.batchDeleteRuns(pipelineCode, Array.from(selectedIds)); toast.success(t('deploy.runDetail.deletedCount', { count: selectedIds.size })); setSelectedIds(new Set()); fetchRuns(); } catch { toast.error(t('deploy.toast.batchDeleteFailed')); } finally { setDeleting(false); } }; const toggleSelect = (id: number) => { setSelectedIds(prev => { const next = new Set(prev); if (next.has(id)) next.delete(id); else next.add(id); return next; }); }; const toggleAll = () => { if (selectedIds.size === runs.length) setSelectedIds(new Set()); else setSelectedIds(new Set(runs.map(r => r.id))); }; // ═══════════ 派生数据 ═══════════ const filteredLogs = selectedStage ? logs.filter(l => l.stage === selectedStage) : logs; const stageSummary = selectedStage ? getStageLogSummary(logs, selectedStage) : ''; const successCount = runs.filter(r => r.status === 'success').length; const runningCount = runs.filter(r => r.status === 'running').length; return ( } contentClassName="space-y-4 h-full flex flex-col" metrics={( )} actions={( <> {pipeline?.environment && ( {pipeline.environment.display_name} )} {selectedIds.size > 0 && ( )} )} > {/* ═══════════ 阶段进度条 (交互式) ═══════════ */} {/* ═══════════ 主内容区: 执行记录表 + 日志 ═══════════ */}
{ setBrowserRun({ id: run.id, buildNumber: run.build_number }); setBrowserOpen(true); }} /> {/* ═══════════ 阶段日志 Console ═══════════ */} {selectedRun && ( )}
{/* ═══════════ 工作区文件浏览器 ═══════════ */}
); }