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
>