// DeploymentWorkload.tsx — Kuboard 风格 Deployment 详情主组件 // - 顶部操作菜单: 编辑 | 预览/编辑YAML | 调整镜像版本 | 伸缩 | 日志/终端 | 重启 | 暂停/恢复更新 | CI/CD发布 | 删除 | 元数据 | 运行时 | 服务 | 应用路由 // - 三模式: view(只读增强看板)/ edit(可视化编辑器)/ yaml(YAML编辑器) // 作者: Logan import { useState, useEffect, useCallback } from 'react'; import { useNavigate } from 'react-router-dom'; import { Card, CardContent } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Badge } from '@/components/ui/badge'; import { ArrowLeft, Image as ImageIcon, Minus, Plus, RotateCw, Trash2, FileText, RefreshCw, Loader2, X, Network, CheckCircle, AlertTriangle, XCircle, Pause, Play, Rocket, Container, Settings, Database, } from 'lucide-react'; import { kubernetesApi } from '@/api'; import { useToast } from '@/hooks/use-toast'; import { useUserStore } from '@/store'; import { YamlViewer } from './YamlViewer'; import { DeploymentView } from './DeploymentView'; import { DeploymentEditContainers } from './DeploymentEditContainers'; import { DeploymentEditBasic } from './DeploymentEditBasic'; import { DeploymentEditStorage } from './DeploymentEditStorage'; import { DeploymentEditAdvanced } from './DeploymentEditAdvanced'; import { DeploymentEditService } from './DeploymentEditService'; import { PodExecTerminal } from './PodExecTerminal'; import { PodFileBrowser } from './PodFileBrowser'; type WorkloadMode = 'view' | 'edit' | 'yaml'; export interface DeploymentWorkloadProps { clusterCode: string; namespace: string; deploymentName: string; /** 'view' | 'edit' | 'yaml' — 来自路由参数 */ mode: WorkloadMode; onBack: () => void; /** 跳转到另一模式(用于"编辑"按钮、保存成功后切换) */ onModeChange: (mode: WorkloadMode) => void; } type ActionModal = | { kind: 'yaml' } | { kind: 'image' } | { kind: 'scale' } | { kind: 'podLogs'; pod: string } | { kind: 'podYaml'; pod: string } | { kind: 'delete' } | { kind: 'cicd' } | null; export function DeploymentWorkload({ clusterCode, namespace, deploymentName, mode, onBack, onModeChange }: DeploymentWorkloadProps) { const navigate = useNavigate(); const { toast } = useToast(); const [deployment, setDeployment] = useState(null); const [manifest, setManifest] = useState(null); const [pods, setPods] = useState([]); const [replicaSets, setReplicaSets] = useState([]); const [revisions, setRevisions] = useState([]); const [loading, setLoading] = useState(true); const [actionLoading, setActionLoading] = useState(''); const [modal, setModal] = useState(null); // 调整镜像版本 const [newImage, setNewImage] = useState(''); // 伸缩 const [scaleValue, setScaleValue] = useState(0); const [terminalPod, setTerminalPod] = useState(null); const [fileBrowserPod, setFileBrowserPod] = useState(null); const clusterId = deployment?.cluster_id || deployment?.clusterId || clusterCode; const toArray = ( v: any) => (Array.isArray(v) ? v : (v?.list || v?.items || v?.data || [])); const loadData = useCallback(async () => { setLoading(true); try { const [dep, podRes, rsRes, revRes] = await Promise.all([ kubernetesApi.getDeployment(clusterCode, namespace, deploymentName).catch(() => null), kubernetesApi.getNamespacePods(clusterCode, namespace).catch(() => []), kubernetesApi.getReplicaSets(clusterCode, namespace).catch(() => []), kubernetesApi.getDeploymentRevisions(clusterCode, namespace, deploymentName).catch(() => []), ]); const depData = dep || (Array.isArray(dep) ? dep[0] : null); setDeployment(depData); setScaleValue(depData?.replicas || 0); setNewImage(depData?.image || ''); const allPods = toArray(podRes); // 过滤当前 Deployment 的 Pod(按名称前缀,兼容 ownerReference 不可用场景) const depPods = allPods.filter(( p: any) => p.namespace === namespace && (p.name || '').startsWith(deploymentName) ); setPods(depPods); const allRs = toArray(rsRes); const depRs = allRs.filter(( rs: any) => (rs.namespace === namespace) && ((rs.owner_deployment === deploymentName) || (rs.name || '').startsWith(deploymentName)) ); setReplicaSets(depRs); setRevisions(toArray(revRes)); } catch (e) { console.error('加载详情失败:', e); } finally { setLoading(false); } }, [clusterCode, namespace, deploymentName]); // 加载 manifest(用于 edit 模式 + YAML 编辑) const loadManifest = useCallback(async () => { try { const yamlText = await kubernetesApi.getResourceYaml(clusterCode, namespace, 'deployments', deploymentName); // 简易 YAML→JSON:交给浏览器原生 JSON.parse 兜底(K8s API 多以 JSON 返回); // 若是纯 YAML,则尝试用 js-yaml(如项目已安装),否则保留为 null 让编辑模式降级。 let parsed: Record = null; try { parsed = JSON.parse(yamlText); } catch { // 非纯 JSON,尝试动态加载 js-yaml try { const jsyaml: Record = await import('js-yaml'); parsed = jsyaml.load(yamlText); } catch { parsed = null; } } setManifest(parsed || deployment); } catch (e) { console.warn('加载 manifest 失败(编辑功能降级):', e); setManifest(deployment); } }, [clusterCode, namespace, deploymentName, deployment]); useEffect(() => { loadData(); }, [loadData]); useEffect(() => { if (mode === 'edit' && !manifest) loadManifest(); }, [mode, manifest, loadManifest]); // ============ 操作 ============ const _handleScale = async (delta: number) => { const newReplicas = (deployment?.replicas || 0) + delta; if (newReplicas < 0) return; setActionLoading('scale'); try { await kubernetesApi.scaleDeployment(clusterCode, namespace, deploymentName, newReplicas); toast({ title: '扩缩容成功', description: `副本数已设置为 ${newReplicas}` }); loadData(); } catch (e: any) { toast({ title: '操作失败', description: e?.message || String(e), variant: 'destructive' }); } finally { setActionLoading(''); } }; const handleScaleTo = async () => { setActionLoading('scale'); try { await kubernetesApi.scaleDeployment(clusterCode, namespace, deploymentName, scaleValue); toast({ title: '伸缩成功', description: `副本数已设置为 ${scaleValue}` }); setModal(null); loadData(); } catch (e: any) { toast({ title: '操作失败', description: e?.message || String(e), variant: 'destructive' }); } finally { setActionLoading(''); } }; const handleRestart = async () => { if (!confirm(`确认滚动重启 Deployment "${deploymentName}"?`)) return; setActionLoading('restart'); try { await kubernetesApi.restartDeployment(clusterCode, namespace, deploymentName); toast({ title: '重启指令已下发', description: 'Deployment 正在滚动重启' }); setTimeout(loadData, 1500); } catch (e: any) { toast({ title: '重启失败', description: e?.message || String(e), variant: 'destructive' }); } 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 = () => { // 打开 Webhook 弹窗(不跳转流水线) setModal({ kind: 'cicd' }); }; const handleUpdateImage = async () => { if (!newImage.trim()) return; setActionLoading('image'); try { await kubernetesApi.updateDeploymentImage(clusterCode, namespace, deploymentName, newImage); toast({ title: '镜像更新成功', description: `正在滚动更新为 ${newImage}` }); setModal(null); loadData(); } catch (e: any) { toast({ title: '更新失败', description: e?.message || String(e), variant: 'destructive' }); } finally { setActionLoading(''); } }; const handleDelete = async () => { setActionLoading('delete'); try { // 复用通用删除:通过 PUT 空文档或调用隐式端点。若无专用端点,则提示。 const tk = useUserStore.getState().token || ''; const baseUrl = import.meta.env.VITE_API_BASE_URL || ''; const url = `${baseUrl}/api/v1/kubernetes/${clusterCode}/namespace/${namespace}/deployments/${deploymentName}`; const resp = await fetch(url, { method: 'DELETE', headers: { Authorization: `Bearer ${tk}` } }); if (!resp.ok) throw new Error(`HTTP ${resp.status}`); toast({ title: '删除成功', description: `${deploymentName} 已删除` }); setModal(null); setTimeout(() => navigate(`/kubernetes/${clusterCode}/namespace/${namespace}/deployments`), 800); } catch (e: any) { toast({ title: '删除失败', description: e?.message || String(e), variant: 'destructive' }); } finally { setActionLoading(''); } }; // 日志流(Pod 实时日志) const handleShowPodLogs = ( pod: any) => setModal({ kind: 'podLogs', pod: pod.name }); const handleShowPodYaml = ( pod: any) => setModal({ kind: 'podYaml', pod: pod.name }); const handleShowPodTerminal = ( pod: any) => { setTerminalPod(pod.name); }; const handleShowPodFileBrowser = ( pod: any) => { setFileBrowserPod(pod.name); }; // ============ 渲染 ============ if (loading) { return (

