refactor: PipelineManager split into 4 files (776 to 438 lines)

This commit is contained in:
attacker committed 2026-06-15 09:05:28 +08:00
1 parent 419dd160b8
commit 3f033e9db8
5 files changed
+506 -376

No files matched your search

@@ -3,31 +3,11 @@ import { Card, CardContent } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Badge } from '@/components/ui/badge';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import { Textarea } from '@/components/ui/textarea';
import { Label } from '@/components/ui/label';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table';
import {
Play,
Plus,
Search,
GitBranch,
GitCommit,
Clock,
CheckCircle,
XCircle,
@@ -36,53 +16,24 @@ import {
Trash2,
Eye,
ChevronRight,
ChevronDown,
Workflow,
PackageOpen,
} from 'lucide-react';
import { pipelineApi } from '@/services/api';
import { useConfirm } from '@/components/ui/confirm-dialog';
import { toast } from 'sonner';
// 流水线阶段定义 // Logan.Li
interface PipelineStage {
id: string;
name: string;
status: 'pending' | 'running' | 'success' | 'failed' | 'skipped';
duration: string;
logs: string[];
}
// 流水线模板(新建时快捷选择) // Logan.Li
const pipelineTemplates = [
{ id: 'template-1', name: 'Node.js 项目', description: '适用于 Node.js 前端项目', stages: ['代码检出', '依赖安装', '代码检查', '构建打包', '部署'] },
{ id: 'template-2', name: 'Go 项目', description: '适用于 Go 后端项目', stages: ['代码检出', '单元测试', '构建', '构建镜像', '部署'] },
{ id: 'template-3', name: 'Java 项目', description: '适用于 Java Spring Boot 项目', stages: ['代码检出', 'Maven构建', '单元测试', '打包', '部署'] },
{ id: 'template-4', name: 'Python 项目', description: '适用于 Python 项目', stages: ['代码检出', '依赖安装', '代码检查', '测试', '部署'] },
];
// 解析 stages 字段(兼容 JSON 数组 / 逗号分隔字符串) // Logan.Li
function parseStages(stages: any): PipelineStage[] {
if (Array.isArray(stages)) return stages;
if (typeof stages === 'string') {
try {
const parsed = JSON.parse(stages);
if (Array.isArray(parsed)) return parsed;
} catch {
return stages.split(',').map((s, i) => ({ id: `stage-${i + 1}`, name: s.trim(), status: 'success' as const, duration: '-', logs: [] }));
}
}
return [];
}
// 由流水线名称生成 code(kebab-case) // Logan.Li
function nameToCode(name: string): string {
return name
.toLowerCase()
.replace(/[^a-z0-9]/g, '-')
.replace(/-+/g, '-')
.replace(/^-|-$/g, '') || `pipeline-${Date.now()}`;
}
import type { PipelineStage } from './pipeline-helpers';
import {
pipelineTemplates,
parseStages,
nameToCode,
getStatusBadge,
getStageStatusIcon,
} from './pipeline-helpers';
import { PipelineFormDialog } from './PipelineFormDialog';
import type { PipelineFormData } from './PipelineFormDialog';
import { PipelineDetailDialog } from './PipelineDetailDialog';
import { PipelineRunDialog } from './PipelineRunDialog';
export function PipelineManager() {
const [searchQuery, setSearchQuery] = useState('');
@@ -97,7 +48,7 @@ export function PipelineManager() {
const [activeStage, setActiveStage] = useState<string | null>(null);
const [pipelineRuns, setPipelineRuns] = useState<any[]>([]);
const [loadingRuns, setLoadingRuns] = useState(false);
const [formData, setFormData] = useState({
const [formData, setFormData] = useState<PipelineFormData>({
name: '',
description: '',
source: '',
@@ -173,54 +124,6 @@ export function PipelineManager() {
(p.description || '').toLowerCase().includes(searchQuery.toLowerCase())
);
// 状态徽章(使用后端 last_status 字段) // Logan.Li
const getStatusBadge = (status: any) => {
switch (status) {
case 'success':
return (
<Badge variant="outline" className="border-emerald-500/30 text-emerald-400 bg-emerald-500/10">
<CheckCircle className="h-3 w-3 mr-1" />
成功
</Badge>
);
case 'failed':
return (
<Badge variant="outline" className="border-red-500/30 text-red-400 bg-red-500/10">
<XCircle className="h-3 w-3 mr-1" />
失败
</Badge>
);
case 'running':
return (
<Badge variant="outline" className="border-blue-500/30 text-blue-400 bg-blue-500/10 animate-pulse">
<Loader2 className="h-3 w-3 mr-1 animate-spin" />
运行中
</Badge>
);
default:
return (
<Badge variant="outline" className="border-[var(--dg-border)] text-[var(--dg-text-secondary)]">
从未运行
</Badge>
);
}
};
const getStageStatusIcon = (status: PipelineStage['status']) => {
switch (status) {
case 'success':
return <CheckCircle className="h-4 w-4 text-emerald-400" />;
case 'failed':
return <XCircle className="h-4 w-4 text-red-400" />;
case 'running':
return <Loader2 className="h-4 w-4 text-blue-400 animate-spin" />;
case 'skipped':
return <span className="text-[var(--dg-text-secondary)] text-xs">跳过</span>;
default:
return <span className="text-[var(--dg-text-secondary)] opacity-60 text-xs">待执行</span>;
}
};
// 创建/编辑流水线 // Logan.Li
const handleSavePipeline = async () => {
if (!formData.name.trim()) {
@@ -503,274 +406,34 @@ export function PipelineManager() {
))}
</div>
{/* Create/Edit Pipeline Dialog */}
<Dialog open={isFormDialogOpen} onOpenChange={setIsFormDialogOpen}>
<DialogContent className="max-w-2xl bg-[var(--dg-bg-primary)] border-[var(--dg-border)] text-[var(--dg-text-primary)]">
<DialogHeader>
<DialogTitle>{editMode ? '编辑流水线' : '新建流水线'}</DialogTitle>
<DialogDescription className="text-[var(--dg-text-secondary)]">
{editMode ? '修改流水线配置' : '配置新的 CI/CD 流水线'}
</DialogDescription>
</DialogHeader>
<div className="space-y-4 py-4">
<div>
<Label className="text-[var(--dg-text-secondary)]">流水线名称</Label>
<Input
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
placeholder="例如: frontend-build"
className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)] text-[var(--dg-text-primary)] mt-1"
/>
</div>
<div>
<Label className="text-[var(--dg-text-secondary)]">描述</Label>
<Textarea
value={formData.description}
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
placeholder="流水线描述..."
className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)] text-[var(--dg-text-primary)] mt-1"
/>
</div>
<div>
<Label className="text-[var(--dg-text-secondary)]">代码仓库</Label>
<Input
value={formData.source}
onChange={(e) => setFormData({ ...formData, source: e.target.value })}
placeholder="https://gitlab.company.com/project/repo.git"
className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)] text-[var(--dg-text-primary)] mt-1"
/>
</div>
<div>
<Label className="text-[var(--dg-text-secondary)]">默认分支</Label>
<Input
value={formData.branch}
onChange={(e) => setFormData({ ...formData, branch: e.target.value })}
placeholder="main"
className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)] text-[var(--dg-text-primary)] mt-1"
/>
</div>
{!editMode && (
<div>
<Label className="text-[var(--dg-text-secondary)]">选择模板</Label>
<div className="grid grid-cols-2 gap-3 mt-1">
{pipelineTemplates.map((template) => (
<Card
key={template.id}
className={`cursor-pointer transition-all ${
formData.template === template.id
? 'border-blue-500 ring-1 ring-blue-500/30 bg-[var(--dg-bg-secondary)]'
: 'border-[var(--dg-border)] hover:border-[var(--dg-border)] bg-[var(--dg-bg-secondary)] opacity-60'
}`}
onClick={() => setFormData({ ...formData, template: template.id })}
>
<CardContent className="p-3">
<p className="font-medium text-[var(--dg-text-primary)]">{template.name}</p>
<p className="text-xs text-[var(--dg-text-secondary)]">{template.description}</p>
<div className="flex flex-wrap gap-1 mt-2">
{template.stages.slice(0, 3).map((stage) => (
<Badge key={stage} variant="secondary" className="bg-[var(--dg-bg-tertiary)] text-[var(--dg-text-secondary)] text-xs">
{stage}
</Badge>
))}
{template.stages.length > 3 && (
<Badge variant="secondary" className="bg-[var(--dg-bg-tertiary)] text-[var(--dg-text-secondary)] text-xs">
+{template.stages.length - 3}
</Badge>
)}
</div>
</CardContent>
</Card>
))}
</div>
</div>
)}
</div>
<DialogFooter className="mt-4 pt-4 border-t border-[var(--dg-border)]">
<Button variant="outline" onClick={() => setIsFormDialogOpen(false)} className="border-[var(--dg-border)] text-[var(--dg-text-secondary)]">
取消
</Button>
<Button onClick={handleSavePipeline}>
{editMode ? '保存修改' : '创建流水线'}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* 新建/编辑弹窗 */}
<PipelineFormDialog
open={isFormDialogOpen}
onOpenChange={setIsFormDialogOpen}
editMode={editMode}
formData={formData}
setFormData={setFormData}
onSave={handleSavePipeline}
/>
{/* Pipeline Detail Dialog */}
<Dialog open={isDetailDialogOpen} onOpenChange={setIsDetailDialogOpen}>
<DialogContent className="max-w-4xl bg-[var(--dg-bg-primary)] border-[var(--dg-border)] text-[var(--dg-text-primary)]">
<DialogHeader>
<DialogTitle>{selectedPipeline?.name}</DialogTitle>
<DialogDescription className="text-[var(--dg-text-secondary)]">{selectedPipeline?.description}</DialogDescription>
</DialogHeader>
{selectedPipeline && (
<div className="space-y-4">
<Tabs defaultValue="stages">
<TabsList className="bg-[var(--dg-bg-secondary)] border border-[var(--dg-border)]">
<TabsTrigger value="stages" className="data-[state=active]:bg-[var(--dg-bg-tertiary)]">
执行阶段
</TabsTrigger>
<TabsTrigger value="config" className="data-[state=active]:bg-[var(--dg-bg-tertiary)]">
配置
</TabsTrigger>
<TabsTrigger value="history" className="data-[state=active]:bg-[var(--dg-bg-tertiary)]">
历史记录{pipelineRuns.length > 0 && ` (${pipelineRuns.length})`}
</TabsTrigger>
</TabsList>
<TabsContent value="stages" className="mt-4">
<div className="space-y-2">
{parseStages(selectedPipeline.stages).length === 0 ? (
<div className="text-[var(--dg-text-secondary)] text-center py-8 text-sm">暂无阶段配置</div>
) : (
parseStages(selectedPipeline.stages).map((stage: PipelineStage, index: number) => (
<div
key={stage.id || index}
className={`p-3 rounded-lg border cursor-pointer transition-all ${
activeStage === stage.id
? 'border-blue-500 bg-blue-500/10'
: 'border-[var(--dg-border)] hover:border-[var(--dg-border)] bg-[var(--dg-bg-secondary)]'
}`}
onClick={() => setActiveStage(activeStage === stage.id ? null : stage.id)}
>
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<span className="text-[var(--dg-text-secondary)] w-6">{index + 1}</span>
{getStageStatusIcon(stage.status)}
<span className="text-[var(--dg-text-primary)]">{stage.name}</span>
</div>
<div className="flex items-center gap-2">
<span className="text-xs text-[var(--dg-text-secondary)]">{stage.duration}</span>
<ChevronDown
className={`h-4 w-4 text-[var(--dg-text-secondary)] transition-transform ${
activeStage === stage.id ? 'rotate-180' : ''
}`}
/>
</div>
</div>
{activeStage === stage.id && stage.logs.length > 0 && (
<div className="mt-3 p-3 bg-[var(--dg-bg-primary)] rounded font-mono text-xs text-[var(--dg-text-secondary)]">
{stage.logs.map((log, i) => (
<div key={i}>{log}</div>
))}
</div>
)}
</div>
))
)}
</div>
</TabsContent>
<TabsContent value="config" className="mt-4">
<Table>
<TableBody>
<TableRow>
<TableCell className="text-[var(--dg-text-secondary)] font-medium w-32">代码仓库</TableCell>
<TableCell className="text-[var(--dg-text-primary)]">{selectedPipeline.source || '--'}</TableCell>
</TableRow>
<TableRow>
<TableCell className="text-[var(--dg-text-secondary)] font-medium">默认分支</TableCell>
<TableCell className="text-[var(--dg-text-primary)]">{selectedPipeline.branch || '--'}</TableCell>
</TableRow>
<TableRow>
<TableCell className="text-[var(--dg-text-secondary)] font-medium">流水线类型</TableCell>
<TableCell className="text-[var(--dg-text-primary)]">{selectedPipeline.type || 'deploy'}</TableCell>
</TableRow>
<TableRow>
<TableCell className="text-[var(--dg-text-secondary)] font-medium">运行次数</TableCell>
<TableCell className="text-[var(--dg-text-primary)]">
{selectedPipeline.run_count || 0} 次(成功 {selectedPipeline.success_count || 0} / 失败 {selectedPipeline.fail_count || 0})
</TableCell>
</TableRow>
</TableBody>
</Table>
</TabsContent>
<TabsContent value="history" className="mt-4">
{loadingRuns ? (
<div className="flex items-center justify-center py-8">
<Loader2 className="h-5 w-5 text-[var(--dg-text-secondary)] animate-spin" />
</div>
) : pipelineRuns.length === 0 ? (
<div className="text-[var(--dg-text-secondary)] text-center py-8 text-sm">暂无执行记录</div>
) : (
<div className="space-y-2">
{pipelineRuns.map((run: any) => (
<div
key={run.id}
className="flex items-center gap-3 p-3 rounded-lg border border-[var(--dg-border)] bg-[var(--dg-bg-secondary)]"
>
<div className="flex items-center gap-2 w-20 shrink-0">
<span className="text-[var(--dg-text-secondary)] text-sm font-mono">#{run.build_number}</span>
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
{getStatusBadge(run.status)}
<Badge variant="outline" className="border-[var(--dg-border)] text-[var(--dg-text-secondary)] text-xs">
<GitBranch className="h-3 w-3 mr-1" />
{run.branch || 'main'}
</Badge>
</div>
<div className="flex items-center gap-3 mt-1 text-xs text-[var(--dg-text-secondary)] opacity-70">
{run.commit_id && (
<span className="flex items-center gap-1">
<GitCommit className="h-3 w-3" />
{run.commit_id.substring(0, 7)}
</span>
)}
<span>{run.triggered_by || 'manual'}</span>
{run.duration > 0 && (
<span className="flex items-center gap-1">
<Clock className="h-3 w-3" />
{run.duration < 60 ? `${run.duration}s` : `${Math.floor(run.duration / 60)}m${run.duration % 60}s`}
</span>
)}
<span>{run.created_at ? new Date(run.created_at).toLocaleString('zh-CN') : '--'}</span>
</div>
</div>
</div>
))}
</div>
)}
</TabsContent>
</Tabs>
</div>
)}
</DialogContent>
</Dialog>
{/* 详情弹窗 */}
<PipelineDetailDialog
open={isDetailDialogOpen}
onOpenChange={setIsDetailDialogOpen}
selectedPipeline={selectedPipeline}
pipelineRuns={pipelineRuns}
loadingRuns={loadingRuns}
activeStage={activeStage}
setActiveStage={setActiveStage}
/>
{/* Run Pipeline Dialog */}
<Dialog open={isRunDialogOpen} onOpenChange={setIsRunDialogOpen}>
<DialogContent className="bg-[var(--dg-bg-primary)] border-[var(--dg-border)] text-[var(--dg-text-primary)]">
<DialogHeader>
<DialogTitle>执行流水线</DialogTitle>
<DialogDescription className="text-[var(--dg-text-secondary)]">{selectedPipeline?.name}</DialogDescription>
</DialogHeader>
<div className="space-y-4 py-4">
<div>
<Label className="text-[var(--dg-text-secondary)]">分支</Label>
<Input defaultValue={selectedPipeline?.branch} className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)] text-[var(--dg-text-primary)] mt-1" />
</div>
<div>
<Label className="text-[var(--dg-text-secondary)]">Commit ID (可选)</Label>
<Input placeholder="留空则使用最新提交" className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)] text-[var(--dg-text-primary)] mt-1" />
</div>
<div>
<Label className="text-[var(--dg-text-secondary)]">环境变量 (可选)</Label>
<Textarea
placeholder="KEY=value&#10;KEY2=value2"
className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)] text-[var(--dg-text-primary)] mt-1 font-mono text-sm"
/>
</div>
</div>
<DialogFooter className="mt-4 pt-4 border-t border-[var(--dg-border)]">
<Button variant="outline" onClick={() => setIsRunDialogOpen(false)} className="border-[var(--dg-border)] text-[var(--dg-text-secondary)]">
取消
</Button>
<Button className="bg-emerald-600 hover:bg-emerald-700" onClick={handleRunPipeline}>
<Play className="h-4 w-4 mr-2" />
开始执行
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* 执行弹窗 */}
<PipelineRunDialog
open={isRunDialogOpen}
onOpenChange={setIsRunDialogOpen}
selectedPipeline={selectedPipeline}
onRun={handleRunPipeline}
/>
</div>
);
}