feat(platform): add alert business impact board
This commit is contained in:
@@ -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>}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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'));
|
||||
|
||||
Reference in New Issue
Block a user