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 d687a9c94 fix(ops): disable writes until configuration reload succeeds 
(#5359)
d687a9c94 is described below

commit d687a9c942fef7e04d1f6038b86ac7dc8c072b41
Author: coder999o <[email protected]>
AuthorDate: Fri Oct 9 11:25:16 2026 +0800

    fix(ops): disable writes until configuration reload succeeds (#5359)
    
    `writeOperationEnabled` ignored `loadState` and was never re-armed, so the 
VIP/TLS switches stayed
    clickable while the page was showing a load failure, and a 
language-triggered refetch did not put
    the flag back to loading either. Gate the flag on `loadState === 'ready'` 
and reset it to loading at
    the start of every effect run; this covers all eight write-control 
consumers.
---
 web/src/pages/studio/Ops.tsx                |  6 ++-
 web/src/pages/studio/__tests__/Ops.test.tsx | 58 +++++++++++++++++++++++++++++
 2 files changed, 63 insertions(+), 1 deletion(-)

diff --git a/web/src/pages/studio/Ops.tsx b/web/src/pages/studio/Ops.tsx
index 5d84e12dc..04ed60811 100644
--- a/web/src/pages/studio/Ops.tsx
+++ b/web/src/pages/studio/Ops.tsx
@@ -68,12 +68,16 @@ const OpsPage: React.FC = () => {
    */
   const [loadState, setLoadState] = useState<'loading' | 'ready' | 
'failed'>('loading');
   const [reloadKey, setReloadKey] = useState(0);
-  const writeOperationEnabled = configurationAvailable && (!userId || admin 
=== true);
+  const writeOperationEnabled =
+    loadState === 'ready' && configurationAvailable && (!userId || admin === 
true);
   const deleteNameServerDisabled =
     !selectedNamesrv || selectedNamesrv === currentNamesrv || 
namesrvAddrList.length <= 1;
 
   useEffect(() => {
     let cancelled = false;
+    void Promise.resolve().then(() => {
+      if (!cancelled) setLoadState('loading');
+    });
 
     const loadOpsData = async () => {
       try {
diff --git a/web/src/pages/studio/__tests__/Ops.test.tsx 
b/web/src/pages/studio/__tests__/Ops.test.tsx
index c0a11e2df..9ce988f89 100644
--- a/web/src/pages/studio/__tests__/Ops.test.tsx
+++ b/web/src/pages/studio/__tests__/Ops.test.tsx
@@ -180,6 +180,64 @@ describe('OpsPage', () => {
     expect(screen.queryByRole('button', { name: /重\s*试|Retry/ 
})).not.toBeInTheDocument();
   });
 
+  it('disables writes when a configuration reload fails after a successful 
load', async () => {
+    const user = userEvent.setup();
+    renderWithProviders(
+      <>
+        <LanguageSwitch />
+        <OpsPage />
+      </>,
+    );
+
+    expect(await 
screen.findByPlaceholderText('NamesrvAddr')).toBeInTheDocument();
+    vi.mocked(queryOpsHomePage).mockRejectedValueOnce(new Error('network 
down'));
+    await user.click(screen.getByRole('button', { name: 'switch-language' }));
+
+    expect(await screen.findByRole('button', { name: /Retry/ 
})).toBeInTheDocument();
+    
expect(screen.queryByPlaceholderText('NamesrvAddr')).not.toBeInTheDocument();
+    screen.getAllByRole('switch').forEach((toggle) => 
expect(toggle).toBeDisabled());
+
+    await user.click(screen.getByRole('button', { name: /Retry/ }));
+    expect(await 
screen.findByPlaceholderText('NamesrvAddr')).toBeInTheDocument();
+    screen.getAllByRole('switch').forEach((toggle) => 
expect(toggle).toBeEnabled());
+  });
+
+  it('disables writes while a configuration reload is pending', async () => {
+    const user = userEvent.setup();
+    renderWithProviders(
+      <>
+        <LanguageSwitch />
+        <OpsPage />
+      </>,
+    );
+
+    expect(await 
screen.findByPlaceholderText('NamesrvAddr')).toBeInTheDocument();
+    let resolveReload!: (value: Awaited<ReturnType<typeof queryOpsHomePage>>) 
=> void;
+    vi.mocked(queryOpsHomePage).mockImplementationOnce(
+      () =>
+        new Promise((resolve) => {
+          resolveReload = resolve;
+        }),
+    );
+    await user.click(screen.getByRole('button', { name: 'switch-language' }));
+    await waitFor(() => expect(queryOpsHomePage).toHaveBeenCalledTimes(2));
+
+    
expect(screen.queryByPlaceholderText('NamesrvAddr')).not.toBeInTheDocument();
+    screen.getAllByRole('switch').forEach((toggle) => 
expect(toggle).toBeDisabled());
+
+    await act(async () => {
+      resolveReload({
+        namesvrAddrList: ['127.0.0.1:9876', '127.0.0.2:9876'],
+        configurationAvailable: true,
+        useVIPChannel: true,
+        useTLS: false,
+        currentNamesrv: '127.0.0.1:9876',
+      });
+    });
+    expect(await 
screen.findByPlaceholderText('NamesrvAddr')).toBeInTheDocument();
+    screen.getAllByRole('switch').forEach((toggle) => 
expect(toggle).toBeEnabled());
+  });
+
   it('hides write controls for read-only users', async () => {
     useAuthStore.setState({ user: 'reader', userId: 101, admin: false });
 

Reply via email to