feat: 流水线队列引擎 — max_executors并发控制 + Jenkins风格构建队列+取消
This commit is contained in:
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>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user