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 c55a45244 fix(studio): keep a failed Ops load out of the 
unsupported-cluster state (#4698)
c55a45244 is described below

commit c55a4524434a55e986544a117eb5fedcbb0c0389
Author: Apulupie <[email protected]>
AuthorDate: Mon Sep 21 21:09:43 2026 +0800

    fix(studio): keep a failed Ops load out of the unsupported-cluster state 
(#4698)
    
    `Ops.tsx` decided whether to render its "this cluster does not support Ops 
configuration" Alert from `configurationAvailable` alone — a boolean 
initialized to false — and `loadOpsData`'s catch only fired a toast without 
setting any state. Any failure of `GET /api/ops/home`, be it network, 5xx or 
401, therefore asserted the unsupported-cluster claim with the fallback 
description and hid every write control behind a reason the page had never 
established, with no way to retry. The same i [...]
    
    A `loadState` of `loading` / `ready` / `failed` now separates the two 
facts: the info Alert renders only when the load succeeded and the server 
really reported the configuration unavailable, while a failed load renders a 
warning Alert with the existing `ops.fetchFailed` text and a retry button that 
bumps a `reloadKey` in the effect's deps. No new i18n keys — the button reuses 
`common.retry`. `Ops.tsx` is the only frontend consumer of 
`configurationAvailable`, and `OpsService` returns  [...]
    
    The catch still fires its `message.error` toast, so a failed load shows the 
same sentence twice; dropping it is left as a follow-up.
    
    Fixes #4699
---
 web/src/pages/studio/Ops.tsx                | 31 +++++++++++++++++++++++++++--
 web/src/pages/studio/__tests__/Ops.test.tsx | 18 +++++++++++++++++
 2 files changed, 47 insertions(+), 2 deletions(-)

diff --git a/web/src/pages/studio/Ops.tsx b/web/src/pages/studio/Ops.tsx
index 880a2951f..667877554 100644
--- a/web/src/pages/studio/Ops.tsx
+++ b/web/src/pages/studio/Ops.tsx
@@ -61,6 +61,13 @@ const OpsPage: React.FC = () => {
   const tlsUpdateInFlight = useRef(false);
   const [configurationAvailable, setConfigurationAvailable] = useState(false);
   const [unavailableReason, setUnavailableReason] = useState('');
+  /**
+   * A third state apart from "available" and "not supported". Until the load 
answers the page knows
+   * nothing, and rendering that as "this cluster does not support reading or 
updating the Ops
+   * configuration" is a claim it cannot make — and with no retry, since the 
failure is invisible.
+   */
+  const [loadState, setLoadState] = useState<'loading' | 'ready' | 
'failed'>('loading');
+  const [reloadKey, setReloadKey] = useState(0);
   const writeOperationEnabled = configurationAvailable && (!userId || admin 
=== true);
   const deleteNameServerDisabled =
     !selectedNamesrv || selectedNamesrv === currentNamesrv || 
namesrvAddrList.length <= 1;
@@ -79,9 +86,11 @@ const OpsPage: React.FC = () => {
           setCurrentNamesrv(data.currentNamesrv);
           setConfigurationAvailable(data.configurationAvailable);
           setUnavailableReason(data.unavailableReason || '');
+          setLoadState('ready');
         }
       } catch {
         if (!cancelled) {
+          setLoadState('failed');
           message.error(fetchFailedMessage);
         }
       }
@@ -92,7 +101,12 @@ const OpsPage: React.FC = () => {
     return () => {
       cancelled = true;
     };
-  }, [fetchFailedMessage, message]);
+  }, [fetchFailedMessage, message, reloadKey]);
+
+  const handleReload = () => {
+    setLoadState('loading');
+    setReloadKey((key) => key + 1);
+  };
 
   const handleUpdateNameSvrAddr = async () => {
     if (namesrvMutationInFlight.current) return;
@@ -191,7 +205,20 @@ const OpsPage: React.FC = () => {
 
   return (
     <div style={{ padding: 24 }}>
-      {!configurationAvailable && (
+      {loadState === 'failed' && (
+        <Alert
+          type="warning"
+          showIcon
+          message={fetchFailedMessage}
+          action={
+            <Button size="small" onClick={handleReload}>
+              {t('common.retry')}
+            </Button>
+          }
+          style={{ marginBottom: 24 }}
+        />
+      )}
+      {loadState === 'ready' && !configurationAvailable && (
         <Alert
           type="info"
           showIcon
diff --git a/web/src/pages/studio/__tests__/Ops.test.tsx 
b/web/src/pages/studio/__tests__/Ops.test.tsx
index 8612cbaa9..a6efa1cfd 100644
--- a/web/src/pages/studio/__tests__/Ops.test.tsx
+++ b/web/src/pages/studio/__tests__/Ops.test.tsx
@@ -121,6 +121,24 @@ describe('OpsPage', () => {
     await waitFor(() => expect(vipSwitch).toBeEnabled());
   });
 
+  it('offers a retry instead of claiming the cluster does not support Ops 
config', async () => {
+    vi.mocked(queryOpsHomePage).mockRejectedValueOnce(new Error('network 
down'));
+
+    renderWithProviders(<OpsPage />);
+
+    // A failed load is not the same fact as "this cluster has no Ops 
configuration", and the page
+    // only knows the first. Claiming the second hides every write control 
behind a reason that is
+    // false, with nothing the user can do about it.
+    const retry = await screen.findByRole('button', { name: /重\s*试|Retry/ });
+    expect(screen.queryByText(/当前集群不支持读取或更新 Ops 配置/)).not.toBeInTheDocument();
+
+    fireEvent.click(retry);
+
+    await waitFor(() => expect(queryOpsHomePage).toHaveBeenCalledTimes(2));
+    expect(await 
screen.findByPlaceholderText('NamesrvAddr')).toBeInTheDocument();
+    expect(screen.queryByRole('button', { name: /重\s*试|Retry/ 
})).not.toBeInTheDocument();
+  });
+
   it('hides write controls for read-only users', async () => {
     useAuthStore.setState({ user: 'reader', userId: 101, admin: false });
 

Reply via email to