feat(platform): add notification execution matrix
This commit is contained in:
@@ -59,6 +59,16 @@ type SlaEscalationRow = {
|
||||
statusColor: 'red' | 'orange' | 'green' | 'grey';
|
||||
};
|
||||
|
||||
type NotificationExecutionRow = {
|
||||
key: string;
|
||||
issue: QualityPriorityIssue;
|
||||
rule?: QualityAlertRule;
|
||||
policy?: QualityNotificationPolicy;
|
||||
slaRow: SlaEscalationRow;
|
||||
evidenceText: string;
|
||||
evidenceColor: 'green' | 'orange';
|
||||
};
|
||||
|
||||
const notificationExportColumns: CsvColumn<NotificationExportRow>[] = [
|
||||
{ title: '类型', value: (row) => row.category },
|
||||
{ title: '名称', value: (row) => row.name },
|
||||
@@ -163,6 +173,69 @@ function slaEscalationReport(rows: SlaEscalationRow[], release?: string) {
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
function notificationExecutionRows(
|
||||
issues: QualityPriorityIssue[],
|
||||
rules: QualityAlertRule[],
|
||||
policies: QualityNotificationPolicy[],
|
||||
nowMs = Date.now()
|
||||
): NotificationExecutionRow[] {
|
||||
const slaByIssue = new Map(slaEscalationRows(issues, policies, nowMs).map((row) => [
|
||||
`${row.issue.priority}-${row.issue.protocol}-${row.issue.issueType}-${row.issue.vehicleLabel}-${row.issue.lastSeen}`,
|
||||
row
|
||||
]));
|
||||
return issues.map((issue) => {
|
||||
const key = `${issue.priority}-${issue.protocol}-${issue.issueType}-${issue.vehicleLabel}-${issue.lastSeen}`;
|
||||
const rule = rules.find((item) => item.issueType === issue.issueType);
|
||||
const policy = policyForPriority(issue, policies);
|
||||
const slaRow = slaByIssue.get(key) ?? {
|
||||
issue,
|
||||
policy,
|
||||
status: 'unknown',
|
||||
statusLabel: '待确认时间',
|
||||
statusColor: 'grey'
|
||||
} as SlaEscalationRow;
|
||||
return {
|
||||
key,
|
||||
issue,
|
||||
rule,
|
||||
policy,
|
||||
slaRow,
|
||||
evidenceText: evidenceSummary(issue),
|
||||
evidenceColor: evidenceColor(issue)
|
||||
};
|
||||
}).sort((left, right) => {
|
||||
const priorityWeight = (value: string) => value === 'P0' ? 2 : value === 'P1' ? 1 : 0;
|
||||
const statusWeight: Record<SlaEscalationRow['status'], number> = { overdue: 3, due_soon: 2, tracking: 1, unknown: 0 };
|
||||
return priorityWeight(right.issue.priority) - priorityWeight(left.issue.priority)
|
||||
|| statusWeight[right.slaRow.status] - statusWeight[left.slaRow.status]
|
||||
|| left.issue.vehicleLabel.localeCompare(right.issue.vehicleLabel);
|
||||
});
|
||||
}
|
||||
|
||||
function notificationExecutionMatrixReport(rows: NotificationExecutionRow[], release?: string) {
|
||||
const p0Count = rows.filter((row) => row.issue.priority === 'P0').length;
|
||||
const incompleteEvidence = rows.filter((row) => countEvidenceLinks(row.issue) < 4).length;
|
||||
const overdueCount = rows.filter((row) => row.slaRow.status === 'overdue').length;
|
||||
return [
|
||||
'【通知执行矩阵】',
|
||||
`运行版本:${release?.trim() || '-'}`,
|
||||
`执行态势:P0 ${p0Count.toLocaleString()} / 已超时 ${overdueCount.toLocaleString()} / 证据不完整 ${incompleteEvidence.toLocaleString()}`,
|
||||
'',
|
||||
...(rows.length > 0 ? rows.slice(0, 12).map((row, index) => [
|
||||
`${index + 1}. [${row.issue.priority}] ${row.issue.vehicleLabel}`,
|
||||
` 问题:${qualityIssueLabel(row.issue.issueType)} / ${row.issue.protocol}`,
|
||||
` 规则:${row.rule ? `${ruleTitle(row.rule)} / ${row.rule.owner}` : '-'}`,
|
||||
` 通知:${row.policy ? `${row.policy.target} / ${row.policy.channel}` : '-'}`,
|
||||
` 升级:${row.policy ? formatEscalationMinutes(row.policy.escalationMinutes) : '-'}`,
|
||||
` 状态:${row.slaRow.statusLabel}`,
|
||||
` 证据:${row.evidenceText}`,
|
||||
` 动作:${row.issue.actionLabel} - ${row.issue.actionDetail || '-'}`,
|
||||
` 车辆服务:${window.location.origin}${window.location.pathname}${row.issue.vehicleHash}`,
|
||||
` RAW证据:${window.location.origin}${window.location.pathname}${row.issue.rawHash}`
|
||||
].join('\n')) : ['暂无待执行通知'])
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
function notificationRulesRunbook(plan: QualityNotificationPlan | null, release?: string) {
|
||||
const rules = plan?.rules ?? [];
|
||||
const policies = plan?.policies ?? [];
|
||||
@@ -391,6 +464,7 @@ export function NotificationRules() {
|
||||
const overdueCount = slaRows.filter((row) => row.status === 'overdue').length;
|
||||
const dueSoonCount = slaRows.filter((row) => row.status === 'due_soon').length;
|
||||
const nextSlaRow = slaRows[0];
|
||||
const executionRows = notificationExecutionRows(priorityIssues, rules, policies);
|
||||
const exportNotificationRules = () => {
|
||||
const rows = notificationExportRows(plan, release);
|
||||
if (rows.length === 0) {
|
||||
@@ -472,6 +546,40 @@ export function NotificationRules() {
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card
|
||||
bordered
|
||||
loading={loading}
|
||||
title={<Space><span>通知执行矩阵</span><Button size="small" aria-label="复制通知执行矩阵" icon={<IconCopy />} disabled={executionRows.length === 0} onClick={() => copyText(notificationExecutionMatrixReport(executionRows, release), '通知执行矩阵')}>复制执行矩阵</Button></Space>}
|
||||
style={{ marginTop: 16 }}
|
||||
>
|
||||
<Table<NotificationExecutionRow>
|
||||
pagination={false}
|
||||
dataSource={executionRows.slice(0, 10)}
|
||||
rowKey={(row?: NotificationExecutionRow) => row?.key ?? ''}
|
||||
columns={[
|
||||
{ title: '优先级', width: 90, render: (_: unknown, row: NotificationExecutionRow) => <Tag color={row.issue.priority === 'P0' ? 'red' : 'orange'}>{row.issue.priority}</Tag> },
|
||||
{ title: '车辆', width: 220, render: (_: unknown, row: NotificationExecutionRow) => row.issue.vehicleLabel },
|
||||
{ title: '问题', width: 130, render: (_: unknown, row: NotificationExecutionRow) => qualityIssueLabel(row.issue.issueType) },
|
||||
{ title: '触发规则', width: 180, render: (_: unknown, row: NotificationExecutionRow) => row.rule ? `${ruleTitle(row.rule)} / ${row.rule.owner}` : '-' },
|
||||
{ title: '通知对象', width: 210, render: (_: unknown, row: NotificationExecutionRow) => row.policy?.target || '-' },
|
||||
{ title: '渠道', width: 150, render: (_: unknown, row: NotificationExecutionRow) => row.policy?.channel || '-' },
|
||||
{ title: '升级窗口', width: 120, render: (_: unknown, row: NotificationExecutionRow) => row.policy ? formatEscalationMinutes(row.policy.escalationMinutes) : '-' },
|
||||
{ title: 'SLA状态', width: 150, render: (_: unknown, row: NotificationExecutionRow) => <Tag color={row.slaRow.statusColor}>{row.slaRow.statusLabel}</Tag> },
|
||||
{ title: '证据', width: 110, render: (_: unknown, row: NotificationExecutionRow) => <Tag color={row.evidenceColor}>{row.evidenceText}</Tag> },
|
||||
{
|
||||
title: '操作',
|
||||
render: (_: unknown, row: NotificationExecutionRow) => (
|
||||
<Space wrap>
|
||||
<Button size="small" disabled={!row.issue.vehicleHash} onClick={() => navigateHash(row.issue.vehicleHash)}>车辆服务</Button>
|
||||
<Button size="small" disabled={!row.issue.rawHash} onClick={() => navigateHash(row.issue.rawHash)}>RAW证据</Button>
|
||||
<Button size="small" icon={<IconCopy />} onClick={() => copyText(row.issue.notificationText, '告警通知')}>复制通知</Button>
|
||||
</Space>
|
||||
)
|
||||
}
|
||||
]}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
<Card
|
||||
bordered
|
||||
loading={loading}
|
||||
|
||||
Reference in New Issue
Block a user