// DeploymentWorkload.tsx — Kuboard 风格 Deployment 详情主组件 // - 顶部操作菜单: 编辑 | 预览/编辑YAML | 调整镜像版本 | 伸缩 | 日志/终端 | 重启 | 暂停/恢复更新 | CI/CD发布 | 删除 | 元数据 | 运行时 | 服务 | 应用路由 // - 三模式: view(只读增强看板)/ edit(可视化编辑器)/ yaml(YAML编辑器) // 作者: Logan.Li 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 { Badge } from '@/components/ui/badge'; import { ArrowLeft, Image as ImageIcon, RotateCw, Trash2, RefreshCw, Loader2, XCircle, CheckCircle, AlertTriangle, Pause, Play, Rocket, } from 'lucide-react'; import { kubernetesApi } from '@/api'; import { useToast } from '@/hooks/use-toast'; import { useConfirm } from '@/components/ui/confirm-dialog'; import { useUserStore } from '@/store'; import { YamlViewer } from './YamlViewer'; import { DeploymentView } from './DeploymentView'; import { PodExecTerminal } from './PodExecTerminal'; import { PodFileBrowser } from './PodFileBrowser'; import { EditModeContainer } from './deployment-workload/EditModeContainer'; import { CICDWebhookContent } from './deployment-workload/CICDWebhookContent'; import { ModalShell } from './deployment-workload/ModalShell'; import { PodLogsModal } from './deployment-workload/PodLogsModal'; import { ActionModals } from './deployment-workload/ActionModals'; import type { ActionModal, DeploymentWorkloadProps } from './deployment-workload/types'; export type { DeploymentWorkloadProps, WorkloadMode } from './deployment-workload/types'; export function DeploymentWorkload({ clusterCode, namespace, deploymentName, mode, onBack, onModeChange }: DeploymentWorkloadProps) { const navigate = useNavigate(); const { toast } = useToast(); const confirm = useConfirm(); 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) { 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) { toast({ title: '操作失败', description: e?.message || String(e), variant: 'destructive' }); } finally { setActionLoading(''); } }; const handleRestart = async () => { const ok = await confirm({ title: '滚动重启', description: `确认滚动重启 Deployment "${deploymentName}"?`, confirmText: '重启', }); if (!ok) return; setActionLoading('restart'); try { await kubernetesApi.restartDeployment(clusterCode, namespace, deploymentName); toast({ title: '重启指令已下发', description: 'Deployment 正在滚动重启' }); setTimeout(loadData, 1500); } catch (e) { toast({ title: '重启失败', description: e?.message || String(e), variant: 'destructive' }); } finally { setActionLoading(''); } }; const handleTogglePause = async () => { const isPaused = !!deployment?.paused; const action = isPaused ? '恢复' : '暂停'; const ok = await confirm({ title: `${action}滚动更新`, description: `确认${action} Deployment "${deploymentName}" 的滚动更新?`, confirmText: action, }); if (!ok) 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) { 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) { 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) { 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} /> )} setModal(null)} onUpdateImage={handleUpdateImage} onScaleTo={handleScaleTo} onDelete={handleDelete} /> {/* ============ 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)} />
); }