feat: unify alert operations queue
This commit is contained in:
@@ -141,7 +141,7 @@ test('clears old alert rows and inspector evidence when the event scope changes'
|
||||
const view = render(<QueryClientProvider client={client}><MemoryRouter future={ROUTER_FUTURE} initialEntries={['/alerts?keyword=OLDVIN']}><AlertsPage /></MemoryRouter></QueryClientProvider>);
|
||||
|
||||
expect((await screen.findAllByText('旧告警车牌')).length).toBeGreaterThan(0);
|
||||
for (const className of ['v2-alert-filter-card', 'v2-alert-kpis-card', 'v2-alert-context-card', 'v2-alert-table-card']) {
|
||||
for (const className of ['v2-alert-filter-card', 'v2-alert-metric-rail', 'v2-alert-table-card']) {
|
||||
expect(view.container.querySelector(`.${className}.semi-card`)).toBeInTheDocument();
|
||||
}
|
||||
expect(view.container.querySelector('.v2-alert-navigation')).toBeInTheDocument();
|
||||
@@ -152,9 +152,9 @@ test('clears old alert rows and inspector evidence when the event scope changes'
|
||||
expect(screen.queryByRole('heading', { level: 2, name: '告警中心' })).not.toBeInTheDocument();
|
||||
expect(view.container.querySelector('.v2-alert-inspector')).not.toBeInTheDocument();
|
||||
expect(view.container.querySelector('.v2-alert-workspace')).not.toHaveClass('is-inspector-open');
|
||||
expect(view.container.querySelectorAll('.v2-alert-kpis .v2-metric-action')).toHaveLength(4);
|
||||
expect(view.container.querySelectorAll('.v2-alert-secondary-states .semi-button')).toHaveLength(3);
|
||||
expect(screen.getByRole('button', { name: '查看未读通知,共 0 条' })).toHaveClass('is-notice');
|
||||
expect(view.container.querySelectorAll('.v2-alert-metric-rail .v2-workspace-metric-action.semi-button')).toHaveLength(4);
|
||||
expect(view.container.querySelectorAll('.v2-alert-metric-context-actions .semi-button')).toHaveLength(3);
|
||||
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();
|
||||
@@ -254,11 +254,11 @@ test('renders only selectable Semi alert cards on mobile', async () => {
|
||||
expect(view.container.querySelector('.v2-alert-table-scroll.is-mobile-scroll')).toHaveAttribute('tabindex', '0');
|
||||
expect(view.container.querySelector('.v2-alert-table-scroll.is-mobile-scroll')).toHaveAttribute('aria-label', '告警事件列表,可上下滚动');
|
||||
expect(view.container.querySelector('.v2-alert-event-table')).not.toBeInTheDocument();
|
||||
const mobileMetrics = Array.from(view.container.querySelectorAll('.v2-alert-kpis .v2-metric-action'));
|
||||
expect(mobileMetrics).toHaveLength(3);
|
||||
expect(mobileMetrics.map((metric) => metric.textContent)).toEqual(['待处置1', '处理中0', '全部事件1']);
|
||||
const mobileMetrics = Array.from(view.container.querySelectorAll('.v2-alert-metric-rail .v2-workspace-metric-action'));
|
||||
expect(mobileMetrics).toHaveLength(2);
|
||||
expect(mobileMetrics.map((metric) => metric.textContent)).toEqual(['待处置1需要核验', '全部事件1当前范围']);
|
||||
expect(screen.getByRole('button', { name: '筛选待处置,共 1 条' })).toHaveAttribute('aria-pressed', 'false');
|
||||
expect(view.container.querySelector('.v2-alert-secondary-states')).not.toBeInTheDocument();
|
||||
expect(view.container.querySelector('.v2-alert-metric-context-actions')).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole('button', { name: /查看未读通知/ })).not.toBeInTheDocument();
|
||||
expect(screen.getByRole('tab', { name: /站内通知/ })).toBeInTheDocument();
|
||||
expect(action).toHaveAttribute('aria-expanded', 'false');
|
||||
|
||||
@@ -7,7 +7,6 @@ import { api } from '../../api/client';
|
||||
import type { AlertEvent, AlertQuery, AlertRule, AlertRuleInput, AlertStatus, MetricDefinition, Page } from '../../api/types';
|
||||
import { actionLabels, alertDeltaText, alertValue, canAct, formatAlertTime, operatorLabels, ruleCondition, severityLabels, statusLabels, thresholdText } from '../domain/alert';
|
||||
import { InlineError, PanelEmpty, PanelLoading } from '../shared/AsyncState';
|
||||
import { MetricActionButton } from '../shared/MetricActionButton';
|
||||
import { MobileFilterToggle } from '../shared/MobileFilterToggle';
|
||||
import { MobileFilterSheet, MobileFilterSheetSection } from '../shared/MobileFilterSheet';
|
||||
import { ProtocolTag } from '../shared/ProtocolTag';
|
||||
@@ -17,6 +16,7 @@ import { WorkspaceCommandBar } from '../shared/WorkspaceCommandBar';
|
||||
import { WorkspaceDetailSideSheet } from '../shared/WorkspaceDetailSideSheet';
|
||||
import { WorkspacePanelHeader } from '../shared/WorkspacePanelHeader';
|
||||
import { WorkspaceFilterPanel } from '../shared/WorkspaceFilterPanel';
|
||||
import { WorkspaceMetricRail, type WorkspaceQueueMetricRailItem } from '../shared/WorkspaceMetricRail';
|
||||
import { WorkspaceSideSheet } from '../shared/WorkspaceSideSheet';
|
||||
import { detailTriggerRow } from '../shared/detailTriggerRow';
|
||||
import { usePlatformSession } from '../auth/AuthGate';
|
||||
@@ -229,17 +229,33 @@ function EventWorkspace({ filters, draft, setDraft, setFilters, rules, unread, e
|
||||
<label><span>起始时间</span><Input aria-label="告警起始时间" type="datetime-local" value={draft.dateFrom} onChange={(value) => setDraft({ ...draft, dateFrom: value })} /></label>
|
||||
<label><span>结束时间</span><Input aria-label="告警结束时间" type="datetime-local" value={draft.dateTo} onChange={(value) => setDraft({ ...draft, dateTo: value })} /></label>
|
||||
</>;
|
||||
const primaryMetrics = mobileLayout
|
||||
const metricItem = (
|
||||
label: string,
|
||||
value: number,
|
||||
status: string,
|
||||
options: Pick<WorkspaceQueueMetricRailItem, 'note' | 'tone' | 'emphasis'>
|
||||
): WorkspaceQueueMetricRailItem => {
|
||||
const formattedValue = value.toLocaleString('zh-CN');
|
||||
const notice = status === 'notice';
|
||||
return {
|
||||
label,
|
||||
value: formattedValue,
|
||||
...options,
|
||||
active: !notice && filters.status === status,
|
||||
ariaLabel: notice ? `查看未读通知,共 ${formattedValue} 条` : `筛选${label},共 ${formattedValue} 条`,
|
||||
onClick: () => notice ? onTab('notifications') : quickStatus(status)
|
||||
};
|
||||
};
|
||||
const primaryMetrics: WorkspaceQueueMetricRailItem[] = mobileLayout
|
||||
? [
|
||||
{ label: '待处置', value: sums?.unprocessed, tone: 'unprocessed', status: 'unprocessed' },
|
||||
{ label: '处理中', value: sums?.processing, tone: 'processing', status: 'processing' },
|
||||
{ label: '全部事件', value: allEventCount, tone: 'active', status: '' }
|
||||
metricItem('待处置', Number(sums?.unprocessed ?? 0), 'unprocessed', { note: '需要核验', tone: 'danger', emphasis: 'primary' }),
|
||||
metricItem('全部事件', allEventCount, '', { note: '当前范围', tone: 'neutral', emphasis: 'primary' })
|
||||
]
|
||||
: [
|
||||
{ label: '全部事件', value: allEventCount, tone: 'active', status: '' },
|
||||
{ label: '未处理', value: sums?.unprocessed, tone: 'unprocessed', status: 'unprocessed' },
|
||||
{ label: '处理中', value: sums?.processing, tone: 'processing', status: 'processing' },
|
||||
{ label: '未读通知', value: unread, tone: 'notice', status: 'notice' }
|
||||
metricItem('未处理', Number(sums?.unprocessed ?? 0), 'unprocessed', { note: '需要核验与处置', tone: 'danger', emphasis: 'primary' }),
|
||||
metricItem('全部事件', allEventCount, '', { note: '当前查询范围', tone: 'neutral', emphasis: 'primary' }),
|
||||
metricItem('处理中', Number(sums?.processing ?? 0), 'processing', { note: '处置进行中', tone: 'primary', emphasis: 'secondary' }),
|
||||
metricItem('未读通知', unread, 'notice', { note: '进入通知队列', tone: 'warning', emphasis: 'secondary' })
|
||||
];
|
||||
return <>
|
||||
{mobileLayout ? <div className="v2-alert-mobile-discovery">
|
||||
@@ -292,19 +308,19 @@ function EventWorkspace({ filters, draft, setDraft, setFilters, rules, unread, e
|
||||
</form>
|
||||
</WorkspaceFilterPanel>}
|
||||
{summary.isError ? <InlineError message={summary.error instanceof Error ? summary.error.message : '告警汇总读取失败'} onRetry={() => summary.refetch()} /> : null}
|
||||
<Card className="v2-alert-kpis-card v2-alert-context-card" bodyStyle={{ padding: 0 }}>
|
||||
<section className="v2-alert-kpis" aria-label="活跃告警概览">{primaryMetrics.map((item) => {
|
||||
const value = Number(item.value ?? 0).toLocaleString('zh-CN');
|
||||
return <MetricActionButton key={item.label} label={item.label} value={value} tone={item.tone} active={item.status !== 'notice' && filters.status === item.status} ariaLabel={item.status === 'notice' ? `查看未读通知,共 ${value} 条` : `筛选${item.label},共 ${value} 条`} onClick={() => item.status === 'notice' ? onTab('notifications') : quickStatus(item.status)} />;
|
||||
})}</section>
|
||||
{!mobileLayout ? <nav className="v2-alert-secondary-states" aria-label="已结束告警状态">{
|
||||
<WorkspaceMetricRail
|
||||
variant="queue"
|
||||
className="v2-alert-metric-rail"
|
||||
ariaLabel="活跃告警概览"
|
||||
items={primaryMetrics}
|
||||
context={!mobileLayout ? <nav className="v2-alert-metric-context-actions" aria-label="已结束告警状态">{
|
||||
[
|
||||
{ label: '已恢复', value: sums?.recovered, status: 'recovered' },
|
||||
{ label: '已关闭', value: sums?.closed, status: 'closed' },
|
||||
{ label: '已忽略', value: sums?.ignored, status: 'ignored' }
|
||||
].map((item) => <Button key={item.status} theme="borderless" type="tertiary" aria-pressed={filters.status === item.status} onClick={() => quickStatus(item.status)}><span>{item.label}</span><b>{Number(item.value ?? 0).toLocaleString('zh-CN')}</b></Button>)
|
||||
}</nav> : null}
|
||||
</Card>
|
||||
].map((item) => <Button key={item.status} theme="borderless" type="tertiary" aria-pressed={filters.status === item.status} onClick={() => quickStatus(item.status)}><span>{item.label}</span><strong>{Number(item.value ?? 0).toLocaleString('zh-CN')}</strong></Button>)
|
||||
}</nav> : undefined}
|
||||
/>
|
||||
{events.isError ? <InlineError message={events.error instanceof Error ? events.error.message : '告警事件读取失败'} onRetry={() => events.refetch()} /> : null}
|
||||
<div className={`v2-alert-workspace${selectedID && !mobileLayout ? ' is-inspector-open' : ''}`}>
|
||||
<Card className="v2-alert-table-card" bodyStyle={{ padding: 0 }}>
|
||||
|
||||
Reference in New Issue
Block a user