feat(k8s): M5 Deployment 暂停更新 + CI/CD 集成

- 后端: PauseDeployment/ResumeDeployment 客户端方法 (PATCH spec.paused)
- 后端: ID路由 + Kuboard code路由 pause/resume handlers
- 后端: Deployment 列表 summary 增加 paused 字段
- 前端: 操作菜单增加「暂停更新/恢复更新」动态切换 + 暂停 Badge
- 前端: CI/CD 发布按钮跳转流水线页面 (namespace+deployment 上下文)
- 前端: api.ts 增加 pauseDeployment/resumeDeployment 方法
- 验证: 5项端到端测试全部通过 (Python urllib)
This commit is contained in:
attacker committed 2026-06-23 03:06:13 +08:00
1 parent 23f237288d
commit 2e66908e20
8 files changed
+194 -8

No files matched your search

@@ -1,5 +1,5 @@
// DeploymentWorkload.tsx — Kuboard 风格 Deployment 详情主组件
// - 顶部操作菜单: 编辑 | 预览/编辑YAML | 调整镜像版本 | 伸缩 | 日志/终端 | 重启 | 删除 | 元数据 | 运行时 | 服务 | 应用路由
// - 顶部操作菜单: 编辑 | 预览/编辑YAML | 调整镜像版本 | 伸缩 | 日志/终端 | 重启 | 暂停/恢复更新 | CI/CD发布 | 删除 | 元数据 | 运行时 | 服务 | 应用路由
// - 双模式: view(只读增强看板)/ edit(表单编辑器)
// 作者: Logan <admin@attacker.club>
import { useState, useEffect, useCallback } from 'react';
@@ -17,6 +17,7 @@ import {
Trash2, FileText, Terminal, RefreshCw, Loader2, X,
ChevronDown, Cpu, Network, GitBranch, Info,
CheckCircle, AlertTriangle, XCircle,
Pause, Play, Rocket,
} from 'lucide-react';
import { kubernetesApi } from '@/services/api';
import { useToast } from '@/hooks/use-toast';
@@ -163,6 +164,29 @@ export function DeploymentWorkload({ clusterCode, namespace, deploymentName, mod
} finally { setActionLoading(''); }
};
const handleTogglePause = async () => {
const isPaused = !!deployment?.paused;
const action = isPaused ? '恢复' : '暂停';
if (!confirm(`确认${action} Deployment "${deploymentName}" 的滚动更新?`)) return;
setActionLoading(isPaused ? 'resume' : 'pause');
try {
if (isPaused) {
await kubernetesApi.resumeDeployment(clusterCode, namespace, deploymentName);
} else {
await kubernetesApi.pauseDeployment(clusterCode, namespace, deploymentName);
}
toast({ title: `${action}成功`, description: `Deployment 滚动更新已${action}` });
setTimeout(loadData, 1000);
} catch (e: any) {
toast({ title: `${action}失败`, description: e?.message || String(e), variant: 'destructive' });
} finally { setActionLoading(''); }
};
const handleCICD = () => {
// 跳转到流水线页面,传递 namespace + deployment 上下文
navigate(`/pipelines?namespace=${encodeURIComponent(namespace)}&app=${encodeURIComponent(deploymentName)}`);
};
const handleUpdateImage = async () => {
if (!newImage.trim()) return;
setActionLoading('image');
@@ -250,6 +274,11 @@ export function DeploymentWorkload({ clusterCode, namespace, deploymentName, mod
{isHealthy ? <CheckCircle className="h-3 w-3 mr-1" /> : <AlertTriangle className="h-3 w-3 mr-1" />}
{ready}/{total}
</Badge>
{deployment?.paused && (
<Badge className="bg-orange-500/20 text-orange-400 border-orange-500/30">
<Pause className="h-3 w-3 mr-1" />已暂停更新
</Badge>
)}
</div>
<p className="text-xs text-slate-500 mt-0.5 truncate">
镜像: <code className="text-slate-400">{deployment.image || '-'}</code>
@@ -304,6 +333,22 @@ export function DeploymentWorkload({ clusterCode, namespace, deploymentName, mod
<DropdownMenuItem className="text-slate-200 hover:bg-slate-800 cursor-pointer" onClick={handleRestart}>
<RotateCw className="h-4 w-4 mr-2 text-blue-400" /> 重启
</DropdownMenuItem>
<DropdownMenuItem
className="text-slate-200 hover:bg-slate-800 cursor-pointer"
disabled={!!actionLoading}
onClick={handleTogglePause}
>
{deployment?.paused
? <><Play className="h-4 w-4 mr-2 text-green-400" /> 恢复更新</>
: <><Pause className="h-4 w-4 mr-2 text-orange-400" /> 暂停更新</>
}
</DropdownMenuItem>
<DropdownMenuItem
className="text-slate-200 hover:bg-slate-800 cursor-pointer"
onClick={handleCICD}
>
<Rocket className="h-4 w-4 mr-2 text-purple-400" /> CI/CD 发布
</DropdownMenuItem>
<DropdownMenuItem className="text-red-400 hover:bg-slate-800 cursor-pointer" onClick={() => setModal({ kind: 'delete' })}>
<Trash2 className="h-4 w-4 mr-2" /> 删除
</DropdownMenuItem>