diff --git a/frontend/src/components/kubernetes/KubernetesClusterDialog.tsx b/frontend/src/components/kubernetes/KubernetesClusterDialog.tsx index 379e93d..3444325 100644 --- a/frontend/src/components/kubernetes/KubernetesClusterDialog.tsx +++ b/frontend/src/components/kubernetes/KubernetesClusterDialog.tsx @@ -15,9 +15,11 @@ import { import { kubernetesApi } from '@/services/api'; import { toast } from 'sonner'; +// 名称即 code,只允许英文/数字/连字符 +const NAME_RE = /^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?$/; + const emptyForm = { name: '', - code: '', version: '', description: '', config_type: 1, // 0=Token, 1=Kubeconfig @@ -39,33 +41,30 @@ export function KubernetesClusterDialog({ }) { const [formData, setFormData] = useState({ ...emptyForm }); const [submitting, setSubmitting] = useState(false); - const [hasToken, setHasToken] = useState(false); // 已配置 Token 标记 // Logan.Li - const [hasKubeConfig, setHasKubeConfig] = useState(false); // 已配置 Kubeconfig 标记 + const [hasToken, setHasToken] = useState(false); + const [hasKubeConfig, setHasKubeConfig] = useState(false); + const [nameError, setNameError] = useState(''); - // 打开时同步表单(编辑时拉取详情,新建时重置) 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 || '', - code: d.code || d.name?.toLowerCase().replace(/[^a-z0-9-]/g, '-').replace(/^-+|-+$/g, '') || '', version: d.version || '', description: d.description || '', config_type: Number(d.config_type ?? 1), api_server: d.api_server || '', - token: '', // 不回填原文,留空不修改 - kube_config: '', // 不回填原文,留空不修改 + token: '', + kube_config: '', }); + setNameError(''); }).catch(() => { - // 降级:用列表数据 setFormData({ name: editingCluster.name || '', - code: editingCluster.code || editingCluster.name?.toLowerCase().replace(/[^a-z0-9-]/g, '-').replace(/^-+|-+$/g, '') || '', version: editingCluster.version || '', description: editingCluster.description || '', config_type: Number(editingCluster.config_type ?? 1), @@ -73,15 +72,27 @@ export function KubernetesClusterDialog({ token: '', kube_config: '', }); + setNameError(''); }); } else { setHasToken(false); setHasKubeConfig(false); setFormData({ ...emptyForm }); + setNameError(''); } }, [open, editingCluster]); - // 从 kubeconfig YAML 中解析 server 地址 + const handleNameChange = (value: string) => { + setFormData({ ...formData, name: value }); + if (!value.trim()) { + setNameError(''); + } else if (!NAME_RE.test(value)) { + setNameError('只允许英文、数字和连字符(-),首尾不能是连字符'); + } else { + setNameError(''); + } + }; + const parseApiServer = (yaml: string): string => { const match = yaml.match(/^\s*server:\s*(\S+)\s*$/m); return match ? match[1] : ''; @@ -97,12 +108,14 @@ export function KubernetesClusterDialog({ }; const handleSubmit = async () => { - if (!formData.name.trim() || !formData.code.trim()) { toast.error('请填写集群名称和代码'); return; } + if (!formData.name.trim() || !NAME_RE.test(formData.name.trim())) { + toast.error('请输入有效的集群名称(英文/数字/连字符)'); return; + } try { setSubmitting(true); const payload = { - name: formData.name, - code: formData.code, + name: formData.name.trim(), + code: formData.name.trim(), // 名称即 code version: formData.version, description: formData.description, config_type: formData.config_type, @@ -124,6 +137,8 @@ export function KubernetesClusterDialog({ } }; + const canSubmit = submitting || !formData.name.trim() || !!nameError; + return ( @@ -141,30 +156,21 @@ export function KubernetesClusterDialog({ { - const name = e.target.value; - const code = name.toLowerCase().replace(/[^a-z0-9-]/g, '-').replace(/^-+|-+$/g, ''); - setFormData({ ...formData, name, code: formData.code || code }); - }} - placeholder="例如:prod-cluster" - className="bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)] placeholder:text-[var(--dg-text-muted)]" - /> - -
- - setFormData({ ...formData, code: e.target.value })} + onChange={(e) => handleNameChange(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)] font-mono text-sm" + className={`bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)] placeholder:text-[var(--dg-text-muted)] ${nameError ? 'border-red-500' : ''}`} /> + {nameError &&

{nameError}

} +

+ 只允许英文、数字和连字符(-),首尾不能是连字符。名称将作为集群唯一标识。 +

setFormData({ ...formData, version: e.target.value })} - placeholder="例如:1.28.0" + 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)]" />
@@ -251,7 +257,7 @@ export function KubernetesClusterDialog({