feat: 流水线管理全栈完善——字段修复+编辑功能+toast反馈+确认框+主题色
This commit is contained in:
1 parent
fd1f30958a
commit
837f50daae
1 file changed
+305
-225
@@ -1,11 +1,9 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect, useCallback } from 'react';
|
||||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
|
import { Card, CardContent } from '@/components/ui/card';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||||
import { Progress } from '@/components/ui/progress';
|
|
||||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
@@ -14,13 +12,6 @@ import {
|
|||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from '@/components/ui/dialog';
|
} from '@/components/ui/dialog';
|
||||||
import {
|
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectItem,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue,
|
|
||||||
} from '@/components/ui/select';
|
|
||||||
import { Textarea } from '@/components/ui/textarea';
|
import { Textarea } from '@/components/ui/textarea';
|
||||||
import { Label } from '@/components/ui/label';
|
import { Label } from '@/components/ui/label';
|
||||||
import {
|
import {
|
||||||
@@ -35,29 +26,25 @@ import {
|
|||||||
Play,
|
Play,
|
||||||
Plus,
|
Plus,
|
||||||
Search,
|
Search,
|
||||||
Settings,
|
|
||||||
GitBranch,
|
GitBranch,
|
||||||
GitCommit,
|
GitCommit,
|
||||||
Clock,
|
Clock,
|
||||||
CheckCircle,
|
CheckCircle,
|
||||||
XCircle,
|
XCircle,
|
||||||
Loader2,
|
Loader2,
|
||||||
MoreHorizontal,
|
|
||||||
Edit,
|
Edit,
|
||||||
Trash2,
|
Trash2,
|
||||||
Copy,
|
|
||||||
Eye,
|
Eye,
|
||||||
Terminal,
|
|
||||||
Pause,
|
|
||||||
RotateCw,
|
|
||||||
History,
|
|
||||||
ChevronRight,
|
ChevronRight,
|
||||||
ChevronDown,
|
ChevronDown,
|
||||||
FileCode,
|
|
||||||
Workflow,
|
Workflow,
|
||||||
|
PackageOpen,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { pipelineApi } from '@/services/api';
|
import { pipelineApi } from '@/services/api';
|
||||||
|
import { useConfirm } from '@/components/ui/confirm-dialog';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
|
// 流水线阶段定义 // Logan.Li
|
||||||
interface PipelineStage {
|
interface PipelineStage {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
@@ -66,6 +53,7 @@ interface PipelineStage {
|
|||||||
logs: string[];
|
logs: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 流水线模板(新建时快捷选择) // Logan.Li
|
||||||
const pipelineTemplates = [
|
const pipelineTemplates = [
|
||||||
{ id: 'template-1', name: 'Node.js 项目', description: '适用于 Node.js 前端项目', stages: ['代码检出', '依赖安装', '代码检查', '构建打包', '部署'] },
|
{ id: 'template-1', name: 'Node.js 项目', description: '适用于 Node.js 前端项目', stages: ['代码检出', '依赖安装', '代码检查', '构建打包', '部署'] },
|
||||||
{ id: 'template-2', name: 'Go 项目', description: '适用于 Go 后端项目', stages: ['代码检出', '单元测试', '构建', '构建镜像', '部署'] },
|
{ id: 'template-2', name: 'Go 项目', description: '适用于 Go 后端项目', stages: ['代码检出', '单元测试', '构建', '构建镜像', '部署'] },
|
||||||
@@ -73,6 +61,7 @@ const pipelineTemplates = [
|
|||||||
{ id: 'template-4', name: 'Python 项目', description: '适用于 Python 项目', stages: ['代码检出', '依赖安装', '代码检查', '测试', '部署'] },
|
{ id: 'template-4', name: 'Python 项目', description: '适用于 Python 项目', stages: ['代码检出', '依赖安装', '代码检查', '测试', '部署'] },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// 解析 stages 字段(兼容 JSON 数组 / 逗号分隔字符串) // Logan.Li
|
||||||
function parseStages(stages: any): PipelineStage[] {
|
function parseStages(stages: any): PipelineStage[] {
|
||||||
if (Array.isArray(stages)) return stages;
|
if (Array.isArray(stages)) return stages;
|
||||||
if (typeof stages === 'string') {
|
if (typeof stages === 'string') {
|
||||||
@@ -86,41 +75,58 @@ function parseStages(stages: any): PipelineStage[] {
|
|||||||
return [];
|
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()}`;
|
||||||
|
}
|
||||||
|
|
||||||
export function PipelineManager() {
|
export function PipelineManager() {
|
||||||
const [searchQuery, setSearchQuery] = useState('');
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
const [pipelines, setPipelines] = useState<any[]>([]);
|
const [pipelines, setPipelines] = useState<any[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [initialLoading, setInitialLoading] = useState(true);
|
||||||
|
const [refreshing, setRefreshing] = useState(false);
|
||||||
const [selectedPipeline, setSelectedPipeline] = useState<any | null>(null);
|
const [selectedPipeline, setSelectedPipeline] = useState<any | null>(null);
|
||||||
const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false);
|
const [isFormDialogOpen, setIsFormDialogOpen] = useState(false);
|
||||||
const [isDetailDialogOpen, setIsDetailDialogOpen] = useState(false);
|
const [isDetailDialogOpen, setIsDetailDialogOpen] = useState(false);
|
||||||
const [isRunDialogOpen, setIsRunDialogOpen] = useState(false);
|
const [isRunDialogOpen, setIsRunDialogOpen] = useState(false);
|
||||||
|
const [editMode, setEditMode] = useState(false);
|
||||||
const [activeStage, setActiveStage] = useState<string | null>(null);
|
const [activeStage, setActiveStage] = useState<string | null>(null);
|
||||||
const [pipelineRuns, setPipelineRuns] = useState<any[]>([]);
|
const [pipelineRuns, setPipelineRuns] = useState<any[]>([]);
|
||||||
const [loadingRuns, setLoadingRuns] = useState(false);
|
const [loadingRuns, setLoadingRuns] = useState(false);
|
||||||
const [newPipeline, setNewPipeline] = useState({
|
const [formData, setFormData] = useState({
|
||||||
name: '',
|
name: '',
|
||||||
description: '',
|
description: '',
|
||||||
repository: '',
|
source: '',
|
||||||
branch: 'main',
|
branch: 'main',
|
||||||
template: '',
|
template: '',
|
||||||
});
|
});
|
||||||
|
|
||||||
const loadData = async () => {
|
const _confirm = useConfirm();
|
||||||
|
|
||||||
|
// 加载流水线列表(支持首次/刷新双模式) // Logan.Li
|
||||||
|
const loadData = useCallback(async (isRefresh = false) => {
|
||||||
|
if (isRefresh) setRefreshing(true); else setInitialLoading(true);
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
|
||||||
const data = await pipelineApi.getPipelines();
|
const data = await pipelineApi.getPipelines();
|
||||||
setPipelines(data.list || []);
|
setPipelines(data.list || []);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to load pipelines:', error);
|
console.error('Failed to load pipelines:', error);
|
||||||
|
toast.error('获取流水线列表失败');
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setRefreshing(false);
|
||||||
|
setInitialLoading(false);
|
||||||
}
|
}
|
||||||
};
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadData();
|
loadData();
|
||||||
}, []);
|
}, [loadData]);
|
||||||
|
|
||||||
|
// 加载流水线执行历史 // Logan.Li
|
||||||
const loadPipelineRuns = async (pipelineId: number | string) => {
|
const loadPipelineRuns = async (pipelineId: number | string) => {
|
||||||
try {
|
try {
|
||||||
setLoadingRuns(true);
|
setLoadingRuns(true);
|
||||||
@@ -140,12 +146,34 @@ export function PipelineManager() {
|
|||||||
loadPipelineRuns(pipeline.id);
|
loadPipelineRuns(pipeline.id);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 打开新建弹窗 // Logan.Li
|
||||||
|
const openCreateDialog = () => {
|
||||||
|
setEditMode(false);
|
||||||
|
setFormData({ name: '', description: '', source: '', branch: 'main', template: '' });
|
||||||
|
setIsFormDialogOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 打开编辑弹窗(回填数据) // Logan.Li
|
||||||
|
const openEditDialog = (pipeline: any) => {
|
||||||
|
setEditMode(true);
|
||||||
|
setSelectedPipeline(pipeline);
|
||||||
|
setFormData({
|
||||||
|
name: pipeline.name || '',
|
||||||
|
description: pipeline.description || '',
|
||||||
|
source: pipeline.source || '',
|
||||||
|
branch: pipeline.branch || 'main',
|
||||||
|
template: '',
|
||||||
|
});
|
||||||
|
setIsFormDialogOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
const filteredPipelines = pipelines.filter(
|
const filteredPipelines = pipelines.filter(
|
||||||
(p) =>
|
(p) =>
|
||||||
(p.name || '').toLowerCase().includes(searchQuery.toLowerCase()) ||
|
(p.name || '').toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||||
(p.description || '').toLowerCase().includes(searchQuery.toLowerCase())
|
(p.description || '').toLowerCase().includes(searchQuery.toLowerCase())
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 状态徽章(使用后端 last_status 字段) // Logan.Li
|
||||||
const getStatusBadge = (status: any) => {
|
const getStatusBadge = (status: any) => {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case 'success':
|
case 'success':
|
||||||
@@ -171,7 +199,7 @@ export function PipelineManager() {
|
|||||||
);
|
);
|
||||||
default:
|
default:
|
||||||
return (
|
return (
|
||||||
<Badge variant="outline" className="border-slate-500/30 text-slate-400 bg-slate-500/10">
|
<Badge variant="outline" className="border-[var(--dg-border)] text-[var(--dg-text-secondary)]">
|
||||||
从未运行
|
从未运行
|
||||||
</Badge>
|
</Badge>
|
||||||
);
|
);
|
||||||
@@ -187,79 +215,110 @@ export function PipelineManager() {
|
|||||||
case 'running':
|
case 'running':
|
||||||
return <Loader2 className="h-4 w-4 text-blue-400 animate-spin" />;
|
return <Loader2 className="h-4 w-4 text-blue-400 animate-spin" />;
|
||||||
case 'skipped':
|
case 'skipped':
|
||||||
return <span className="text-slate-500 text-xs">跳过</span>;
|
return <span className="text-[var(--dg-text-secondary)] text-xs">跳过</span>;
|
||||||
default:
|
default:
|
||||||
return <span className="text-slate-600 text-xs">待执行</span>;
|
return <span className="text-[var(--dg-text-secondary)] opacity-60 text-xs">待执行</span>;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCreatePipeline = async () => {
|
// 创建/编辑流水线 // Logan.Li
|
||||||
|
const handleSavePipeline = async () => {
|
||||||
|
if (!formData.name.trim()) {
|
||||||
|
toast.error('请填写流水线名称');
|
||||||
|
return;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
const selectedTemplate = pipelineTemplates.find(t => t.id === newPipeline.template);
|
const selectedTemplate = pipelineTemplates.find(t => t.id === formData.template);
|
||||||
const stages = selectedTemplate
|
const stages = selectedTemplate && !editMode
|
||||||
? JSON.stringify(selectedTemplate.stages.map((name, i) => ({
|
? JSON.stringify(selectedTemplate.stages.map((name, i) => ({
|
||||||
id: `stage-${i + 1}`, name, status: 'pending', duration: '-', logs: [],
|
id: `stage-${i + 1}`, name, status: 'pending', duration: '-', logs: [],
|
||||||
})))
|
})))
|
||||||
: '';
|
: editMode ? (selectedPipeline?.stages || '') : '';
|
||||||
const code = newPipeline.name
|
const payload = {
|
||||||
.toLowerCase()
|
name: formData.name,
|
||||||
.replace(/[^a-z0-9]/g, '-')
|
code: editMode ? (selectedPipeline?.code || nameToCode(formData.name)) : nameToCode(formData.name),
|
||||||
.replace(/-+/g, '-')
|
description: formData.description,
|
||||||
.replace(/^-|-$/g, '') || `pipeline-${Date.now()}`;
|
source: formData.source,
|
||||||
await pipelineApi.createPipeline({
|
branch: formData.branch,
|
||||||
name: newPipeline.name,
|
|
||||||
code,
|
|
||||||
description: newPipeline.description,
|
|
||||||
source: newPipeline.repository,
|
|
||||||
branch: newPipeline.branch,
|
|
||||||
type: 'deploy',
|
type: 'deploy',
|
||||||
stages,
|
stages,
|
||||||
});
|
};
|
||||||
setIsCreateDialogOpen(false);
|
if (editMode && selectedPipeline) {
|
||||||
setNewPipeline({ name: '', description: '', repository: '', branch: 'main', template: '' });
|
await pipelineApi.updatePipeline(selectedPipeline.id, payload);
|
||||||
loadData();
|
toast.success('流水线已更新');
|
||||||
|
} else {
|
||||||
|
await pipelineApi.createPipeline(payload);
|
||||||
|
toast.success('流水线已创建');
|
||||||
|
}
|
||||||
|
setIsFormDialogOpen(false);
|
||||||
|
loadData(true);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to create pipeline:', error);
|
console.error('Failed to save pipeline:', error);
|
||||||
|
toast.error(editMode ? '更新失败' : '创建失败');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDeletePipeline = async (id: any) => {
|
// 删除流水线(带确认框) // Logan.Li
|
||||||
|
const handleDeletePipeline = async (pipeline: any) => {
|
||||||
|
const ok = await _confirm({
|
||||||
|
title: '删除流水线',
|
||||||
|
description: `确定删除「${pipeline.name}」吗?此操作不可恢复。`,
|
||||||
|
variant: 'destructive',
|
||||||
|
});
|
||||||
|
if (!ok) return;
|
||||||
try {
|
try {
|
||||||
await pipelineApi.deletePipeline(id);
|
await pipelineApi.deletePipeline(pipeline.id);
|
||||||
loadData();
|
toast.success('流水线已删除');
|
||||||
|
loadData(true);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to delete pipeline:', error);
|
console.error('Failed to delete pipeline:', error);
|
||||||
|
toast.error('删除失败');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 触发流水线执行 // Logan.Li
|
||||||
const handleRunPipeline = async () => {
|
const handleRunPipeline = async () => {
|
||||||
if (!selectedPipeline) return;
|
if (!selectedPipeline) return;
|
||||||
try {
|
try {
|
||||||
await pipelineApi.triggerPipeline(selectedPipeline.id);
|
await pipelineApi.triggerPipeline(selectedPipeline.id);
|
||||||
|
toast.success('流水线已触发');
|
||||||
setIsRunDialogOpen(false);
|
setIsRunDialogOpen(false);
|
||||||
loadData();
|
loadData(true);
|
||||||
if (isDetailDialogOpen) {
|
if (isDetailDialogOpen) {
|
||||||
loadPipelineRuns(selectedPipeline.id);
|
loadPipelineRuns(selectedPipeline.id);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to trigger pipeline:', error);
|
console.error('Failed to trigger pipeline:', error);
|
||||||
|
toast.error('触发失败');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 统计卡片数据 // Logan.Li
|
||||||
|
const stats = {
|
||||||
|
total: pipelines.length,
|
||||||
|
running: pipelines.filter(p => p.last_status === 'running').length,
|
||||||
|
success: pipelines.filter(p => p.last_status === 'success').length,
|
||||||
|
failed: pipelines.filter(p => p.last_status === 'failed').length,
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl font-bold text-slate-100">流水线管理</h1>
|
<h1 className="text-2xl font-bold text-[var(--dg-text-primary)]">流水线管理</h1>
|
||||||
<p className="text-sm text-slate-500 mt-1">CI/CD 流水线配置与管理</p>
|
<p className="text-sm text-[var(--dg-text-secondary)] mt-1">CI/CD 流水线配置与管理</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Button variant="outline" className="border-slate-700 text-slate-300">
|
<Button
|
||||||
<History className="h-4 w-4 mr-2" />
|
variant="outline"
|
||||||
执行历史
|
className="border-[var(--dg-border)] text-[var(--dg-text-secondary)]"
|
||||||
|
onClick={() => loadData(true)}
|
||||||
|
>
|
||||||
|
<Clock className={`h-4 w-4 mr-2 ${refreshing ? 'animate-spin' : ''}`} />
|
||||||
|
刷新
|
||||||
</Button>
|
</Button>
|
||||||
<Button className="bg-blue-600 hover:bg-blue-700" onClick={() => setIsCreateDialogOpen(true)}>
|
<Button onClick={openCreateDialog}>
|
||||||
<Plus className="h-4 w-4 mr-2" />
|
<Plus className="h-4 w-4 mr-2" />
|
||||||
新建流水线
|
新建流水线
|
||||||
</Button>
|
</Button>
|
||||||
@@ -268,12 +327,12 @@ export function PipelineManager() {
|
|||||||
|
|
||||||
{/* Stats */}
|
{/* Stats */}
|
||||||
<div className="grid grid-cols-4 gap-4">
|
<div className="grid grid-cols-4 gap-4">
|
||||||
<Card className="bg-slate-800 border-slate-700">
|
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
|
||||||
<CardContent className="p-4">
|
<CardContent className="p-4">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-slate-400 text-sm">流水线总数</p>
|
<p className="text-[var(--dg-text-secondary)] text-sm">流水线总数</p>
|
||||||
<p className="text-2xl font-bold text-slate-100">{pipelines.length}</p>
|
<p className="text-2xl font-bold text-[var(--dg-text-primary)]">{stats.total}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-10 h-10 rounded-lg bg-blue-500/20 flex items-center justify-center">
|
<div className="w-10 h-10 rounded-lg bg-blue-500/20 flex items-center justify-center">
|
||||||
<Workflow className="h-5 w-5 text-blue-400" />
|
<Workflow className="h-5 w-5 text-blue-400" />
|
||||||
@@ -281,14 +340,12 @@ export function PipelineManager() {
|
|||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
<Card className="bg-slate-800 border-slate-700">
|
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
|
||||||
<CardContent className="p-4">
|
<CardContent className="p-4">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-slate-400 text-sm">运行中</p>
|
<p className="text-[var(--dg-text-secondary)] text-sm">运行中</p>
|
||||||
<p className="text-2xl font-bold text-blue-400">
|
<p className="text-2xl font-bold text-blue-400">{stats.running}</p>
|
||||||
{pipelines.filter((p) => p.status === 'running').length}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="w-10 h-10 rounded-lg bg-blue-500/20 flex items-center justify-center">
|
<div className="w-10 h-10 rounded-lg bg-blue-500/20 flex items-center justify-center">
|
||||||
<Loader2 className="h-5 w-5 text-blue-400" />
|
<Loader2 className="h-5 w-5 text-blue-400" />
|
||||||
@@ -296,14 +353,12 @@ export function PipelineManager() {
|
|||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
<Card className="bg-slate-800 border-slate-700">
|
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
|
||||||
<CardContent className="p-4">
|
<CardContent className="p-4">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-slate-400 text-sm">今日成功</p>
|
<p className="text-[var(--dg-text-secondary)] text-sm">成功</p>
|
||||||
<p className="text-2xl font-bold text-emerald-400">
|
<p className="text-2xl font-bold text-emerald-400">{stats.success}</p>
|
||||||
{pipelines.filter((p) => p.status === 'success').length}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="w-10 h-10 rounded-lg bg-emerald-500/20 flex items-center justify-center">
|
<div className="w-10 h-10 rounded-lg bg-emerald-500/20 flex items-center justify-center">
|
||||||
<CheckCircle className="h-5 w-5 text-emerald-400" />
|
<CheckCircle className="h-5 w-5 text-emerald-400" />
|
||||||
@@ -311,14 +366,12 @@ export function PipelineManager() {
|
|||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
<Card className="bg-slate-800 border-slate-700">
|
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
|
||||||
<CardContent className="p-4">
|
<CardContent className="p-4">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-slate-400 text-sm">今日失败</p>
|
<p className="text-[var(--dg-text-secondary)] text-sm">失败</p>
|
||||||
<p className="text-2xl font-bold text-red-400">
|
<p className="text-2xl font-bold text-red-400">{stats.failed}</p>
|
||||||
{pipelines.filter((p) => p.status === 'failed').length}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="w-10 h-10 rounded-lg bg-red-500/20 flex items-center justify-center">
|
<div className="w-10 h-10 rounded-lg bg-red-500/20 flex items-center justify-center">
|
||||||
<XCircle className="h-5 w-5 text-red-400" />
|
<XCircle className="h-5 w-5 text-red-400" />
|
||||||
@@ -331,47 +384,50 @@ export function PipelineManager() {
|
|||||||
{/* Filters */}
|
{/* Filters */}
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<div className="relative flex-1 max-w-md">
|
<div className="relative flex-1 max-w-md">
|
||||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-slate-400" />
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-[var(--dg-text-secondary)]" />
|
||||||
<Input
|
<Input
|
||||||
placeholder="搜索流水线..."
|
placeholder="搜索流水线..."
|
||||||
value={searchQuery}
|
value={searchQuery}
|
||||||
onChange={(e) => setSearchQuery(e.target.value)}
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
className="pl-10 bg-slate-800 border-slate-700 text-slate-100"
|
className="pl-10 bg-[var(--dg-bg-secondary)] border-[var(--dg-border)] text-[var(--dg-text-primary)]"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{loading ? (
|
{initialLoading ? (
|
||||||
<div className="flex items-center justify-center py-12">
|
<div className="flex items-center justify-center py-12">
|
||||||
<Loader2 className="h-6 w-6 text-slate-500 animate-spin" />
|
<Loader2 className="h-6 w-6 text-[var(--dg-text-secondary)] animate-spin" />
|
||||||
</div>
|
</div>
|
||||||
) : filteredPipelines.length === 0 ? (
|
) : filteredPipelines.length === 0 ? (
|
||||||
<div className="text-center text-slate-500 py-12">暂无流水线数据</div>
|
<div className="flex flex-col items-center justify-center py-16 text-[var(--dg-text-secondary)]">
|
||||||
|
<PackageOpen className="h-12 w-12 mb-3 opacity-40" />
|
||||||
|
<p className="text-sm">暂无流水线数据</p>
|
||||||
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{/* Pipeline List */}
|
{/* Pipeline List */}
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{filteredPipelines.map((pipeline) => (
|
{filteredPipelines.map((pipeline) => (
|
||||||
<Card key={pipeline.id} className="bg-slate-800 border-slate-700">
|
<Card key={pipeline.id} className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
|
||||||
<CardContent className="p-4">
|
<CardContent className="p-4">
|
||||||
<div className="flex items-start justify-between">
|
<div className="flex items-start justify-between">
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<h3 className="font-medium text-slate-100">{pipeline.name}</h3>
|
<h3 className="font-medium text-[var(--dg-text-primary)]">{pipeline.name}</h3>
|
||||||
{getStatusBadge(pipeline.status)}
|
{getStatusBadge(pipeline.last_status)}
|
||||||
<Badge variant="outline" className="border-slate-700 text-slate-400">
|
<Badge variant="outline" className="border-[var(--dg-border)] text-[var(--dg-text-secondary)]">
|
||||||
{pipeline.trigger_type === 'auto' ? '自动触发' : '手动触发'}
|
{pipeline.run_count > 0 ? `已运行 ${pipeline.run_count} 次` : '未执行'}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-slate-400 mt-1">{pipeline.description}</p>
|
<p className="text-sm text-[var(--dg-text-secondary)] mt-1">{pipeline.description}</p>
|
||||||
<div className="flex items-center gap-4 mt-2 text-xs text-slate-500">
|
<div className="flex items-center gap-4 mt-2 text-xs text-[var(--dg-text-secondary)] opacity-70">
|
||||||
<span className="flex items-center gap-1">
|
<span className="flex items-center gap-1">
|
||||||
<GitBranch className="h-3 w-3" />
|
<GitBranch className="h-3 w-3" />
|
||||||
{pipeline.branch}
|
{pipeline.branch || 'main'}
|
||||||
</span>
|
</span>
|
||||||
<span className="flex items-center gap-1">
|
<span className="flex items-center gap-1">
|
||||||
<Clock className="h-3 w-3" />
|
<Clock className="h-3 w-3" />
|
||||||
上次运行: {pipeline.updated_at || '--'}
|
上次运行: {pipeline.last_run_at ? new Date(pipeline.last_run_at).toLocaleString('zh-CN') : '--'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -388,14 +444,14 @@ export function PipelineManager() {
|
|||||||
? 'bg-red-500/20 text-red-400'
|
? 'bg-red-500/20 text-red-400'
|
||||||
: stage.status === 'running'
|
: stage.status === 'running'
|
||||||
? 'bg-blue-500/20 text-blue-400'
|
? 'bg-blue-500/20 text-blue-400'
|
||||||
: 'bg-slate-700 text-slate-400'
|
: 'bg-[var(--dg-bg-tertiary)] text-[var(--dg-text-secondary)]'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{getStageStatusIcon(stage.status)}
|
{getStageStatusIcon(stage.status)}
|
||||||
<span className="ml-1">{stage.name}</span>
|
<span className="ml-1">{stage.name}</span>
|
||||||
</div>
|
</div>
|
||||||
{index < parseStages(pipeline.stages).length - 1 && (
|
{index < parseStages(pipeline.stages).length - 1 && (
|
||||||
<ChevronRight className="h-4 w-4 text-slate-600 mx-1" />
|
<ChevronRight className="h-4 w-4 text-[var(--dg-text-secondary)] opacity-40 mx-1" />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
@@ -419,15 +475,25 @@ export function PipelineManager() {
|
|||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
className="h-8 w-8 text-slate-400 hover:text-blue-400"
|
className="h-8 w-8 text-[var(--dg-text-secondary)] hover:text-blue-400"
|
||||||
onClick={() => handleOpenDetail(pipeline)}
|
onClick={() => handleOpenDetail(pipeline)}
|
||||||
>
|
>
|
||||||
<Eye className="h-4 w-4" />
|
<Eye className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="ghost" size="icon" className="h-8 w-8 text-slate-400 hover:text-blue-400">
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="h-8 w-8 text-[var(--dg-text-secondary)] hover:text-blue-400"
|
||||||
|
onClick={() => openEditDialog(pipeline)}
|
||||||
|
>
|
||||||
<Edit className="h-4 w-4" />
|
<Edit className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="ghost" size="icon" className="h-8 w-8 text-slate-400 hover:text-red-400" onClick={() => handleDeletePipeline(pipeline.id)}>
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="h-8 w-8 text-[var(--dg-text-secondary)] hover:text-red-400"
|
||||||
|
onClick={() => handleDeletePipeline(pipeline)}
|
||||||
|
>
|
||||||
<Trash2 className="h-4 w-4" />
|
<Trash2 className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -437,90 +503,94 @@ export function PipelineManager() {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Create Pipeline Dialog */}
|
{/* Create/Edit Pipeline Dialog */}
|
||||||
<Dialog open={isCreateDialogOpen} onOpenChange={setIsCreateDialogOpen}>
|
<Dialog open={isFormDialogOpen} onOpenChange={setIsFormDialogOpen}>
|
||||||
<DialogContent className="max-w-2xl bg-slate-900 border-slate-700 text-slate-100">
|
<DialogContent className="max-w-2xl bg-[var(--dg-bg-primary)] border-[var(--dg-border)] text-[var(--dg-text-primary)]">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>新建流水线</DialogTitle>
|
<DialogTitle>{editMode ? '编辑流水线' : '新建流水线'}</DialogTitle>
|
||||||
<DialogDescription className="text-slate-500">配置新的 CI/CD 流水线</DialogDescription>
|
<DialogDescription className="text-[var(--dg-text-secondary)]">
|
||||||
|
{editMode ? '修改流水线配置' : '配置新的 CI/CD 流水线'}
|
||||||
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<div className="space-y-4 py-4">
|
<div className="space-y-4 py-4">
|
||||||
<div>
|
<div>
|
||||||
<Label className="text-slate-300">流水线名称</Label>
|
<Label className="text-[var(--dg-text-secondary)]">流水线名称</Label>
|
||||||
<Input
|
<Input
|
||||||
value={newPipeline.name}
|
value={formData.name}
|
||||||
onChange={(e) => setNewPipeline({ ...newPipeline, name: e.target.value })}
|
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
||||||
placeholder="例如: frontend-build"
|
placeholder="例如: frontend-build"
|
||||||
className="bg-slate-800 border-slate-700 text-slate-100 mt-1"
|
className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)] text-[var(--dg-text-primary)] mt-1"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Label className="text-slate-300">描述</Label>
|
<Label className="text-[var(--dg-text-secondary)]">描述</Label>
|
||||||
<Textarea
|
<Textarea
|
||||||
value={newPipeline.description}
|
value={formData.description}
|
||||||
onChange={(e) => setNewPipeline({ ...newPipeline, description: e.target.value })}
|
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
|
||||||
placeholder="流水线描述..."
|
placeholder="流水线描述..."
|
||||||
className="bg-slate-800 border-slate-700 text-slate-100 mt-1"
|
className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)] text-[var(--dg-text-primary)] mt-1"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Label className="text-slate-300">代码仓库</Label>
|
<Label className="text-[var(--dg-text-secondary)]">代码仓库</Label>
|
||||||
<Input
|
<Input
|
||||||
value={newPipeline.repository}
|
value={formData.source}
|
||||||
onChange={(e) => setNewPipeline({ ...newPipeline, repository: e.target.value })}
|
onChange={(e) => setFormData({ ...formData, source: e.target.value })}
|
||||||
placeholder="https://gitlab.company.com/project/repo.git"
|
placeholder="https://gitlab.company.com/project/repo.git"
|
||||||
className="bg-slate-800 border-slate-700 text-slate-100 mt-1"
|
className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)] text-[var(--dg-text-primary)] mt-1"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Label className="text-slate-300">默认分支</Label>
|
<Label className="text-[var(--dg-text-secondary)]">默认分支</Label>
|
||||||
<Input
|
<Input
|
||||||
value={newPipeline.branch}
|
value={formData.branch}
|
||||||
onChange={(e) => setNewPipeline({ ...newPipeline, branch: e.target.value })}
|
onChange={(e) => setFormData({ ...formData, branch: e.target.value })}
|
||||||
placeholder="main"
|
placeholder="main"
|
||||||
className="bg-slate-800 border-slate-700 text-slate-100 mt-1"
|
className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)] text-[var(--dg-text-primary)] mt-1"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
{!editMode && (
|
||||||
<Label className="text-slate-300">选择模板</Label>
|
<div>
|
||||||
<div className="grid grid-cols-2 gap-3 mt-1">
|
<Label className="text-[var(--dg-text-secondary)]">选择模板</Label>
|
||||||
{pipelineTemplates.map((template) => (
|
<div className="grid grid-cols-2 gap-3 mt-1">
|
||||||
<Card
|
{pipelineTemplates.map((template) => (
|
||||||
key={template.id}
|
<Card
|
||||||
className={`cursor-pointer transition-all ${
|
key={template.id}
|
||||||
newPipeline.template === template.id
|
className={`cursor-pointer transition-all ${
|
||||||
? 'border-blue-500 ring-1 ring-blue-500/30 bg-slate-800'
|
formData.template === template.id
|
||||||
: 'border-slate-700 hover:border-slate-600 bg-slate-800/50'
|
? '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={() => setNewPipeline({ ...newPipeline, template: template.id })}
|
}`}
|
||||||
>
|
onClick={() => setFormData({ ...formData, template: template.id })}
|
||||||
<CardContent className="p-3">
|
>
|
||||||
<p className="font-medium text-slate-200">{template.name}</p>
|
<CardContent className="p-3">
|
||||||
<p className="text-xs text-slate-500">{template.description}</p>
|
<p className="font-medium text-[var(--dg-text-primary)]">{template.name}</p>
|
||||||
<div className="flex flex-wrap gap-1 mt-2">
|
<p className="text-xs text-[var(--dg-text-secondary)]">{template.description}</p>
|
||||||
{template.stages.slice(0, 3).map((stage) => (
|
<div className="flex flex-wrap gap-1 mt-2">
|
||||||
<Badge key={stage} variant="secondary" className="bg-slate-700 text-slate-300 text-xs">
|
{template.stages.slice(0, 3).map((stage) => (
|
||||||
{stage}
|
<Badge key={stage} variant="secondary" className="bg-[var(--dg-bg-tertiary)] text-[var(--dg-text-secondary)] text-xs">
|
||||||
</Badge>
|
{stage}
|
||||||
))}
|
</Badge>
|
||||||
{template.stages.length > 3 && (
|
))}
|
||||||
<Badge variant="secondary" className="bg-slate-700 text-slate-300 text-xs">
|
{template.stages.length > 3 && (
|
||||||
+{template.stages.length - 3}
|
<Badge variant="secondary" className="bg-[var(--dg-bg-tertiary)] text-[var(--dg-text-secondary)] text-xs">
|
||||||
</Badge>
|
+{template.stages.length - 3}
|
||||||
)}
|
</Badge>
|
||||||
</div>
|
)}
|
||||||
</CardContent>
|
</div>
|
||||||
</Card>
|
</CardContent>
|
||||||
))}
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
</div>
|
</div>
|
||||||
<DialogFooter>
|
<DialogFooter className="mt-4 pt-4 border-t border-[var(--dg-border)]">
|
||||||
<Button variant="outline" onClick={() => setIsCreateDialogOpen(false)} className="border-slate-700 text-slate-300">
|
<Button variant="outline" onClick={() => setIsFormDialogOpen(false)} className="border-[var(--dg-border)] text-[var(--dg-text-secondary)]">
|
||||||
取消
|
取消
|
||||||
</Button>
|
</Button>
|
||||||
<Button className="bg-blue-600 hover:bg-blue-700" onClick={handleCreatePipeline}>
|
<Button onClick={handleSavePipeline}>
|
||||||
创建流水线
|
{editMode ? '保存修改' : '创建流水线'}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
@@ -528,107 +598,117 @@ export function PipelineManager() {
|
|||||||
|
|
||||||
{/* Pipeline Detail Dialog */}
|
{/* Pipeline Detail Dialog */}
|
||||||
<Dialog open={isDetailDialogOpen} onOpenChange={setIsDetailDialogOpen}>
|
<Dialog open={isDetailDialogOpen} onOpenChange={setIsDetailDialogOpen}>
|
||||||
<DialogContent className="max-w-4xl bg-slate-900 border-slate-700 text-slate-100">
|
<DialogContent className="max-w-4xl bg-[var(--dg-bg-primary)] border-[var(--dg-border)] text-[var(--dg-text-primary)]">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>{selectedPipeline?.name}</DialogTitle>
|
<DialogTitle>{selectedPipeline?.name}</DialogTitle>
|
||||||
<DialogDescription className="text-slate-500">{selectedPipeline?.description}</DialogDescription>
|
<DialogDescription className="text-[var(--dg-text-secondary)]">{selectedPipeline?.description}</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
{selectedPipeline && (
|
{selectedPipeline && (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<Tabs defaultValue="stages">
|
<Tabs defaultValue="stages">
|
||||||
<TabsList className="bg-slate-800 border border-slate-700">
|
<TabsList className="bg-[var(--dg-bg-secondary)] border border-[var(--dg-border)]">
|
||||||
<TabsTrigger value="stages" className="data-[state=active]:bg-slate-700">
|
<TabsTrigger value="stages" className="data-[state=active]:bg-[var(--dg-bg-tertiary)]">
|
||||||
执行阶段
|
执行阶段
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
<TabsTrigger value="config" className="data-[state=active]:bg-slate-700">
|
<TabsTrigger value="config" className="data-[state=active]:bg-[var(--dg-bg-tertiary)]">
|
||||||
配置
|
配置
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
<TabsTrigger value="history" className="data-[state=active]:bg-slate-700">
|
<TabsTrigger value="history" className="data-[state=active]:bg-[var(--dg-bg-tertiary)]">
|
||||||
历史记录{pipelineRuns.length > 0 && ` (${pipelineRuns.length})`}
|
历史记录{pipelineRuns.length > 0 && ` (${pipelineRuns.length})`}
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
<TabsContent value="stages" className="mt-4">
|
<TabsContent value="stages" className="mt-4">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{parseStages(selectedPipeline.stages).map((stage: PipelineStage, index: number) => (
|
{parseStages(selectedPipeline.stages).length === 0 ? (
|
||||||
<div
|
<div className="text-[var(--dg-text-secondary)] text-center py-8 text-sm">暂无阶段配置</div>
|
||||||
key={stage.id || index}
|
) : (
|
||||||
className={`p-3 rounded-lg border cursor-pointer transition-all ${
|
parseStages(selectedPipeline.stages).map((stage: PipelineStage, index: number) => (
|
||||||
activeStage === stage.id
|
<div
|
||||||
? 'border-blue-500 bg-blue-500/10'
|
key={stage.id || index}
|
||||||
: 'border-slate-700 hover:border-slate-600 bg-slate-800/50'
|
className={`p-3 rounded-lg border cursor-pointer transition-all ${
|
||||||
}`}
|
activeStage === stage.id
|
||||||
onClick={() => setActiveStage(activeStage === stage.id ? null : stage.id)}
|
? 'border-blue-500 bg-blue-500/10'
|
||||||
>
|
: 'border-[var(--dg-border)] hover:border-[var(--dg-border)] bg-[var(--dg-bg-secondary)]'
|
||||||
<div className="flex items-center justify-between">
|
}`}
|
||||||
<div className="flex items-center gap-3">
|
onClick={() => setActiveStage(activeStage === stage.id ? null : stage.id)}
|
||||||
<span className="text-slate-500 w-6">{index + 1}</span>
|
>
|
||||||
{getStageStatusIcon(stage.status)}
|
<div className="flex items-center justify-between">
|
||||||
<span className="text-slate-200">{stage.name}</span>
|
<div className="flex items-center gap-3">
|
||||||
</div>
|
<span className="text-[var(--dg-text-secondary)] w-6">{index + 1}</span>
|
||||||
<div className="flex items-center gap-2">
|
{getStageStatusIcon(stage.status)}
|
||||||
<span className="text-xs text-slate-500">{stage.duration}</span>
|
<span className="text-[var(--dg-text-primary)]">{stage.name}</span>
|
||||||
<ChevronDown
|
</div>
|
||||||
className={`h-4 w-4 text-slate-500 transition-transform ${
|
<div className="flex items-center gap-2">
|
||||||
activeStage === stage.id ? 'rotate-180' : ''
|
<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>
|
</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>
|
||||||
{activeStage === stage.id && stage.logs.length > 0 && (
|
))
|
||||||
<div className="mt-3 p-3 bg-slate-900 rounded font-mono text-xs text-slate-400">
|
)}
|
||||||
{stage.logs.map((log, i) => (
|
|
||||||
<div key={i}>{log}</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value="config" className="mt-4">
|
<TabsContent value="config" className="mt-4">
|
||||||
<div className="space-y-4">
|
<Table>
|
||||||
<div>
|
<TableBody>
|
||||||
<Label className="text-slate-400">代码仓库</Label>
|
<TableRow>
|
||||||
<p className="text-slate-200">{selectedPipeline.repository}</p>
|
<TableCell className="text-[var(--dg-text-secondary)] font-medium w-32">代码仓库</TableCell>
|
||||||
</div>
|
<TableCell className="text-[var(--dg-text-primary)]">{selectedPipeline.source || '--'}</TableCell>
|
||||||
<div>
|
</TableRow>
|
||||||
<Label className="text-slate-400">默认分支</Label>
|
<TableRow>
|
||||||
<p className="text-slate-200">{selectedPipeline.branch}</p>
|
<TableCell className="text-[var(--dg-text-secondary)] font-medium">默认分支</TableCell>
|
||||||
</div>
|
<TableCell className="text-[var(--dg-text-primary)]">{selectedPipeline.branch || '--'}</TableCell>
|
||||||
<div>
|
</TableRow>
|
||||||
<Label className="text-slate-400">触发方式</Label>
|
<TableRow>
|
||||||
<p className="text-slate-200">
|
<TableCell className="text-[var(--dg-text-secondary)] font-medium">流水线类型</TableCell>
|
||||||
{selectedPipeline.trigger_type === 'auto' ? '自动触发' : '手动触发'}
|
<TableCell className="text-[var(--dg-text-primary)]">{selectedPipeline.type || 'deploy'}</TableCell>
|
||||||
</p>
|
</TableRow>
|
||||||
</div>
|
<TableRow>
|
||||||
</div>
|
<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>
|
||||||
<TabsContent value="history" className="mt-4">
|
<TabsContent value="history" className="mt-4">
|
||||||
{loadingRuns ? (
|
{loadingRuns ? (
|
||||||
<div className="flex items-center justify-center py-8">
|
<div className="flex items-center justify-center py-8">
|
||||||
<Loader2 className="h-5 w-5 text-slate-500 animate-spin" />
|
<Loader2 className="h-5 w-5 text-[var(--dg-text-secondary)] animate-spin" />
|
||||||
</div>
|
</div>
|
||||||
) : pipelineRuns.length === 0 ? (
|
) : pipelineRuns.length === 0 ? (
|
||||||
<div className="text-slate-500 text-center py-8">暂无执行记录</div>
|
<div className="text-[var(--dg-text-secondary)] text-center py-8 text-sm">暂无执行记录</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{pipelineRuns.map((run: any) => (
|
{pipelineRuns.map((run: any) => (
|
||||||
<div
|
<div
|
||||||
key={run.id}
|
key={run.id}
|
||||||
className="flex items-center gap-3 p-3 rounded-lg border border-slate-700 bg-slate-800/50"
|
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">
|
<div className="flex items-center gap-2 w-20 shrink-0">
|
||||||
<span className="text-slate-500 text-sm font-mono">#{run.build_number}</span>
|
<span className="text-[var(--dg-text-secondary)] text-sm font-mono">#{run.build_number}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{getStatusBadge(run.status)}
|
{getStatusBadge(run.status)}
|
||||||
<Badge variant="outline" className="border-slate-700 text-slate-400 text-xs">
|
<Badge variant="outline" className="border-[var(--dg-border)] text-[var(--dg-text-secondary)] text-xs">
|
||||||
<GitBranch className="h-3 w-3 mr-1" />
|
<GitBranch className="h-3 w-3 mr-1" />
|
||||||
{run.branch || 'main'}
|
{run.branch || 'main'}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-3 mt-1 text-xs text-slate-500">
|
<div className="flex items-center gap-3 mt-1 text-xs text-[var(--dg-text-secondary)] opacity-70">
|
||||||
{run.commit_id && (
|
{run.commit_id && (
|
||||||
<span className="flex items-center gap-1">
|
<span className="flex items-center gap-1">
|
||||||
<GitCommit className="h-3 w-3" />
|
<GitCommit className="h-3 w-3" />
|
||||||
@@ -658,30 +738,30 @@ export function PipelineManager() {
|
|||||||
|
|
||||||
{/* Run Pipeline Dialog */}
|
{/* Run Pipeline Dialog */}
|
||||||
<Dialog open={isRunDialogOpen} onOpenChange={setIsRunDialogOpen}>
|
<Dialog open={isRunDialogOpen} onOpenChange={setIsRunDialogOpen}>
|
||||||
<DialogContent className="bg-slate-900 border-slate-700 text-slate-100">
|
<DialogContent className="bg-[var(--dg-bg-primary)] border-[var(--dg-border)] text-[var(--dg-text-primary)]">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>执行流水线</DialogTitle>
|
<DialogTitle>执行流水线</DialogTitle>
|
||||||
<DialogDescription className="text-slate-500">{selectedPipeline?.name}</DialogDescription>
|
<DialogDescription className="text-[var(--dg-text-secondary)]">{selectedPipeline?.name}</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<div className="space-y-4 py-4">
|
<div className="space-y-4 py-4">
|
||||||
<div>
|
<div>
|
||||||
<Label className="text-slate-300">分支</Label>
|
<Label className="text-[var(--dg-text-secondary)]">分支</Label>
|
||||||
<Input defaultValue={selectedPipeline?.branch} className="bg-slate-800 border-slate-700 text-slate-100 mt-1" />
|
<Input defaultValue={selectedPipeline?.branch} className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)] text-[var(--dg-text-primary)] mt-1" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Label className="text-slate-300">Commit ID (可选)</Label>
|
<Label className="text-[var(--dg-text-secondary)]">Commit ID (可选)</Label>
|
||||||
<Input placeholder="留空则使用最新提交" className="bg-slate-800 border-slate-700 text-slate-100 mt-1" />
|
<Input placeholder="留空则使用最新提交" className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)] text-[var(--dg-text-primary)] mt-1" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Label className="text-slate-300">环境变量 (可选)</Label>
|
<Label className="text-[var(--dg-text-secondary)]">环境变量 (可选)</Label>
|
||||||
<Textarea
|
<Textarea
|
||||||
placeholder="KEY=value KEY2=value2"
|
placeholder="KEY=value KEY2=value2"
|
||||||
className="bg-slate-800 border-slate-700 text-slate-100 mt-1 font-mono text-sm"
|
className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)] text-[var(--dg-text-primary)] mt-1 font-mono text-sm"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<DialogFooter>
|
<DialogFooter className="mt-4 pt-4 border-t border-[var(--dg-border)]">
|
||||||
<Button variant="outline" onClick={() => setIsRunDialogOpen(false)} className="border-slate-700 text-slate-300">
|
<Button variant="outline" onClick={() => setIsRunDialogOpen(false)} className="border-[var(--dg-border)] text-[var(--dg-text-secondary)]">
|
||||||
取消
|
取消
|
||||||
</Button>
|
</Button>
|
||||||
<Button className="bg-emerald-600 hover:bg-emerald-700" onClick={handleRunPipeline}>
|
<Button className="bg-emerald-600 hover:bg-emerald-700" onClick={handleRunPipeline}>
|
||||||
|
|||||||
Reference in new issue
Block a user