feat(platform): add alert handling command queue
This commit is contained in:
@@ -76,6 +76,19 @@ function priorityIssueDigest(plan: QualityNotificationPlan | null, release?: str
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
function countEvidenceLinks(issue: QualityPriorityIssue) {
|
||||
return [issue.vehicleHash, issue.realtimeHash, issue.historyHash, issue.rawHash].filter(Boolean).length;
|
||||
}
|
||||
|
||||
function evidenceSummary(issue: QualityPriorityIssue) {
|
||||
const count = countEvidenceLinks(issue);
|
||||
return count >= 4 ? '证据完整' : `缺 ${4 - count} 类证据`;
|
||||
}
|
||||
|
||||
function evidenceColor(issue: QualityPriorityIssue): 'green' | 'orange' {
|
||||
return countEvidenceLinks(issue) >= 4 ? 'green' : 'orange';
|
||||
}
|
||||
|
||||
function navigateHash(hash: string) {
|
||||
if (!hash) return;
|
||||
window.location.hash = hash;
|
||||
@@ -119,6 +132,10 @@ export function NotificationRules() {
|
||||
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 ?? '';
|
||||
const p0IssueCount = priorityIssues.filter((issue) => issue.priority === 'P0').length;
|
||||
const evidenceCompleteCount = priorityIssues.filter((issue) => countEvidenceLinks(issue) >= 4).length;
|
||||
const primaryOwner = rules.find((rule) => rule.count > 0)?.owner || policies[0]?.target || '-';
|
||||
const nextPriorityIssue = priorityIssues[0];
|
||||
|
||||
return (
|
||||
<div className="vp-page">
|
||||
@@ -146,6 +163,51 @@ export function NotificationRules() {
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<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 }}
|
||||
>
|
||||
<div className="vp-alert-command-board">
|
||||
<div className="vp-alert-command-grid">
|
||||
{[
|
||||
{ label: '待通知告警', value: priorityIssues.length.toLocaleString(), color: priorityIssues.length > 0 ? 'orange' as const : 'green' as const },
|
||||
{ label: 'P0优先', value: p0IssueCount.toLocaleString(), color: p0IssueCount > 0 ? 'red' as const : 'green' as const },
|
||||
{ label: '证据完整', value: `${evidenceCompleteCount}/${priorityIssues.length || 0}`, color: evidenceCompleteCount === priorityIssues.length ? 'green' as const : 'orange' as const },
|
||||
{ label: '主责任方', value: primaryOwner, color: 'blue' as const }
|
||||
].map((item) => (
|
||||
<div key={item.label} className="vp-alert-command-item">
|
||||
<Tag color={item.color}>{item.label}</Tag>
|
||||
<div>{item.value}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="vp-alert-next-action">
|
||||
<div className="vp-current-service-title">下一步处置</div>
|
||||
{nextPriorityIssue ? (
|
||||
<>
|
||||
<Typography.Text strong>{nextPriorityIssue.vehicleLabel}</Typography.Text>
|
||||
<Typography.Text type="secondary">{nextPriorityIssue.actionDetail || nextPriorityIssue.actionLabel}</Typography.Text>
|
||||
<Space wrap>
|
||||
<Tag color={nextPriorityIssue.priority === 'P0' ? 'red' : 'orange'}>{nextPriorityIssue.priority}</Tag>
|
||||
<Tag color={evidenceColor(nextPriorityIssue)}>{evidenceSummary(nextPriorityIssue)}</Tag>
|
||||
<Tag color="blue">{nextPriorityIssue.sla}</Tag>
|
||||
</Space>
|
||||
<Space wrap>
|
||||
<Button size="small" aria-label="下一步车辆服务" disabled={!nextPriorityIssue.vehicleHash} onClick={() => navigateHash(nextPriorityIssue.vehicleHash)}>车辆服务</Button>
|
||||
<Button size="small" aria-label="下一步实时证据" disabled={!nextPriorityIssue.realtimeHash} onClick={() => navigateHash(nextPriorityIssue.realtimeHash)}>实时证据</Button>
|
||||
<Button size="small" aria-label="下一步轨迹证据" disabled={!nextPriorityIssue.historyHash} onClick={() => navigateHash(nextPriorityIssue.historyHash)}>轨迹证据</Button>
|
||||
<Button size="small" aria-label="下一步RAW证据" disabled={!nextPriorityIssue.rawHash} onClick={() => navigateHash(nextPriorityIssue.rawHash)}>RAW证据</Button>
|
||||
</Space>
|
||||
</>
|
||||
) : (
|
||||
<Tag color="green">暂无待通知告警</Tag>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<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>}
|
||||
@@ -185,7 +247,7 @@ export function NotificationRules() {
|
||||
<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>}
|
||||
title="当前待通知告警"
|
||||
style={{ marginTop: 16 }}
|
||||
>
|
||||
<Table<QualityPriorityIssue>
|
||||
@@ -197,6 +259,7 @@ export function NotificationRules() {
|
||||
{ title: '车辆', width: 210, dataIndex: 'vehicleLabel' },
|
||||
{ title: '问题', width: 130, render: (_: unknown, row: QualityPriorityIssue) => qualityIssueLabel(row.issueType) },
|
||||
{ title: 'SLA', width: 120, dataIndex: 'sla' },
|
||||
{ title: '证据完整度', width: 120, render: (_: unknown, row: QualityPriorityIssue) => <Tag color={evidenceColor(row)}>{evidenceSummary(row)}</Tag> },
|
||||
{ title: '建议动作', width: 160, dataIndex: 'actionLabel' },
|
||||
{ title: '最后时间', width: 170, dataIndex: 'lastSeen' },
|
||||
{
|
||||
|
||||
@@ -681,6 +681,48 @@ button.vp-realtime-command-item:focus-visible {
|
||||
line-height: 28px;
|
||||
}
|
||||
|
||||
.vp-alert-command-board {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) 360px;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.vp-alert-command-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.vp-alert-command-item {
|
||||
min-height: 92px;
|
||||
padding: 12px;
|
||||
border: 1px solid var(--vp-border);
|
||||
border-radius: var(--vp-radius);
|
||||
background: #fbfcff;
|
||||
display: grid;
|
||||
align-content: space-between;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.vp-alert-command-item > div {
|
||||
color: var(--vp-text);
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
line-height: 28px;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.vp-alert-next-action {
|
||||
min-height: 132px;
|
||||
padding: 12px;
|
||||
border: 1px solid rgba(22, 100, 255, 0.28);
|
||||
border-radius: var(--vp-radius);
|
||||
background: #f5f9ff;
|
||||
display: grid;
|
||||
align-content: start;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.vp-risk-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
@@ -1184,6 +1226,8 @@ button.vp-realtime-command-item:focus-visible {
|
||||
.vp-conclusion-grid,
|
||||
.vp-monitor-layout,
|
||||
.vp-alert-flow,
|
||||
.vp-alert-command-board,
|
||||
.vp-alert-command-grid,
|
||||
.vp-risk-grid,
|
||||
.vp-alert-ops-grid,
|
||||
.vp-stat-workspace,
|
||||
|
||||
@@ -3349,8 +3349,13 @@ 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('下一步处置')).toBeInTheDocument();
|
||||
expect(screen.getByText('主责任方')).toBeInTheDocument();
|
||||
expect(screen.getAllByText('证据完整').length).toBeGreaterThan(0);
|
||||
expect(screen.getByText('核对平台转发配置')).toBeInTheDocument();
|
||||
expect(screen.getByText('当前待通知告警')).toBeInTheDocument();
|
||||
expect(screen.getByText('粤A规则1 / VIN-RULES-001')).toBeInTheDocument();
|
||||
expect(screen.getAllByText('粤A规则1 / VIN-RULES-001').length).toBeGreaterThan(0);
|
||||
expect(screen.getByText('确认平台转发')).toBeInTheDocument();
|
||||
expect(screen.getByText('通知规则页待通知告警')).toBeInTheDocument();
|
||||
expect(screen.getByText('platform-rules-test')).toBeInTheDocument();
|
||||
|
||||
Reference in New Issue
Block a user