feat(web): refine Semi UI alert center
This commit is contained in:
@@ -157,9 +157,12 @@ test('clears old alert rows and inspector evidence when the event scope changes'
|
||||
expect(screen.getByRole('button', { name: '查看未读通知,共 0 条' }).closest('[role="listitem"]')).toHaveClass('is-warning', 'is-secondary');
|
||||
expect(view.container.querySelector('.v2-alert-event-table.semi-table-wrapper')).toBeInTheDocument();
|
||||
expect(view.container.querySelector('.v2-alert-event-table.semi-table-wrapper')).not.toHaveClass('is-compact');
|
||||
expect(screen.getByRole('columnheader', { name: '触发 / 阈值' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('heading', { level: 5, name: '当前事件队列' })).toBeInTheDocument();
|
||||
expect(screen.getByText('1 条待处置 · 共 1 条')).toBeInTheDocument();
|
||||
expect(screen.getByRole('columnheader', { name: '触发证据' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('columnheader', { name: '位置' })).toBeInTheDocument();
|
||||
expect(view.container.querySelector('.v2-alert-event-table .v2-alert-protocol-tag.semi-tag')).toHaveTextContent('JT/T 808');
|
||||
expect(view.container.querySelector('.v2-alert-event-table input[type="radio"]')).not.toBeInTheDocument();
|
||||
expect(view.container.querySelector('.v2-alert-table-scroll > table')).not.toBeInTheDocument();
|
||||
expect(view.container.querySelector('.v2-alert-mobile-list')).not.toBeInTheDocument();
|
||||
expect(view.container.querySelector('time[datetime="2026-07-16T04:00:00Z"]')).toHaveTextContent('07-16 12:00:00');
|
||||
@@ -172,7 +175,8 @@ test('clears old alert rows and inspector evidence when the event scope changes'
|
||||
expect(view.container.querySelector('.v2-alert-inspector.semi-card')).toBeInTheDocument();
|
||||
expect(view.container.querySelector('.v2-alert-workspace')).toHaveClass('is-inspector-open');
|
||||
expect(view.container.querySelector('.v2-alert-event-table.semi-table-wrapper')).toHaveClass('is-compact');
|
||||
expect(screen.queryByRole('columnheader', { name: '触发 / 阈值' })).not.toBeInTheDocument();
|
||||
expect(desktopAlertRow).toHaveClass('is-severity-major', 'is-selected');
|
||||
expect(screen.queryByRole('columnheader', { name: '触发证据' })).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole('columnheader', { name: '位置' })).not.toBeInTheDocument();
|
||||
expect(screen.getByText('已展开处置详情 · 点击其他事件快速切换')).toBeInTheDocument();
|
||||
expect(desktopAlertRow).toHaveAttribute('aria-expanded', 'true');
|
||||
@@ -194,7 +198,7 @@ test('clears old alert rows and inspector evidence when the event scope changes'
|
||||
fireEvent.click(screen.getByRole('button', { name: '关闭告警详情' }));
|
||||
expect(view.container.querySelector('.v2-alert-inspector')).not.toBeInTheDocument();
|
||||
expect(view.container.querySelector('.v2-alert-event-table.semi-table-wrapper')).not.toHaveClass('is-compact');
|
||||
expect(screen.getByRole('columnheader', { name: '触发 / 阈值' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('columnheader', { name: '触发证据' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('columnheader', { name: '位置' })).toBeInTheDocument();
|
||||
expect(desktopAlertRow).toHaveAttribute('aria-expanded', 'false');
|
||||
fireEvent.click(desktopAlertRow);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { IconAlarm, IconBell, IconChevronRight, IconClose, IconFilter, IconPlus, IconRefresh, IconSearch, IconSetting } from '@douyinfe/semi-icons';
|
||||
import { Button, Card, CardGroup, Collapse, Descriptions, Empty, Input, Radio, Select, Spin, Table, Tag, TextArea, Timeline, Typography } from '@douyinfe/semi-ui';
|
||||
import { Button, Card, CardGroup, Collapse, Descriptions, Empty, Input, Select, Spin, Table, Tag, TextArea, Timeline, Typography } from '@douyinfe/semi-ui';
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { FormEvent, memo, useCallback, useEffect, useMemo, useState, type ReactNode } from 'react';
|
||||
import { Link, useSearchParams } from 'react-router-dom';
|
||||
@@ -32,13 +32,12 @@ const PROTOCOLS = ['GB32960', 'JT808', 'YUTONG_MQTT'];
|
||||
const NUMERIC_OPERATORS = ['gt', 'gte', 'lt', 'lte', 'eq', 'neq', 'between', 'outside'];
|
||||
const BOOLEAN_OPERATORS = ['eq', 'neq', 'changed'];
|
||||
const COMPACT_ALERT_COLUMN_WIDTHS: Record<string, number> = {
|
||||
selection: 40,
|
||||
severity: 80,
|
||||
plate: 132,
|
||||
ruleName: 148,
|
||||
protocol: 100,
|
||||
triggeredAt: 120,
|
||||
status: 78
|
||||
severity: 70,
|
||||
plate: 116,
|
||||
ruleName: 124,
|
||||
protocol: 86,
|
||||
triggeredAt: 110,
|
||||
status: 72
|
||||
};
|
||||
type RuleTemplate = { id: string; title: string; summary: string; draft: AlertRuleInput };
|
||||
|
||||
@@ -90,24 +89,20 @@ function AlertTime({ value, className = '' }: { value?: string; className?: stri
|
||||
const AlertEventTable = memo(function AlertEventTable({ rows, selectedID, compact, onSelect }: { rows: AlertEvent[]; selectedID: string; compact: boolean; onSelect: (id: string) => void }) {
|
||||
const columns = useMemo(() => {
|
||||
const allColumns = [
|
||||
{ title: '级别', dataIndex: 'severity', width: 78, render: (_: AlertEvent['severity'], event: AlertEvent) => <SeverityTag severity={event.severity} /> },
|
||||
{
|
||||
title: '', dataIndex: 'selection', width: 42,
|
||||
render: (_: unknown, event: AlertEvent) => <Radio name="alert-event" checked={selectedID === event.id} onChange={() => onSelect(event.id)} aria-label={`选择 ${event.plate || event.vin} ${event.ruleName}`} />
|
||||
},
|
||||
{ title: '严重程度', dataIndex: 'severity', width: 90, render: (_: AlertEvent['severity'], event: AlertEvent) => <SeverityTag severity={event.severity} /> },
|
||||
{
|
||||
title: '车牌 / VIN', dataIndex: 'plate', width: 142,
|
||||
title: '车牌 / VIN', dataIndex: 'plate', width: 132,
|
||||
render: (_: string, event: AlertEvent) => <div className="v2-alert-event-vehicle"><strong>{event.plate || '未绑定车牌'}</strong><small>{event.vin}</small></div>
|
||||
},
|
||||
{ title: '规则', dataIndex: 'ruleName', width: 170, render: (value: string) => <span className="v2-alert-event-rule" title={value}>{value}</span> },
|
||||
{ title: '协议', dataIndex: 'protocol', width: 112, render: (value: string) => <ProtocolTag className="v2-alert-protocol-tag" protocol={value} compact /> },
|
||||
{ title: '触发时间', dataIndex: 'triggeredAt', width: 132, render: (value: string) => <AlertTime value={value} /> },
|
||||
{ title: '状态', dataIndex: 'status', width: 90, render: (_: AlertEvent['status'], event: AlertEvent) => <StatusTag status={event.status} /> },
|
||||
{ title: '规则', dataIndex: 'ruleName', width: 146, render: (value: string) => <span className="v2-alert-event-rule" title={value}>{value}</span> },
|
||||
{ title: '协议', dataIndex: 'protocol', width: 98, render: (value: string) => <ProtocolTag className="v2-alert-protocol-tag" protocol={value} compact /> },
|
||||
{ title: '触发时间', dataIndex: 'triggeredAt', width: 124, render: (value: string) => <AlertTime value={value} /> },
|
||||
{ title: '状态', dataIndex: 'status', width: 78, render: (_: AlertEvent['status'], event: AlertEvent) => <StatusTag status={event.status} /> },
|
||||
{
|
||||
title: '触发 / 阈值', dataIndex: 'evidence', width: 164,
|
||||
title: '触发证据', dataIndex: 'evidence', width: 150,
|
||||
render: (_: unknown, event: AlertEvent) => <div className="v2-alert-event-evidence"><strong>{alertValue(event)}</strong><small>{thresholdText(event)}</small></div>
|
||||
},
|
||||
{ title: '位置', dataIndex: 'location', width: 180, render: (value: string) => <span className="v2-alert-event-location" title={value}>{value || '—'}</span> },
|
||||
{ title: '位置', dataIndex: 'location', width: 164, render: (value: string) => <span className="v2-alert-event-location" title={value}>{value || '—'}</span> },
|
||||
];
|
||||
return compact
|
||||
? allColumns
|
||||
@@ -124,7 +119,7 @@ const AlertEventTable = memo(function AlertEventTable({ rows, selectedID, compac
|
||||
pagination={false}
|
||||
empty={null}
|
||||
onRow={(event) => event ? detailTriggerRow({
|
||||
className: selectedID === event.id ? 'is-selected' : '',
|
||||
className: [`is-severity-${event.severity}`, selectedID === event.id ? 'is-selected' : ''].filter(Boolean).join(' '),
|
||||
expanded: selectedID === event.id,
|
||||
label: `查看 ${event.plate || event.vin} ${event.ruleName} 告警详情`,
|
||||
testId: `alert-row-${event.id}`,
|
||||
@@ -354,9 +349,9 @@ function EventWorkspace({ filters, draft, setDraft, setFilters, rules, unread, e
|
||||
<div className={`v2-alert-workspace${selectedID && !mobileLayout ? ' is-inspector-open' : ''}`}>
|
||||
<Card className="v2-alert-table-card" bodyStyle={{ padding: 0 }}>
|
||||
<WorkspacePanelHeader
|
||||
title="告警事件"
|
||||
title="当前事件队列"
|
||||
description={selectedID && !mobileLayout ? '已展开处置详情 · 点击其他事件快速切换' : '点击事件查看触发证据、处理进度与处置动作'}
|
||||
meta={`共 ${(events.data?.total ?? 0).toLocaleString('zh-CN')} 条`}
|
||||
meta={`${Number(sums?.unprocessed ?? 0).toLocaleString('zh-CN')} 条待处置 · 共 ${(events.data?.total ?? 0).toLocaleString('zh-CN')} 条`}
|
||||
actions={<Button theme="borderless" icon={<IconRefresh />} loading={events.isFetching} onClick={() => Promise.all([events.refetch(), summary.refetch(), ...(selectedID ? [detail.refetch()] : [])])}>刷新</Button>}
|
||||
/>
|
||||
<div
|
||||
|
||||
@@ -198,7 +198,8 @@ describe('V2 production entry', () => {
|
||||
expect(corePageSources.AlertsPage).toContain('<Input');
|
||||
expect(corePageSources.AlertsPage).toContain('<Select');
|
||||
expect(corePageSources.AlertsPage).toContain('<TextArea');
|
||||
expect(corePageSources.AlertsPage).toContain('<Radio');
|
||||
expect(corePageSources.AlertsPage).not.toContain('<Radio');
|
||||
expect(corePageSources.AlertsPage).toContain('is-severity-${event.severity}');
|
||||
expect(corePageSources.AlertsPage).toContain('<Card key={event.id} className={`v2-alert-mobile-card');
|
||||
expect(corePageSources.AlertsPage).toContain('<WorkspaceMetricRail');
|
||||
expect(corePageSources.AlertsPage).toContain('className="v2-alert-metric-rail"');
|
||||
|
||||
@@ -8402,7 +8402,7 @@
|
||||
* command rail and reserves the remaining height for the operational list.
|
||||
*/
|
||||
.v2-alert-page {
|
||||
gap: 12px;
|
||||
gap: 10px;
|
||||
padding: 14px 18px 18px;
|
||||
background: #f6f8fb;
|
||||
}
|
||||
@@ -8503,6 +8503,7 @@
|
||||
|
||||
.v2-alert-filter-card.semi-card {
|
||||
overflow: visible;
|
||||
border-color: #d8e1ec;
|
||||
}
|
||||
|
||||
.v2-alert-filter-card > .semi-card-body > .v2-workspace-filter-heading {
|
||||
@@ -8510,24 +8511,36 @@
|
||||
}
|
||||
|
||||
.v2-alert-filter.v2-alert-filter-primary {
|
||||
min-height: 70px;
|
||||
min-height: 54px;
|
||||
grid-template-columns: minmax(250px, 1.45fr) minmax(122px, .62fr) minmax(136px, .68fr) auto auto auto;
|
||||
gap: 10px;
|
||||
padding: 11px 13px;
|
||||
align-items: center;
|
||||
padding: 8px 10px;
|
||||
}
|
||||
|
||||
.v2-alert-filter.v2-alert-filter-primary label {
|
||||
position: relative;
|
||||
gap: 5px;
|
||||
color: #66778e;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.v2-alert-filter.v2-alert-filter-primary label > span {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0 0 0 0);
|
||||
clip-path: inset(50%);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.v2-alert-filter.v2-alert-filter-primary label > div,
|
||||
.v2-alert-filter.v2-alert-filter-primary .semi-select,
|
||||
.v2-alert-filter.v2-alert-filter-primary .semi-input-wrapper,
|
||||
.v2-alert-filter.v2-alert-filter-primary > .semi-button {
|
||||
min-height: 38px;
|
||||
min-height: 36px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
@@ -8588,7 +8601,7 @@
|
||||
}
|
||||
|
||||
.v2-alert-workspace.is-inspector-open {
|
||||
grid-template-columns: minmax(680px, 1fr) 390px;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(360px, 390px);
|
||||
}
|
||||
|
||||
.v2-alert-table-card > .semi-card-body > .v2-workspace-panel-header {
|
||||
@@ -8646,12 +8659,51 @@
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.v2-alert-table-scroll .v2-alert-event-table .semi-table-tbody > .semi-table-row {
|
||||
position: relative;
|
||||
transition: background-color .16s ease, box-shadow .16s ease;
|
||||
}
|
||||
|
||||
.v2-alert-table-scroll .v2-alert-event-table .semi-table-tbody > .semi-table-row::before {
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
top: 7px;
|
||||
bottom: 7px;
|
||||
left: 0;
|
||||
width: 3px;
|
||||
border-radius: 0 4px 4px 0;
|
||||
background: #c2cad5;
|
||||
content: "";
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.v2-alert-table-scroll .v2-alert-event-table .semi-table-tbody > .semi-table-row.is-severity-critical::before {
|
||||
background: #e5484d;
|
||||
}
|
||||
|
||||
.v2-alert-table-scroll .v2-alert-event-table .semi-table-tbody > .semi-table-row.is-severity-major::before {
|
||||
background: #ed8d32;
|
||||
}
|
||||
|
||||
.v2-alert-table-scroll .v2-alert-event-table .semi-table-tbody > .semi-table-row.is-severity-minor::before {
|
||||
background: #d9a31d;
|
||||
}
|
||||
|
||||
.v2-alert-table-scroll .v2-alert-event-table .semi-table-tbody > .semi-table-row:hover {
|
||||
background: #f7faff;
|
||||
}
|
||||
|
||||
.v2-alert-table-scroll .v2-alert-event-table .semi-table-tbody > .semi-table-row.is-selected {
|
||||
background: #eef5ff;
|
||||
box-shadow: inset 0 0 0 1px rgba(18, 104, 243, .16);
|
||||
}
|
||||
|
||||
.v2-alert-event-table.semi-table-wrapper {
|
||||
min-width: 1102px;
|
||||
min-width: 970px;
|
||||
}
|
||||
|
||||
.v2-alert-event-table.is-compact.semi-table-wrapper {
|
||||
min-width: 758px;
|
||||
min-width: 660px;
|
||||
}
|
||||
|
||||
.v2-alert-event-evidence {
|
||||
@@ -9529,10 +9581,15 @@
|
||||
padding: 5px 8px;
|
||||
}
|
||||
|
||||
.v2-alert-table-card .v2-workspace-panel-copy > .semi-typography {
|
||||
.v2-alert-table-card .v2-workspace-panel-copy > .semi-typography:not(h5) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.v2-alert-table-card .v2-workspace-panel-copy > h5.semi-typography {
|
||||
display: block;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.v2-alert-table-scroll.is-mobile-scroll {
|
||||
min-height: 0;
|
||||
flex: 1 1 0;
|
||||
@@ -25382,8 +25439,8 @@
|
||||
}
|
||||
|
||||
.v2-alert-filter.v2-alert-filter-primary {
|
||||
min-height: 66px;
|
||||
padding: 9px 13px;
|
||||
min-height: 52px;
|
||||
padding: 7px 10px;
|
||||
}
|
||||
|
||||
.v2-alert-filter.v2-alert-filter-primary label > div,
|
||||
@@ -25417,13 +25474,18 @@
|
||||
}
|
||||
|
||||
.v2-alert-table-scroll .v2-alert-event-table.semi-table-wrapper .semi-table-tbody > .semi-table-row > .semi-table-row-cell {
|
||||
height: 56px;
|
||||
padding: 8px 10px;
|
||||
height: 52px;
|
||||
padding: 7px 10px;
|
||||
color: #405269;
|
||||
font-size: 12.5px;
|
||||
transition: background-color .16s ease;
|
||||
}
|
||||
|
||||
.v2-alert-event-table .semi-table {
|
||||
width: 100%;
|
||||
table-layout: fixed;
|
||||
}
|
||||
|
||||
.v2-alert-table-scroll .v2-alert-event-table.semi-table-wrapper .semi-table-tbody > .semi-table-row:hover > .semi-table-row-cell {
|
||||
background: #f6f9fe;
|
||||
}
|
||||
@@ -25445,7 +25507,7 @@
|
||||
}
|
||||
|
||||
.v2-alert-workspace.is-inspector-open {
|
||||
grid-template-columns: minmax(650px, 1fr) 420px;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(360px, 390px);
|
||||
}
|
||||
|
||||
.v2-alert-workspace.is-inspector-open .v2-alert-event-table.is-compact.semi-table-wrapper {
|
||||
|
||||
Reference in New Issue
Block a user