feat(platform): add alert business impact board

This commit is contained in:
lingniu
2026-07-04 22:24:06 +08:00
parent b3b4e2203a
commit 24897850f3
3 changed files with 172 additions and 0 deletions

View File

@@ -604,6 +604,60 @@ function notificationHandoffText({
].join('\n');
}
function alertBusinessImpactText({
summary,
rules,
priorityRows,
health,
filters,
platformRelease
}: {
summary: QualitySummary;
rules: AlertRuleRow[];
priorityRows: PriorityIssueRow[];
health: OpsHealth | null;
filters: Record<string, string>;
platformRelease?: string;
}) {
const release = platformRelease?.trim();
const p0Rows = priorityRows.filter((row) => row.priority === 'P0');
const p1Rows = priorityRows.filter((row) => row.priority === 'P1');
const activeRules = rules.filter((row) => row.count > 0);
const p0Rules = activeRules.filter((row) => row.level === 'P0');
const primaryProtocol = summary.protocols?.[0];
const primaryIssue = summary.issueTypes?.[0];
const storageOk = !health || (health.tdengineWritable && health.mysqlWritable);
const capacityCount = Number(health?.capacityFindings?.length ?? 0);
const severity = !storageOk || p0Rows.length > 0 || p0Rules.length > 0 || Number(summary.errorCount ?? 0) > 0
? '高风险'
: Number(summary.issueVehicleCount ?? 0) > 0 || capacityCount > 0
? '需关注'
: '正常';
const filterLines = [
filters.keyword ? `关键词=${filters.keyword}` : '',
filters.protocol ? `数据来源=${qualityProtocolLabel(filters.protocol)}` : '',
filters.issueType ? `问题类型=${qualityIssueLabel(filters.issueType)}` : ''
].filter(Boolean);
return [
'【告警业务影响报告】',
...(release ? [`运行版本:${release}`] : []),
`影响等级:${severity}`,
`当前筛选:${filterLines.length > 0 ? filterLines.join('') : '全部告警'}`,
`影响车辆:${Number(summary.issueVehicleCount ?? 0).toLocaleString()}`,
`问题记录:${Number(summary.issueRecordCount ?? 0).toLocaleString()}`,
`错误/警告:${Number(summary.errorCount ?? 0).toLocaleString()}/${Number(summary.warningCount ?? 0).toLocaleString()}`,
`优先级P0 ${p0Rows.length.toLocaleString()} 条 / P1 ${p1Rows.length.toLocaleString()}`,
`活跃规则:${activeRules.length.toLocaleString()} 类 / P0规则 ${p0Rules.length.toLocaleString()}`,
`主要来源:${primaryProtocol ? `${qualityProtocolLabel(primaryProtocol.name)} ${primaryProtocol.count.toLocaleString()}` : '-'}`,
`主要问题:${primaryIssue ? `${qualityIssueLabel(primaryIssue.name)} ${primaryIssue.count.toLocaleString()}` : '-'}`,
`容量风险:${storageOk ? '存储可写' : '存储异常'}Kafka Lag ${formatLag(health?.kafkaLag)};容量发现 ${capacityCount.toLocaleString()}`,
`建议动作:${severity === '高风险' ? '立即通知责任团队并按 P0/P1 队列闭环' : severity === '需关注' ? '纳入当日治理,持续观察来源覆盖和字段完整性' : '保持监控'}`,
`告警事件:${qualityShareURL()}`,
`通知规则:${appURL(buildAppHash({ page: 'notification-rules' }))}`,
`运维质量:${appURL(buildAppHash({ page: 'ops-quality' }))}`
].join('\n');
}
async function copyText(value: string, label: string) {
const text = value.trim();
if (!text) {
@@ -667,6 +721,25 @@ export function Quality({
const p0RuleCount = notificationPlan?.p0RuleCount ?? rules.filter((item) => item.count > 0 && item.level === 'P0').length;
const priorityRows = normalizePriorityIssues(notificationPlan?.priorityIssues) ?? priorityIssueRows(issues);
const dispatchOwnerRows = alertDispatchOwnerRows(rules);
const priorityP0Count = priorityRows.filter((row) => row.priority === 'P0').length;
const priorityP1Count = priorityRows.filter((row) => row.priority === 'P1').length;
const activeRulesForImpact = rules.filter((row) => row.count > 0);
const storageWritable = !health || (health.tdengineWritable && health.mysqlWritable);
const capacityFindingCount = health?.capacityFindings?.length ?? 0;
const impactSeverity = !storageWritable || priorityP0Count > 0 || p0RuleCount > 0 || errorCount > 0
? { label: '高风险', color: 'red' as const, detail: '需要立即通知责任团队并按 P0/P1 队列闭环。' }
: issueVehicleCount > 0 || capacityFindingCount > 0
? { label: '需关注', color: 'orange' as const, detail: '纳入当日治理,持续观察来源覆盖和字段完整性。' }
: { label: '正常', color: 'green' as const, detail: '当前没有明显业务影响,保持监控。' };
const primaryProtocolImpact = summaryProtocols[0];
const primaryIssueImpact = summaryIssueTypes[0];
const impactCards = [
{ label: '影响车辆', value: `${issueVehicleCount.toLocaleString()}`, detail: `${issueRecordCount.toLocaleString()} 条问题记录`, color: issueVehicleCount > 0 ? 'orange' as const : 'green' as const },
{ label: '优先级', value: `P0 ${priorityP0Count} / P1 ${priorityP1Count}`, detail: `${p0RuleCount} 类 P0 规则`, color: priorityP0Count > 0 || p0RuleCount > 0 ? 'red' as const : 'green' as const },
{ label: '主要来源', value: primaryProtocolImpact ? qualityProtocolLabel(primaryProtocolImpact.name) : '-', detail: primaryProtocolImpact ? `${primaryProtocolImpact.count.toLocaleString()}` : '暂无来源问题', color: primaryProtocolImpact ? 'blue' as const : 'green' as const },
{ label: '主要问题', value: primaryIssueImpact ? qualityIssueLabel(primaryIssueImpact.name) : '-', detail: primaryIssueImpact ? `${primaryIssueImpact.count.toLocaleString()}` : '暂无问题类型', color: primaryIssueImpact ? 'orange' as const : 'green' as const },
{ label: '容量与存储', value: storageWritable ? '可写' : '异常', detail: `Kafka Lag ${formatLag(health?.kafkaLag)} / 发现 ${capacityFindingCount}`, color: storageWritable && capacityFindingCount === 0 ? 'green' as const : 'red' as const }
];
const loadIssues = (values: Record<string, string> = filters, page = pagination.currentPage, pageSize = pagination.pageSize) => {
setLoadingIssues(true);
@@ -809,6 +882,16 @@ export function Quality({
platformRelease: health?.runtime?.platformRelease
}), '告警通知交接包');
};
const copyBusinessImpact = () => {
copyText(alertBusinessImpactText({
summary: notificationPlan?.summary ?? summary,
rules,
priorityRows,
health,
filters,
platformRelease: health?.runtime?.platformRelease
}), '业务影响报告');
};
return (
<div className="vp-page">
@@ -857,6 +940,33 @@ export function Quality({
</Card>
</Col>
</Row>
<Card
bordered
title={<Space><span></span><Button size="small" onClick={copyBusinessImpact}></Button></Space>}
style={{ marginTop: 16 }}
>
<div className="vp-alert-impact-board">
<div className="vp-alert-impact-summary">
<Tag color={impactSeverity.color}>{impactSeverity.label}</Tag>
<div className="vp-alert-flow-value">{issueVehicleCount.toLocaleString()} </div>
<div>{impactSeverity.detail}</div>
<Space wrap>
<Tag color={storageWritable ? 'green' : 'red'}>{storageWritable ? '存储可写' : '存储异常'}</Tag>
<Tag color={activeRulesForImpact.length > 0 ? 'orange' : 'green'}>{activeRulesForImpact.length} </Tag>
<Tag color={capacityFindingCount > 0 ? 'orange' : 'green'}>{capacityFindingCount} </Tag>
</Space>
</div>
<div className="vp-alert-impact-grid">
{impactCards.map((item) => (
<div key={item.label} className="vp-alert-impact-item">
<Tag color={item.color}>{item.label}</Tag>
<strong>{item.value}</strong>
<div>{item.detail}</div>
</div>
))}
</div>
</div>
</Card>
<Card
bordered
title={<Space><span></span><Button size="small" onClick={copyNotificationHandoff}></Button></Space>}

View File

@@ -736,6 +736,48 @@ button.vp-realtime-command-item:focus-visible {
line-height: 28px;
}
.vp-alert-impact-board {
display: grid;
grid-template-columns: minmax(280px, 0.72fr) minmax(0, 1.28fr);
gap: 16px;
}
.vp-alert-impact-summary {
min-height: 148px;
padding: 12px;
border: 1px solid rgba(22, 100, 255, 0.28);
border-radius: var(--vp-radius);
background: #f5f9ff;
display: grid;
gap: 10px;
align-content: start;
}
.vp-alert-impact-grid {
display: grid;
grid-template-columns: repeat(5, minmax(0, 1fr));
gap: 12px;
}
.vp-alert-impact-item {
min-height: 148px;
padding: 12px;
border: 1px solid var(--vp-border);
border-radius: var(--vp-radius);
background: #fbfcff;
display: grid;
gap: 10px;
align-content: start;
}
.vp-alert-impact-item strong {
color: var(--vp-text);
font-size: 20px;
line-height: 26px;
font-weight: 700;
word-break: break-word;
}
.vp-alert-dispatch-grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
@@ -1926,6 +1968,8 @@ button.vp-realtime-command-item:focus-visible {
.vp-conclusion-grid,
.vp-monitor-layout,
.vp-alert-flow,
.vp-alert-impact-board,
.vp-alert-impact-grid,
.vp-alert-dispatch-grid,
.vp-alert-command-board,
.vp-alert-command-grid,

View File

@@ -3236,6 +3236,16 @@ test('shows alert rule and notification policy workspace on quality page', async
render(<App />);
expect(await screen.findByText('告警触发规则')).toBeInTheDocument();
expect(screen.getByText('业务影响评估')).toBeInTheDocument();
expect(screen.getByRole('button', { name: '复制业务影响报告' })).toBeInTheDocument();
expect(screen.getAllByText('高风险').length).toBeGreaterThan(0);
expect(screen.getByText('3 辆受影响')).toBeInTheDocument();
expect(screen.getByText('影响车辆')).toBeInTheDocument();
expect(screen.getByText('3 辆')).toBeInTheDocument();
expect(screen.getByText('P0 0 / P1 0')).toBeInTheDocument();
expect(screen.getAllByText('车辆服务').length).toBeGreaterThan(0);
expect(screen.getAllByText('暂无数据来源').length).toBeGreaterThan(0);
expect(screen.getAllByText('异常').length).toBeGreaterThan(0);
expect(screen.getByText('4 类活跃')).toBeInTheDocument();
expect(screen.getByText('3 类 P0')).toBeInTheDocument();
expect(screen.getByText('容量与存储风险')).toBeInTheDocument();
@@ -3251,6 +3261,14 @@ test('shows alert rule and notification policy workspace on quality page', async
expect(screen.getByText('30 分钟升级')).toBeInTheDocument();
expect(screen.getByText('验收:来源恢复并持续 10 分钟,车辆服务可查到实时与历史证据')).toBeInTheDocument();
fireEvent.click(screen.getByRole('button', { name: '复制业务影响报告' }));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('【告警业务影响报告】'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('运行版本platform-20260704155607'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('影响等级:高风险'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('影响车辆3 辆'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('主要来源:车辆服务 3 条'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('主要问题:暂无数据来源 2 条'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('容量风险存储异常Kafka Lag 42容量发现 1 项'));
fireEvent.click(screen.getByRole('button', { name: '复制通知策略Runbook' }));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('【告警通知策略Runbook】'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('运行版本platform-20260704155607'));