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={{

Reply via email to