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 });