fix: 集群表单简化 + CPU/内存显示优化
- 移除独立 code 字段,名称即 code(正则校验英文/数字/连字符) - 名称输入实时校验 + 红色边框提示,首尾不允许连字符 - CPU/内存使用率:有数据时正常显示,无 metrics 时显示「已接入 N 节点 · N Pod · 资源使用率待采集」
This commit is contained in:
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>
|
||||
|
||||
Reference in new issue
Block a user