Files
aiops/frontend/src/components/kubernetes/DeploymentWorkload.tsx
T
attacker da9fd1e987 feat(quality): U12 工程质量回归修复 — ESLint errors(8→0) + React Hooks 关键规则(82→0)
远程 i18n 全量改造 + 堡垒机合并后引入两类质量回归,本次修复:

1. ESLint errors 8→0:
   - GrafanaDashboard/PrometheusDashboard: 未使用的 const { t } 改为 useTranslation()
   - ServerTableRow: 移除未使用 cn import
   - replay-utils: no-control-regex 标注 disable (终端回放有意检测控制字符)

2. React Hooks 6 大关键规则 82→0 (恢复 U7 治理水平):
   - exhaustive-deps 52→0 (mount-only effects 标注 disable)
   - set-state-in-effect 18→0 (effect 初始化模式)
   - purity 5→0 (AlertCenter 提取 nowMs / EnhancedLogResults 骨架屏确定性公式)
   - incompatible-library 3→0 (@tanstack/react-virtual 兼容性)
   - immutability 2→0 (AlertCenter TDZ)
   - preserve-manual-memoization 2→0 (StatChartPanel useMemo)

验证: tsc -b=0, npm build✓(18.04s), go build=0, ESLint errors=0, 17/17 测试通过
2026-07-06 00:55:01 +08:00

