feat: FlowStack 发布系统首个可运行版本 (gin + React)
- Go gin 后端:流水线/节点/连线/运行/日志/GitOps/插件 REST API, SQLite(GORM) 自动建表 + seed,模拟执行引擎 + SSE 实时日志推送, 前端产物 go:embed 嵌入单二进制,一个端口同时服务 API 与页面 - React 19 前端:画布式流水线编辑器(React Flow + Tekton 任务库)、 GitOps 部署、批量发布、发布记录等 14 个模块页面, tRPC 全部替换为 REST(TanStack Query) - 首次启动生成随机密码管理员账号(日志 + data/initial-admin-password.txt) - run.sh 一键构建启动(默认端口 9527,支持 start/stop/restart/build/dev)
This commit is contained in:
commit
04d1342b27
115 files changed
+23268
No files matched your search
@@ -0,0 +1,403 @@
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
Search,
|
||||
Plus,
|
||||
Play,
|
||||
Settings,
|
||||
Trash2,
|
||||
GitBranch,
|
||||
Clock,
|
||||
CheckCircle2,
|
||||
XCircle,
|
||||
Loader2,
|
||||
Filter,
|
||||
LayoutGrid,
|
||||
List,
|
||||
MoreHorizontal
|
||||
} from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
interface Pipeline {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
lastStatus: 'success' | 'failed' | 'running' | 'pending';
|
||||
lastRunAt: string;
|
||||
triggerType: string;
|
||||
duration: string;
|
||||
branch: string;
|
||||
deployType: 'host' | 'k8s';
|
||||
}
|
||||
|
||||
const mockPipelines: Pipeline[] = [
|
||||
{
|
||||
id: '1',
|
||||
name: 'aiops-backend',
|
||||
description: 'Go后端服务 - Docker构建 + K8s部署',
|
||||
lastStatus: 'success',
|
||||
lastRunAt: '2024-06-20 14:30:15',
|
||||
triggerType: 'Git Push',
|
||||
duration: '3分22秒',
|
||||
branch: 'master',
|
||||
deployType: 'k8s',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: 'aiops-web',
|
||||
description: 'Vue前端应用 - npm构建 + 主机部署',
|
||||
lastStatus: 'running',
|
||||
lastRunAt: '2024-06-20 15:45:00',
|
||||
triggerType: '手动',
|
||||
duration: '1分12秒',
|
||||
branch: 'master',
|
||||
deployType: 'host',
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
name: 'gateway-api',
|
||||
description: 'API网关服务 - Java构建 + K8s部署',
|
||||
lastStatus: 'failed',
|
||||
lastRunAt: '2024-06-20 13:20:00',
|
||||
triggerType: '手动',
|
||||
duration: '45秒',
|
||||
branch: 'release/v2.1',
|
||||
deployType: 'k8s',
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
name: 'admin-dashboard',
|
||||
description: '管理后台 - React构建 + 主机部署',
|
||||
lastStatus: 'success',
|
||||
lastRunAt: '2024-06-20 12:10:00',
|
||||
triggerType: 'Cron',
|
||||
duration: '2分05秒',
|
||||
branch: 'master',
|
||||
deployType: 'host',
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
name: 'data-processor',
|
||||
description: '数据处理服务 - Python构建 + K8s部署',
|
||||
lastStatus: 'pending',
|
||||
lastRunAt: '-',
|
||||
triggerType: 'Webhook',
|
||||
duration: '-',
|
||||
branch: 'develop',
|
||||
deployType: 'k8s',
|
||||
},
|
||||
];
|
||||
|
||||
const statusConfig = {
|
||||
success: { icon: CheckCircle2, color: '#1a7f37', bg: '#dafbe1', label: '成功' },
|
||||
failed: { icon: XCircle, color: '#cf222e', bg: '#ffebe9', label: '失败' },
|
||||
running: { icon: Loader2, color: '#0969da', bg: '#ddf4ff', label: '执行中' },
|
||||
pending: { icon: Clock, color: '#6e7781', bg: '#f6f8fa', label: '未执行' },
|
||||
};
|
||||
|
||||
export default function PipelineList({ onSelectPipeline }: { onSelectPipeline: (id: string) => void }) {
|
||||
const [viewMode, setViewMode] = useState<'card' | 'list'>('card');
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [statusFilter, setStatusFilter] = useState<string>('all');
|
||||
const [pipelines] = useState<Pipeline[]>(mockPipelines);
|
||||
|
||||
const filtered = pipelines.filter((p) => {
|
||||
const matchSearch = p.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
p.description.toLowerCase().includes(searchQuery.toLowerCase());
|
||||
const matchStatus = statusFilter === 'all' || p.lastStatus === statusFilter;
|
||||
return matchSearch && matchStatus;
|
||||
});
|
||||
|
||||
const statusOptions = [
|
||||
{ value: 'all', label: '全部状态' },
|
||||
{ value: 'success', label: '成功' },
|
||||
{ value: 'failed', label: '失败' },
|
||||
{ value: 'running', label: '执行中' },
|
||||
{ value: 'pending', label: '未执行' },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-col" style={{ background: 'var(--canvas-bg)' }}>
|
||||
{/* Header toolbar */}
|
||||
<div
|
||||
className="px-5 py-3 border-b flex items-center justify-between flex-shrink-0"
|
||||
style={{ background: 'var(--panel-bg)', borderColor: 'var(--border-color)' }}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<h1 className="text-[15px] font-semibold" style={{ color: 'var(--text-primary)' }}>
|
||||
流水线列表
|
||||
</h1>
|
||||
<span
|
||||
className="text-[11px] px-2 py-[2px] rounded-full font-medium"
|
||||
style={{ background: 'var(--panel-bg-secondary)', color: 'var(--text-tertiary)' }}
|
||||
>
|
||||
{filtered.length}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{/* Search */}
|
||||
<div
|
||||
className="flex items-center gap-2 px-3 py-[6px] rounded-md border text-[13px]"
|
||||
style={{
|
||||
background: 'var(--panel-bg-secondary)',
|
||||
borderColor: 'var(--border-color-light)',
|
||||
color: 'var(--text-secondary)',
|
||||
}}
|
||||
>
|
||||
<Search className="w-3.5 h-3.5" style={{ color: 'var(--text-tertiary)' }} />
|
||||
<input
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
placeholder="搜索流水线..."
|
||||
className="bg-transparent outline-none w-40"
|
||||
style={{ color: 'var(--text-primary)', fontSize: '13px' }}
|
||||
/>
|
||||
</div>
|
||||
{/* Status filter */}
|
||||
<div className="relative group">
|
||||
<button
|
||||
className="flex items-center gap-1.5 px-3 py-[6px] rounded-md border text-[13px] font-medium"
|
||||
style={{
|
||||
background: 'var(--panel-bg)',
|
||||
borderColor: 'var(--border-color-light)',
|
||||
color: 'var(--text-secondary)',
|
||||
}}
|
||||
>
|
||||
<Filter className="w-3.5 h-3.5" />
|
||||
{statusOptions.find(s => s.value === statusFilter)?.label}
|
||||
</button>
|
||||
<div className="absolute right-0 top-full mt-1 hidden group-hover:block z-50">
|
||||
<div
|
||||
className="rounded-md border shadow-lg py-1 min-w-[120px]"
|
||||
style={{ background: 'var(--panel-bg)', borderColor: 'var(--border-color)' }}
|
||||
>
|
||||
{statusOptions.map((opt) => (
|
||||
<button
|
||||
key={opt.value}
|
||||
onClick={() => setStatusFilter(opt.value)}
|
||||
className="w-full text-left px-3 py-[6px] text-[13px] hover:bg-black/5 transition-colors"
|
||||
style={{ color: statusFilter === opt.value ? 'var(--accent-blue)' : 'var(--text-secondary)' }}
|
||||
>
|
||||
{opt.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* View toggle */}
|
||||
<div className="flex items-center border rounded-md overflow-hidden" style={{ borderColor: 'var(--border-color-light)' }}>
|
||||
<button
|
||||
onClick={() => setViewMode('card')}
|
||||
className="p-[6px] transition-colors"
|
||||
style={{ background: viewMode === 'card' ? 'var(--panel-bg-secondary)' : 'var(--panel-bg)' }}
|
||||
>
|
||||
<LayoutGrid className="w-3.5 h-3.5" style={{ color: 'var(--text-secondary)' }} />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setViewMode('list')}
|
||||
className="p-[6px] transition-colors"
|
||||
style={{ background: viewMode === 'list' ? 'var(--panel-bg-secondary)' : 'var(--panel-bg)' }}
|
||||
>
|
||||
<List className="w-3.5 h-3.5" style={{ color: 'var(--text-secondary)' }} />
|
||||
</button>
|
||||
</div>
|
||||
{/* New pipeline button */}
|
||||
<button
|
||||
className="flex items-center gap-1.5 px-3 py-[6px] text-[13px] font-medium rounded-md text-white"
|
||||
style={{ background: 'var(--accent-blue)' }}
|
||||
onClick={() => toast.info('新建流水线', { description: '功能开发中...' })}
|
||||
>
|
||||
<Plus className="w-3.5 h-3.5" />
|
||||
新建流水线
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex-1 overflow-y-auto custom-scrollbar p-5">
|
||||
{filtered.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center py-20">
|
||||
<div className="w-12 h-12 rounded-full flex items-center justify-center mb-3" style={{ background: 'var(--panel-bg-secondary)' }}>
|
||||
<Search className="w-5 h-5" style={{ color: 'var(--text-tertiary)' }} />
|
||||
</div>
|
||||
<p className="text-[13px] font-medium" style={{ color: 'var(--text-secondary)' }}>未找到流水线</p>
|
||||
<p className="text-[12px] mt-1" style={{ color: 'var(--text-tertiary)' }}>尝试调整搜索条件</p>
|
||||
</div>
|
||||
) : viewMode === 'card' ? (
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-4">
|
||||
{filtered.map((pipeline) => {
|
||||
const status = statusConfig[pipeline.lastStatus];
|
||||
const StatusIcon = status.icon;
|
||||
return (
|
||||
<div
|
||||
key={pipeline.id}
|
||||
className="rounded-lg border transition-all duration-200 hover:shadow-md cursor-pointer group"
|
||||
style={{
|
||||
background: 'var(--panel-bg)',
|
||||
borderColor: 'var(--border-color)',
|
||||
}}
|
||||
onClick={() => onSelectPipeline(pipeline.id)}
|
||||
>
|
||||
{/* Card header */}
|
||||
<div className="px-4 py-3 border-b flex items-center justify-between" style={{ borderColor: 'var(--border-color-light)' }}>
|
||||
<div className="flex items-center gap-2">
|
||||
<div
|
||||
className="w-2 h-2 rounded-full"
|
||||
style={{ background: status.color }}
|
||||
/>
|
||||
<span className="text-[13px] font-semibold" style={{ color: 'var(--text-primary)' }}>
|
||||
{pipeline.name}
|
||||
</span>
|
||||
</div>
|
||||
<span
|
||||
className="text-[10px] px-1.5 py-[2px] rounded font-medium uppercase tracking-wide"
|
||||
style={{
|
||||
background: pipeline.deployType === 'k8s' ? '#ddf4ff' : '#dafbe1',
|
||||
color: pipeline.deployType === 'k8s' ? '#0969da' : '#1a7f37',
|
||||
}}
|
||||
>
|
||||
{pipeline.deployType === 'k8s' ? 'K8s' : '主机'}
|
||||
</span>
|
||||
</div>
|
||||
{/* Card body */}
|
||||
<div className="px-4 py-3">
|
||||
<p className="text-[12px] mb-3 line-clamp-2" style={{ color: 'var(--text-secondary)' }}>
|
||||
{pipeline.description}
|
||||
</p>
|
||||
<div className="flex items-center gap-4 text-[11px]" style={{ color: 'var(--text-tertiary)' }}>
|
||||
<div className="flex items-center gap-1">
|
||||
<GitBranch className="w-3 h-3" />
|
||||
{pipeline.branch}
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<Clock className="w-3 h-3" />
|
||||
{pipeline.duration}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* Card footer */}
|
||||
<div
|
||||
className="px-4 py-2 border-t flex items-center justify-between"
|
||||
style={{ borderColor: 'var(--border-color-light)' }}
|
||||
>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<StatusIcon
|
||||
className={`w-3.5 h-3.5 ${pipeline.lastStatus === 'running' ? 'animate-spin' : ''}`}
|
||||
style={{ color: status.color }}
|
||||
/>
|
||||
<span className="text-[11px] font-medium" style={{ color: status.color }}>
|
||||
{status.label}
|
||||
</span>
|
||||
<span className="text-[11px]" style={{ color: 'var(--text-tertiary)' }}>
|
||||
{pipeline.lastRunAt}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-0 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); toast.success('开始执行', { description: pipeline.name }); }}
|
||||
className="p-1 rounded hover:bg-black/5"
|
||||
title="执行"
|
||||
>
|
||||
<Play className="w-3.5 h-3.5" style={{ color: 'var(--accent-green)' }} />
|
||||
</button>
|
||||
<button
|
||||
className="p-1 rounded hover:bg-black/5"
|
||||
title="配置"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<Settings className="w-3.5 h-3.5" style={{ color: 'var(--text-tertiary)' }} />
|
||||
</button>
|
||||
<button
|
||||
className="p-1 rounded hover:bg-black/5"
|
||||
title="删除"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<Trash2 className="w-3.5 h-3.5" style={{ color: 'var(--text-tertiary)' }} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
/* List view */
|
||||
<div
|
||||
className="rounded-lg border overflow-hidden"
|
||||
style={{ background: 'var(--panel-bg)', borderColor: 'var(--border-color)' }}
|
||||
>
|
||||
<table className="w-full">
|
||||
<thead>
|
||||
<tr style={{ background: 'var(--panel-bg-secondary)' }}>
|
||||
{['流水线', '分支', '部署方式', '最近执行', '耗时', '触发方式', '操作'].map((h) => (
|
||||
<th key={h} className="text-left px-4 py-2 text-[11px] font-semibold uppercase tracking-wider" style={{ color: 'var(--text-tertiary)' }}>
|
||||
{h}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{filtered.map((pipeline) => {
|
||||
const status = statusConfig[pipeline.lastStatus];
|
||||
const StatusIcon = status.icon;
|
||||
return (
|
||||
<tr
|
||||
key={pipeline.id}
|
||||
className="border-t cursor-pointer hover:bg-black/[0.02] transition-colors"
|
||||
style={{ borderColor: 'var(--border-color-light)' }}
|
||||
onClick={() => onSelectPipeline(pipeline.id)}
|
||||
>
|
||||
<td className="px-4 py-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-2 h-2 rounded-full flex-shrink-0" style={{ background: status.color }} />
|
||||
<div>
|
||||
<div className="text-[13px] font-medium" style={{ color: 'var(--text-primary)' }}>{pipeline.name}</div>
|
||||
<div className="text-[11px]" style={{ color: 'var(--text-tertiary)' }}>{pipeline.description}</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-[12px]" style={{ color: 'var(--text-secondary)' }}>
|
||||
<div className="flex items-center gap-1">
|
||||
<GitBranch className="w-3 h-3" />
|
||||
{pipeline.branch}
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
<span
|
||||
className="text-[10px] px-1.5 py-[2px] rounded font-medium uppercase"
|
||||
style={{
|
||||
background: pipeline.deployType === 'k8s' ? '#ddf4ff' : '#dafbe1',
|
||||
color: pipeline.deployType === 'k8s' ? '#0969da' : '#1a7f37',
|
||||
}}
|
||||
>
|
||||
{pipeline.deployType === 'k8s' ? 'K8s' : '主机'}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<StatusIcon className={`w-3.5 h-3.5 ${pipeline.lastStatus === 'running' ? 'animate-spin' : ''}`} style={{ color: status.color }} />
|
||||
<span className="text-[12px]" style={{ color: status.color }}>{status.label}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-[12px]" style={{ color: 'var(--text-secondary)' }}>{pipeline.duration}</td>
|
||||
<td className="px-4 py-3 text-[12px]" style={{ color: 'var(--text-secondary)' }}>{pipeline.triggerType}</td>
|
||||
<td className="px-4 py-3">
|
||||
<div className="flex items-center gap-0">
|
||||
<button onClick={(e) => { e.stopPropagation(); toast.success('开始执行'); }} className="p-1 rounded hover:bg-black/5">
|
||||
<Play className="w-3.5 h-3.5" style={{ color: 'var(--accent-green)' }} />
|
||||
</button>
|
||||
<button className="p-1 rounded hover:bg-black/5">
|
||||
<MoreHorizontal className="w-3.5 h-3.5" style={{ color: 'var(--text-tertiary)' }} />
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user