feat: 完善告警规则配置页面 — 增删改查弹窗 + 修复白屏bug + UpdateRule notify_channels序列化修复

- AlertManager: 新建/编辑告警规则弹窗(规则名称/描述/类型/数据源/表达式/级别/持续时间/通知渠道Switch)
- AlertManager: 修复白屏bug — getRules返回{list,total}未解包导致rules.map崩溃(Frontend Pitfall #1)
- AlertManager: 修复规则列表字段显示 — 原引用不存在的rule.metric/operator/threshold字段,改为rule.source/expression
- AlertManager: 新增规则删除按钮(带确认)
- alert.go: 修复UpdateRule notify_channels序列化bug — 原直接赋值[]string到text列导致500,改用toJSON()
- 同步提交前序未提交改动(WebShell/K8s/Server/Login等组件改进)
This commit is contained in:
attacker committed 2026-06-14 03:07:44 +08:00
1 parent 170818c141
commit 00d5510366
13 files changed
+2170 -496

No files matched your search

+217 -2
View File
@@ -3,6 +3,9 @@ import { cn } from '@/lib/utils';
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
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 { Badge } from '@/components/ui/badge';
import { Progress } from '@/components/ui/progress';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
@@ -66,6 +69,20 @@ export function K8sClusters() {
const [selectedCluster, setSelectedCluster] = useState<string | null>(null);
const [searchQuery, setSearchQuery] = useState('');
// 创建/编辑集群表单
const [dialogOpen, setDialogOpen] = useState(false);
const [editingCluster, setEditingCluster] = useState<any>(null);
const [submitting, setSubmitting] = useState(false);
const [formData, setFormData] = useState({
name: '',
version: '',
description: '',
config_type: 1, // 0=Token, 1=Kubeconfig
api_server: '',
token: '',
kube_config: '',
});
// 集群详情关联资源
const [nodes, setNodes] = useState<any[]>([]);
const [pods, setPods] = useState<any[]>([]);
@@ -130,6 +147,81 @@ export function K8sClusters() {
}
};
const resetForm = () => {
setFormData({
name: '',
version: '',
description: '',
config_type: 1,
api_server: '',
token: '',
kube_config: '',
});
setEditingCluster(null);
};
const openCreateDialog = () => {
resetForm();
setDialogOpen(true);
};
const openEditDialog = (cluster: any) => {
setEditingCluster(cluster);
setFormData({
name: cluster.name || '',
version: cluster.version || '',
description: cluster.description || '',
config_type: Number(cluster.config_type ?? 1),
api_server: cluster.api_server || '',
token: cluster.token || '',
kube_config: cluster.kube_config || '',
});
setDialogOpen(true);
};
// 从 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 k8sApi.updateCluster(editingCluster.id, payload);
} else {
await k8sApi.createCluster(payload);
}
setDialogOpen(false);
resetForm();
loadData();
} catch (e) {
console.error('保存集群失败:', e);
} finally {
setSubmitting(false);
}
};
const filteredClusters = clusters.filter(
(cluster) =>
(cluster.name || '').toLowerCase().includes(searchQuery.toLowerCase()) ||
@@ -194,7 +286,7 @@ export function K8sClusters() {
<RefreshCw className={cn('h-4 w-4 mr-2', loading && 'animate-spin')} />
刷新
</Button>
<Button className="bg-blue-600 hover:bg-blue-700 text-white">
<Button className="bg-blue-600 hover:bg-blue-700 text-white" onClick={openCreateDialog}>
<Plus className="h-4 w-4 mr-2" />
添加集群
</Button>
@@ -339,7 +431,13 @@ export function K8sClusters() {
<Terminal className="h-4 w-4 mr-2" />
Kubectl
</DropdownMenuItem>
<DropdownMenuItem className="text-slate-300 focus:bg-slate-700 focus:text-slate-100">
<DropdownMenuItem
className="text-slate-300 focus:bg-slate-700 focus:text-slate-100"
onClick={(e) => {
e.stopPropagation();
openEditDialog(cluster);
}}
>
<Settings className="h-4 w-4 mr-2" />
配置
</DropdownMenuItem>
@@ -840,6 +938,123 @@ export function K8sClusters() {
</CardContent>
</Card>
)}
{/* 创建/编辑集群 Dialog */}
<Dialog open={dialogOpen} onOpenChange={(o) => { setDialogOpen(o); if (!o) resetForm(); }}>
<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</Label>
<Textarea
value={formData.token}
onChange={(e) => setFormData({ ...formData, token: e.target.value })}
placeholder="粘贴 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</Label>
<Textarea
value={formData.kube_config}
onChange={(e) => handleKubeConfigChange(e.target.value)}
placeholder={'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"
/>
<p className="text-xs text-[var(--dg-text-muted)]">
粘贴 kubeconfig 内容后将自动解析 API Server 地址
</p>
</div>
)}
</div>
<DialogFooter>
<Button
variant="outline"
onClick={() => { setDialogOpen(false); resetForm(); }}
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>
</div>
);
}