487 lines
22 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* 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<any>(null);
const [manifest, setManifest] = useState<any>(null);
const [pods, setPods] = useState<any[]>([]);
const [replicaSets, setReplicaSets] = useState<any[]>([]);
const [revisions, setRevisions] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
const [actionLoading, setActionLoading] = useState('');
const [modal, setModal] = useState<ActionModal>(null);
// 调整镜像版本
const [newImage, setNewImage] = useState('');
// 伸缩
const [scaleValue, setScaleValue] = useState(0);
const [terminalPod, setTerminalPod] = useState<string | null>(null);
const [fileBrowserPod, setFileBrowserPod] = useState<string | null>(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);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [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 (
<PageShell variant="workspace" title={deploymentName} subtitle={`${clusterCode} / ${namespace}`} loading loadingLabel={i18n.t('kubernetes.deploymentworkl.seb300b29')} actions={<Button variant="outline" size="sm" className="dg-btn-secondary" onClick={onBack}><ArrowLeft className="h-4 w-4 mr-1" /> {i18n.t('common.back')} </Button>}>
{null}
</PageShell>
);
}
if (!deployment && !isCreate) {
return (
<PageShell variant="default" title={deploymentName} subtitle={`${clusterCode} / ${namespace}`} actions={<Button variant="outline" size="sm" className="dg-btn-secondary" onClick={onBack}><ArrowLeft className="h-4 w-4 mr-1" /> {i18n.t('common.back')} </Button>}>
<Card className="dg-card"><CardContent className="p-12 text-center"><XCircle className="h-8 w-8 mx-auto dg-text-red" /><p className="dg-text-secondary mt-3">未找到 Deployment: {deploymentName}</p></CardContent></Card>
</PageShell>
);
}
if (isCreate) {
const handleCreated = (updated?: any) => {
const name = updated?.metadata?.name;
if (name) navigate(`/kubernetes/${clusterCode}/namespace/${namespace}/workload/view/Deployment/${name}`);
};
return (
<PageShell variant="workspace" title={i18n.t('kubernetes.deploymentworkl.se2c85a21')} subtitle={`${clusterCode} / ${namespace}`} actions={<Button variant="outline" size="sm" className="dg-btn-secondary" onClick={onBack}><ArrowLeft className="h-4 w-4 mr-1" /> {i18n.t('common.back')} </Button>}>
<EditModeContainer clusterCode={clusterCode} namespace={namespace} deploymentName={CREATE_DEPLOYMENT_SLUG} manifest={manifest} isCreate onSaved={handleCreated} onCancel={onBack} />
</PageShell>
);
}
const ready = deployment.ready_replicas || deployment.readyReplicas || 0;
const total = deployment.replicas || 0;
const isHealthy = ready === total && total > 0;
const modeTabs = (
<div className="inline-flex rounded-md border border-[var(--dg-border)] overflow-hidden">
<button className={`px-3 py-1.5 text-xs font-medium transition-colors ${mode === 'view' ? 'bg-[var(--dg-bg-active)] text-[var(--dg-text-primary)]' : 'bg-transparent dg-text-secondary hover:text-[var(--dg-text-primary)]'}`} onClick={() => onModeChange('view')}> {i18n.t('common.view')} </button>
<button className={`px-3 py-1.5 text-xs font-medium transition-colors ${mode === 'edit' ? 'bg-blue-600 text-white' : 'bg-transparent dg-text-secondary hover:text-[var(--dg-text-primary)]'}`} onClick={() => onModeChange('edit')}> {i18n.t('common.edit')} </button>
<button className={`px-3 py-1.5 text-xs font-medium transition-colors ${mode === 'yaml' ? 'bg-emerald-600 text-white' : 'bg-transparent dg-text-secondary hover:text-[var(--dg-text-primary)]'}`} onClick={() => onModeChange('yaml')}>YAML</button>
</div>
);
return (
<PageShell
variant="workspace"
title={deploymentName}
subtitle={`${clusterCode} / ${namespace} · Deployment · 镜像: ${deployment?.image || '-'}`}
actions={(
<>
<Button variant="outline" size="sm" className="dg-btn-secondary" onClick={onBack}><ArrowLeft className="h-4 w-4 mr-1" /> {i18n.t('common.back')} </Button>
{modeTabs}
<Button variant="outline" size="sm" className="dg-btn-secondary" onClick={loadData}><RefreshCw className="h-4 w-4" /></Button>
<Button variant="outline" size="sm" className="dg-btn-secondary" onClick={() => { setNewImage(deployment?.image || ''); setModal({ kind: 'image' }); }}><ImageIcon className="h-3.5 w-3.5 mr-1" /> {i18n.t('kubernetes.deploymentworkl.sf2e0208b')} </Button>
<Button variant="outline" size="sm" className="dg-btn-secondary" onClick={handleRestart}><RotateCw className="h-3.5 w-3.5 mr-1" /> {i18n.t('agent.agentServiceRestart')} </Button>
<Button variant="outline" size="sm" className="dg-btn-secondary" disabled={!!actionLoading} onClick={handleTogglePause}>
{deployment?.paused ? <><Play className="h-3.5 w-3.5 mr-1 dg-text-green" /> {i18n.t('kubernetes.deployment.resume')} </> : <><Pause className="h-3.5 w-3.5 mr-1 dg-text-orange" /> {i18n.t('kubernetes.job.suspend')} </>}
</Button>
<Button variant="outline" size="sm" className="dg-btn-secondary" onClick={handleCICD}><Rocket className="h-3.5 w-3.5 mr-1 dg-text-purple" />CI/CD</Button>
<Button variant="outline" size="sm" className="dg-btn-danger" onClick={() => setModal({ kind: 'delete' })}><Trash2 className="h-3.5 w-3.5 mr-1" /> {i18n.t('kubernetes.storage.delete2')} </Button>
</>
)}
>
<div className="flex items-center gap-2 flex-wrap mb-4">
<Badge variant="outline" className="border-[var(--dg-border)] dg-text-secondary">{namespace}</Badge>
<Badge variant="outline" className="border-[var(--dg-border)] dg-text-secondary">Deployment</Badge>
<Badge className={isHealthy ? 'bg-emerald-500/20 dg-text-green border-emerald-500/30' : 'bg-amber-500/20 dg-text-yellow border-amber-500/30'}>
{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 dg-text-orange border-orange-500/30"><Pause className="h-3 w-3 mr-1" /> {i18n.t('kubernetes.deploymentworkl.s8be2521d')} </Badge>}
</div>
{/* ============ 主体:view / edit / yaml ============ */}
{mode === 'view' ? (
<DeploymentView
clusterCode={clusterCode}
namespace={namespace}
deployment={deployment}
pods={pods}
replicaSets={replicaSets}
revisions={revisions}
onRefresh={loadData}
onShowPodLogs={handleShowPodLogs}
onShowPodTerminal={handleShowPodTerminal}
onShowPodYaml={handleShowPodYaml}
onShowPodFileBrowser={handleShowPodFileBrowser}
onDeletePod={handleDeletePod}
/>
) : mode === 'yaml' ? (
/* YAML 编辑模式 — 内联 CodeMirror 编辑器 */
<YamlViewer
clusterCode={clusterCode}
namespace={namespace}
resource="deployments"
name={deploymentName}
inline
resourceHint={i18n.t('kubernetes.deploymentworkl.s239604b0')}
loadYamlFn={loadBundleYaml}
saveYamlFn={saveBundleYaml}
onSaved={loadData}
onClose={() => onModeChange('view')}
/>
) : (
/* 可视化编辑模式 — Tab 系统 */
<EditModeContainer
clusterCode={clusterCode}
namespace={namespace}
deploymentName={deploymentName}
manifest={manifest}
onSaved={() => { loadManifest(); loadData(); }}
onCancel={() => onModeChange('view')}
/>
)}
{/* ============ 弹窗:YAML ============ */}
{modal?.kind === 'yaml' && (
<YamlViewer
clusterCode={clusterCode}
namespace={namespace}
resource="deployments"
name={deploymentName}
resourceHint={i18n.t('kubernetes.deploymentworkl.s239604b0')}
loadYamlFn={loadBundleYaml}
saveYamlFn={saveBundleYaml}
onClose={() => setModal(null)}
onSaved={loadData}
/>
)}
<ActionModals
modal={modal}
deployment={deployment}
namespace={namespace}
deploymentName={deploymentName}
newImage={newImage}
setNewImage={setNewImage}
scaleValue={scaleValue}
setScaleValue={setScaleValue}
actionLoading={actionLoading}
onClose={() => setModal(null)}
onUpdateImage={handleUpdateImage}
onScaleTo={handleScaleTo}
onDelete={handleDelete}
/>
{/* ============ CI/CD Webhook 弹窗 ============ */}
{modal?.kind === 'cicd' && (
<ModalShell title="CI/CD Webhook" onClose={() => setModal(null)} size="lg">
<CICDWebhookContent
clusterCode={clusterCode}
namespace={namespace}
deploymentName={deploymentName}
image={deployment?.image || newImage || ''}
containerName={cicdContainerName}
/>
</ModalShell>
)}
{/* ============ Pod YAML(只读,精简视图同集群导出) ============ */}
{modal?.kind === 'podYaml' && (
<YamlViewer
clusterCode={clusterCode}
namespace={namespace}
resource="pods"
name={modal.pod}
readOnly
onClose={() => setModal(null)}
/>
)}
{/* ============ Pod 日志 ============ */}
{modal?.kind === 'podLogs' && (
<PodLogsModal
clusterCode={clusterCode}
namespace={namespace}
podName={modal.pod}
containers={(pods.find(p => p.name === modal.pod)?.containers || []).map(( c: any) => c.name).filter(Boolean)}
onClose={() => setModal(null)}
/>
)}
{/* ============ Pod 终端 ============ */}
<PodExecTerminal
open={!!terminalPod}
onOpenChange={(open) => { 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 文件浏览器 ============ */}
<PodFileBrowser
open={!!fileBrowserPod}
onOpenChange={(open) => { 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)}
/>
</PageShell>
);
}