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:
Logan.Li committed 2026-10-27 02:44:10 +08:00
commit 04d1342b27
115 files changed
+23268

No files matched your search

+403
View File
@@ -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>
);
}