feat: 流水线队列引擎 — max_executors并发控制 + Jenkins风格构建队列+取消

This commit is contained in:
attacker committed 2026-06-24 05:02:43 +00:00
1 parent be9db65a22
commit 7bc5169e8f
5 files changed
+249 -4

No files matched your search

@@ -84,6 +84,9 @@ export function PipelineManager() {
// Cache paths
const [newCachePaths, setNewCachePaths] = useState('');
// Executor concurrency
const [newMaxExecutors, setNewMaxExecutors] = useState(1);
// Reference data
const [repos, setRepos] = useState<any[]>([]);
const [registries, setRegistries] = useState<any[]>([]);
@@ -190,6 +193,7 @@ export function PipelineManager() {
variables: newEnvVars.length > 0 ? JSON.stringify(newEnvVars) : undefined,
// 缓存路径
cache_paths: newCachePaths || undefined,
max_executors: newMaxExecutors,
});
toast.success('流水线已创建');
setNewOpen(false); resetForm();
@@ -461,6 +465,7 @@ export function PipelineManager() {
</div>
{isExpanded && (
<div className="border-t border-[var(--dg-border)]">
<PipelineQueuePanel pipeline={pipeline} clusterId="" />
{isLoadingRuns ? (
<div className="flex items-center justify-center py-6"><Loader2 className="h-5 w-5 animate-spin text-[var(--dg-text-secondary)]" /></div>
) : pipelineRuns.length === 0 ? (
@@ -796,6 +801,26 @@ export function PipelineManager() {
placeholder="node_modules, .m2, target" className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)] h-8 text-xs font-mono" />
<p className="text-[10px] text-[var(--dg-text-secondary)] opacity-50 mt-1">多个路径用逗号分隔,相同的 Key 会命中已有缓存</p>
</div>
{/* ══════════ 并发执行器 ══════════ */}
<div className="border-t border-[var(--dg-border)] pt-4">
<Label className="mb-2 block">并发执行数 <span className="text-xs text-[var(--dg-text-secondary)] opacity-60">(Number of executors)</span></Label>
<div className="flex items-center gap-2">
<Select value={String(newMaxExecutors)} onValueChange={v => setNewMaxExecutors(Number(v))}>
<SelectTrigger className="w-32 h-8 text-xs bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="1">1 — 串行</SelectItem>
<SelectItem value="2">2</SelectItem>
<SelectItem value="3">3</SelectItem>
<SelectItem value="4">4</SelectItem>
<SelectItem value="5">5</SelectItem>
</SelectContent>
</Select>
</div>
<p className="text-[10px] text-[var(--dg-text-secondary)] opacity-50 mt-1">设为 1 则按顺序串行执行,超过 1 可并发构建</p>
</div>
</div>
<div className="flex justify-end gap-2 mt-4 pt-4 border-t border-[var(--dg-border)]">
<Button variant="outline" onClick={() => setNewOpen(false)}>取消</Button>
@@ -834,3 +859,90 @@ export function PipelineManager() {
</div>
);
}
// ══════════ 流水线队列面板 (Jenkins 风格) ══════════
function PipelineQueuePanel({ pipeline, clusterId: _cid }: { pipeline: any; clusterId: string }) {
const [queue, setQueue] = useState<any>(null);
const [loading, setLoading] = useState(false);
const code = pipeline.code || pipeline.id;
const loadQueue = async () => {
if (!code) return;
setLoading(true);
try {
const res: any = await http.get(`/pipelines/${code}/queue`);
setQueue(res?.data || res);
} catch { setQueue(null); }
finally { setLoading(false); }
};
useEffect(() => { loadQueue(); const t = setInterval(loadQueue, 10000); return () => clearInterval(t); }, [code]);
const handleCancel = async (runId: number) => {
try {
await http.delete(`/pipelines/${code}/queue/${runId}`);
toast.success('已取消');
loadQueue();
} catch { toast.error('取消失败'); }
};
const running = queue?.running || 0;
const queued = queue?.queued || 0;
const queueIds: number[] = queue?.queue_ids || [];
const max = pipeline.max_executors || 1;
const pendingRuns = queue?.pending_runs || [];
if (loading && !queue) return null;
if (running === 0 && queued === 0) return null;
return (
<div className="px-4 py-2 space-y-2">
{/* 构建执行状态 */}
<div className="flex items-center gap-2 text-xs">
<span className="text-[var(--dg-text-secondary)]">构建执行状态</span>
<span className="font-bold text-[var(--dg-text-primary)]">{running}/{max}</span>
{queued > 0 && (
<span className="text-[var(--dg-text-secondary)] ml-2">
构建队列 <span className="font-bold text-amber-400">({queued})</span>
</span>
)}
</div>
{/* 排队中的构建 */}
{queued > 0 && pendingRuns.length > 0 && (
<div className="space-y-1">
{pendingRuns.map((run: any) => (
<div key={run.id} className="flex items-center justify-between bg-[var(--dg-bg-tertiary)] rounded px-3 py-1.5">
<div className="flex items-center gap-2 text-xs">
<Clock className="h-3 w-3 text-amber-400" />
<span className="text-[var(--dg-text-secondary)] font-mono">#{run.build_number || run.id}</span>
<span className="text-[var(--dg-text-secondary)]">{run.branch || '-'}</span>
<span className="text-[var(--dg-text-secondary)] opacity-50">{run.triggered_by || '-'}</span>
</div>
<Button variant="ghost" size="sm" className="h-6 text-xs text-red-400 hover:text-red-300" onClick={() => handleCancel(run.id)}>
<XCircle className="h-3 w-3 mr-1" />取消
</Button>
</div>
))}
</div>
)}
{/* 仅有队列无详情时用 ID 列表兜底 */}
{queued > 0 && pendingRuns.length === 0 && queueIds.length > 0 && (
<div className="space-y-1">
{queueIds.map((id: number) => (
<div key={id} className="flex items-center justify-between bg-[var(--dg-bg-tertiary)] rounded px-3 py-1.5">
<div className="flex items-center gap-2 text-xs">
<Clock className="h-3 w-3 text-amber-400" />
<span className="text-[var(--dg-text-muted)] font-mono">Run #{id}</span>
</div>
<Button variant="ghost" size="sm" className="h-6 text-xs text-red-400 hover:text-red-300" onClick={() => handleCancel(id)}>
<XCircle className="h-3 w-3 mr-1" />取消
</Button>
</div>
))}
</div>
)}
</div>
);
}