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
+90
-27
@@ -50,10 +50,6 @@ import {
|
|||||||
ResponsiveContainer,
|
ResponsiveContainer,
|
||||||
} from 'recharts';
|
} from 'recharts';
|
||||||
|
|
||||||
function hasChannel(notifyChannels: any, channel: string): boolean {
|
|
||||||
return parseChannels(notifyChannels).includes(channel);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 将 notify_channels (可能是数组/JSON字符串/逗号分隔) 解析为数组
|
// 将 notify_channels (可能是数组/JSON字符串/逗号分隔) 解析为数组
|
||||||
function parseChannels(notifyChannels: any): string[] {
|
function parseChannels(notifyChannels: any): string[] {
|
||||||
if (!notifyChannels) return [];
|
if (!notifyChannels) return [];
|
||||||
@@ -70,6 +66,32 @@ function parseChannels(notifyChannels: any): string[] {
|
|||||||
return [];
|
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() {
|
export function AlertManager() {
|
||||||
const [activeTab, setActiveTab] = useState('events');
|
const [activeTab, setActiveTab] = useState('events');
|
||||||
const [searchQuery, setSearchQuery] = useState('');
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
@@ -288,15 +310,57 @@ export function AlertManager() {
|
|||||||
return CHANNEL_TYPES.find((t) => t.key === type)?.label || type;
|
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 () => {
|
const loadData = async () => {
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const [alertsData, rulesData] = await Promise.all([
|
const [alertsData, rulesData, channelsData] = await Promise.all([
|
||||||
alertApi.getAlerts(),
|
alertApi.getAlerts(),
|
||||||
alertApi.getRules(),
|
alertApi.getRules(),
|
||||||
|
alertApi.getNotifyChannels(),
|
||||||
]);
|
]);
|
||||||
setAlerts((alertsData as any)?.list || []);
|
setAlerts((alertsData as any)?.list || []);
|
||||||
setRules((rulesData as any)?.list || (Array.isArray(rulesData) ? rulesData : []));
|
setRules((rulesData as any)?.list || (Array.isArray(rulesData) ? rulesData : []));
|
||||||
|
setNotifyChannels((channelsData as any)?.list || []);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to load alert data:', error);
|
console.error('Failed to load alert data:', error);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -702,17 +766,7 @@ export function AlertManager() {
|
|||||||
<span className="text-sm text-slate-300">{(rule.duration || 0) / 60}分钟</span>
|
<span className="text-sm text-slate-300">{(rule.duration || 0) / 60}分钟</span>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<div className="flex gap-1">
|
{renderRuleChannels(rule)}
|
||||||
{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>
|
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<Switch
|
<Switch
|
||||||
@@ -865,26 +919,35 @@ export function AlertManager() {
|
|||||||
|
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
<Label className="text-slate-300">通知渠道</Label>
|
<Label className="text-slate-300">通知渠道</Label>
|
||||||
<div className="flex items-center gap-4 pt-1">
|
{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">
|
||||||
{ key: 'email', label: '邮件', icon: Mail },
|
暂无已配置的通知渠道,请先在「通知渠道配置」中添加
|
||||||
{ key: 'sms', label: '短信', icon: Phone },
|
</div>
|
||||||
{ key: 'dingtalk', label: '钉钉', icon: MessageSquare },
|
) : (
|
||||||
].map(({ key, label, icon: Icon }) => (
|
<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
|
<label
|
||||||
key={key}
|
key={ch.id}
|
||||||
className="flex items-center gap-2 cursor-pointer select-none text-sm text-slate-300"
|
className="flex items-center gap-2 cursor-pointer select-none text-sm text-slate-300"
|
||||||
>
|
>
|
||||||
<Switch
|
<Switch
|
||||||
checked={ruleForm.notify_channels.includes(key)}
|
checked={checked}
|
||||||
onCheckedChange={() => toggleNotifyChannel(key)}
|
onCheckedChange={() => toggleNotifyChannel(String(ch.id))}
|
||||||
className="data-[state=checked]:bg-blue-600"
|
className="data-[state=checked]:bg-blue-600"
|
||||||
/>
|
/>
|
||||||
<Icon className="h-4 w-4 text-slate-400" />
|
<Icon className="h-4 w-4 text-slate-400" />
|
||||||
{label}
|
<span>
|
||||||
|
{ch.name}
|
||||||
|
<span className="text-xs text-slate-500 ml-1">({getChannelTypeLabel(ch.type)})</span>
|
||||||
|
</span>
|
||||||
</label>
|
</label>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user