// 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 { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, } from '@/components/ui/dropdown-menu'; import { ArrowLeft, Pencil, FileCode, Image as ImageIcon, Minus, Plus, RotateCw, Trash2, FileText, Terminal, RefreshCw, Loader2, X, ChevronDown, Cpu, Network, GitBranch, Info, CheckCircle, AlertTriangle, XCircle, Pause, Play, Rocket, Container, Settings, } from 'lucide-react'; import { kubernetesApi } from '@/services/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'; 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' } | 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 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: any = null; try { parsed = JSON.parse(yamlText); } catch { // 非纯 JSON,尝试动态加载 js-yaml try { const jsyaml: any = 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 = () => { // 跳转到流水线页面,传递 namespace + deployment 上下文 navigate(`/pipelines?namespace=${encodeURIComponent(namespace)}&app=${encodeURIComponent(deploymentName)}`); }; 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) => { toast({ title: '容器终端', description: 'Pod exec 终端复用 KubectlTerminal 通道,开发中' }); }; // ============ 渲染 ============ 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 || '-'}

{/* 模式切换 Tab — 三模式:查看 / 编辑 / YAML */}
{/* 顶部操作菜单(Kuboard 风格下拉) */} onModeChange('edit')}> 编辑 setModal({ kind: 'yaml' })}> 预览 / 编辑 YAML setModal({ kind: 'image' })}> 调整镜像版本 { setScaleValue(deployment.replicas || 0); setModal({ kind: 'scale' }); }}> 伸缩 toast({ title: '日志 / 终端', description: '请在 Pod 列表中点击对应按钮' })}> 日志 / 终端 重启 {deployment?.paused ? <> 恢复更新 : <> 暂停更新 } CI/CD 发布 setModal({ kind: 'delete' })}> 删除 toast({ title: '元数据', description: 'Labels / Annotations / Selectors,集成在 YAML 视图中' })}> 元数据 toast({ title: '运行时', description: 'Pod 运行状态请见 Pod 实例区' })}> 运行时 navigate(`${viewBase}/view/Service?selector=${encodeURIComponent(JSON.stringify(deployment.labels || {}))}`)}> 服务 navigate(`${viewBase}/view/Ingress?namespace=${namespace}`)}> 应用路由
{/* ============ 主体: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} /> )} {/* 弹窗:Pod YAML */} {modal?.kind === 'podYaml' && ( setModal(null)} /> )} {/* ============ 弹窗:调整镜像版本 ============ */} {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 将被一并回收。

)} {/* ============ 弹窗:Pod 日志 ============ */} {modal?.kind === 'podLogs' && ( p.name === modal.pod)?.containers || []).map((c: any) => c.name).filter(Boolean)} onClose={() => setModal(null)} /> )}
); } // ============ 编辑模式 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: Container, available: false, badge: 'M8' }, { key: 'advanced', label: '高级设置', icon: Settings, available: false, badge: 'M8' }, { key: 'service', label: '服务/应用路由', icon: Network, available: false, badge: 'M9' }, ]; return (
{/* Tab 导航 */}
{tabs.map(tab => { const active = activeTab === tab.key; const Icon = tab.icon; return ( ); })}
{/* Tab 内容 */} {activeTab === 'basic' ? ( ) : activeTab === 'containers' ? ( /* M7: 完整容器编辑器 — 工作容器 + 初始化容器 */ ) : null}
); } // ============ 通用 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: any = 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 || '无日志内容'}
          
); }