feat(platform): add alert dispatch board
This commit is contained in:
@@ -123,6 +123,16 @@ type PriorityIssueRow = QualityIssueRow & {
|
||||
notificationText?: string;
|
||||
};
|
||||
|
||||
type AlertDispatchOwnerRow = {
|
||||
owner: string;
|
||||
activeRuleCount: number;
|
||||
hitCount: number;
|
||||
p0Count: number;
|
||||
primaryIssueType: string;
|
||||
primarySla: string;
|
||||
primaryHitCount: number;
|
||||
};
|
||||
|
||||
function qualityParams(values: Record<string, string>) {
|
||||
const params = new URLSearchParams();
|
||||
if (values?.keyword) params.set('keyword', values.keyword);
|
||||
@@ -217,6 +227,35 @@ function alertRuleRows(summary: QualitySummary, health: OpsHealth | null): Alert
|
||||
return rows;
|
||||
}
|
||||
|
||||
function alertDispatchOwnerRows(rules: AlertRuleRow[]): AlertDispatchOwnerRow[] {
|
||||
const ownerMap = new Map<string, AlertDispatchOwnerRow>();
|
||||
rules.forEach((rule) => {
|
||||
const owner = rule.owner || '未分配';
|
||||
const current = ownerMap.get(owner) ?? {
|
||||
owner,
|
||||
activeRuleCount: 0,
|
||||
hitCount: 0,
|
||||
p0Count: 0,
|
||||
primaryIssueType: rule.issueType,
|
||||
primarySla: rule.sla,
|
||||
primaryHitCount: 0
|
||||
};
|
||||
current.activeRuleCount += rule.count > 0 ? 1 : 0;
|
||||
current.hitCount += rule.count;
|
||||
current.p0Count += rule.count > 0 && rule.level === 'P0' ? 1 : 0;
|
||||
const shouldReplacePrimary = rule.count > current.primaryHitCount || (rule.count === current.primaryHitCount && rule.level === 'P0');
|
||||
if (shouldReplacePrimary) {
|
||||
current.primaryIssueType = rule.issueType;
|
||||
current.primarySla = rule.sla;
|
||||
current.primaryHitCount = rule.count;
|
||||
}
|
||||
ownerMap.set(owner, current);
|
||||
});
|
||||
return [...ownerMap.values()]
|
||||
.filter((row) => row.hitCount > 0 || row.activeRuleCount > 0)
|
||||
.sort((left, right) => right.p0Count - left.p0Count || right.hitCount - left.hitCount || left.owner.localeCompare(right.owner));
|
||||
}
|
||||
|
||||
function issueSla(issueType: string) {
|
||||
return alertRuleTemplates.find((item) => item.issueType === issueType)?.sla ?? '当日闭环';
|
||||
}
|
||||
@@ -627,6 +666,7 @@ export function Quality({
|
||||
const activeRuleCount = notificationPlan?.activeRuleCount ?? rules.filter((item) => item.count > 0).length;
|
||||
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 loadIssues = (values: Record<string, string> = filters, page = pagination.currentPage, pageSize = pagination.pageSize) => {
|
||||
setLoadingIssues(true);
|
||||
@@ -836,6 +876,33 @@ export function Quality({
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
<Card bordered title="告警分派看板" style={{ marginTop: 16 }}>
|
||||
<div className="vp-alert-dispatch-grid">
|
||||
{dispatchOwnerRows.length === 0 ? (
|
||||
<Tag color="green">暂无待分派告警</Tag>
|
||||
) : dispatchOwnerRows.map((item) => (
|
||||
<button
|
||||
key={item.owner}
|
||||
type="button"
|
||||
className="vp-alert-dispatch-item"
|
||||
aria-label={`分派团队 ${item.owner}`}
|
||||
onClick={() => drillIssueType(item.primaryIssueType)}
|
||||
>
|
||||
<div className="vp-alert-dispatch-head">
|
||||
<Tag color={item.p0Count > 0 ? 'red' : 'orange'}>{item.owner}</Tag>
|
||||
<strong>{item.hitCount.toLocaleString()} 条</strong>
|
||||
</div>
|
||||
<div className="vp-alert-policy-detail">
|
||||
活跃规则 {item.activeRuleCount.toLocaleString()} 类 / P0 {item.p0Count.toLocaleString()} 类
|
||||
</div>
|
||||
<div className="vp-alert-policy-detail">
|
||||
主问题:{item.primaryIssueType === 'CAPACITY_RISK' ? '容量与存储风险' : qualityIssueLabel(item.primaryIssueType)}
|
||||
</div>
|
||||
<div className="vp-alert-policy-detail">SLA:{item.primarySla}</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
<Card
|
||||
bordered
|
||||
title={(
|
||||
|
||||
@@ -736,6 +736,46 @@ button.vp-realtime-command-item:focus-visible {
|
||||
line-height: 28px;
|
||||
}
|
||||
|
||||
.vp-alert-dispatch-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.vp-alert-dispatch-item {
|
||||
min-height: 132px;
|
||||
padding: 12px;
|
||||
border: 1px solid var(--vp-border);
|
||||
border-radius: var(--vp-radius);
|
||||
background: #fbfcff;
|
||||
display: grid;
|
||||
align-content: start;
|
||||
gap: 8px;
|
||||
text-align: left;
|
||||
font: inherit;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.vp-alert-dispatch-item:hover,
|
||||
.vp-alert-dispatch-item:focus-visible {
|
||||
border-color: rgba(22, 100, 255, 0.42);
|
||||
background: #f5f9ff;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.vp-alert-dispatch-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.vp-alert-dispatch-head strong {
|
||||
color: var(--vp-text);
|
||||
font-size: 22px;
|
||||
line-height: 28px;
|
||||
}
|
||||
|
||||
.vp-alert-command-board {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) 360px;
|
||||
@@ -1467,6 +1507,7 @@ button.vp-realtime-command-item:focus-visible {
|
||||
.vp-conclusion-grid,
|
||||
.vp-monitor-layout,
|
||||
.vp-alert-flow,
|
||||
.vp-alert-dispatch-grid,
|
||||
.vp-alert-command-board,
|
||||
.vp-alert-command-grid,
|
||||
.vp-risk-grid,
|
||||
|
||||
@@ -3172,6 +3172,10 @@ test('shows alert rule and notification policy workspace on quality page', async
|
||||
expect(screen.getByText('3 类 P0')).toBeInTheDocument();
|
||||
expect(screen.getByText('容量与存储风险')).toBeInTheDocument();
|
||||
expect(screen.getByText('15 分钟恢复')).toBeInTheDocument();
|
||||
expect(screen.getByText('告警分派看板')).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: '分派团队 平台接入' })).toHaveTextContent('2 条');
|
||||
expect(screen.getByRole('button', { name: '分派团队 车辆档案' })).toHaveTextContent('主问题:VIN 缺失');
|
||||
expect(screen.getByRole('button', { name: '分派团队 基础设施' })).toHaveTextContent('SLA:15 分钟恢复');
|
||||
expect(screen.getByText('通知策略')).toBeInTheDocument();
|
||||
expect(screen.getByText('P0 实时中断')).toBeInTheDocument();
|
||||
expect(screen.getByText('接入运维 + 业务责任人')).toBeInTheDocument();
|
||||
@@ -3185,6 +3189,8 @@ test('shows alert rule and notification policy workspace on quality page', async
|
||||
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('P0 实时中断 / 接入运维 + 业务责任人'));
|
||||
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('升级:30 分钟升级'));
|
||||
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('验收:来源恢复并持续 10 分钟,车辆服务可查到实时与历史证据'));
|
||||
fireEvent.click(screen.getByRole('button', { name: '分派团队 车辆档案' }));
|
||||
expect(window.location.hash).toBe('#/alert-events?issueType=VIN_MISSING');
|
||||
});
|
||||
|
||||
test('shows actionable priority queue on quality page', async () => {
|
||||
|
||||
Reference in New Issue
Block a user