feat: 告警规则关联真实通知渠道 — 规则表单选择已配置渠道替代硬编码类型
- 规则创建/编辑表单: 用实际已配置的通知渠道列表(运维钉钉群/DBA邮箱等)替代硬编码的email/sms/dingtalk类型开关 - 规则表notify_channels列: 渠道ID解析为渠道名称+类型图标(替代原先仅显示3个固定图标) - 组件挂载时预加载notifyChannels(原先仅在通知渠道弹窗打开时加载) - 向后兼容: resolveChannelRef同时支持新格式(渠道ID)和旧格式(类型字符串) - 数据修复: 恢复3条被软删除的种子通知渠道记录
This commit is contained in:
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>
|
||||
|
||||
|
||||
Reference in new issue
Block a user