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 { kubernetesApi } from '@/services/api';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
|
// 名称即 code,只允许英文/数字/连字符
|
||||||
|
const NAME_RE = /^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?$/;
|
||||||
|
|
||||||
const emptyForm = {
|
const emptyForm = {
|
||||||
name: '',
|
name: '',
|
||||||
code: '',
|
|
||||||
version: '',
|
version: '',
|
||||||
description: '',
|
description: '',
|
||||||
config_type: 1, // 0=Token, 1=Kubeconfig
|
config_type: 1, // 0=Token, 1=Kubeconfig
|
||||||
@@ -39,33 +41,30 @@ export function KubernetesClusterDialog({
|
|||||||
}) {
|
}) {
|
||||||
const [formData, setFormData] = useState({ ...emptyForm });
|
const [formData, setFormData] = useState({ ...emptyForm });
|
||||||
const [submitting, setSubmitting] = useState(false);
|
const [submitting, setSubmitting] = useState(false);
|
||||||
const [hasToken, setHasToken] = useState(false); // 已配置 Token 标记 // Logan.Li
|
const [hasToken, setHasToken] = useState(false);
|
||||||
const [hasKubeConfig, setHasKubeConfig] = useState(false); // 已配置 Kubeconfig 标记
|
const [hasKubeConfig, setHasKubeConfig] = useState(false);
|
||||||
|
const [nameError, setNameError] = useState('');
|
||||||
|
|
||||||
// 打开时同步表单(编辑时拉取详情,新建时重置)
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!open) return;
|
if (!open) return;
|
||||||
if (editingCluster && editingCluster.id) {
|
if (editingCluster && editingCluster.id) {
|
||||||
// 编辑模式:拉取详情获取 has_token/has_kube_config 标记 // Logan.Li
|
|
||||||
kubernetesApi.getClusterDetail(editingCluster.id).then((res: any) => {
|
kubernetesApi.getClusterDetail(editingCluster.id).then((res: any) => {
|
||||||
const d = res || {};
|
const d = res || {};
|
||||||
setHasToken(!!d.has_token);
|
setHasToken(!!d.has_token);
|
||||||
setHasKubeConfig(!!d.has_kube_config);
|
setHasKubeConfig(!!d.has_kube_config);
|
||||||
setFormData({
|
setFormData({
|
||||||
name: d.name || '',
|
name: d.name || '',
|
||||||
code: d.code || d.name?.toLowerCase().replace(/[^a-z0-9-]/g, '-').replace(/^-+|-+$/g, '') || '',
|
|
||||||
version: d.version || '',
|
version: d.version || '',
|
||||||
description: d.description || '',
|
description: d.description || '',
|
||||||
config_type: Number(d.config_type ?? 1),
|
config_type: Number(d.config_type ?? 1),
|
||||||
api_server: d.api_server || '',
|
api_server: d.api_server || '',
|
||||||
token: '', // 不回填原文,留空不修改
|
token: '',
|
||||||
kube_config: '', // 不回填原文,留空不修改
|
kube_config: '',
|
||||||
});
|
});
|
||||||
|
setNameError('');
|
||||||
}).catch(() => {
|
}).catch(() => {
|
||||||
// 降级:用列表数据
|
|
||||||
setFormData({
|
setFormData({
|
||||||
name: editingCluster.name || '',
|
name: editingCluster.name || '',
|
||||||
code: editingCluster.code || editingCluster.name?.toLowerCase().replace(/[^a-z0-9-]/g, '-').replace(/^-+|-+$/g, '') || '',
|
|
||||||
version: editingCluster.version || '',
|
version: editingCluster.version || '',
|
||||||
description: editingCluster.description || '',
|
description: editingCluster.description || '',
|
||||||
config_type: Number(editingCluster.config_type ?? 1),
|
config_type: Number(editingCluster.config_type ?? 1),
|
||||||
@@ -73,15 +72,27 @@ export function KubernetesClusterDialog({
|
|||||||
token: '',
|
token: '',
|
||||||
kube_config: '',
|
kube_config: '',
|
||||||
});
|
});
|
||||||
|
setNameError('');
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
setHasToken(false);
|
setHasToken(false);
|
||||||
setHasKubeConfig(false);
|
setHasKubeConfig(false);
|
||||||
setFormData({ ...emptyForm });
|
setFormData({ ...emptyForm });
|
||||||
|
setNameError('');
|
||||||
}
|
}
|
||||||
}, [open, editingCluster]);
|
}, [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 parseApiServer = (yaml: string): string => {
|
||||||
const match = yaml.match(/^\s*server:\s*(\S+)\s*$/m);
|
const match = yaml.match(/^\s*server:\s*(\S+)\s*$/m);
|
||||||
return match ? match[1] : '';
|
return match ? match[1] : '';
|
||||||
@@ -97,12 +108,14 @@ export function KubernetesClusterDialog({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
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 {
|
try {
|
||||||
setSubmitting(true);
|
setSubmitting(true);
|
||||||
const payload = {
|
const payload = {
|
||||||
name: formData.name,
|
name: formData.name.trim(),
|
||||||
code: formData.code,
|
code: formData.name.trim(), // 名称即 code
|
||||||
version: formData.version,
|
version: formData.version,
|
||||||
description: formData.description,
|
description: formData.description,
|
||||||
config_type: formData.config_type,
|
config_type: formData.config_type,
|
||||||
@@ -124,6 +137,8 @@ export function KubernetesClusterDialog({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const canSubmit = submitting || !formData.name.trim() || !!nameError;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
<DialogContent className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)] max-w-2xl">
|
<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>
|
<Label className="text-[var(--dg-text-secondary)]">集群名称</Label>
|
||||||
<Input
|
<Input
|
||||||
value={formData.name}
|
value={formData.name}
|
||||||
onChange={(e) => {
|
onChange={(e) => handleNameChange(e.target.value)}
|
||||||
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 })}
|
|
||||||
placeholder="如: prod-cluster"
|
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>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label className="text-[var(--dg-text-secondary)]">版本(可选)</Label>
|
<Label className="text-[var(--dg-text-secondary)]">版本(可选)</Label>
|
||||||
<Input
|
<Input
|
||||||
value={formData.version}
|
value={formData.version}
|
||||||
onChange={(e) => setFormData({ ...formData, version: e.target.value })}
|
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)]"
|
className="bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)] placeholder:text-[var(--dg-text-muted)]"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -251,7 +257,7 @@ export function KubernetesClusterDialog({
|
|||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
onClick={handleSubmit}
|
onClick={handleSubmit}
|
||||||
disabled={submitting || !formData.name.trim() || !formData.code.trim()}
|
disabled={canSubmit}
|
||||||
className="bg-[var(--dg-blue)] hover:opacity-90 text-white"
|
className="bg-[var(--dg-blue)] hover:opacity-90 text-white"
|
||||||
>
|
>
|
||||||
{submitting ? '保存中...' : editingCluster ? '保存' : '添加'}
|
{submitting ? '保存中...' : editingCluster ? '保存' : '添加'}
|
||||||
|
|||||||
@@ -226,6 +226,7 @@ export function KubernetesClusters() {
|
|||||||
filteredClusters.map((cluster) => {
|
filteredClusters.map((cluster) => {
|
||||||
const cpuUsage = Number(cluster.cpu_usage || 0);
|
const cpuUsage = Number(cluster.cpu_usage || 0);
|
||||||
const memoryUsage = Number(cluster.memory_usage || 0);
|
const memoryUsage = Number(cluster.memory_usage || 0);
|
||||||
|
const hasMetrics = cpuUsage > 0 || memoryUsage > 0;
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
key={cluster.id}
|
key={cluster.id}
|
||||||
@@ -326,32 +327,38 @@ export function KubernetesClusters() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<div className="flex items-center gap-2">
|
{hasMetrics ? (
|
||||||
<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="flex items-center gap-2">
|
||||||
<div
|
<span className="text-xs text-slate-500 w-10">CPU</span>
|
||||||
className={cn(
|
<div className="flex-1 h-1.5 bg-slate-800 rounded-full overflow-hidden">
|
||||||
'h-full rounded-full',
|
<div
|
||||||
cpuUsage > 80 ? 'bg-red-400' : cpuUsage > 60 ? 'bg-amber-400' : 'bg-emerald-400'
|
className={cn(
|
||||||
)}
|
'h-full rounded-full',
|
||||||
style={{ width: `${cpuUsage}%` }}
|
cpuUsage > 80 ? 'bg-red-400' : cpuUsage > 60 ? 'bg-amber-400' : 'bg-emerald-400'
|
||||||
/>
|
)}
|
||||||
</div>
|
style={{ width: `${Math.min(cpuUsage, 100)}%` }}
|
||||||
<span className="text-xs text-slate-400 w-10 text-right">{cpuUsage.toFixed(1)}%</span>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<span className="text-xs text-slate-400 w-10 text-right">{cpuUsage.toFixed(1)}%</span>
|
||||||
<span className="text-xs text-slate-500 w-10">内存</span>
|
</div>
|
||||||
<div className="flex-1 h-1.5 bg-slate-800 rounded-full overflow-hidden">
|
<div className="flex items-center gap-2">
|
||||||
<div
|
<span className="text-xs text-slate-500 w-10">内存</span>
|
||||||
className={cn(
|
<div className="flex-1 h-1.5 bg-slate-800 rounded-full overflow-hidden">
|
||||||
'h-full rounded-full',
|
<div
|
||||||
memoryUsage > 80 ? 'bg-red-400' : memoryUsage > 60 ? 'bg-amber-400' : 'bg-emerald-400'
|
className={cn(
|
||||||
)}
|
'h-full rounded-full',
|
||||||
style={{ width: `${memoryUsage}%` }}
|
memoryUsage > 80 ? 'bg-red-400' : memoryUsage > 60 ? 'bg-amber-400' : 'bg-emerald-400'
|
||||||
/>
|
)}
|
||||||
</div>
|
style={{ width: `${Math.min(memoryUsage, 100)}%` }}
|
||||||
<span className="text-xs text-slate-400 w-10 text-right">{memoryUsage.toFixed(1)}%</span>
|
/>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
Reference in new issue
Block a user