This is an automated email from the ASF dual-hosted git repository.
lizhimins pushed a commit to branch rocketmq-studio
in repository https://gitbox.apache.org/repos/asf/rocketmq-dashboard.git
The following commit(s) were added to refs/heads/rocketmq-studio by this push:
new 77dcf962e fix(ops): report a failed alert read instead of an empty
backlog (#6492)
77dcf962e is described below
commit 77dcf962e0e7af9408b2493218090d7ba488ed31
Author: bai mengxuan <[email protected]>
AuthorDate: Thu Oct 15 18:17:10 2026 +0800
fix(ops): report a failed alert read instead of an empty backlog (#6492)
The system alerts feed treated a failed read as an empty feed: the page
header claimed "本页 0 条未确认" and the level chips reported zeroed
counters for a store that was never reached, while the failure was only
announced as a transient toast an operator can miss.
Keep the failure on the page — an error banner with a retry that re-runs
the query — and stop asserting numbers derived from the failed read.
---
.../pages/ops/__tests__/SystemAlertsPage.test.tsx | 39 ++++++++++++++++++++++
web/src/pages/ops/systemAlerts.tsx | 34 ++++++++++++++++---
2 files changed, 69 insertions(+), 4 deletions(-)
diff --git a/web/src/pages/ops/__tests__/SystemAlertsPage.test.tsx
b/web/src/pages/ops/__tests__/SystemAlertsPage.test.tsx
index 51ebd582d..b282c145d 100644
--- a/web/src/pages/ops/__tests__/SystemAlertsPage.test.tsx
+++ b/web/src/pages/ops/__tests__/SystemAlertsPage.test.tsx
@@ -72,6 +72,11 @@ const renderPage = () =>
</App>,
);
+// The level filter renders as an antd Segmented: each option exposes
role="radio" inside
+// its .ant-segmented-item wrapper, so the chip's own text has to be read from
that wrapper.
+const levelFilterItem = (label: RegExp): HTMLElement =>
+ screen.getByRole('radio', { name: label }).closest('.ant-segmented-item') as
HTMLElement;
+
const deferred = <T,>() => {
let resolve!: (value: T) => void;
const promise = new Promise<T>((resolvePromise) => {
@@ -1024,4 +1029,38 @@ describe('SystemAlertsPage', () => {
expect(screen.getByText(/BUSINESS.*page-three/)).toBeInTheDocument();
expect(screen.queryByText(/page-two/)).not.toBeInTheDocument();
});
+
+ it('reports a failed alert read instead of an empty backlog', async () => {
+ vi.mocked(listSystemAlertsPage).mockRejectedValueOnce(new Error('alert
store offline'));
+ const user = userEvent.setup();
+
+ renderPage();
+
+ expect(await screen.findByText('获取数据失败')).toBeInTheDocument();
+ // A store that was never reached must not be reported as a page with
nothing left to acknowledge.
+ expect(screen.queryByText(/本页 0 条未确认/)).not.toBeInTheDocument();
+ expect(screen.getByText(/本页 - 条未确认/)).toBeInTheDocument();
+ expect(levelFilterItem(/严重/).textContent).not.toMatch(/[0-9]/);
+
+ await user.click(screen.getByRole('button', { name: /重试/ }));
+
+ expect(await screen.findByText('Broker unavailable')).toBeInTheDocument();
+ await waitFor(() =>
expect(screen.queryByText('获取数据失败')).not.toBeInTheDocument());
+ expect(screen.getByText(/本页 2 条未确认/)).toBeInTheDocument();
+ expect(levelFilterItem(/严重/).textContent).toMatch(/1/);
+ });
+
+ it('drops the per-level counters when a later read fails', async () => {
+ const user = userEvent.setup();
+ renderPage();
+ expect(await screen.findByText('Broker unavailable')).toBeInTheDocument();
+
+ vi.mocked(listSystemAlertsPage).mockRejectedValueOnce(new Error('alert
store offline'));
+ await user.click(levelFilterItem(/警告/));
+
+ expect(await screen.findByText('获取数据失败')).toBeInTheDocument();
+ // The last page stays readable, but numbers derived from the failed read
must not look current.
+ expect(screen.getByText('Broker unavailable')).toBeInTheDocument();
+ expect(levelFilterItem(/严重/).textContent).not.toMatch(/[0-9]/);
+ });
});
diff --git a/web/src/pages/ops/systemAlerts.tsx
b/web/src/pages/ops/systemAlerts.tsx
index a537b01b5..11ae405a7 100644
--- a/web/src/pages/ops/systemAlerts.tsx
+++ b/web/src/pages/ops/systemAlerts.tsx
@@ -22,6 +22,7 @@ import {
Tag,
Flex,
Typography,
+ Alert,
Badge,
Button,
message,
@@ -36,7 +37,7 @@ import {
theme,
} from 'antd';
import dayjs from 'dayjs';
-import { CheckCircle, DownloadSimple, Trash } from '@phosphor-icons/react';
+import { ArrowClockwise, CheckCircle, DownloadSimple, Trash } from
'@phosphor-icons/react';
import PageHeader from '../../components/PageHeader';
import { useLang } from '../../i18n/LangContext';
import useAuthStore from '../../stores/authStore';
@@ -153,6 +154,7 @@ const SystemAlertsPage = () => {
};
const [alerts, setAlerts] = useState<SystemAlert[]>([]);
+ const [loadFailed, setLoadFailed] = useState(false);
const [levelFilter, setLevelFilter] = useState<string>('all');
const [domainFilter, setDomainFilter] = useState<string>('all');
const [transitionFilter, setTransitionFilter] = useState<string>('all');
@@ -218,10 +220,16 @@ const SystemAlertsPage = () => {
if (!cancelled) {
setAlerts(data.items);
setTotal(data.total);
+ setLoadFailed(false);
}
})
.catch(() => {
- if (!cancelled) message.error(t('sysAlerts.loadFailed'));
+ if (!cancelled) {
+ // A failed read must not leave the page reporting "0
unacknowledged" for a store it
+ // never reached, and the alert surfaces below must offer a retry.
+ message.error(t('sysAlerts.loadFailed'));
+ setLoadFailed(true);
+ }
})
.finally(() => {
if (!cancelled) setLoading(false);
@@ -486,7 +494,7 @@ const SystemAlertsPage = () => {
<div style={{ padding: 24 }}>
<PageHeader
title={t('sysAlerts.title')}
- subtitle={t('sysAlerts.subtitle', { n: unackCount })}
+ subtitle={t('sysAlerts.subtitle', { n: loadFailed ? '-' : unackCount
})}
extra={
<Flex gap={8} align="center">
{collectorStatus && (
@@ -536,6 +544,24 @@ const SystemAlertsPage = () => {
}
/>
+ {loadFailed && (
+ <Alert
+ showIcon
+ type="error"
+ message={t('common.fetchDataFailed')}
+ action={
+ <Button
+ size="small"
+ icon={<ArrowClockwise size={16} />}
+ onClick={() => setRefreshNonce((value) => value + 1)}
+ >
+ {t('common.retry')}
+ </Button>
+ }
+ style={{ marginBottom: 12 }}
+ />
+ )}
+
{/* 筛选条:全部控件统一默认尺寸(32px 高)。时间范围用单个 RangePicker 而不是两个
datetime-local:368px 是填了值之后两段 `YYYY-MM-DD HH:mm` 加箭头和日历图标的实测宽,
再窄会裁字。整条约 1292px,在 1600 视口(容器 1332px)下保持一行。 */}
@@ -551,7 +577,7 @@ const SystemAlertsPage = () => {
label: (
<span style={{ display: 'inline-flex', alignItems: 'center',
gap: 4 }}>
{level === 'all' ? t('common.all') :
alertLevelConfig[level]?.label}
- {level !== 'all' && (
+ {!loadFailed && level !== 'all' && (
<Badge
count={alerts.filter((a) => normalizeAlertLevel(a.level)
=== level).length}
style={{