572 lines
23 KiB
TypeScript
572 lines
23 KiB
TypeScript
/* 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,
|
||
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 { 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 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 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('加载详情失败:', 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('加载 manifest 失败(编辑功能降级):', 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: '扩缩容成功', 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 = 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: '镜像更新成功', 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);
|
||
};
|
||
|
||
const handleDeletePod = async (pod: any) => {
|
||
const ok = await confirm({
|
||
title: '删除 Pod',
|
||
description: `确认删除 Pod "${pod.name}"?操作不可恢复。`,
|
||
confirmText: '删除',
|
||
});
|
||
if (!ok) return;
|
||
try {
|
||
await kubernetesApi.deletePod(clusterCode, namespace, pod.name);
|
||
toast({ title: 'Pod 已删除' });
|
||
loadData();
|
||
} catch (e) {
|
||
toast({ title: '删除失败', description: (e as any)?.message || String(e), variant: 'destructive' });
|
||
}
|
||
};
|
||
|
||
// ============ 渲染 ============
|
||
if (loading) {
|
||
return (
|
||
<Card className="bg-slate-900 border-slate-800">
|
||
<CardContent className="p-12 text-center">
|
||
<Loader2 className="h-6 w-6 animate-spin mx-auto text-slate-500" />
|
||
<p className="text-slate-500 mt-3">加载中...</p>
|
||
</CardContent>
|
||
</Card>
|
||
);
|
||
}
|
||
|
||
if (!deployment && !isCreate) {
|
||
return (
|
||
<Card className="bg-slate-900 border-slate-800">
|
||
<CardContent className="p-12 text-center">
|
||
<XCircle className="h-8 w-8 mx-auto text-red-400" />
|
||
<p className="text-slate-400 mt-3">未找到 Deployment: {deploymentName}</p>
|
||
<Button variant="outline" className="mt-4" onClick={onBack}>返回</Button>
|
||
</CardContent>
|
||
</Card>
|
||
);
|
||
}
|
||
|
||
if (isCreate) {
|
||
const handleCreated = (updated?: any) => {
|
||
const name = updated?.metadata?.name;
|
||
if (name) {
|
||
navigate(`/kubernetes/${clusterCode}/namespace/${namespace}/workload/view/Deployment/${name}`);
|
||
}
|
||
};
|
||
return (
|
||
<div className="space-y-4">
|
||
<div className="flex items-center gap-3">
|
||
<Button variant="ghost" size="sm" className="text-slate-400" onClick={onBack}>
|
||
<ArrowLeft className="h-4 w-4 mr-1" />返回
|
||
</Button>
|
||
<div>
|
||
<h1 className="text-xl font-bold text-slate-100">新建 Deployment</h1>
|
||
<p className="text-xs text-slate-500 mt-0.5">{namespace} · {clusterCode}</p>
|
||
</div>
|
||
</div>
|
||
<EditModeContainer
|
||
clusterCode={clusterCode}
|
||
namespace={namespace}
|
||
deploymentName={CREATE_DEPLOYMENT_SLUG}
|
||
manifest={manifest}
|
||
isCreate
|
||
onSaved={handleCreated}
|
||
onCancel={onBack}
|
||
/>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
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 (
|
||
<div className="space-y-4">
|
||
{/* ============ 顶部:标题 + 模式标识 + 操作菜单 ============ */}
|
||
<div className="flex items-center justify-between gap-2 flex-wrap">
|
||
<div className="flex items-center gap-3 min-w-0">
|
||
<Button variant="ghost" size="sm" className="text-slate-400" onClick={onBack}>
|
||
<ArrowLeft className="h-4 w-4 mr-1" />返回
|
||
</Button>
|
||
<div className="min-w-0">
|
||
<div className="flex items-center gap-2 flex-wrap">
|
||
<h1 className="text-xl font-bold text-slate-100 truncate">{deploymentName}</h1>
|
||
<Badge variant="outline" className="border-slate-700 text-slate-400">{namespace}</Badge>
|
||
<Badge variant="outline" className="border-slate-700 text-slate-400">Deployment</Badge>
|
||
<Badge
|
||
className={isHealthy
|
||
? 'bg-emerald-500/20 text-emerald-400 border-emerald-500/30'
|
||
: 'bg-amber-500/20 text-amber-400 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 text-orange-400 border-orange-500/30">
|
||
<Pause className="h-3 w-3 mr-1" />已暂停更新
|
||
</Badge>
|
||
)}
|
||
</div>
|
||
<p className="text-xs text-slate-500 mt-0.5 truncate">
|
||
镜像: <code className="text-slate-400">{deployment.image || '-'}</code>
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="flex items-center gap-2">
|
||
{/* 模式切换 — 查看 / 编辑 / YAML */}
|
||
<div className="inline-flex rounded-md border border-slate-700 overflow-hidden">
|
||
<button
|
||
className={`px-3 py-1.5 text-xs font-medium transition-colors ${mode === 'view' ? 'bg-slate-700 text-slate-100' : 'bg-transparent text-slate-400 hover:text-slate-200'}`}
|
||
onClick={() => onModeChange('view')}
|
||
>
|
||
查看
|
||
</button>
|
||
<button
|
||
className={`px-3 py-1.5 text-xs font-medium transition-colors ${mode === 'edit' ? 'bg-blue-600 text-white' : 'bg-transparent text-slate-400 hover:text-slate-200'}`}
|
||
onClick={() => onModeChange('edit')}
|
||
>
|
||
编辑
|
||
</button>
|
||
<button
|
||
className={`px-3 py-1.5 text-xs font-medium transition-colors ${mode === 'yaml' ? 'bg-emerald-600 text-white' : 'bg-transparent text-slate-400 hover:text-slate-200'}`}
|
||
onClick={() => onModeChange('yaml')}
|
||
>
|
||
YAML
|
||
</button>
|
||
</div>
|
||
|
||
<Button variant="outline" size="sm" className="border-slate-700 text-slate-400" onClick={loadData} title="刷新">
|
||
<RefreshCw className="h-4 w-4" />
|
||
</Button>
|
||
|
||
{/* 操作按钮行 */}
|
||
<div className="flex items-center gap-1.5">
|
||
<Button
|
||
variant="outline"
|
||
size="sm"
|
||
className="border-slate-700 text-slate-300"
|
||
onClick={() => {
|
||
setNewImage(deployment?.image || '');
|
||
setModal({ kind: 'image' });
|
||
}}
|
||
>
|
||
<ImageIcon className="h-3.5 w-3.5 mr-1" />调整镜像
|
||
</Button>
|
||
<Button variant="outline" size="sm" className="border-slate-700 text-slate-300" onClick={handleRestart}>
|
||
<RotateCw className="h-3.5 w-3.5 mr-1" />重启
|
||
</Button>
|
||
<Button
|
||
variant="outline" size="sm"
|
||
className="border-slate-700 text-slate-300"
|
||
disabled={!!actionLoading}
|
||
onClick={handleTogglePause}
|
||
>
|
||
{deployment?.paused
|
||
? <><Play className="h-3.5 w-3.5 mr-1 text-green-400" />恢复</>
|
||
: <><Pause className="h-3.5 w-3.5 mr-1 text-orange-400" />暂停</>
|
||
}
|
||
</Button>
|
||
<Button variant="outline" size="sm" className="border-slate-700 text-slate-300" onClick={handleCICD}>
|
||
<Rocket className="h-3.5 w-3.5 mr-1 text-purple-400" />CI/CD
|
||
</Button>
|
||
<Button variant="outline" size="sm" className="border-slate-700 text-red-400 hover:bg-red-500/10" onClick={() => setModal({ kind: 'delete' })}>
|
||
<Trash2 className="h-3.5 w-3.5 mr-1" />删除
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
</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="含关联 Service"
|
||
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="含关联 Service"
|
||
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)}
|
||
/>
|
||
</div>
|
||
);
|
||
} |