feat: 云账号编辑功能 + 通知渠道种子数据 + 修复mock.go编译错误
- 新增 PUT /cloud-accounts/:id 后端端点(UpdateCloudAccount handler + 路由注册) - 前端 CloudAccountManager 添加编辑弹窗:复用创建表单,凭证留空保持不变 - 修复 scripts/mock.go 编译错误:补充缺失的 generateNotifyChannels 函数(5种通知渠道种子) - 包含前序未提交的告警通知渠道 CRUD 后端API + AlertManager 通知配置前端UI
This commit is contained in:
1 parent
01a9bebb0c
commit
af2e60acd9
9 files changed
+822
-51
No files matched your search
@@ -22,7 +22,7 @@ import { Switch } from '@/components/ui/switch';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||
import { toast } from 'sonner';
|
||||
import { alertCenterApi } from '@/services/api';
|
||||
import { alertCenterApi, alertApi } from '@/services/api';
|
||||
|
||||
interface Alert {
|
||||
id: string;
|
||||
@@ -59,11 +59,12 @@ interface AlertRule {
|
||||
}
|
||||
|
||||
interface NotificationChannel {
|
||||
id: string;
|
||||
id: number;
|
||||
name: string;
|
||||
type: 'email' | 'slack' | 'webhook' | 'sms' | 'pagerduty';
|
||||
type: string;
|
||||
enabled: boolean;
|
||||
config: Record<string, string>;
|
||||
config: string;
|
||||
test_status?: string;
|
||||
}
|
||||
|
||||
const AlertCenter: React.FC = () => {
|
||||
@@ -125,8 +126,9 @@ const AlertCenter: React.FC = () => {
|
||||
|
||||
const fetchChannels = async () => {
|
||||
try {
|
||||
const res = await alertCenterApi.getChannels();
|
||||
setChannels(res.data || []);
|
||||
const res = await alertApi.getNotifyChannels();
|
||||
const list = (res as any)?.list || [];
|
||||
setChannels(list);
|
||||
} catch (error) {
|
||||
toast.error('获取通知渠道失败');
|
||||
}
|
||||
@@ -173,6 +175,85 @@ const AlertCenter: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
// 通知渠道管理
|
||||
const [channelDialogOpen, setChannelDialogOpen] = useState(false);
|
||||
const [editingChannel, setEditingChannel] = useState<NotificationChannel | null>(null);
|
||||
const [channelForm, setChannelForm] = useState({ name: '', type: 'dingtalk', config: '' });
|
||||
const [channelSaving, setChannelSaving] = useState(false);
|
||||
|
||||
const openCreateChannel = () => {
|
||||
setEditingChannel(null);
|
||||
setChannelForm({ name: '', type: 'dingtalk', config: '' });
|
||||
setChannelDialogOpen(true);
|
||||
};
|
||||
|
||||
const openEditChannel = (ch: NotificationChannel) => {
|
||||
setEditingChannel(ch);
|
||||
setChannelForm({ name: ch.name, type: ch.type, config: ch.config || '' });
|
||||
setChannelDialogOpen(true);
|
||||
};
|
||||
|
||||
const handleSaveChannel = async () => {
|
||||
if (!channelForm.name.trim()) return;
|
||||
setChannelSaving(true);
|
||||
try {
|
||||
const payload = { name: channelForm.name.trim(), type: channelForm.type, config: channelForm.config.trim() };
|
||||
if (editingChannel) {
|
||||
await alertApi.updateNotifyChannel(editingChannel.id, payload);
|
||||
toast.success('渠道已更新');
|
||||
} else {
|
||||
await alertApi.createNotifyChannel(payload);
|
||||
toast.success('渠道已添加');
|
||||
}
|
||||
setChannelDialogOpen(false);
|
||||
fetchChannels();
|
||||
} catch (error) {
|
||||
toast.error('保存失败');
|
||||
} finally {
|
||||
setChannelSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteChannel = async (id: number) => {
|
||||
if (!window.confirm('确定删除此通知渠道?')) return;
|
||||
try {
|
||||
await alertApi.deleteNotifyChannel(id);
|
||||
toast.success('渠道已删除');
|
||||
fetchChannels();
|
||||
} catch (error) {
|
||||
toast.error('删除失败');
|
||||
}
|
||||
};
|
||||
|
||||
const handleTestChannel = async (id: number) => {
|
||||
try {
|
||||
const res = await alertApi.testNotifyChannel(id);
|
||||
const status = (res as any)?.status;
|
||||
if (status === 'success') {
|
||||
toast.success('测试通知已发送');
|
||||
} else {
|
||||
toast.warning('渠道未配置参数,请先完善配置');
|
||||
}
|
||||
fetchChannels();
|
||||
} catch (error) {
|
||||
toast.error('测试失败');
|
||||
}
|
||||
};
|
||||
|
||||
const handleToggleChannel = async (ch: NotificationChannel) => {
|
||||
try {
|
||||
await alertApi.updateNotifyChannel(ch.id, { enabled: !ch.enabled });
|
||||
fetchChannels();
|
||||
} catch (error) {
|
||||
toast.error('操作失败');
|
||||
}
|
||||
};
|
||||
|
||||
const getChannelTypeLabel = (type: string) => {
|
||||
const map: Record<string, string> = { dingtalk: '钉钉', wechat: '企业微信', email: '邮件', sms: '短信', webhook: 'Webhook' };
|
||||
return map[type] || type;
|
||||
};
|
||||
|
||||
const getSeverityBadge = (severity: string) => {
|
||||
switch (severity) {
|
||||
case 'critical':
|
||||
@@ -489,43 +570,55 @@ const AlertCenter: React.FC = () => {
|
||||
刷新
|
||||
</Button>
|
||||
</div>
|
||||
<Button onClick={() => toast.info('添加渠道功能开发中')}>
|
||||
<Button onClick={openCreateChannel}>
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
添加渠道
|
||||
</Button>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{channels.map((channel) => (
|
||||
<Card key={channel.id}>
|
||||
<CardHeader className="pb-3">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
{getChannelIcon(channel.type)}
|
||||
<CardTitle className="text-base">{channel.name}</CardTitle>
|
||||
{channels.length === 0 ? (
|
||||
<div className="text-center py-16 text-muted-foreground border border-dashed rounded-lg">
|
||||
暂无通知渠道,点击「添加渠道」创建
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{channels.map((channel) => (
|
||||
<Card key={channel.id} className="cursor-pointer hover:border-primary/50 transition-colors" >
|
||||
<CardHeader className="pb-3" onClick={() => openEditChannel(channel)}>
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
{getChannelIcon(channel.type)}
|
||||
<CardTitle className="text-base">{channel.name}</CardTitle>
|
||||
</div>
|
||||
<Badge variant={channel.enabled ? 'default' : 'secondary'} className={channel.enabled ? 'bg-green-600' : ''}>
|
||||
{channel.enabled ? '已启用' : '已禁用'}
|
||||
</Badge>
|
||||
</div>
|
||||
<Badge variant={channel.enabled ? 'default' : 'secondary'}>
|
||||
{channel.enabled ? '已启用' : '已禁用'}
|
||||
</Badge>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="pt-0">
|
||||
<div className="text-sm text-muted-foreground capitalize mb-4">
|
||||
类型: {channel.type}
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button variant="outline" size="sm" className="flex-1">
|
||||
<Settings className="w-4 h-4 mr-2" />
|
||||
配置
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" className="flex-1">
|
||||
<Zap className="w-4 h-4 mr-2" />
|
||||
测试
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="pt-0">
|
||||
<div className="text-sm text-muted-foreground mb-3">
|
||||
类型: {getChannelTypeLabel(channel.type)}
|
||||
{channel.test_status === 'success' && (
|
||||
<span className="ml-2 text-green-500">测试通过</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button variant="outline" size="sm" className="flex-1" onClick={() => handleTestChannel(channel.id)}>
|
||||
<Zap className="w-4 h-4 mr-1.5" />
|
||||
测试
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" className="flex-1" onClick={() => handleToggleChannel(channel)}>
|
||||
<Settings className="w-4 h-4 mr-1.5" />
|
||||
{channel.enabled ? '禁用' : '启用'}
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" onClick={() => handleDeleteChannel(channel.id)}>
|
||||
<X className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
@@ -624,6 +717,63 @@ const AlertCenter: React.FC = () => {
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* 通知渠道创建/编辑对话框 */}
|
||||
<Dialog open={channelDialogOpen} onOpenChange={setChannelDialogOpen}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{editingChannel ? '编辑通知渠道' : '添加通知渠道'}</DialogTitle>
|
||||
<DialogDescription>配置告警通知渠道(钉钉/企业微信/邮件/短信/Webhook)</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label>渠道名称 *</Label>
|
||||
<Input
|
||||
value={channelForm.name}
|
||||
onChange={(e) => setChannelForm({ ...channelForm, name: e.target.value })}
|
||||
placeholder="如:运维告警群"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>渠道类型</Label>
|
||||
<Select value={channelForm.type} onValueChange={(v) => setChannelForm({ ...channelForm, type: v })}>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="dingtalk">钉钉</SelectItem>
|
||||
<SelectItem value="wechat">企业微信</SelectItem>
|
||||
<SelectItem value="email">邮件</SelectItem>
|
||||
<SelectItem value="sms">短信</SelectItem>
|
||||
<SelectItem value="webhook">Webhook</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>配置(JSON)</Label>
|
||||
<Input
|
||||
value={channelForm.config}
|
||||
onChange={(e) => setChannelForm({ ...channelForm, config: e.target.value })}
|
||||
placeholder='{"webhook":"https://...","secret":"..."}'
|
||||
className="font-mono text-sm"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{channelForm.type === 'dingtalk' && '钉钉机器人: webhook URL + secret'}
|
||||
{channelForm.type === 'wechat' && '企业微信: webhook URL'}
|
||||
{channelForm.type === 'email' && '邮件: {"to":"ops@ezops.io"}'}
|
||||
{channelForm.type === 'sms' && '短信: {"phones":"13800138000"}'}
|
||||
{channelForm.type === 'webhook' && 'Webhook: {"url":"https://..."}'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setChannelDialogOpen(false)}>取消</Button>
|
||||
<Button onClick={handleSaveChannel} disabled={!channelForm.name.trim() || channelSaving}>
|
||||
{channelSaving ? '保存中...' : editingChannel ? '保存修改' : '添加渠道'}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in new issue
Block a user