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 c9795c1b7 fix(studio): ignore stale topic sync results (#4548)
c9795c1b7 is described below

commit c9795c1b7add1778d18156a9fe7f6f76b3ba1acc
Author: Yexi Xiang <[email protected]>
AuthorDate: Mon Sep 21 17:35:23 2026 +0800

    fix(studio): ignore stale topic sync results (#4548)
    
    openSyncModal let an older route-check batch finish after a newer one and 
overwrite the sync modal state. A syncRequestIdRef generation is now bumped on 
open, on modal close and on instance change (plus on unmount), and only the 
owning request may write syncMissing / syncChecking or raise the failure toast.
    
    Fixes #4547
---
 .../pages/instance/__tests__/TopicPage.test.tsx    | 53 +++++++++++++++++++++-
 web/src/pages/instance/topic.tsx                   | 27 +++++++++--
 2 files changed, 76 insertions(+), 4 deletions(-)

diff --git a/web/src/pages/instance/__tests__/TopicPage.test.tsx 
b/web/src/pages/instance/__tests__/TopicPage.test.tsx
index 752ac4f4b..270371f62 100644
--- a/web/src/pages/instance/__tests__/TopicPage.test.tsx
+++ b/web/src/pages/instance/__tests__/TopicPage.test.tsx
@@ -16,7 +16,7 @@
  */
 
 import { describe, it, expect, vi, beforeAll, beforeEach, afterEach } from 
'vitest';
-import { cleanup, fireEvent, render, screen, waitFor, within } from 
'@testing-library/react';
+import { act, cleanup, fireEvent, render, screen, waitFor, within } from 
'@testing-library/react';
 import userEvent from '@testing-library/user-event';
 import { MemoryRouter, Route, Routes } from 'react-router-dom';
 import { App, Modal } from 'antd';
@@ -1139,4 +1139,55 @@ describe('TopicPage', () => {
     const groupLink = await screen.findByText('cg-orders');
     expect(groupLink.closest('a')).not.toBeNull();
   });
+
+  it('keeps a reopened sync modal owned by its newest route check', async () 
=> {
+    const user = userEvent.setup();
+    let resolveFirst!: (routes: BrokerRoute[]) => void;
+    let resolveSecond!: (routes: BrokerRoute[]) => void;
+    const firstCheck = new Promise<BrokerRoute[]>((resolve) => {
+      resolveFirst = resolve;
+    });
+    const secondCheck = new Promise<BrokerRoute[]>((resolve) => {
+      resolveSecond = resolve;
+    });
+    const healthyRoute: BrokerRoute = {
+      brokerName: 'broker-b',
+      brokerAddr: '10.0.0.2:10911',
+      masterAddr: '10.0.0.2:10911',
+      writeQueues: 8,
+      readQueues: 8,
+      perm: 'RW',
+      readable: true,
+      writable: true,
+      replicaCount: 1,
+    };
+    let routeCheckCount = 0;
+    mockTopicsList([buildTopics(1)[0]]);
+    topicServiceMocks.getTopicRoutes.mockImplementation(() => {
+      routeCheckCount += 1;
+      return routeCheckCount === 1 ? firstCheck : secondCheck;
+    });
+    renderWithProviders();
+
+    await screen.findByText('topic-01');
+    const syncButton = await screen.findByRole('button', { name: /同步/ });
+    await user.click(syncButton);
+    await waitFor(() => 
expect(topicServiceMocks.getTopicRoutes).toHaveBeenCalledTimes(1));
+    await user.click(document.querySelector('.ant-modal-close') as 
HTMLElement);
+
+    await user.click(syncButton);
+    await waitFor(() => 
expect(topicServiceMocks.getTopicRoutes).toHaveBeenCalledTimes(2));
+
+    await act(async () => {
+      resolveSecond([healthyRoute]);
+      await secondCheck;
+    });
+    expect(screen.getByText(/所有 Topic 在 Broker 上均有路由/)).toBeInTheDocument();
+
+    await act(async () => {
+      resolveFirst([]);
+      await firstCheck;
+    });
+    expect(screen.queryByText('缺失路由')).not.toBeInTheDocument();
+  });
 });
diff --git a/web/src/pages/instance/topic.tsx b/web/src/pages/instance/topic.tsx
index 823f049f0..d9667e2a9 100644
--- a/web/src/pages/instance/topic.tsx
+++ b/web/src/pages/instance/topic.tsx
@@ -402,8 +402,16 @@ const TopicPageContent = ({
   const topicRequestIdRef = useRef(0);
   const detailRequestIdRef = useRef(0);
   const consumersRequestIdRef = useRef(0);
+  const syncRequestIdRef = useRef(0);
   const createInFlightRef = useRef(false);
 
+  useEffect(
+    () => () => {
+      syncRequestIdRef.current += 1;
+    },
+    [],
+  );
+
   const sendPayloadPreview = useMemo(
     () =>
       analyzeMessagePayloadPreview({
@@ -568,7 +576,18 @@ const TopicPageContent = ({
     consumersByTopic[name] ?? { items: [], total: 0, page: 1, pageSize: 20 };
 
   // ─── Sync data: find topics without broker routes and sync them ──
+  const invalidateSyncRequest = () => {
+    syncRequestIdRef.current += 1;
+  };
+
+  const closeSyncModal = () => {
+    invalidateSyncRequest();
+    setSyncModalOpen(false);
+  };
+
   const openSyncModal = async () => {
+    const requestId = syncRequestIdRef.current + 1;
+    syncRequestIdRef.current = requestId;
     setSyncModalOpen(true);
     setSyncChecking(true);
     setSyncMissing([]);
@@ -584,6 +603,7 @@ const TopicPageContent = ({
           }
         }),
       );
+      if (syncRequestIdRef.current !== requestId) return;
       const checked = results.filter((r) => r.routes !== null);
       if (checked.length < results.length) {
         message.error('部分 Topic 路由校验失败,请稍后重试');
@@ -599,7 +619,7 @@ const TopicPageContent = ({
         checked.filter(({ routes }) => (routes as BrokerRoute[]).length === 
0).map((r) => r.topic),
       );
     } finally {
-      setSyncChecking(false);
+      if (syncRequestIdRef.current === requestId) setSyncChecking(false);
     }
   };
 
@@ -1498,6 +1518,7 @@ const TopicPageContent = ({
           <InstanceSelect
             value={selectedInstanceId || undefined}
             onChange={(value) => {
+              closeSyncModal();
               setSelectedRowKeys([]);
               resetTablePage();
               selectInstance(value);
@@ -2022,8 +2043,8 @@ const TopicPageContent = ({
       <Modal
         title="同步数据"
         open={syncModalOpen}
-        onCancel={() => setSyncModalOpen(false)}
-        footer={<Button onClick={() => setSyncModalOpen(false)}>关闭</Button>}
+        onCancel={closeSyncModal}
+        footer={<Button onClick={closeSyncModal}>关闭</Button>}
         width={680}
         destroyOnHidden
       >

Reply via email to