Files
aiops/frontend/src/components/kubernetes/KubernetesClusterDialog.tsx
T
attacker cac1414917 refactor: 全局重命名 k8s → kubernetes (前后端+路由+表名+文件)
后端:
  - pkg/k8sclient → pkg/kubernetesclient
  - models/k8s.go → models/kubernetes.go
  - handlers/k8s.go → handlers/kubernetes.go
  - K8sCluster/K8sNode/.. → KubernetesCluster/KubernetesNode/..
  - K8sHandler → KubernetesHandler
  - 路由: /k8s-clusters → /kubernetes-clusters
  - 路由: /k8s/:code/namespace/:ns/... → /kubernetes/:code/namespace/:ns/... (Kuboard风格)
  - 表前缀: container_k8s_ → container_kubernetes_

前端:
  - components/k8s/ → components/kubernetes/
  - K8sClusters → KubernetesClusters
  - K8sWorkloads → KubernetesWorkloads
  - K8sNodes → KubernetesNodes
  - k8sApi → kubernetesApi
  - 路由前缀: /k8s/ → /kubernetes/

测试验证:
✅ /kubernetes/k8s-cluster-liren/namespace/kube-system/deployments → 3 items
✅ /kubernetes/k8s-cluster-liren/namespace/kube-system/pods → 7 items
✅ /kubernetes/k8s-cluster-liren/namespace/kube-system/services → 4 items
✅ /kubernetes/k8s-cluster-liren/namespace/kube-system/configmaps → 6 items
✅ /kubernetes/k8s-cluster-liren/namespace/kube-system/secrets → 1 item
✅ /kubernetes/k8s-cluster-liren/namespaces → 4 items
2026-06-19 08:10:12 +00:00

246 lines
9.9 KiB
TypeScript

