Files
lingniu-vehicle-ingest/vehicle-data-platform/apps/web/src/pages/NotificationRules.tsx

178 lines
7.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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>
);
}