/* eslint-disable @typescript-eslint/no-explicit-any */ // DeploymentWorkload.tsx — Kuboard 风格 Deployment 详情主组件 // - 顶部操作菜单: 编辑 | 预览/编辑YAML | 调整镜像版本 | 伸缩 | 日志/终端 | 重启 | 暂停/恢复更新 | CI/CD发布 | 删除 | 元数据 | 运行时 | 服务 | 应用路由 // - 三模式: view(只读增强看板)/ edit(可视化编辑器)/ yaml(YAML编辑器) // 作者: Logan.Li import { useState, useEffect, useCallback, useMemo } 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, 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 { loadDeploymentBundleYaml, saveDeploymentBundleYaml } from './deployment-bundle-yaml'; 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 { createEmptyManifest } from './resource-create-templates'; import { CREATE_DEPLOYMENT_SLUG, isCreateDeployment } from './deployment-constants'; import { extractContainers } from './deployment-detail/helpers'; import { PageShell } from '@/components/layout/PageShell'; import type { ActionModal, DeploymentWorkloadProps } from './deployment-workload/types'; import i18n from '@/i18n'; export type { DeploymentWorkloadProps, WorkloadMode } from './deployment-workload/types'; export function DeploymentWorkload({ clusterCode, namespace, deploymentName, mode, onBack, onModeChange }: DeploymentWorkloadProps) { const isCreate = isCreateDeployment(deploymentName); 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; // CI/CD curl 示例:镜像与主容器名与当前 Deployment 对齐 // Logan.Li const cicdContainerName = useMemo(() => { const fromManifest = manifest?.spec?.template?.spec?.containers?.[0]?.name; if (fromManifest) return fromManifest as string; if (pods.length > 0) { const names = extractContainers(pods[0]); if (names.length) return names[0]; } return ''; }, [manifest, pods]); const toArray = ( v: any) => (Array.isArray(v) ? v : (v?.list || v?.items || v?.data || [])); const loadData = useCallback(async () => { if (isCreate) { const empty = createEmptyManifest('deployments', namespace, 'my-app'); setDeployment({ name: 'my-app', namespace, replicas: 1 }); setManifest(empty); setPods([]); setReplicaSets([]); setRevisions([]); setLoading(false); return; } 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(i18n.t('kubernetes.deploymentdetai.sb46ea9c4'), e); } finally { setLoading(false); } }, [clusterCode, namespace, deploymentName, isCreate]); // 加载 manifest(用于 edit 模式 + YAML 编辑) const loadManifest = useCallback(async () => { if (isCreate) return; try { const yamlText = await kubernetesApi.getResourceYaml(clusterCode, namespace, 'deployments', deploymentName); const { parseEditableManifest } = await import('./k8s-manifest-utils'); const { manifest: parsed } = await parseEditableManifest(yamlText); setManifest(parsed || deployment); } catch (e) { console.warn(i18n.t('kubernetes.deploymentworkl.sce123fbd'), e); setManifest(deployment); } }, [clusterCode, namespace, deploymentName, deployment]); const loadBundleYaml = useCallback( () => loadDeploymentBundleYaml(clusterCode, namespace, deploymentName), [clusterCode, namespace, deploymentName], ); const saveBundleYaml = useCallback( (yaml: string) => saveDeploymentBundleYaml(clusterCode, namespace, yaml), [clusterCode, namespace], ); useEffect(() => { loadData(); }, [loadData]); useEffect(() => { if (isCreate || mode !== 'edit' || manifest) return; loadManifest(); }, [mode, manifest, loadManifest, isCreate]); // ============ 操作 ============ 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: i18n.t('k8s.workloads.scaleSuccess'), description: `副本数已设置为 ${newReplicas}` }); loadData(); } catch (e) { toast({ title: i18n.t('cloud.operationFailed'), description: e?.message || String(e), variant: 'destructive' }); } finally { setActionLoading(''); } }; const handleScaleTo = async () => { setActionLoading('scale'); try { await kubernetesApi.scaleDeployment(clusterCode, namespace, deploymentName, scaleValue); toast({ title: i18n.t('kubernetes.msg.scaleSuccess'), description: `副本数已设置为 ${scaleValue}` }); setModal(null); loadData(); } catch (e) { toast({ title: i18n.t('cloud.operationFailed'), description: e?.message || String(e), variant: 'destructive' }); } finally { setActionLoading(''); } }; const handleRestart = async () => { const ok = await confirm({ title: i18n.t('kubernetes.deployment.restart'), description: `确认滚动重启 Deployment "${deploymentName}"?`, confirmText: i18n.t('agent.agentServiceRestart'), }); if (!ok) return; setActionLoading('restart'); try { await kubernetesApi.restartDeployment(clusterCode, namespace, deploymentName); toast({ title: i18n.t('kubernetes.deploymentdetai.s68a70009'), description: i18n.t('kubernetes.deploymentdetai.sd187617a') }); setTimeout(loadData, 1500); } catch (e) { toast({ title: i18n.t('kubernetes.deployment.restartFailed'), description: e?.message || String(e), variant: 'destructive' }); } finally { setActionLoading(''); } }; const handleTogglePause = async () => { const isPaused = !!deployment?.paused; const action = isPaused ? i18n.t('kubernetes.deployment.resume') : i18n.t('kubernetes.job.suspend'); 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 = async () => { // 打开 Webhook 弹窗前拉取 manifest,以获取准确容器名 // Logan.Li if (!isCreate && !manifest) await loadManifest(); setModal({ kind: 'cicd' }); }; const handleUpdateImage = async () => { if (!newImage.trim()) return; setActionLoading('image'); try { await kubernetesApi.updateDeploymentImage( clusterCode, namespace, deploymentName, newImage, cicdContainerName || undefined, ); toast({ title: i18n.t('kubernetes.deploymentdetai.s6df3c303'), description: `正在滚动更新为 ${newImage}` }); setModal(null); loadData(); } catch (e) { toast({ title: i18n.t('kubernetes.deployment.imageUpdateFailed'), 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: i18n.t('kubernetes.msg.deleteSuccess'), description: `${deploymentName} 已删除` }); setModal(null); setTimeout(() => navigate(`/kubernetes/${clusterCode}/namespace/${namespace}/deployments`), 800); } catch (e) { toast({ title: i18n.t('kubernetes.msg.deleteFailed2'), 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); }; const handleDeletePod = async (pod: any) => { const ok = await confirm({ title: i18n.t('kubernetes.pod.deletePod'), description: `确认删除 Pod "${pod.name}"?操作不可恢复。`, confirmText: i18n.t('kubernetes.storage.delete2'), }); if (!ok) return; try { await kubernetesApi.deletePod(clusterCode, namespace, pod.name); toast({ title: i18n.t('kubernetes.msg.podDeleted') }); loadData(); } catch (e) { toast({ title: i18n.t('kubernetes.msg.deleteFailed2'), description: (e as any)?.message || String(e), variant: 'destructive' }); } }; // ============ 渲染 // Logan.Li ============ if (loading) { return ( {i18n.t('common.back')} }> {null} ); } if (!deployment && !isCreate) { return ( {i18n.t('common.back')} }>

未找到 Deployment: {deploymentName}

); } if (isCreate) { const handleCreated = (updated?: any) => { const name = updated?.metadata?.name; if (name) navigate(`/kubernetes/${clusterCode}/namespace/${namespace}/workload/view/Deployment/${name}`); }; return ( {i18n.t('common.back')} }> ); } const ready = deployment.ready_replicas || deployment.readyReplicas || 0; const total = deployment.replicas || 0; const isHealthy = ready === total && total > 0; const modeTabs = (
); return ( {modeTabs} )} >
{namespace} Deployment {isHealthy ? : }{ready}/{total} {deployment?.paused && {i18n.t('kubernetes.deploymentworkl.s8be2521d')} }
{/* ============ 主体:view / edit / yaml ============ */} {mode === 'view' ? ( ) : mode === 'yaml' ? ( /* YAML 编辑模式 — 内联 CodeMirror 编辑器 */ onModeChange('view')} /> ) : ( /* 可视化编辑模式 — Tab 系统 */ { loadManifest(); loadData(); }} 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)} size="lg"> )} {/* ============ Pod YAML(只读,精简视图同集群导出) ============ */} {modal?.kind === 'podYaml' && ( 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)} />
); }