import { useState, useEffect } from 'react';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Textarea } from '@/components/ui/textarea';
import { Label } from '@/components/ui/label';
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import { kubernetesApi } from '@/services/api';
const emptyForm = {
name: '',
version: '',
description: '',
config_type: 1, // 0=Token, 1=Kubeconfig
api_server: '',
token: '',
kube_config: '',
};
export function KubernetesClusterDialog({
open,
onOpenChange,
editingCluster,
onSaved,
}: {
open: boolean;
onOpenChange: (open: boolean) => void;
editingCluster: any;
onSaved: () => void;
}) {
const [formData, setFormData] = useState({ ...emptyForm });
const [submitting, setSubmitting] = useState(false);
const [hasToken, setHasToken] = useState(false); // 已配置 Token 标记 // Logan.Li
const [hasKubeConfig, setHasKubeConfig] = useState(false); // 已配置 Kubeconfig 标记
// 打开时同步表单(编辑时拉取详情,新建时重置)
useEffect(() => {
if (!open) return;
if (editingCluster && editingCluster.id) {
// 编辑模式:拉取详情获取 has_token/has_kube_config 标记 // Logan.Li
kubernetesApi.getClusterDetail(editingCluster.id).then((res: any) => {
const d = res || {};
setHasToken(!!d.has_token);
setHasKubeConfig(!!d.has_kube_config);
setFormData({
name: d.name || '',
version: d.version || '',
description: d.description || '',
config_type: Number(d.config_type ?? 1),
api_server: d.api_server || '',
token: '', // 不回填原文,留空不修改
kube_config: '', // 不回填原文,留空不修改
});
}).catch(() => {
// 降级:用列表数据
setFormData({
name: editingCluster.name || '',
version: editingCluster.version || '',
description: editingCluster.description || '',
config_type: Number(editingCluster.config_type ?? 1),
api_server: editingCluster.api_server || '',
token: '',
kube_config: '',
});
});
} else {
setHasToken(false);
setHasKubeConfig(false);
setFormData({ ...emptyForm });
}
}, [open, editingCluster]);
// 从 kubeconfig YAML 中解析 server 地址
const parseApiServer = (yaml: string): string => {
const match = yaml.match(/^\s*server:\s*(\S+)\s*$/m);
return match ? match[1] : '';
};
const handleKubeConfigChange = (value: string) => {
const server = parseApiServer(value);
setFormData((prev) => ({
...prev,
kube_config: value,
api_server: server || prev.api_server,
}));
};
const handleSubmit = async () => {
if (!formData.name.trim()) return;
try {
setSubmitting(true);
const payload = {
name: formData.name,
version: formData.version,
description: formData.description,
config_type: formData.config_type,
api_server: formData.api_server,
token: formData.config_type === 0 ? formData.token : '',
kube_config: formData.config_type === 1 ? formData.kube_config : '',
};
if (editingCluster) {
await kubernetesApi.updateCluster(editingCluster.id, payload);
} else {
await kubernetesApi.createCluster(payload);
}
onOpenChange(false);
onSaved();
} catch (e) {
console.error('保存集群失败:', e);
} finally {
setSubmitting(false);
}
};
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)] max-w-2xl">
<DialogHeader>
<DialogTitle className="text-[var(--dg-text-primary)]">
{editingCluster ? '编辑集群' : '添加集群'}
</DialogTitle>
<DialogDescription className="text-[var(--dg-text-secondary)]">
{editingCluster ? '修改集群接入配置' : '配置新的 Kubernetes 集群接入信息'}
</DialogDescription>
</DialogHeader>
<div className="space-y-4 py-2 max-h-[60vh] overflow-y-auto pr-1">
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label className="text-[var(--dg-text-secondary)]">集群名称</Label>
<Input
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
placeholder="例如:prod-cluster"
className="bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)] placeholder:text-[var(--dg-text-muted)]"
/>
</div>
<div className="space-y-2">
<Label className="text-[var(--dg-text-secondary)]">版本(可选)</Label>
<Input
value={formData.version}
onChange={(e) => setFormData({ ...formData, version: e.target.value })}
placeholder="例如:1.28.0"
className="bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)] placeholder:text-[var(--dg-text-muted)]"
/>
</div>
</div>
<div className="space-y-2">
<Label className="text-[var(--dg-text-secondary)]">描述</Label>
<Input
value={formData.description}
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
placeholder="集群用途描述"
className="bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)] placeholder:text-[var(--dg-text-muted)]"
/>
</div>
<div className="space-y-2">
<Label className="text-[var(--dg-text-secondary)]">接入方式</Label>
<RadioGroup
value={String(formData.config_type)}
onValueChange={(v) => setFormData({ ...formData, config_type: Number(v) })}
className="flex gap-4"
>
<div className="flex items-center gap-2 px-3 py-2 rounded-md border border-[var(--dg-border)] cursor-pointer hover:bg-[var(--dg-bg-hover)]">
<RadioGroupItem value="0" id="ct-0" />
<Label htmlFor="ct-0" className="cursor-pointer text-[var(--dg-text-primary)] font-normal">Token</Label>
</div>
<div className="flex items-center gap-2 px-3 py-2 rounded-md border border-[var(--dg-border)] cursor-pointer hover:bg-[var(--dg-bg-hover)]">
<RadioGroupItem value="1" id="ct-1" />
<Label htmlFor="ct-1" className="cursor-pointer text-[var(--dg-text-primary)] font-normal">Kubeconfig</Label>
</div>
</RadioGroup>
</div>
<div className="space-y-2">
<Label className="text-[var(--dg-text-secondary)]">API Server</Label>
<Input
value={formData.api_server}
onChange={(e) => setFormData({ ...formData, api_server: e.target.value })}
placeholder="https://10.0.0.1:6443"
className="bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)] placeholder:text-[var(--dg-text-muted)]"
/>
</div>
{formData.config_type === 0 ? (
<div className="space-y-2">
<Label className="text-[var(--dg-text-secondary)]">
Token {hasToken && <span className="text-emerald-400 text-xs ml-1">✓ 已配置(留空不修改)</span>}
</Label>
<Textarea
value={formData.token}
onChange={(e) => setFormData({ ...formData, token: e.target.value })}
placeholder={hasToken ? '已配置,输入新值覆盖...' : '粘贴 Service Account Token...'}
rows={4}
className="bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)] placeholder:text-[var(--dg-text-muted)] font-mono text-xs"
/>
</div>
) : (
<div className="space-y-2">
<Label className="text-[var(--dg-text-secondary)]">
Kubeconfig {hasKubeConfig && <span className="text-emerald-400 text-xs ml-1">✓ 已配置(留空不修改)</span>}
</Label>
<Textarea
value={formData.kube_config}
onChange={(e) => handleKubeConfigChange(e.target.value)}
placeholder={hasKubeConfig ? '已配置,输入新值覆盖...' : 'apiVersion: v1\nkind: Config\nclusters:\n- cluster:\n server: https://10.0.0.1:6443\n ...'}
rows={10}
className="bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)] placeholder:text-[var(--dg-text-muted)] font-mono text-xs"
/>
{!hasKubeConfig && (
<p className="text-xs text-[var(--dg-text-muted)]">
粘贴 kubeconfig 内容后将自动解析 API Server 地址
</p>
)}
</div>
)}
</div>
<DialogFooter>
<Button
variant="outline"
onClick={() => { onOpenChange(false); }}
className="border-[var(--dg-border)] text-[var(--dg-text-secondary)] hover:text-[var(--dg-text-primary)] hover:bg-[var(--dg-bg-hover)]"
>
取消
</Button>
<Button
onClick={handleSubmit}
disabled={submitting || !formData.name.trim()}
className="bg-[var(--dg-blue)] hover:opacity-90 text-white"
>
{submitting ? '保存中...' : editingCluster ? '保存' : '添加'}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}