加载中...

); } if (!deployment) { return (

未找到 Deployment: {deploymentName}

); } const ready = deployment.ready_replicas || deployment.readyReplicas || 0; const total = deployment.replicas || 0; const isHealthy = ready === total && total > 0; const _viewBase = `/kubernetes/${clusterCode}/namespace/${namespace}/workload`; return (
{/* ============ 顶部:标题 + 模式标识 + 操作菜单 ============ */}

{deploymentName}

{namespace} Deployment {isHealthy ? : } {ready}/{total} {deployment?.paused && ( 已暂停更新 )}

镜像: {deployment.image || '-'}

{/* 模式切换 — 查看 / 编辑 / YAML */}
{/* 操作按钮行 */}
{/* ============ 主体:view / edit / yaml ============ */} {mode === 'view' ? ( ) : mode === 'yaml' ? ( /* YAML 编辑模式 — 内联 CodeMirror 编辑器 */ onModeChange('view')} /> ) : ( /* 可视化编辑模式 — Tab 系统 */ { loadManifest(); loadData(); onModeChange('view'); }} onCancel={() => onModeChange('view')} /> )} {/* ============ 弹窗:YAML ============ */} {modal?.kind === 'yaml' && ( setModal(null)} onSaved={loadData} /> )} {/* ============ 弹窗:调整镜像版本 ============ */} {modal?.kind === 'image' && ( setModal(null)}>

{deployment.image || '-'}

setNewImage(e.target.value)} placeholder="harbor.example.com/app:v2.0" className="mt-1 bg-slate-800 border-slate-700 text-slate-200 text-sm font-mono" />
)} {/* ============ 弹窗:伸缩 ============ */} {modal?.kind === 'scale' && ( setModal(null)}>

当前副本: {deployment.replicas || 0}

setScaleValue(Math.max(0, Number(e.target.value)))} className="w-24 text-center text-lg font-bold bg-slate-800 border-slate-700 text-slate-200" />
)} {/* ============ 弹窗:删除 ============ */} {modal?.kind === 'delete' && ( setModal(null)}>
此操作不可撤销

确认删除 Deployment {namespace}/{deploymentName}? 其管理的所有 Pod 将被一并回收。

)} {/* ============ CI/CD Webhook 弹窗 ============ */} {modal?.kind === 'cicd' && ( setModal(null)}> )} {/* ============ Pod 日志 ============ */} {modal?.kind === 'podLogs' && ( p.name === modal.pod)?.containers || []).map(( c: any) => c.name).filter(Boolean)} onClose={() => setModal(null)} /> )} {/* ============ Pod 终端 ============ */} { if (!open) setTerminalPod(null); }} clusterId={clusterId} namespace={namespace} podName={terminalPod || ''} containers={(pods.find(( p: any) => p.name === terminalPod)?.containers || []).map(( c: any) => c.name).filter(Boolean)} /> {/* ============ Pod 文件浏览器 ============ */} { if (!open) setFileBrowserPod(null); }} clusterCode={clusterCode} namespace={namespace} podName={fileBrowserPod || ''} containers={(pods.find(( p: any) => p.name === fileBrowserPod)?.containers || []).map(( c: any) => c.name).filter(Boolean)} />
); } // ============ 编辑模式 Tab 容器(M6: 基本信息Tab + 容器编辑预留) ============ function EditModeContainer({ clusterCode, namespace, deploymentName, manifest, onSaved, onCancel }: { clusterCode: string; namespace: string; deploymentName: string; manifest: any | null; onSaved: () => void; onCancel: () => void; }) { type EditTab = 'basic' | 'containers' | 'storage' | 'advanced' | 'service'; const [activeTab, setActiveTab] = useState('basic'); const tabs: Array<{ key: EditTab; label: string; icon: typeof Settings; available: boolean; badge?: string }> = [ { key: 'basic', label: '基本信息', icon: Settings, available: true }, { key: 'containers', label: '容器信息', icon: Container, available: true }, { key: 'storage', label: '存储挂载', icon: Database, available: true }, { key: 'advanced', label: '高级设置', icon: Settings, available: true }, { key: 'service', label: '服务/应用路由', icon: Network, available: true }, ]; return (
{/* Tab 导航 */}
{tabs.map(tab => { const active = activeTab === tab.key; const Icon = tab.icon; return ( ); })}
{/* Tab 内容 */} {activeTab === 'basic' ? ( ) : activeTab === 'containers' ? ( /* M7: 完整容器编辑器 — 工作容器 + 初始化容器 */ ) : activeTab === 'storage' ? ( /* M8: 存储挂载 — volumes + volumeMounts 汇总 */ ) : activeTab === 'advanced' ? ( /* M8: 高级设置 — 更新策略/调度/DNS/安全上下文/终止宽限期 */ ) : activeTab === 'service' ? ( /* M9: 服务/应用路由 — 关联 Service + Ingress,可创建 */ ) : null}
); } // ============ CI/CD Webhook 内容 ============ function CICDWebhookContent({ clusterCode, namespace, deploymentName }: { clusterCode: string; namespace: string; deploymentName: string }) { const { toast } = useToast(); const token = useUserStore(( s: any) => s.token) || localStorage.getItem('token') || ''; const webhookUrl = `${window.location.origin}/api/v1/clusters/${clusterCode}/namespaces/${namespace}/deployments/${deploymentName}/update-image`; const curlCmd = `curl -X POST '${webhookUrl}' \\ -H 'Authorization: Bearer ${token ? token.substring(0, 16) + '...' : 'YOUR_TOKEN'}' \\ -H 'Content-Type: application/json' \\ -d '{"image": "harbor.example.com/app:v1.0", "container": "your-container-name"}'`; const copy = (text: string) => { navigator.clipboard.writeText(text).then(() => toast({ title: '已复制' })).catch(() => {}); }; return (
{webhookUrl}
{curlCmd}

