feat(platform): surface notification priority queue
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
import { Button, Card, Space, Table, Tag, Toast } from '@douyinfe/semi-ui';
|
||||
import { Button, Card, Space, Table, Tag, Toast, Typography } 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 type { OpsHealth, QualityAlertRule, QualityNotificationPlan, QualityNotificationPolicy, QualityPriorityIssue } from '../api/types';
|
||||
import { PageHeader } from '../components/PageHeader';
|
||||
import { qualityIssueLabel } from '../domain/qualityIssue';
|
||||
|
||||
@@ -56,6 +56,31 @@ function notificationRulesRunbook(plan: QualityNotificationPlan | null, release?
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
function priorityIssueDigest(plan: QualityNotificationPlan | null, release?: string) {
|
||||
const issues = plan?.priorityIssues ?? [];
|
||||
return [
|
||||
'【当前待通知告警】',
|
||||
`运行版本:${release?.trim() || '-'}`,
|
||||
`待通知:${issues.length.toLocaleString()} 条`,
|
||||
...issues.map((issue, index) => [
|
||||
`${index + 1}. [${issue.priority}] ${issue.vehicleLabel}`,
|
||||
` 问题:${qualityIssueLabel(issue.issueType)} / ${issue.protocol}`,
|
||||
` SLA:${issue.sla}`,
|
||||
` 建议动作:${issue.actionLabel}`,
|
||||
` 详情:${issue.detail || '-'}`,
|
||||
` 车辆服务:${window.location.origin}${window.location.pathname}${issue.vehicleHash}`,
|
||||
` 实时定位:${window.location.origin}${window.location.pathname}${issue.realtimeHash}`,
|
||||
` 轨迹证据:${window.location.origin}${window.location.pathname}${issue.historyHash}`,
|
||||
` RAW证据:${window.location.origin}${window.location.pathname}${issue.rawHash}`
|
||||
].join('\n'))
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
function navigateHash(hash: string) {
|
||||
if (!hash) return;
|
||||
window.location.hash = hash;
|
||||
}
|
||||
|
||||
async function copyText(value: string, label: string) {
|
||||
try {
|
||||
await navigator.clipboard.writeText(value);
|
||||
@@ -90,6 +115,7 @@ export function NotificationRules() {
|
||||
|
||||
const rules = plan?.rules ?? [];
|
||||
const policies = plan?.policies ?? [];
|
||||
const priorityIssues = plan?.priorityIssues ?? [];
|
||||
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 ?? '';
|
||||
@@ -110,6 +136,10 @@ export function NotificationRules() {
|
||||
<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">{priorityIssues.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>
|
||||
@@ -152,6 +182,46 @@ export function NotificationRules() {
|
||||
/>
|
||||
</Card>
|
||||
|
||||
<Card
|
||||
bordered
|
||||
loading={loading}
|
||||
title={<Space><span>当前待通知告警</span><Button size="small" aria-label="复制待通知汇总" disabled={priorityIssues.length === 0} icon={<IconCopy />} onClick={() => copyText(priorityIssueDigest(plan, release), '待通知告警')}>复制待通知汇总</Button></Space>}
|
||||
style={{ marginTop: 16 }}
|
||||
>
|
||||
<Table<QualityPriorityIssue>
|
||||
pagination={false}
|
||||
dataSource={priorityIssues}
|
||||
rowKey={(row?: QualityPriorityIssue) => `${row?.priority ?? ''}-${row?.protocol ?? ''}-${row?.issueType ?? ''}-${row?.vehicleLabel ?? ''}-${row?.lastSeen ?? ''}`}
|
||||
columns={[
|
||||
{ title: '优先级', width: 90, render: (_: unknown, row: QualityPriorityIssue) => <Tag color={row.priority === 'P0' ? 'red' : 'orange'}>{row.priority}</Tag> },
|
||||
{ title: '车辆', width: 210, dataIndex: 'vehicleLabel' },
|
||||
{ title: '问题', width: 130, render: (_: unknown, row: QualityPriorityIssue) => qualityIssueLabel(row.issueType) },
|
||||
{ title: 'SLA', width: 120, dataIndex: 'sla' },
|
||||
{ title: '建议动作', width: 160, dataIndex: 'actionLabel' },
|
||||
{ title: '最后时间', width: 170, dataIndex: 'lastSeen' },
|
||||
{
|
||||
title: '说明',
|
||||
render: (_: unknown, row: QualityPriorityIssue) => (
|
||||
<Typography.Text ellipsis={{ showTooltip: true }}>{row.detail || row.actionDetail || '-'}</Typography.Text>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: '证据',
|
||||
width: 360,
|
||||
render: (_: unknown, row: QualityPriorityIssue) => (
|
||||
<Space spacing={4} wrap>
|
||||
<Button size="small" disabled={!row.vehicleHash} onClick={() => navigateHash(row.vehicleHash)}>车辆服务</Button>
|
||||
<Button size="small" disabled={!row.realtimeHash} onClick={() => navigateHash(row.realtimeHash)}>实时</Button>
|
||||
<Button size="small" disabled={!row.historyHash} onClick={() => navigateHash(row.historyHash)}>轨迹</Button>
|
||||
<Button size="small" disabled={!row.rawHash} onClick={() => navigateHash(row.rawHash)}>RAW</Button>
|
||||
<Button size="small" aria-label="复制单条告警通知" icon={<IconCopy />} onClick={() => copyText(row.notificationText, '告警通知')}>复制通知</Button>
|
||||
</Space>
|
||||
)
|
||||
}
|
||||
]}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
<Card bordered title="通知策略" loading={loading} style={{ marginTop: 16 }}>
|
||||
<div className="vp-notification-policy-list">
|
||||
{policies.map((policy: QualityNotificationPolicy) => (
|
||||
|
||||
@@ -3294,7 +3294,27 @@ test('renders notification rules as a standalone operations page', async () => {
|
||||
{ name: 'P0 实时中断', target: '接入运维 + 业务责任人', channel: '邮件 / 企业微信', condition: '无来源、VIN 缺失、存储不可写', escalationMinutes: 30, acceptanceCriteria: '来源恢复并持续 10 分钟' },
|
||||
{ name: 'P1 数据质量', target: '协议解析 + 数据治理', channel: '每日汇总邮件', condition: '字段缺失、链路间断', escalationMinutes: 120, acceptanceCriteria: '核心字段恢复解析' }
|
||||
],
|
||||
priorityIssues: [],
|
||||
priorityIssues: [{
|
||||
vin: 'VIN-RULES-001',
|
||||
plate: '粤A规则1',
|
||||
phone: '',
|
||||
sourceEndpoint: 'vehicle_identity_binding',
|
||||
protocol: 'VEHICLE_SERVICE',
|
||||
issueType: 'NO_SOURCE',
|
||||
severity: 'error',
|
||||
lastSeen: '2026-07-03 20:12:10',
|
||||
detail: '通知规则页待通知告警',
|
||||
priority: 'P0',
|
||||
actionLabel: '确认平台转发',
|
||||
actionDetail: '核对平台转发配置',
|
||||
sla: '30 分钟确认',
|
||||
vehicleLabel: '粤A规则1 / VIN-RULES-001',
|
||||
realtimeHash: '#/realtime?keyword=VIN-RULES-001',
|
||||
historyHash: '#/history?keyword=VIN-RULES-001',
|
||||
rawHash: '#/history-query?keyword=VIN-RULES-001&tab=raw',
|
||||
vehicleHash: '#/detail?keyword=VIN-RULES-001',
|
||||
notificationText: '【P0 告警通知】通知规则页待通知'
|
||||
}],
|
||||
activeRuleCount: 2,
|
||||
p0RuleCount: 1
|
||||
},
|
||||
@@ -3320,12 +3340,23 @@ test('renders notification rules as a standalone operations page', async () => {
|
||||
expect(screen.getByText('车辆无任何来源证据')).toBeInTheDocument();
|
||||
expect(screen.getByText('P0 实时中断')).toBeInTheDocument();
|
||||
expect(screen.getByText('接入运维 + 业务责任人')).toBeInTheDocument();
|
||||
expect(screen.getByText('当前待通知告警')).toBeInTheDocument();
|
||||
expect(screen.getByText('粤A规则1 / VIN-RULES-001')).toBeInTheDocument();
|
||||
expect(screen.getByText('确认平台转发')).toBeInTheDocument();
|
||||
expect(screen.getByText('通知规则页待通知告警')).toBeInTheDocument();
|
||||
expect(screen.getByText('platform-rules-test')).toBeInTheDocument();
|
||||
expect(fetchMock).toHaveBeenCalledWith(expect.stringContaining('/api/alert-events/notification-plan?limit=50'), undefined);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '复制通知规则Runbook' }));
|
||||
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('【通知规则Runbook】'));
|
||||
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('无来源规则 / P0 / 平台接入'));
|
||||
fireEvent.click(screen.getByRole('button', { name: '复制待通知汇总' }));
|
||||
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('【当前待通知告警】'));
|
||||
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('粤A规则1 / VIN-RULES-001'));
|
||||
fireEvent.click(screen.getByRole('button', { name: '复制单条告警通知' }));
|
||||
expect(writeText).toHaveBeenCalledWith('【P0 告警通知】通知规则页待通知');
|
||||
fireEvent.click(screen.getByRole('button', { name: '车辆服务' }));
|
||||
expect(window.location.hash).toBe('#/detail?keyword=VIN-RULES-001');
|
||||
});
|
||||
|
||||
test('renders ops quality as a standalone runtime health page', async () => {
|
||||
|
||||
Reference in New Issue
Block a user