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:
1 parent
170818c141
commit
00d5510366
13 files changed
+2170
-496
No files matched your search
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user