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 fff275140 fix(topic): discard a comparison that resolves after the 
instance pair changed (#4586)
fff275140 is described below

commit fff27514035d2a9df29c7d0753bb1a4ad7a3e8f8
Author: 烤化の初雪 <[email protected]>
AuthorDate: Thu Sep 24 17:45:51 2026 +0800

    fix(topic): discard a comparison that resolves after the instance pair 
changed (#4586)
    
    test(topic): cover the in-flight indicator when the comparison pair changes
    
    The invalidation helper also clears the loading flag: without it the
    superseded response's finally block no longer runs and the compare control
    keeps spinning. Assert that the control is loading while the comparison runs
    and no longer loading once the pair changes. The node is captured before the
    click because the loading spinner contributes to the accessible name.
    
    fix(topic): discard a comparison that resolves after the instance pair 
changed
    
    The drawer guarded its comparison with requestIdRef, but only runComparison
    bumped it. Changing the source or target instance (or swapping them) cleared
    the result without invalidating the in-flight request, so a response for the
    previous pair was rendered under the new selection: summary cards, the field
    table headers and the exported file name all named the new pair while the
    rows came from the old one.
    
    Invalidate the in-flight comparison from every change of the compared pair.
---
 web/src/components/TopicConfigComparisonDrawer.tsx | 14 +++++++--
 .../__tests__/TopicConfigComparisonDrawer.test.tsx | 35 +++++++++++++++++++++-
 2 files changed, 45 insertions(+), 4 deletions(-)

diff --git a/web/src/components/TopicConfigComparisonDrawer.tsx 
b/web/src/components/TopicConfigComparisonDrawer.tsx
index 72e1c5de3..3204efa2c 100644
--- a/web/src/components/TopicConfigComparisonDrawer.tsx
+++ b/web/src/components/TopicConfigComparisonDrawer.tsx
@@ -144,10 +144,18 @@ const TopicConfigComparisonDrawer = ({
     }
   };
 
+  // Any change of the compared pair invalidates a comparison that is still 
loading, so a
+  // response for the previous pair can never be rendered under the new 
selection.
+  const invalidateComparison = () => {
+    requestIdRef.current += 1;
+    setResult(null);
+    setLoading(false);
+  };
+
   const swapInstances = () => {
     setSourceInstanceId(targetInstanceId);
     setTargetInstanceId(sourceInstanceId);
-    setResult(null);
+    invalidateComparison();
   };
 
   const exportComparison = () => {
@@ -205,7 +213,7 @@ const TopicConfigComparisonDrawer = ({
               style={{ width: '100%', marginTop: 4 }}
               onChange={(value) => {
                 setSourceInstanceId(value);
-                setResult(null);
+                invalidateComparison();
               }}
             />
           </label>
@@ -223,7 +231,7 @@ const TopicConfigComparisonDrawer = ({
               style={{ width: '100%', marginTop: 4 }}
               onChange={(value) => {
                 setTargetInstanceId(value);
-                setResult(null);
+                invalidateComparison();
               }}
             />
           </label>
diff --git a/web/src/components/__tests__/TopicConfigComparisonDrawer.test.tsx 
b/web/src/components/__tests__/TopicConfigComparisonDrawer.test.tsx
index 93ecbe2f5..cbe29e551 100644
--- a/web/src/components/__tests__/TopicConfigComparisonDrawer.test.tsx
+++ b/web/src/components/__tests__/TopicConfigComparisonDrawer.test.tsx
@@ -16,7 +16,7 @@
  */
 
 import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
-import { render, screen, waitFor } from '@testing-library/react';
+import { act, render, screen, waitFor } from '@testing-library/react';
 import userEvent from '@testing-library/user-event';
 import { App } from 'antd';
 import type { Instance } from '../../api/instance';
@@ -216,6 +216,39 @@ describe('TopicConfigComparisonDrawer', () => {
     expect(screen.queryByText('配置一致')).not.toBeInTheDocument();
   });
 
+  it('discards a comparison that resolves after the instance pair changed', 
async () => {
+    const archiveInstance: Instance = { ...instances[1], id: 3, name: 
'archive' };
+    const pending = new Map<string, (value: Topic[]) => void>();
+    topicServiceMocks.listAllTopics.mockImplementation(
+      ({ instanceId }: { instanceId: string }) =>
+        new Promise<Topic[]>((resolve) => {
+          pending.set(instanceId, resolve);
+        }),
+    );
+    const user = userEvent.setup();
+    renderDrawer({ instances: [...instances, archiveInstance] });
+    // Hold the node: while it is loading, the spinner contributes to its 
accessible name.
+    const compareButton = screen.getByRole('button', { name: '开始对比' });
+
+    await user.click(compareButton);
+    await waitFor(() => expect(pending.size).toBe(2));
+    expect(compareButton).toHaveClass('ant-btn-loading');
+
+    // Change the source instance while the production/staging comparison is 
still in flight.
+    await user.click(screen.getByRole('combobox', { name: '源实例' }));
+    await user.click(await screen.findByTitle('archive'));
+
+    await act(async () => {
+      pending.get('production')?.(productionTopics);
+      pending.get('staging')?.(stagingTopics);
+    });
+
+    expect(screen.queryByText('配置一致')).not.toBeInTheDocument();
+    expect(screen.queryByText('source-only-topic')).not.toBeInTheDocument();
+    // Changing the pair must also end the in-flight indicator, so the control 
cannot stay spinning.
+    await waitFor(() => 
expect(compareButton).not.toHaveClass('ant-btn-loading'));
+  });
+
   it('calls onClose from the drawer close control', async () => {
     const onClose = vi.fn();
     const user = userEvent.setup();

Reply via email to