feat(platform): add alert notification center
This commit is contained in:
@@ -896,6 +896,82 @@ export function Quality({
|
||||
return (
|
||||
<div className="vp-page">
|
||||
<PageHeader title="告警事件" description="围绕车辆服务沉淀断链、VIN 缺失、字段缺失和链路健康事件,并形成通知闭环" />
|
||||
<Card bordered title="告警事件与通知中心">
|
||||
<div className="vp-alert-center-board">
|
||||
<div className="vp-alert-center-summary">
|
||||
<Space wrap>
|
||||
<Tag color={impactSeverity.color}>{impactSeverity.label}</Tag>
|
||||
<Tag color={priorityP0Count > 0 ? 'red' : 'green'}>P0 {priorityP0Count.toLocaleString()}</Tag>
|
||||
<Tag color={activeRuleCount > 0 ? 'orange' : 'green'}>活跃规则 {activeRuleCount.toLocaleString()}</Tag>
|
||||
</Space>
|
||||
<strong>{issueVehicleCount.toLocaleString()} 辆车受影响</strong>
|
||||
<span>
|
||||
告警中心面向客户处置:先判断影响车辆和优先级,再通知责任团队,并用实时、轨迹、RAW、里程证据闭环。
|
||||
</span>
|
||||
<Space wrap>
|
||||
<Button size="small" theme="solid" type="primary" disabled={priorityRows.length === 0} onClick={copyNotificationHandoff}>复制中心交接包</Button>
|
||||
<Button size="small" onClick={copyBusinessImpact}>复制影响摘要</Button>
|
||||
{onOpenNotificationRules ? <Button size="small" onClick={onOpenNotificationRules}>管理通知规则</Button> : null}
|
||||
</Space>
|
||||
</div>
|
||||
<div className="vp-alert-center-grid">
|
||||
{[
|
||||
{
|
||||
label: '影响车辆',
|
||||
value: `${issueVehicleCount.toLocaleString()} 辆`,
|
||||
detail: `${issueRecordCount.toLocaleString()} 条告警记录,优先确认车辆服务是否仍可用。`,
|
||||
color: issueVehicleCount > 0 ? 'orange' as const : 'green' as const,
|
||||
onClick: () => undefined,
|
||||
disabled: true
|
||||
},
|
||||
{
|
||||
label: '优先队列',
|
||||
value: `P0 ${priorityP0Count.toLocaleString()} / P1 ${priorityP1Count.toLocaleString()}`,
|
||||
detail: priorityRows.length > 0 ? '已按优先级生成可复制通知和处置工单。' : '当前没有待通知车辆。',
|
||||
color: priorityP0Count > 0 ? 'red' as const : priorityP1Count > 0 ? 'orange' as const : 'green' as const,
|
||||
onClick: copyPriorityDigest,
|
||||
disabled: priorityRows.length === 0
|
||||
},
|
||||
{
|
||||
label: '主要来源',
|
||||
value: primaryProtocolImpact ? qualityProtocolLabel(primaryProtocolImpact.name) : '-',
|
||||
detail: primaryProtocolImpact ? `${primaryProtocolImpact.count.toLocaleString()} 条问题,点击筛选该来源。` : '暂无来源问题。',
|
||||
color: primaryProtocolImpact ? 'blue' as const : 'green' as const,
|
||||
onClick: () => primaryProtocolImpact && drillProtocol(primaryProtocolImpact.name),
|
||||
disabled: !primaryProtocolImpact
|
||||
},
|
||||
{
|
||||
label: '主要问题',
|
||||
value: primaryIssueImpact ? qualityIssueLabel(primaryIssueImpact.name) : '-',
|
||||
detail: primaryIssueImpact ? `${primaryIssueImpact.count.toLocaleString()} 条命中,点击筛选该问题。` : '暂无问题类型。',
|
||||
color: primaryIssueImpact ? 'orange' as const : 'green' as const,
|
||||
onClick: () => primaryIssueImpact && drillIssueType(primaryIssueImpact.name),
|
||||
disabled: !primaryIssueImpact
|
||||
},
|
||||
{
|
||||
label: '链路状态',
|
||||
value: storageWritable ? '可写' : '异常',
|
||||
detail: `Kafka Lag ${formatLag(health?.kafkaLag)},容量发现 ${capacityFindingCount.toLocaleString()} 项。`,
|
||||
color: storageWritable && capacityFindingCount === 0 ? 'green' as const : 'red' as const,
|
||||
onClick: loadHealth,
|
||||
disabled: false
|
||||
}
|
||||
].map((item) => (
|
||||
<button
|
||||
key={item.label}
|
||||
className="vp-alert-center-item"
|
||||
type="button"
|
||||
disabled={item.disabled}
|
||||
onClick={item.onClick}
|
||||
>
|
||||
<Tag color={item.color}>{item.label}</Tag>
|
||||
<strong>{item.value}</strong>
|
||||
<span>{item.detail}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
<div className="vp-kpi-grid">
|
||||
{[
|
||||
{ label: '问题车辆', value: issueVehicleCount.toLocaleString() },
|
||||
|
||||
@@ -1237,6 +1237,81 @@ button.vp-realtime-command-item:focus-visible {
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.vp-alert-center-board {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(280px, 0.7fr) minmax(0, 1.3fr);
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.vp-alert-center-summary {
|
||||
min-height: 168px;
|
||||
padding: 12px;
|
||||
border: 1px solid rgba(245, 158, 11, 0.32);
|
||||
border-radius: var(--vp-radius);
|
||||
background: #fff8ed;
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
align-content: start;
|
||||
}
|
||||
|
||||
.vp-alert-center-summary strong {
|
||||
color: var(--vp-text);
|
||||
font-size: 20px;
|
||||
line-height: 26px;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.vp-alert-center-summary span {
|
||||
color: var(--vp-text-muted);
|
||||
font-size: 13px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.vp-alert-center-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(5, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.vp-alert-center-item {
|
||||
min-height: 168px;
|
||||
padding: 12px;
|
||||
border: 1px solid var(--vp-border);
|
||||
border-radius: var(--vp-radius);
|
||||
background: #fbfcff;
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
align-content: start;
|
||||
text-align: left;
|
||||
font: inherit;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.vp-alert-center-item:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.68;
|
||||
}
|
||||
|
||||
.vp-alert-center-item:not(:disabled):hover,
|
||||
.vp-alert-center-item:not(:disabled):focus-visible {
|
||||
border-color: rgba(245, 158, 11, 0.48);
|
||||
background: #fff8ed;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.vp-alert-center-item strong {
|
||||
color: var(--vp-text);
|
||||
font-size: 20px;
|
||||
line-height: 26px;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.vp-alert-center-item span {
|
||||
color: var(--vp-text-muted);
|
||||
font-size: 13px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.vp-alert-flow-item {
|
||||
min-height: 96px;
|
||||
padding: 12px;
|
||||
@@ -3056,6 +3131,8 @@ button.vp-realtime-command-item:focus-visible {
|
||||
.vp-alert-impact-board,
|
||||
.vp-alert-impact-grid,
|
||||
.vp-alert-dispatch-grid,
|
||||
.vp-alert-center-board,
|
||||
.vp-alert-center-grid,
|
||||
.vp-alert-command-board,
|
||||
.vp-alert-command-grid,
|
||||
.vp-notification-coverage-board,
|
||||
|
||||
@@ -3543,15 +3543,19 @@ test('shows alert rule and notification policy workspace on quality page', async
|
||||
|
||||
render(<App />);
|
||||
|
||||
expect(await screen.findByText('告警事件与通知中心')).toBeInTheDocument();
|
||||
expect(screen.getByText('客户处置:先判断影响车辆和优先级,再通知责任团队,并用实时、轨迹、RAW、里程证据闭环。', { exact: false })).toBeInTheDocument();
|
||||
expect(screen.getByText('链路状态')).toBeInTheDocument();
|
||||
expect(screen.getAllByText('优先队列').length).toBeGreaterThanOrEqual(1);
|
||||
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).toBeGreaterThanOrEqual(1);
|
||||
expect(screen.getAllByText('3 辆').length).toBeGreaterThanOrEqual(1);
|
||||
expect(screen.getAllByText('P0 0 / P1 0').length).toBeGreaterThanOrEqual(1);
|
||||
expect(screen.getAllByText('车辆服务').length).toBeGreaterThan(0);
|
||||
expect(screen.getAllByText('暂无数据来源').length).toBeGreaterThan(0);
|
||||
expect(screen.getAllByText('异常').length).toBeGreaterThan(0);
|
||||
expect(screen.getByText('4 类活跃')).toBeInTheDocument();
|
||||
|
||||
Reference in New Issue
Block a user