fix: 集群表单简化 + CPU/内存显示优化

- 移除独立 code 字段,名称即 code(正则校验英文/数字/连字符)
- 名称输入实时校验 + 红色边框提示,首尾不允许连字符
- CPU/内存使用率:有数据时正常显示,无 metrics 时显示「已接入 N 节点 · N Pod · 资源使用率待采集」
This commit is contained in:
attacker committed 2026-06-21 10:47:23 +00:00
1 parent ce438c9f05
commit e5276161ea
2 files changed
+70 -57

No files matched your search

@@ -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 (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)] max-w-2xl">
@@ -141,30 +156,21 @@ export function KubernetesClusterDialog({
<Label className="text-[var(--dg-text-secondary)]">集群名称</Label>
<Input
value={formData.name}
onChange={(e) => {
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)]"
/>
</div>
<div className="space-y-2">
<Label className="text-[var(--dg-text-secondary)]">集群代码 (Code)</Label>
<Input
value={formData.code}
onChange={(e) => 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 && <p className="text-xs text-red-400">{nameError}</p>}
<p className="text-xs text-[var(--dg-text-muted)]">
只允许英文、数字和连字符(-),首尾不能是连字符。名称将作为集群唯一标识。
</p>
</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"
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>
@@ -251,7 +257,7 @@ export function KubernetesClusterDialog({
</Button>
<Button
onClick={handleSubmit}
disabled={submitting || !formData.name.trim() || !formData.code.trim()}
disabled={canSubmit}
className="bg-[var(--dg-blue)] hover:opacity-90 text-white"
>
{submitting ? '保存中...' : editingCluster ? '保存' : '添加'}
@@ -226,6 +226,7 @@ export function KubernetesClusters() {
filteredClusters.map((cluster) => {
const cpuUsage = Number(cluster.cpu_usage || 0);
const memoryUsage = Number(cluster.memory_usage || 0);
const hasMetrics = cpuUsage > 0 || memoryUsage > 0;
return (
<Card
key={cluster.id}
@@ -326,32 +327,38 @@ export function KubernetesClusters() {
</div>
</div>
<div className="space-y-2">
<div className="flex items-center gap-2">
<span className="text-xs text-slate-500 w-10">CPU</span>
<div className="flex-1 h-1.5 bg-slate-800 rounded-full overflow-hidden">
<div
className={cn(
'h-full rounded-full',
cpuUsage > 80 ? 'bg-red-400' : cpuUsage > 60 ? 'bg-amber-400' : 'bg-emerald-400'
)}
style={{ width: `${cpuUsage}%` }}
/>
</div>
<span className="text-xs text-slate-400 w-10 text-right">{cpuUsage.toFixed(1)}%</span>
</div>
<div className="flex items-center gap-2">
<span className="text-xs text-slate-500 w-10">内存</span>
<div className="flex-1 h-1.5 bg-slate-800 rounded-full overflow-hidden">
<div
className={cn(
'h-full rounded-full',
memoryUsage > 80 ? 'bg-red-400' : memoryUsage > 60 ? 'bg-amber-400' : 'bg-emerald-400'
)}
style={{ width: `${memoryUsage}%` }}
/>
</div>
<span className="text-xs text-slate-400 w-10 text-right">{memoryUsage.toFixed(1)}%</span>
</div>
{hasMetrics ? (
<>
<div className="flex items-center gap-2">
<span className="text-xs text-slate-500 w-10">CPU</span>
<div className="flex-1 h-1.5 bg-slate-800 rounded-full overflow-hidden">
<div
className={cn(
'h-full rounded-full',
cpuUsage > 80 ? 'bg-red-400' : cpuUsage > 60 ? 'bg-amber-400' : 'bg-emerald-400'
)}
style={{ width: `${Math.min(cpuUsage, 100)}%` }}
/>
</div>
<span className="text-xs text-slate-400 w-10 text-right">{cpuUsage.toFixed(1)}%</span>
</div>
<div className="flex items-center gap-2">
<span className="text-xs text-slate-500 w-10">内存</span>
<div className="flex-1 h-1.5 bg-slate-800 rounded-full overflow-hidden">
<div
className={cn(
'h-full rounded-full',
memoryUsage > 80 ? 'bg-red-400' : memoryUsage > 60 ? 'bg-amber-400' : 'bg-emerald-400'
)}
style={{ width: `${Math.min(memoryUsage, 100)}%` }}
/>
</div>
<span className="text-xs text-slate-400 w-10 text-right">{memoryUsage.toFixed(1)}%</span>
</div>
</>
) : (
<div className="text-xs text-slate-600 text-center">已接入 {cluster.node_count || 0} 节点 · {cluster.pod_count || 0} Pod · 资源使用率待采集</div>
)}
</div>
</CardContent>
</Card>