feat: 告警规则关联真实通知渠道 — 规则表单选择已配置渠道替代硬编码类型

- 规则创建/编辑表单: 用实际已配置的通知渠道列表(运维钉钉群/DBA邮箱等)替代硬编码的email/sms/dingtalk类型开关
- 规则表notify_channels列: 渠道ID解析为渠道名称+类型图标(替代原先仅显示3个固定图标)
- 组件挂载时预加载notifyChannels(原先仅在通知渠道弹窗打开时加载)
- 向后兼容: resolveChannelRef同时支持新格式(渠道ID)和旧格式(类型字符串)
- 数据修复: 恢复3条被软删除的种子通知渠道记录
This commit is contained in:
attacker committed 2026-06-14 12:08:36 +08:00
1 parent 7fc0701acb
commit f3e1b81a3a
1 file changed
+99 -36
@@ -50,10 +50,6 @@ import {
ResponsiveContainer,
} from 'recharts';
function hasChannel(notifyChannels: any, channel: string): boolean {
return parseChannels(notifyChannels).includes(channel);
}
// 将 notify_channels (可能是数组/JSON字符串/逗号分隔) 解析为数组
function parseChannels(notifyChannels: any): string[] {
if (!notifyChannels) return [];
@@ -70,6 +66,32 @@ function parseChannels(notifyChannels: any): string[] {
return [];
}
// 已知的渠道类型(用于旧数据兼容 — 旧规则存储类型字符串而非渠道 ID)
const KNOWN_CHANNEL_TYPES = ['email', 'sms', 'dingtalk', 'wechat', 'webhook'];
// 根据渠道类型获取图标组件名(用字符串映射,组件内用 getChannelIcon 返回实际组件)
const CHANNEL_TYPE_LABELS: Record<string, string> = {
dingtalk: '钉钉',
wechat: '企业微信',
email: '邮件',
sms: '短信',
webhook: 'Webhook',
};
// 解析单个 notify_channels 引用值 — 返回 {id?, type, name, isLegacy}
// 新格式存渠道 ID (string),旧格式存类型字符串 (email/dingtalk 等)
function resolveChannelRef(value: string, channels: any[]): { type: string; name: string; isLegacy: boolean } | null {
if (!value) return null;
// 尝试按 ID 匹配已配置渠道
const ch = channels.find((c) => String(c.id) === String(value));
if (ch) return { type: ch.type || 'unknown', name: ch.name || ch.type || '未命名', isLegacy: false };
// 兼容旧格式:值本身是类型字符串
if (KNOWN_CHANNEL_TYPES.includes(value)) {
return { type: value, name: CHANNEL_TYPE_LABELS[value] || value, isLegacy: true };
}
return null;
}
export function AlertManager() {
const [activeTab, setActiveTab] = useState('events');
const [searchQuery, setSearchQuery] = useState('');
@@ -288,15 +310,57 @@ export function AlertManager() {
return CHANNEL_TYPES.find((t) => t.key === type)?.label || type;
};
// 根据渠道类型返回对应图标组件
const getChannelIcon = (type: string) => {
switch (type) {
case 'email':
return Mail;
case 'sms':
return Phone;
case 'dingtalk':
case 'wechat':
return MessageSquare;
default:
return Bell;
}
};
// 渲染规则的通知渠道引用列表(兼容新格式=渠道ID 和 旧格式=类型字符串)
const renderRuleChannels = (rule: any) => {
const refs = parseChannels(rule.notify_channels);
if (refs.length === 0) return <span className="text-xs text-slate-600">--</span>;
return (
<div className="flex flex-wrap gap-1">
{refs.map((ref) => {
const resolved = resolveChannelRef(ref, notifyChannels);
if (!resolved) return null;
const Icon = getChannelIcon(resolved.type);
return (
<span
key={ref}
title={`${resolved.name} (${getChannelTypeLabel(resolved.type)})`}
className="inline-flex items-center gap-1 text-xs px-1.5 py-0.5 rounded bg-slate-800 text-slate-300"
>
<Icon className="h-3 w-3 text-slate-400" />
{resolved.name}
</span>
);
})}
</div>
);
};
const loadData = async () => {
try {
setLoading(true);
const [alertsData, rulesData] = await Promise.all([
const [alertsData, rulesData, channelsData] = await Promise.all([
alertApi.getAlerts(),
alertApi.getRules(),
alertApi.getNotifyChannels(),
]);
setAlerts((alertsData as any)?.list || []);
setRules((rulesData as any)?.list || (Array.isArray(rulesData) ? rulesData : []));
setNotifyChannels((channelsData as any)?.list || []);
} catch (error) {
console.error('Failed to load alert data:', error);
} finally {
@@ -702,17 +766,7 @@ export function AlertManager() {
<span className="text-sm text-slate-300">{(rule.duration || 0) / 60}分钟</span>
</TableCell>
<TableCell>
<div className="flex gap-1">
{hasChannel(rule.notify_channels, 'email') && (
<Mail className="h-4 w-4 text-slate-400" />
)}
{hasChannel(rule.notify_channels, 'sms') && (
<Phone className="h-4 w-4 text-slate-400" />
)}
{hasChannel(rule.notify_channels, 'dingtalk') && (
<MessageSquare className="h-4 w-4 text-slate-400" />
)}
</div>
{renderRuleChannels(rule)}
</TableCell>
<TableCell>
<Switch
@@ -865,26 +919,35 @@ export function AlertManager() {
<div className="space-y-1.5">
<Label className="text-slate-300">通知渠道</Label>
<div className="flex items-center gap-4 pt-1">
{[
{ key: 'email', label: '邮件', icon: Mail },
{ key: 'sms', label: '短信', icon: Phone },
{ key: 'dingtalk', label: '钉钉', icon: MessageSquare },
].map(({ key, label, icon: Icon }) => (
<label
key={key}
className="flex items-center gap-2 cursor-pointer select-none text-sm text-slate-300"
>
<Switch
checked={ruleForm.notify_channels.includes(key)}
onCheckedChange={() => toggleNotifyChannel(key)}
className="data-[state=checked]:bg-blue-600"
/>
<Icon className="h-4 w-4 text-slate-400" />
{label}
</label>
))}
</div>
{notifyChannels.length === 0 ? (
<div className="text-xs text-amber-400/80 bg-amber-500/5 border border-amber-500/20 rounded-md px-3 py-2">
暂无已配置的通知渠道,请先在「通知渠道配置」中添加
</div>
) : (
<div className="flex flex-wrap gap-3 pt-1">
{notifyChannels.map((ch) => {
const Icon = getChannelIcon(ch.type);
const checked = ruleForm.notify_channels.includes(String(ch.id));
return (
<label
key={ch.id}
className="flex items-center gap-2 cursor-pointer select-none text-sm text-slate-300"
>
<Switch
checked={checked}
onCheckedChange={() => toggleNotifyChannel(String(ch.id))}
className="data-[state=checked]:bg-blue-600"
/>
<Icon className="h-4 w-4 text-slate-400" />
<span>
{ch.name}
<span className="text-xs text-slate-500 ml-1">({getChannelTypeLabel(ch.type)})</span>
</span>
</label>
);
})}
</div>
)}
</div>
</div>