feat(platform): add quality notification plan
This commit is contained in:
@@ -2,7 +2,7 @@ import { Button, Card, Col, Form, Row, Select, Space, Table, Tag, Toast } from '
|
||||
import { IconCopy } from '@douyinfe/semi-icons';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { api } from '../api/client';
|
||||
import type { OpsHealth, QualitySummary, QualityIssueRow } from '../api/types';
|
||||
import type { OpsHealth, QualityAlertRule, QualityNotificationPlan, QualityNotificationPolicy, QualitySummary, QualityIssueRow, QualityPriorityIssue } from '../api/types';
|
||||
import { PageHeader } from '../components/PageHeader';
|
||||
import { buildAppHash } from '../domain/appRoute';
|
||||
import { qualityIssueLabel, qualityIssueOptions, qualityProtocolLabel, qualityProtocolOptions } from '../domain/qualityIssue';
|
||||
@@ -95,6 +95,7 @@ const notificationPolicies = [
|
||||
|
||||
type AlertRuleRow = {
|
||||
issueType: string;
|
||||
title?: string;
|
||||
level: string;
|
||||
owner: string;
|
||||
trigger: string;
|
||||
@@ -106,8 +107,14 @@ type AlertRuleRow = {
|
||||
type PriorityIssueRow = QualityIssueRow & {
|
||||
priority: 'P0' | 'P1';
|
||||
actionLabel: string;
|
||||
actionDetail?: string;
|
||||
sla: string;
|
||||
vehicleLabel: string;
|
||||
realtimeHash?: string;
|
||||
historyHash?: string;
|
||||
rawHash?: string;
|
||||
vehicleHash?: string;
|
||||
notificationText?: string;
|
||||
};
|
||||
|
||||
function qualityParams(values: Record<string, string>) {
|
||||
@@ -293,6 +300,21 @@ function ruleStatusColor(count: number, level: string): 'green' | 'orange' | 're
|
||||
return 'grey';
|
||||
}
|
||||
|
||||
function normalizeAlertRules(rows?: QualityAlertRule[]): AlertRuleRow[] | null {
|
||||
if (!Array.isArray(rows)) return null;
|
||||
return rows;
|
||||
}
|
||||
|
||||
function normalizeNotificationPolicies(rows?: QualityNotificationPolicy[]): QualityNotificationPolicy[] | null {
|
||||
if (!Array.isArray(rows)) return null;
|
||||
return rows;
|
||||
}
|
||||
|
||||
function normalizePriorityIssues(rows?: QualityPriorityIssue[]): PriorityIssueRow[] | null {
|
||||
if (!Array.isArray(rows)) return null;
|
||||
return rows;
|
||||
}
|
||||
|
||||
async function copyText(value: string, label: string) {
|
||||
const text = value.trim();
|
||||
if (!text) {
|
||||
@@ -333,16 +355,18 @@ export function Quality({
|
||||
const [issues, setIssues] = useState<QualityIssueRow[]>([]);
|
||||
const [summary, setSummary] = useState<QualitySummary>(emptySummary);
|
||||
const [health, setHealth] = useState<OpsHealth | null>(null);
|
||||
const [notificationPlan, setNotificationPlan] = useState<QualityNotificationPlan | null>(null);
|
||||
const [loadingIssues, setLoadingIssues] = useState(true);
|
||||
const [loadingSummary, setLoadingSummary] = useState(true);
|
||||
const [loadingHealth, setLoadingHealth] = useState(true);
|
||||
const [filters, setFilters] = useState<Record<string, string>>(initialFilters);
|
||||
const [pagination, setPagination] = useState({ currentPage: 1, pageSize: 20, total: 0 });
|
||||
const primaryIssueType = summary.issueTypes[0]?.name;
|
||||
const rules = alertRuleRows(summary, health);
|
||||
const activeRuleCount = rules.filter((item) => item.count > 0).length;
|
||||
const p0RuleCount = rules.filter((item) => item.count > 0 && item.level === 'P0').length;
|
||||
const priorityRows = priorityIssueRows(issues);
|
||||
const rules = normalizeAlertRules(notificationPlan?.rules) ?? alertRuleRows(summary, health);
|
||||
const policies = normalizeNotificationPolicies(notificationPlan?.policies) ?? notificationPolicies;
|
||||
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 loadIssues = (values: Record<string, string> = filters, page = pagination.currentPage, pageSize = pagination.pageSize) => {
|
||||
setLoadingIssues(true);
|
||||
@@ -376,12 +400,26 @@ export function Quality({
|
||||
.catch((error: Error) => Toast.error(error.message))
|
||||
.finally(() => setLoadingHealth(false));
|
||||
};
|
||||
const loadNotificationPlan = (values: Record<string, string> = filters, pageSize = pagination.pageSize) => {
|
||||
const params = qualityParams(values);
|
||||
params.set('limit', String(pageSize));
|
||||
api.qualityNotificationPlan(params)
|
||||
.then((plan) => {
|
||||
if (Array.isArray(plan.rules) && Array.isArray(plan.policies) && Array.isArray(plan.priorityIssues)) {
|
||||
setNotificationPlan(plan);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
setNotificationPlan(null);
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setFilters(initialFilters);
|
||||
loadSummary(initialFilters);
|
||||
loadIssues(initialFilters, 1, pagination.pageSize);
|
||||
loadHealth();
|
||||
loadNotificationPlan(initialFilters, pagination.pageSize);
|
||||
}, [JSON.stringify(initialFilters)]);
|
||||
|
||||
const applyFilters = (nextFilters: Record<string, string>) => {
|
||||
@@ -389,6 +427,7 @@ export function Quality({
|
||||
onFiltersChange?.(nextFilters);
|
||||
loadSummary(nextFilters);
|
||||
loadIssues(nextFilters, 1, pagination.pageSize);
|
||||
loadNotificationPlan(nextFilters, pagination.pageSize);
|
||||
};
|
||||
|
||||
const drillPrimaryIssue = () => {
|
||||
@@ -454,7 +493,7 @@ export function Quality({
|
||||
Toast.warning('当前没有可复制的优先告警');
|
||||
return;
|
||||
}
|
||||
copyText(priorityIssueDigestText(priorityRows, summary), '优先队列通知汇总');
|
||||
copyText(priorityIssueDigestText(priorityRows, notificationPlan?.summary ?? summary), '优先队列通知汇总');
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -551,7 +590,7 @@ export function Quality({
|
||||
<Button size="small" disabled={!lookup.key || !onOpenRealtime} onClick={() => openIssueRealtime(row)}>实时定位</Button>
|
||||
<Button size="small" disabled={!lookup.key || !onOpenHistory} onClick={() => openIssueHistory(row)}>轨迹证据</Button>
|
||||
<Button size="small" disabled={!lookup.key || !onOpenRaw} onClick={() => openIssueRaw(row)}>RAW证据</Button>
|
||||
<Button size="small" onClick={() => copyText(priorityIssueNotificationText(row), '告警通知')}>复制通知</Button>
|
||||
<Button size="small" onClick={() => copyText(row.notificationText || priorityIssueNotificationText(row), '告警通知')}>复制通知</Button>
|
||||
<Button size="small" disabled={!lookup.key} onClick={() => onOpenVehicle(lookup.key, row.protocol)}>进入车辆服务</Button>
|
||||
</Space>
|
||||
);
|
||||
@@ -567,7 +606,7 @@ export function Quality({
|
||||
dataSource={rules}
|
||||
rowKey="issueType"
|
||||
columns={[
|
||||
{ title: '规则', render: (_: unknown, row: AlertRuleRow) => row.issueType === 'CAPACITY_RISK' ? '容量与存储风险' : qualityIssueLabel(row.issueType) },
|
||||
{ title: '规则', render: (_: unknown, row: AlertRuleRow) => row.title || (row.issueType === 'CAPACITY_RISK' ? '容量与存储风险' : qualityIssueLabel(row.issueType)) },
|
||||
{ title: '级别', width: 90, render: (_: unknown, row: AlertRuleRow) => <Tag color={ruleStatusColor(row.count, row.level)}>{row.level}</Tag> },
|
||||
{ title: '当前命中', width: 110, render: (_: unknown, row: AlertRuleRow) => row.count.toLocaleString() },
|
||||
{ title: '触发条件', dataIndex: 'trigger' },
|
||||
@@ -577,7 +616,7 @@ export function Quality({
|
||||
</Card>
|
||||
<Card bordered title="通知策略">
|
||||
<div className="vp-notification-policy-list">
|
||||
{notificationPolicies.map((item) => (
|
||||
{policies.map((item) => (
|
||||
<div key={item.name} className="vp-notification-policy">
|
||||
<div>
|
||||
<Space>
|
||||
|
||||
Reference in New Issue
Block a user