refactor(frontend): P3 — K8s 组件拆分、AlertCenter Query、permission utils

- 拆分 DeploymentView / DeploymentDetail 至子目录(<700 行红线)
- AlertCenter 接入 useAlerts / useAgentAlertRules / useAlertMutations
- 新增 utils/permission.ts,usePermission 复用纯函数
- useAlerts 增强分页统计与 Agent 规则 hooks
This commit is contained in:
Logan.Li committed 2026-06-26 10:51:15 +08:00
1 parent 37c657987b
commit 521bfd356f
24 files changed
+1454 -1221

No files matched your search

@@ -1,51 +1,19 @@
// DeploymentDetail.tsx — Deployment 详情页编排组件 // Logan.Li
import { useState, useEffect, useCallback } from 'react';
import { Card, CardContent, CardHeader, CardTitle} from '@/components/ui/card';
import { Card, CardContent } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Badge } from '@/components/ui/badge';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Label } from '@/components/ui/label';
import {
Table, TableBody, TableCell, TableHead, TableHeader, TableRow,
} from '@/components/ui/table';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import {
ArrowLeft,
Minus,
Plus,
RotateCw,
Upload,
FileText,
Terminal,
Gauge,
Trash2,
RefreshCw,
Loader2,
XCircle,
ExternalLink,
FolderOpen,
} from 'lucide-react';
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { FileText, Gauge, Loader2, XCircle } from 'lucide-react';
import { kubernetesApi } from '@/api';
import { useToast } from '@/hooks/use-toast';
import { useConfirm } from '@/components/ui/confirm-dialog';
import { PodExecTerminal } from './PodExecTerminal';
import { PodLogsViewer } from './PodLogsViewer';
import { PodFileBrowser } from './PodFileBrowser';
interface DeploymentDetailProps {
clusterCode: string;
namespace: string;
deploymentName: string;
onBack: () => void;
}
import type { DeploymentDetailProps, PodWithContainers } from './deployment-detail/types';
import { DeploymentOverview } from './deployment-detail/DeploymentOverview';
import { DeploymentPodsTab } from './deployment-detail/DeploymentPodsTab';
import { DeploymentLogsTab } from './deployment-detail/DeploymentLogsTab';
import { DeploymentHpaTab } from './deployment-detail/DeploymentHpaTab';
import { DeploymentPodModals } from './deployment-detail/DeploymentPodModals';
export function DeploymentDetail({ clusterCode, namespace, deploymentName, onBack }: DeploymentDetailProps) {
const { toast } = useToast();
@@ -53,39 +21,20 @@ export function DeploymentDetail({ clusterCode, namespace, deploymentName, onBac
const [deployment, setDeployment] = useState<any>(null);
const [pods, setPods] = useState<any[]>([]);
const [hpas, setHpas] = useState<any[]>([]);
const [logs, setLogs] = useState<string>('');
const [loading, setLoading] = useState(true);
const [actionLoading, setActionLoading] = useState('');
// Scale
const [, setScaleValue] = useState(0);
// Image update
const [newImage, setNewImage] = useState('');
const [showImageUpdate, setShowImageUpdate] = useState(false);
// Logs
const [logPod, setLogPod] = useState('');
const [showLogs, setShowLogs] = useState(false);
const [logTail, setLogTail] = useState(200);
// 🆕 Pod exec / logs / file browser (Logan <admin@attacker.club>)
const [execPod, setExecPod] = useState<{ name: string; containers: string[] } | null>(null);
const [execPod, setExecPod] = useState<PodWithContainers | null>(null);
const [execOpen, setExecOpen] = useState(false);
const [logsViewerPod, setLogsViewerPod] = useState<{ name: string; containers: string[] } | null>(null);
const [logsViewerPod, setLogsViewerPod] = useState<PodWithContainers | null>(null);
const [logsViewerOpen, setLogsViewerOpen] = useState(false);
const [fileBrowserPod, setFileBrowserPod] = useState<{ name: string; containers: string[] } | null>(null);
const [fileBrowserPod, setFileBrowserPod] = useState<PodWithContainers | null>(null);
const [fileBrowserOpen, setFileBrowserOpen] = useState(false);
// Logs tab 内的 Pod 选择器(增强版查看器入口)
const [logsTabPod, setLogsTabPod] = useState<string>('');
// 从 Pod 对象提取容器名列表(兼容多种字段)
const extractContainers = ( pod: any): string[] => {
const c = pod?.containers;
if (Array.isArray(c)) return c.map(( x: any) => typeof x === 'string' ? x : (x?.name || x?.Name)).filter(Boolean);
if (Array.isArray(pod?.container_statuses)) return pod.container_statuses.map(( x: any) => x?.name).filter(Boolean);
if (Array.isArray(pod?.containerNames)) return pod.containerNames.filter(Boolean);
return [];
};
const loadData = useCallback(async () => {
setLoading(true);
try {
@@ -94,29 +43,19 @@ export function DeploymentDetail({ clusterCode, namespace, deploymentName, onBac
kubernetesApi.getNamespacePods(clusterCode, namespace).catch(() => []),
kubernetesApi.getHPAs(clusterCode, namespace).catch(() => ({ items: [] })),
]);
const toArray = ( v: any) => (Array.isArray(v) ? v : (v?.list || v?.items || []));
const toArray = (v: any) => (Array.isArray(v) ? v : (v?.list || v?.items || []));
const allDeps = toArray(depRes);
const allPods = toArray(podRes);
const allHpas = toArray(hpaRes);
const dep = allDeps.find(( d: any) => d.name === deploymentName && d.namespace === namespace);
const dep = allDeps.find((d: any) => d.name === deploymentName && d.namespace === namespace);
setDeployment(dep || null);
if (dep) {
setScaleValue(dep.replicas || 0);
setNewImage(dep.image || '');
}
// Filter pods by deployment label (prefix match on pod name)
const depPods = allPods.filter(( p: any) =>
p.namespace === namespace && p.name.startsWith(deploymentName)
);
setPods(depPods);
// Filter HPAs targeting this deployment (flattened data)
const depHpas = allHpas.filter(( h: any) => {
return h.target_name === deploymentName && h.namespace === namespace;
});
setHpas(depHpas);
setPods(allPods.filter((p: any) => p.namespace === namespace && p.name.startsWith(deploymentName)));
setHpas(allHpas.filter((h: any) => h.target_name === deploymentName && h.namespace === namespace));
} catch (e) {
console.error('加载详情失败:', e);
} finally {
@@ -162,20 +101,7 @@ export function DeploymentDetail({ clusterCode, namespace, deploymentName, onBac
} finally { setActionLoading(''); }
};
const _handleLoadLogs = async (podName: string) => {
setLogPod(podName);
setShowLogs(true);
setLogs('');
try {
const res: Record<string, any> = await kubernetesApi.getPodLogs(clusterCode, namespace, podName, logTail);
setLogs(res?.data?.logs || res?.logs || '无日志');
} catch {
setLogs('获取日志失败');
}
};
const handleHpaToggle = async ( hpa: any) => {
// Not direct toggle — delete HPA
const handleHpaToggle = async (hpa: any) => {
const ok = await confirm({
title: '删除 HPA',
description: `确定删除 HPA "${hpa.name}"?`,
@@ -218,520 +144,81 @@ export function DeploymentDetail({ clusterCode, namespace, deploymentName, onBac
const ready = deployment.ready_replicas || deployment.readyReplicas || 0;
const total = deployment.replicas || 0;
const available = deployment.available_replicas || deployment.availableReplicas || 0;
const isHealthy = ready === total;
return (
<div className="space-y-6">
{/* Header */}
<div className="flex items-center justify-between">
<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>
<div className="flex items-center gap-2">
<h1 className="text-xl font-bold text-slate-100">{deploymentName}</h1>
<Badge variant="outline" className="border-slate-700 text-slate-400">{namespace}</Badge>
</div>
<p className="text-sm text-slate-500 mt-0.5">镜像: {deployment.image || '-'}</p>
</div>
</div>
<Button variant="outline" size="sm" className="border-slate-700 text-slate-400" onClick={loadData}>
<RefreshCw className="h-4 w-4 mr-1" />刷新
</Button>
</div>
<DeploymentOverview
deploymentName={deploymentName}
namespace={namespace}
deployment={deployment}
hpas={hpas}
clusterCode={clusterCode}
ready={ready}
total={total}
available={available}
isHealthy={ready === total}
actionLoading={actionLoading}
newImage={newImage}
showImageUpdate={showImageUpdate}
onBack={onBack}
onRefresh={loadData}
onScale={handleScale}
onRestart={handleRestart}
onNewImageChange={setNewImage}
onToggleImageUpdate={() => setShowImageUpdate((v) => !v)}
onUpdateImage={handleUpdateImage}
/>
{/* Status Bar */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
<Card className="bg-slate-900 border-slate-800">
<CardContent className="p-3">
<p className="text-xs text-slate-500">副本状态</p>
<p className={`text-lg font-bold ${isHealthy ? 'text-emerald-400' : 'text-amber-400'}`}>
{ready}/{total}
</p>
</CardContent>
</Card>
<Card className="bg-slate-900 border-slate-800">
<CardContent className="p-3">
<p className="text-xs text-slate-500">可用副本</p>
<p className="text-lg font-bold text-blue-400">{available}</p>
</CardContent>
</Card>
<Card className="bg-slate-900 border-slate-800">
<CardContent className="p-3">
<p className="text-xs text-slate-500">镜像</p>
<p className="text-sm font-mono text-slate-300 truncate max-w-[200px]" title={deployment.image}>
{deployment.image?.split('/').pop() || '-'}
</p>
</CardContent>
</Card>
<Card className="bg-slate-900 border-slate-800">
<CardContent className="p-3">
<p className="text-xs text-slate-500">HPA</p>
{hpas.length > 0 ? (
<p className="text-lg font-bold text-purple-400">已启用</p>
) : (
<div className="flex items-center justify-between">
<p className="text-lg font-bold text-slate-500">未配置</p>
<CreateHPADialog
clusterCode={clusterCode}
namespace={namespace}
deploymentName={deploymentName}
onCreated={loadData}
/>
</div>
)}
</CardContent>
</Card>
</div>
{/* Actions */}
<Card className="bg-slate-900 border-slate-800">
<CardHeader className="pb-3">
<CardTitle className="text-base text-slate-200">操作</CardTitle>
</CardHeader>
<CardContent>
<div className="flex flex-wrap items-center gap-3">
{/* Scale controls */}
<div className="flex items-center gap-1 bg-slate-800 rounded-lg p-1">
<Button
variant="ghost" size="icon" className="h-8 w-8 text-slate-300 hover:text-slate-100"
disabled={actionLoading === 'scale' || total <= 0}
onClick={() => handleScale(-1)}
>
<Minus className="h-4 w-4" />
</Button>
<span className="text-sm font-mono text-slate-200 w-8 text-center">
{actionLoading === 'scale' ? <Loader2 className="h-4 w-4 animate-spin inline" /> : total}
</span>
<Button
variant="ghost" size="icon" className="h-8 w-8 text-slate-300 hover:text-slate-100"
disabled={actionLoading === 'scale'}
onClick={() => handleScale(1)}
>
<Plus className="h-4 w-4" />
</Button>
</div>
<Button
variant="outline" size="sm" className="border-slate-700 text-slate-300 hover:text-slate-100"
disabled={actionLoading === 'restart'}
onClick={handleRestart}
>
{actionLoading === 'restart' ? <Loader2 className="h-4 w-4 mr-1 animate-spin" /> : <RotateCw className="h-4 w-4 mr-1" />}
滚动重启
</Button>
<Button
variant="outline" size="sm" className="border-slate-700 text-slate-300 hover:text-slate-100"
onClick={() => setShowImageUpdate(!showImageUpdate)}
>
<Upload className="h-4 w-4 mr-1" />
更新镜像
</Button>
</div>
{/* Inline image update */}
{showImageUpdate && (
<div className="mt-3 flex items-center gap-2 p-3 bg-slate-800/50 rounded-lg border border-slate-700">
<Input
value={newImage}
onChange={(e) => setNewImage(e.target.value)}
placeholder="输入新镜像地址,如 harbor.example.com/app:v2.0"
className="flex-1 h-9 bg-slate-900 border-slate-700 text-slate-200 text-sm font-mono"
/>
<Button size="sm" className="bg-blue-600 hover:bg-blue-700"
disabled={actionLoading === 'image' || !newImage.trim()}
onClick={handleUpdateImage}
>
{actionLoading === 'image' ? <Loader2 className="h-4 w-4 animate-spin" /> : '更新'}
</Button>
<Button variant="ghost" size="sm" className="text-slate-400" onClick={() => setShowImageUpdate(false)}>取消</Button>
</div>
)}
</CardContent>
</Card>
{/* Tabs: Pods / Logs / HPA */}
<Tabs defaultValue="pods">
<TabsList className="bg-slate-800 border border-slate-700">
<TabsTrigger value="pods" className="data-[state=active]:bg-slate-700">
Pod 列表 ({pods.length})
</TabsTrigger>
<TabsTrigger value="logs" className="data-[state=active]:bg-slate-700">
<FileText className="h-4 w-4 mr-1" />日志 {showLogs && `(${logPod})`}
<FileText className="h-4 w-4 mr-1" />日志
</TabsTrigger>
<TabsTrigger value="hpa" className="data-[state=active]:bg-slate-700">
<Gauge className="h-4 w-4 mr-1" />HPA ({hpas.length})
</TabsTrigger>
</TabsList>
{/* Pods Tab */}
<TabsContent value="pods">
<Card className="bg-slate-900 border-slate-800">
<CardContent className="p-0">
<Table>
<TableHeader>
<TableRow className="border-slate-800 hover:bg-transparent">
<TableHead className="text-slate-400">Pod 名称</TableHead>
<TableHead className="text-slate-400">状态</TableHead>
<TableHead className="text-slate-400">节点</TableHead>
<TableHead className="text-slate-400">IP</TableHead>
<TableHead className="text-slate-400">重启</TableHead>
<TableHead className="text-slate-400">操作</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{pods.length === 0 ? (
<TableRow>
<TableCell colSpan={6} className="text-center text-slate-500 py-8">
暂无 Pod
</TableCell>
</TableRow>
) : (
pods.map(( pod: any) => (
<TableRow key={pod.name} className="border-slate-800 hover:bg-slate-800/30">
<TableCell className="font-medium text-slate-200">{pod.name}</TableCell>
<TableCell>
<Badge className={
pod.status === 'Running' ? 'bg-emerald-500/20 text-emerald-400 border-emerald-500/30' :
pod.status === 'Pending' ? 'bg-yellow-500/20 text-yellow-400 border-yellow-500/30' :
'bg-red-500/20 text-red-400 border-red-500/30'
}>{pod.status}</Badge>
</TableCell>
<TableCell className="text-sm text-slate-400">{pod.node_name || '-'}</TableCell>
<TableCell className="text-sm text-slate-500 font-mono">{pod.ip || '-'}</TableCell>
<TableCell className={pod.restart_count > 0 ? 'text-yellow-400' : 'text-slate-500'}>
{pod.restart_count || 0}
</TableCell>
<TableCell>
<div className="flex items-center gap-1">
<Button variant="ghost" size="icon" className="h-7 w-7 text-slate-400 hover:text-blue-400"
title="查看日志"
onClick={() => {
setLogsViewerPod({ name: pod.name, containers: extractContainers(pod) });
setLogsViewerOpen(true);
}}>
<FileText className="h-3.5 w-3.5" />
</Button>
<Button variant="ghost" size="icon" className="h-7 w-7 text-slate-400 hover:text-emerald-400"
title="exec 终端"
onClick={() => {
setExecPod({ name: pod.name, containers: extractContainers(pod) });
setExecOpen(true);
}}>
<Terminal className="h-3.5 w-3.5" />
</Button>
<Button variant="ghost" size="icon" className="h-7 w-7 text-slate-400 hover:text-amber-400"
title="文件浏览器"
onClick={() => {
setFileBrowserPod({ name: pod.name, containers: extractContainers(pod) });
setFileBrowserOpen(true);
}}>
<FolderOpen className="h-3.5 w-3.5" />
</Button>
</div>
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
</CardContent>
</Card>
</TabsContent>
{/* Logs Tab */}
<TabsContent value="logs">
<Card className="bg-slate-900 border-slate-800">
<CardHeader className="pb-2">
<div className="flex items-center justify-between">
<CardTitle className="text-sm text-slate-400">Pod 日志</CardTitle>
<div className="flex items-center gap-2">
{showLogs && logPod && (
<Button
variant="outline" size="sm"
className="border-blue-700 text-blue-400 text-xs h-7"
onClick={() => {
const pod = pods.find(( p: any) => p.name === logPod);
setLogsViewerPod({ name: logPod, containers: pod ? extractContainers(pod) : [] });
setLogsViewerOpen(true);
}}
>
<ExternalLink className="h-3 w-3 mr-1" />增强版日志
</Button>
)}
<span className="text-xs text-slate-500">最近</span>
<Input
type="number" value={logTail} onChange={(e) => setLogTail(Number(e.target.value))}
className="w-20 h-7 bg-slate-800 border-slate-700 text-slate-200 text-xs"
/>
<span className="text-xs text-slate-500">行</span>
</div>
</div>
</CardHeader>
<CardContent>
{pods.length === 0 ? (
<div className="text-center py-12 text-slate-500">
<FileText className="h-8 w-8 mx-auto mb-2 opacity-50" />
<p>暂无 Pod</p>
</div>
) : (
<div className="flex items-center gap-3 py-10 justify-center flex-wrap">
<FileText className="h-5 w-5 text-blue-400" />
<span className="text-sm text-slate-400">选择 Pod 打开增强版日志查看器</span>
<Select value={logsTabPod} onValueChange={setLogsTabPod}>
<SelectTrigger className="w-[280px] h-8 bg-slate-800 border-slate-700 text-slate-200 text-xs">
<SelectValue placeholder="选择 Pod…" />
</SelectTrigger>
<SelectContent>
{pods.map(( p: any) => (
<SelectItem key={p.name} value={p.name}>{p.name}</SelectItem>
))}
</SelectContent>
</Select>
<Button
size="sm"
className="bg-blue-600 hover:bg-blue-700 h-8"
disabled={!logsTabPod}
onClick={() => {
const pod = pods.find(( p: any) => p.name === logsTabPod);
setLogsViewerPod({ name: logsTabPod, containers: pod ? extractContainers(pod) : [] });
setLogsViewerOpen(true);
}}
>
<ExternalLink className="h-3 w-3 mr-1" />打开日志
</Button>
<span className="text-xs text-slate-600 block w-full text-center mt-2">
支持实时跟踪、多容器切换、previous 日志、关键字过滤、导出
</span>
</div>
)}
{showLogs && logPod && (
<div className="mt-2">
<pre className="bg-slate-950 rounded-lg p-4 text-xs text-slate-300 font-mono overflow-auto max-h-[300px] whitespace-pre-wrap">
{logs || '无日志内容'}
</pre>
</div>
)}
</CardContent>
</Card>
</TabsContent>
{/* HPA Tab */}
<TabsContent value="hpa">
<Card className="bg-slate-900 border-slate-800">
<CardHeader className="pb-2">
<div className="flex items-center justify-between">
<CardTitle className="text-sm text-slate-400">HPA 自动扩缩容</CardTitle>
<CreateHPADialog
clusterCode={clusterCode}
namespace={namespace}
deploymentName={deploymentName}
onCreated={loadData}
/>
</div>
</CardHeader>
<CardContent className="p-0">
{hpas.length === 0 ? (
<div className="text-center py-12 text-slate-500">
<Gauge className="h-8 w-8 mx-auto mb-2 opacity-50" />
<p>未配置 HPA</p>
<p className="text-xs mt-1">点击右上角「创建 HPA」设置自动扩缩容</p>
</div>
) : (
<Table>
<TableHeader>
<TableRow className="border-slate-800 hover:bg-transparent">
<TableHead className="text-slate-400">名称</TableHead>
<TableHead className="text-slate-400">最小/最大</TableHead>
<TableHead className="text-slate-400">指标</TableHead>
<TableHead className="text-slate-400">当前/目标</TableHead>
<TableHead className="text-slate-400">操作</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{hpas.map(( hpa: any) => {
const cpu = hpa.cpu_percent || 0;
const mem = hpa.mem_percent || 0;
const metricsParts = [];
if (cpu) metricsParts.push(`CPU: ${cpu}%`);
if (mem) metricsParts.push(`MEM: ${mem}%`);
const metrics = metricsParts.join(', ') || '-';
return (
<TableRow key={hpa.name} className="border-slate-800 hover:bg-slate-800/30">
<TableCell className="font-medium text-slate-200">{hpa.name}</TableCell>
<TableCell className="text-slate-300">{hpa.min_replicas} / {hpa.max_replicas}</TableCell>
<TableCell className="text-sm text-slate-400">{metrics}</TableCell>
<TableCell className="text-slate-300">{hpa.current_replicas || 0}</TableCell>
<TableCell>
<Button variant="ghost" size="icon" className="h-7 w-7 text-red-400 hover:text-red-300"
onClick={() => handleHpaToggle(hpa)} title="删除 HPA">
<Trash2 className="h-3.5 w-3.5" />
</Button>
</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
)}
</CardContent>
</Card>
</TabsContent>
<DeploymentPodsTab
pods={pods}
onOpenLogsViewer={(pod) => { setLogsViewerPod(pod); setLogsViewerOpen(true); }}
onOpenExec={(pod) => { setExecPod(pod); setExecOpen(true); }}
onOpenFileBrowser={(pod) => { setFileBrowserPod(pod); setFileBrowserOpen(true); }}
/>
<DeploymentLogsTab
pods={pods}
logTail={logTail}
onLogTailChange={setLogTail}
logsTabPod={logsTabPod}
onLogsTabPodChange={setLogsTabPod}
onOpenLogsViewer={(name, containers) => { setLogsViewerPod({ name, containers }); setLogsViewerOpen(true); }}
/>
<DeploymentHpaTab
hpas={hpas}
clusterCode={clusterCode}
namespace={namespace}
deploymentName={deploymentName}
onCreated={loadData}
onDeleteHpa={handleHpaToggle}
/>
</Tabs>
{/* 🆕 Pod exec 终端 */}
{execPod && (
<PodExecTerminal
open={execOpen}
onOpenChange={(o) => {
setExecOpen(o);
if (!o) setExecPod(null);
}}
clusterId={clusterCode}
namespace={namespace}
podName={execPod.name}
containers={execPod.containers}
/>
)}
{/* 🆕 增强版日志查看器 */}
{logsViewerPod && (
<PodLogsViewer
open={logsViewerOpen}
onOpenChange={(o) => {
setLogsViewerOpen(o);
if (!o) setLogsViewerPod(null);
}}
clusterCode={clusterCode}
namespace={namespace}
podName={logsViewerPod.name}
containers={logsViewerPod.containers}
/>
)}
{/* 🆕 容器文件浏览器 */}
{fileBrowserPod && (
<PodFileBrowser
open={fileBrowserOpen}
onOpenChange={(o) => {
setFileBrowserOpen(o);
if (!o) setFileBrowserPod(null);
}}
clusterCode={clusterCode}
namespace={namespace}
podName={fileBrowserPod.name}
containers={fileBrowserPod.containers}
/>
)}
<DeploymentPodModals
clusterCode={clusterCode}
namespace={namespace}
execPod={execPod}
execOpen={execOpen}
onExecOpenChange={(o) => { setExecOpen(o); if (!o) setExecPod(null); }}
logsViewerPod={logsViewerPod}
logsViewerOpen={logsViewerOpen}
onLogsViewerOpenChange={(o) => { setLogsViewerOpen(o); if (!o) setLogsViewerPod(null); }}
fileBrowserPod={fileBrowserPod}
fileBrowserOpen={fileBrowserOpen}
onFileBrowserOpenChange={(o) => { setFileBrowserOpen(o); if (!o) setFileBrowserPod(null); }}
/>
</div>
);
}
// ============================================================================
// Create HPA Dialog (inline)
// ============================================================================
function CreateHPADialog({ clusterCode, namespace, deploymentName, onCreated }: {
clusterCode: string; namespace: string; deploymentName: string; onCreated: () => void;
}) {
const { toast } = useToast();
const [open, setOpen] = useState(false);
const [loading, setLoading] = useState(false);
const [form, setForm] = useState({
name: '',
minReplicas: 1,
maxReplicas: 10,
cpuPercent: 80,
memPercent: 0,
});
const handleCreate = async () => {
if (!form.name.trim()) {
toast({ title: '名称不能为空', variant: 'destructive' });
return;
}
setLoading(true);
try {
await kubernetesApi.createHPA(clusterCode, {
name: form.name,
namespace,
targetKind: 'Deployment',
targetName: deploymentName,
minReplicas: form.minReplicas,
maxReplicas: form.maxReplicas,
cpuPercent: form.cpuPercent,
memPercent: form.memPercent,
});
toast({ title: 'HPA 创建成功' });
setOpen(false);
onCreated();
setForm({ name: '', minReplicas: 1, maxReplicas: 10, cpuPercent: 80, memPercent: 0 });
} catch (e) {
toast({ title: '创建失败', description: e?.message || String(e), variant: 'destructive' });
} finally { setLoading(false); }
};
return (
<>
<Button variant="outline" size="sm" className="border-slate-700 text-slate-300" onClick={() => setOpen(true)}>
<Plus className="h-3.5 w-3.5 mr-1" />创建 HPA
</Button>
{open && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60" onClick={() => setOpen(false)}>
<div className="bg-slate-900 border border-slate-700 rounded-xl p-6 w-[480px] max-h-[80vh] overflow-auto" onClick={(e) => e.stopPropagation()}>
<h2 className="text-lg font-bold text-slate-100 mb-4">创建 HPA</h2>
<div className="space-y-3">
<div>
<Label className="text-slate-400 text-sm">名称</Label>
<Input value={form.name} onChange={(e) => setForm({ ...form, name: e.target.value })}
placeholder={`hpa-${deploymentName}`}
className="bg-slate-800 border-slate-700 text-slate-200 mt-1" />
</div>
<div>
<Label className="text-slate-400 text-sm">目标</Label>
<p className="text-slate-300 text-sm mt-1 bg-slate-800 rounded px-3 py-2">
Deployment: {deploymentName} · 命名空间: {namespace}
</p>
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<Label className="text-slate-400 text-sm">最小副本数</Label>
<Input type="number" value={form.minReplicas} min={1}
onChange={(e) => setForm({ ...form, minReplicas: Number(e.target.value) })}
className="bg-slate-800 border-slate-700 text-slate-200 mt-1" />
</div>
<div>
<Label className="text-slate-400 text-sm">最大副本数</Label>
<Input type="number" value={form.maxReplicas} min={1}
onChange={(e) => setForm({ ...form, maxReplicas: Number(e.target.value) })}
className="bg-slate-800 border-slate-700 text-slate-200 mt-1" />
</div>
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<Label className="text-slate-400 text-sm">CPU 目标 (%)</Label>
<Input type="number" value={form.cpuPercent} min={0} max={100}
onChange={(e) => setForm({ ...form, cpuPercent: Number(e.target.value) })}
className="bg-slate-800 border-slate-700 text-slate-200 mt-1" />
</div>
<div>
<Label className="text-slate-400 text-sm">内存目标 (%) <span className="text-slate-600">可选</span></Label>
<Input type="number" value={form.memPercent} min={0} max={100}
onChange={(e) => setForm({ ...form, memPercent: Number(e.target.value) })}
className="bg-slate-800 border-slate-700 text-slate-200 mt-1" />
</div>
</div>
</div>
<div className="flex justify-end gap-2 mt-6">
<Button variant="outline" className="border-slate-700 text-slate-400" onClick={() => setOpen(false)}>取消</Button>
<Button className="bg-blue-600 hover:bg-blue-700" disabled={loading} onClick={handleCreate}>
{loading ? <Loader2 className="h-4 w-4 animate-spin" /> : '创建'}
</Button>
</div>
</div>
</div>
)}
</>
);
}