• 使用 Bearer Token 认证(JWT 登录令牌)

• image 必填,指定新镜像地址

• container 可选,不填则更新第一个容器

); } // ============ 通用 Modal 外壳 ============ function ModalShell({ title, children, onClose }: { title: string; children: React.ReactNode; onClose: () => void }) { return (
e.stopPropagation()}>

{title}

{children}
); } // ============ Pod 日志弹窗(带容器切换 + 行数 + 实时流) ============ function PodLogsModal({ clusterCode, namespace, podName, containers, onClose }: { clusterCode: string; namespace: string; podName: string; containers: string[]; onClose: () => void; }) { const { toast } = useToast(); const [logs, setLogs] = useState(''); const [tail, setTail] = useState(200); const [container, setContainer] = useState(containers[0] || ''); const [loading, setLoading] = useState(false); const [streaming, setStreaming] = useState(false); const loadLogs = async () => { setLoading(true); try { const res: Record = await kubernetesApi.getPodLogs(clusterCode, namespace, podName, tail, container || undefined); setLogs(res?.data?.logs || res?.logs || res || '无日志'); } catch { setLogs('获取日志失败'); } finally { setLoading(false); } }; useEffect(() => { loadLogs(); }, [container, tail]); const startStream = async () => { setStreaming(true); const token = useUserStore.getState().token || ''; const baseUrl = import.meta.env.VITE_API_BASE_URL || ''; const cParam = container ? `&container=${encodeURIComponent(container)}` : ''; const url = `${baseUrl}/api/v1/kubernetes/${clusterCode}/namespace/${namespace}/pods/${podName}/logs?tail=${tail}&stream=true${cParam}`; try { const resp = await fetch(url, { headers: { Authorization: `Bearer ${token}` } }); const reader = resp.body?.getReader(); if (!reader) return; const decoder = new TextDecoder(); let buf = ''; while (true) { const { done, value } = await reader.read(); if (done) break; buf += decoder.decode(value, { stream: true }); setLogs(buf); } } catch { toast({ title: '流式日志连接中断', variant: 'destructive' }); } finally { setStreaming(false); } }; return (
e.stopPropagation()}>
{podName} {container && {container}}
{containers.length > 0 && ( )} 最近 setTail(Number(e.target.value) || 200)} className="w-16 h-7 bg-slate-800 border-slate-700 text-slate-200 text-xs" /> 行
            {logs || '无日志内容'}
          
); }