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 71ffb696a fix(deliveries): offer a retry when the instance filter 
cannot load (#4700)
71ffb696a is described below

commit 71ffb696a273c500d89c091e8ebd96f374751519
Author: Apulupie <[email protected]>
AuthorDate: Mon Sep 21 21:10:02 2026 +0800

    fix(deliveries): offer a retry when the instance filter cannot load (#4700)
    
    `notificationDeliveries.tsx` loaded its instance filter with `void 
listInstances().then(setInstances).catch(() => undefined)`, so a failed `GET 
/api/instances` left `instances` at its empty initial value: the Select 
rendered no options, with no message and no retry, and the page read as "this 
deployment has no instances" — a conclusion a failed request cannot support. It 
was the only silent degrade on the page, since the delivery-list load below it 
reports `deliveries.loadFailed` and  [...]
    
    A failure is now a state. `instanceLoadFailed` puts the Select into its 
error status and renders a retry button under a tooltip explaining why, backed 
by a new `deliveries.instancesLoadFailed` key in both languages and the 
existing `common.retry`; a reload nonce in the effect's deps re-runs the load, 
and the request picked up the `cancelled` guard it did not have.
    
    Open follow-up: `home/dashboard.tsx` and `settings/DataSourceTab.tsx` still 
clear their instance lists silently, while `useInstanceFilter` documents silent 
degradation as deliberate. Whether instance-filter load failures should get one 
shared affordance repo-wide is a product decision this change does not settle.
    
    Fixes #4701
---
 web/src/i18n/translations.ts                       |  4 +++
 .../__tests__/NotificationDeliveriesPage.test.tsx  | 26 ++++++++++++++++++
 web/src/pages/ops/notificationDeliveries.tsx       | 32 ++++++++++++++++++++--
 3 files changed, 59 insertions(+), 3 deletions(-)

diff --git a/web/src/i18n/translations.ts b/web/src/i18n/translations.ts
index 0a200d625..bb1bceae1 100644
--- a/web/src/i18n/translations.ts
+++ b/web/src/i18n/translations.ts
@@ -1168,6 +1168,10 @@ const translations: Record<string, Record<Lang, string>> 
= {
     zh: '告警投递记录加载失败,请稍后重试',
     en: 'Failed to load alert deliveries. Please try again later.',
   },
+  'deliveries.instancesLoadFailed': {
+    zh: '实例列表加载失败,实例筛选暂时不可用',
+    en: 'Failed to load the instance list; the instance filter is unavailable',
+  },
   'deliveries.retryQueued': { zh: '已加入重新投递队列', en: 'Added to the redelivery 
queue.' },
   'deliveries.retryFailed': {
     zh: '重新投递失败,请稍后重试',
diff --git a/web/src/pages/ops/__tests__/NotificationDeliveriesPage.test.tsx 
b/web/src/pages/ops/__tests__/NotificationDeliveriesPage.test.tsx
index e5c590c11..99ab398df 100644
--- a/web/src/pages/ops/__tests__/NotificationDeliveriesPage.test.tsx
+++ b/web/src/pages/ops/__tests__/NotificationDeliveriesPage.test.tsx
@@ -169,4 +169,30 @@ describe('NotificationDeliveriesPage', () => {
       expect.objectContaining({ status: 'DELIVERED' }),
     );
   });
+
+  it('surfaces a failed instance-list load with a retry instead of an empty 
filter', async () => {
+    const user = userEvent.setup();
+    vi.mocked(listInstances)
+      .mockRejectedValueOnce(new Error('the instance service is down'))
+      .mockResolvedValueOnce([]);
+    render(
+      <App>
+        <LangProvider>
+          <NotificationDeliveriesPage />
+        </LangProvider>
+      </App>,
+    );
+
+    await screen.findByText('Broker disk usage');
+    // An empty instance filter reads as "this deployment has no instances", 
which is not something a
+    // failed request can establish. The retry is the affordance the silent 
catch never offered.
+    const retry = await screen.findByRole('button', { name: /^重\s*试$/ });
+
+    await user.click(retry);
+
+    await waitFor(() => expect(listInstances).toHaveBeenCalledTimes(2));
+    await waitFor(() =>
+      expect(screen.queryByRole('button', { name: /^重\s*试$/ 
})).not.toBeInTheDocument(),
+    );
+  });
 });
diff --git a/web/src/pages/ops/notificationDeliveries.tsx 
b/web/src/pages/ops/notificationDeliveries.tsx
index 4e335ec49..31150cd40 100644
--- a/web/src/pages/ops/notificationDeliveries.tsx
+++ b/web/src/pages/ops/notificationDeliveries.tsx
@@ -52,6 +52,8 @@ const NotificationDeliveriesPage = () => {
   const [channel, setChannel] = useState<string>();
   const [status, setStatus] = useState<NotificationDeliveryRecord['status']>();
   const [instanceId, setInstanceId] = useState<string>();
+  const [instanceLoadFailed, setInstanceLoadFailed] = useState(false);
+  const [instanceReloadNonce, setInstanceReloadNonce] = useState(0);
   const [selectedDelivery, setSelectedDelivery] = 
useState<NotificationDeliveryRecord>();
   const [retryingIds, setRetryingIds] = useState<Set<number>>(() => new Set());
   const [retryingVisible, setRetryingVisible] = useState(false);
@@ -115,10 +117,22 @@ const NotificationDeliveriesPage = () => {
   };
 
   useEffect(() => {
+    let cancelled = false;
     void listInstances()
-      .then(setInstances)
-      .catch(() => undefined);
-  }, []);
+      .then((loaded) => {
+        if (cancelled) return;
+        setInstances(loaded);
+        setInstanceLoadFailed(false);
+      })
+      .catch(() => {
+        // Swallowing this leaves an empty filter, which reads as "this 
deployment has no instances"
+        // — a claim a failed request cannot make, and one the user has no way 
to retry.
+        if (!cancelled) setInstanceLoadFailed(true);
+      });
+    return () => {
+      cancelled = true;
+    };
+  }, [instanceReloadNonce]);
 
   useEffect(() => {
     let cancelled = false;
@@ -263,6 +277,7 @@ const NotificationDeliveriesPage = () => {
               optionFilterProp="label"
               placeholder={t('deliveries.allInstances')}
               value={instanceId}
+              status={instanceLoadFailed ? 'error' : undefined}
               style={{ width: 280, flex: '1 1 280px' }}
               options={instances.map((instance) => ({
                 value: instance.name,
@@ -270,6 +285,17 @@ const NotificationDeliveriesPage = () => {
               }))}
               onChange={(value) => resetPage(() => setInstanceId(value))}
             />
+            {instanceLoadFailed && (
+              <Tooltip title={t('deliveries.instancesLoadFailed')}>
+                <Button
+                  size="small"
+                  icon={<ArrowClockwise size={16} />}
+                  onClick={() => setInstanceReloadNonce((nonce) => nonce + 1)}
+                >
+                  {t('common.retry')}
+                </Button>
+              </Tooltip>
+            )}
           </Flex>
           <Table
             rowKey="id"

Reply via email to