feat(platform): add notification rules workspace

This commit is contained in:
lingniu
2026-07-04 16:27:37 +08:00
parent 33da8ba5a4
commit c060317fb0
8 changed files with 287 additions and 9 deletions

View File

@@ -0,0 +1,177 @@
import { Button, Card, Space, Table, Tag, Toast } from '@douyinfe/semi-ui';
import { IconCopy } from '@douyinfe/semi-icons';
import { useEffect, useState } from 'react';
import { api } from '../api/client';
import type { OpsHealth, QualityAlertRule, QualityNotificationPlan, QualityNotificationPolicy } from '../api/types';
import { PageHeader } from '../components/PageHeader';
import { qualityIssueLabel } from '../domain/qualityIssue';
function formatEscalationMinutes(minutes?: number) {
if (!Number.isFinite(minutes) || Number(minutes) <= 0) return '-';
const value = Number(minutes);
if (value < 60) return `${value} 分钟升级`;
const hours = Math.floor(value / 60);
const rest = value % 60;
return rest > 0 ? `${hours} 小时 ${rest} 分钟升级` : `${hours} 小时升级`;
}
function ruleColor(rule: QualityAlertRule): 'green' | 'orange' | 'red' | 'grey' {
if ((rule.count ?? 0) <= 0) return 'green';
if (rule.level === 'P0') return 'red';
if (rule.level === 'P1') return 'orange';
return 'grey';
}
function ruleTitle(rule: QualityAlertRule) {
return rule.title || qualityIssueLabel(rule.issueType);
}
function notificationRulesRunbook(plan: QualityNotificationPlan | null, release?: string) {
const rules = plan?.rules ?? [];
const policies = plan?.policies ?? [];
const lines = [
'【通知规则Runbook】',
`运行版本:${release?.trim() || '-'}`,
`活跃规则:${(plan?.activeRuleCount ?? rules.filter((rule) => rule.count > 0).length).toLocaleString()}`,
`P0规则${(plan?.p0RuleCount ?? rules.filter((rule) => rule.count > 0 && rule.level === 'P0').length).toLocaleString()}`,
'',
'触发规则:',
...rules.map((rule, index) => [
`${index + 1}. ${ruleTitle(rule)} / ${rule.level} / ${rule.owner}`,
` 触发:${rule.trigger}`,
` 通知:${rule.notify}`,
` SLA${rule.sla}`,
` 当前命中:${Number(rule.count ?? 0).toLocaleString()}`
].join('\n')),
'',
'通知策略:',
...policies.map((policy, index) => [
`${index + 1}. ${policy.name} / ${policy.target}`,
` 条件:${policy.condition}`,
` 渠道:${policy.channel}`,
` 升级:${formatEscalationMinutes(policy.escalationMinutes)}`,
` 验收:${policy.acceptanceCriteria || '-'}`
].join('\n'))
];
return lines.join('\n');
}
async function copyText(value: string, label: string) {
try {
await navigator.clipboard.writeText(value);
Toast.success(`已复制${label}`);
} catch {
Toast.error(`复制${label}失败`);
}
}
export function NotificationRules() {
const [plan, setPlan] = useState<QualityNotificationPlan | null>(null);
const [health, setHealth] = useState<OpsHealth | null>(null);
const [loading, setLoading] = useState(true);
const load = () => {
setLoading(true);
Promise.all([
api.qualityNotificationPlan(new URLSearchParams({ limit: '50' })),
api.opsHealth().catch(() => null)
])
.then(([nextPlan, nextHealth]) => {
setPlan(nextPlan);
setHealth(nextHealth);
})
.catch((error: Error) => Toast.error(error.message))
.finally(() => setLoading(false));
};
useEffect(() => {
load();
}, []);
const rules = plan?.rules ?? [];
const policies = plan?.policies ?? [];
const activeRuleCount = plan?.activeRuleCount ?? rules.filter((rule) => rule.count > 0).length;
const p0RuleCount = plan?.p0RuleCount ?? rules.filter((rule) => rule.count > 0 && rule.level === 'P0').length;
const release = health?.runtime?.platformRelease ?? '';
return (
<div className="vp-page">
<PageHeader title="通知规则" description="集中管理告警触发条件、通知对象、升级时间和验收标准,让断链与数据质量问题可追踪、可通知、可闭环" />
<div className="vp-kpi-grid">
<Card bordered loading={loading}>
<div className="vp-kpi-value">{activeRuleCount.toLocaleString()}</div>
<div className="vp-kpi-label"></div>
</Card>
<Card bordered loading={loading}>
<div className="vp-kpi-value">{p0RuleCount.toLocaleString()}</div>
<div className="vp-kpi-label">P0规则</div>
</Card>
<Card bordered loading={loading}>
<div className="vp-kpi-value">{policies.length.toLocaleString()}</div>
<div className="vp-kpi-label"></div>
</Card>
<Card bordered loading={loading}>
<div className="vp-kpi-value">{release || '-'}</div>
<div className="vp-kpi-label"></div>
</Card>
</div>
<Card
bordered
title={<Space><span></span><Button size="small" aria-label="复制通知规则Runbook" icon={<IconCopy />} onClick={() => copyText(notificationRulesRunbook(plan, release), '通知规则Runbook')}>Runbook</Button><Button size="small" loading={loading} onClick={load}></Button></Space>}
>
<div className="vp-alert-flow">
{[
{ label: '触发', value: `${activeRuleCount} 类规则`, detail: '规则从车辆服务质量、来源断链、字段缺失和容量风险中生成。' },
{ label: '通知', value: `${policies.length} 套策略`, detail: '策略定义目标人群、渠道、升级窗口和验收口径。' },
{ label: '闭环', value: `${p0RuleCount} 类 P0`, detail: 'P0 问题必须有证据链接、恢复时间和验收结果。' }
].map((item) => (
<div key={item.label} className="vp-alert-flow-item">
<Tag color="blue">{item.label}</Tag>
<div className="vp-alert-flow-value">{item.value}</div>
<div>{item.detail}</div>
</div>
))}
</div>
</Card>
<Card bordered title="触发规则" loading={loading} style={{ marginTop: 16 }}>
<Table<QualityAlertRule>
pagination={false}
dataSource={rules}
rowKey="issueType"
columns={[
{ title: '规则', width: 180, render: (_: unknown, row: QualityAlertRule) => ruleTitle(row) },
{ title: '级别', width: 90, render: (_: unknown, row: QualityAlertRule) => <Tag color={ruleColor(row)}>{row.level}</Tag> },
{ title: '责任团队', width: 130, dataIndex: 'owner' },
{ title: '当前命中', width: 110, render: (_: unknown, row: QualityAlertRule) => Number(row.count ?? 0).toLocaleString() },
{ title: '触发条件', dataIndex: 'trigger' },
{ title: '通知动作', dataIndex: 'notify' },
{ title: 'SLA', width: 120, dataIndex: 'sla' }
]}
/>
</Card>
<Card bordered title="通知策略" loading={loading} style={{ marginTop: 16 }}>
<div className="vp-notification-policy-list">
{policies.map((policy: QualityNotificationPolicy) => (
<div key={policy.name} className="vp-notification-policy">
<div>
<Space>
<Tag color={policy.name.startsWith('P0') ? 'red' : policy.name.startsWith('P1') ? 'orange' : 'grey'}>{policy.name}</Tag>
<strong>{policy.target}</strong>
</Space>
<div className="vp-alert-policy-detail">{policy.condition}</div>
<div className="vp-alert-policy-detail">{policy.acceptanceCriteria || '-'}</div>
</div>
<Space wrap>
<Tag color="red">{formatEscalationMinutes(policy.escalationMinutes)}</Tag>
<Tag color="blue">{policy.channel}</Tag>
</Space>
</div>
))}
</div>
</Card>
</div>
);
}