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 52e8f9c48 fix(settings): isolate data source modal connection tests 
(#6926)
52e8f9c48 is described below

commit 52e8f9c48722139499f1e3d7dd4ddf0dbdf9e665
Author: andy_chan <[email protected]>
AuthorDate: Mon Oct 26 16:33:15 2026 +0800

    fix(settings): isolate data source modal connection tests (#6926)
    
    * fix(settings): isolate data source modal connection tests
---
 web/src/pages/settings/DataSourceTab.tsx           |  51 ++++++---
 .../settings/__tests__/DataSourceTab.test.tsx      | 114 ++++++++++++++++++++-
 2 files changed, 149 insertions(+), 16 deletions(-)

diff --git a/web/src/pages/settings/DataSourceTab.tsx 
b/web/src/pages/settings/DataSourceTab.tsx
index e6de0b90d..9d164be74 100644
--- a/web/src/pages/settings/DataSourceTab.tsx
+++ b/web/src/pages/settings/DataSourceTab.tsx
@@ -132,6 +132,7 @@ export const DataSourceTab = () => {
   const [dsForm] = Form.useForm();
   const authValue = Form.useWatch('auth', dsForm);
   const [testingKeys, setTestingKeys] = useState<Set<string>>(() => new Set());
+  const modalTestSessionRef = useRef(0);
   const [submitting, setSubmitting] = useState(false);
   const [exporting, setExporting] = useState(false);
   const requestSeqRef = useRef(0);
@@ -193,6 +194,7 @@ export const DataSourceTab = () => {
   useEffect(
     () => () => {
       requestSeqRef.current += 1;
+      modalTestSessionRef.current += 1;
     },
     [],
   );
@@ -214,7 +216,10 @@ export const DataSourceTab = () => {
   const handleTestConnection = async (
     data: Pick<DataSource, 'type' | 'url' | 'auth'> & Partial<DataSource>,
     key: string,
+    modalSession = modalTestSessionRef.current,
   ) => {
+    const isCurrent = () => key !== 'modal' || modalSession === 
modalTestSessionRef.current;
+    if (!isCurrent()) return;
     if (key !== 'modal' && listActionsDisabled) return;
     if (key !== 'modal' && authNeedsSecret(data.auth)) {
       message.warning(t('settings.dataSourceAuthTestHint'));
@@ -223,20 +228,41 @@ export const DataSourceTab = () => {
     setTestingKeys((previous) => new Set(previous).add(key));
     try {
       const result = await testDataSource(data);
+      if (!isCurrent()) return;
       if (result.success) message.success(result.message);
       else message.error(result.message);
     } catch {
-      message.error(t('settings.connectionTestFailed'));
+      if (isCurrent()) message.error(t('settings.connectionTestFailed'));
     } finally {
-      setTestingKeys((previous) => {
-        const next = new Set(previous);
-        next.delete(key);
-        return next;
-      });
+      if (isCurrent()) {
+        setTestingKeys((previous) => {
+          const next = new Set(previous);
+          next.delete(key);
+          return next;
+        });
+      }
     }
   };
 
+  const resetModalTest = () => {
+    // A closed dialog's request must not notify or clear a newer dialog's 
spinner.
+    modalTestSessionRef.current += 1;
+    setTestingKeys((previous) => {
+      const next = new Set(previous);
+      next.delete('modal');
+      return next;
+    });
+  };
+
+  const closeModal = () => {
+    resetModalTest();
+    setModalOpen(false);
+    setEditingDataSource(null);
+    dsForm.resetFields();
+  };
+
   const openCreateModal = () => {
+    resetModalTest();
     setEditingDataSource(null);
     dsForm.resetFields();
     dsForm.setFieldValue('auth', 'None');
@@ -245,6 +271,7 @@ export const DataSourceTab = () => {
 
   const openEditModal = (dataSource: DataSource) => {
     if (listActionsDisabled) return;
+    resetModalTest();
     setEditingDataSource(dataSource);
     dsForm.setFieldsValue(dataSource);
     setModalOpen(true);
@@ -271,8 +298,7 @@ export const DataSourceTab = () => {
       message.success(
         t(editingDataSource ? 'settings.dataSourceUpdated' : 
'settings.dataSourceAdded'),
       );
-      setModalOpen(false);
-      dsForm.resetFields();
+      closeModal();
     } catch (error) {
       if (error && typeof error === 'object' && 'errorFields' in error) {
         return; // validation failure; antd already shows field-level errors
@@ -483,11 +509,7 @@ export const DataSourceTab = () => {
       <Modal
         title={t(editingDataSource ? 'settings.editDataSource' : 
'settings.addDataSource')}
         open={modalOpen}
-        onCancel={() => {
-          setModalOpen(false);
-          setEditingDataSource(null);
-          dsForm.resetFields();
-        }}
+        onCancel={closeModal}
         onOk={() => void handleSubmit()}
         confirmLoading={submitting}
         destroyOnHidden
@@ -591,9 +613,10 @@ export const DataSourceTab = () => {
             icon={<ApiOutlined />}
             loading={testingKeys.has('modal')}
             onClick={() => {
+              const modalSession = modalTestSessionRef.current;
               void dsForm
                 .validateFields(testFieldNames(authValue))
-                .then((values) => handleTestConnection(values, 'modal'))
+                .then((values) => handleTestConnection(values, 'modal', 
modalSession))
                 .catch(() => undefined);
             }}
             style={{ marginTop: 8 }}
diff --git a/web/src/pages/settings/__tests__/DataSourceTab.test.tsx 
b/web/src/pages/settings/__tests__/DataSourceTab.test.tsx
index ab6475883..0295b103a 100644
--- a/web/src/pages/settings/__tests__/DataSourceTab.test.tsx
+++ b/web/src/pages/settings/__tests__/DataSourceTab.test.tsx
@@ -15,10 +15,10 @@
  * limitations under the License.
  */
 
-import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
+import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 
'vitest';
 import { act, fireEvent, render, screen, waitFor, within } from 
'@testing-library/react';
 import userEvent from '@testing-library/user-event';
-import { App } from 'antd';
+import { App, ConfigProvider, message } from 'antd';
 import type { DataSource, DataSourcePage } from '../../../api/settings';
 import {
   createDataSource,
@@ -105,6 +105,10 @@ beforeAll(() => {
 });
 
 describe('DataSourceTab', () => {
+  afterEach(() => {
+    vi.restoreAllMocks();
+  });
+
   beforeEach(() => {
     vi.clearAllMocks();
     localStorage.removeItem(LANGUAGE_STORAGE_KEY);
@@ -421,6 +425,112 @@ describe('DataSourceTab', () => {
     });
   });
 
+  it.each(['success', 'failure', 'rejection'] as const)(
+    'ignores a previous modal connection test after reopening (%s) Test',
+    async (outcome) => {
+      const previousTest = deferred<{ success: boolean; message: string }>();
+      const currentTest = deferred<{ success: boolean; message: string }>();
+      vi.mocked(testDataSource)
+        .mockReturnValueOnce(previousTest.promise)
+        .mockReturnValueOnce(currentTest.promise);
+      const successMessage = vi.spyOn(message, 'success');
+      const errorMessage = vi.spyOn(message, 'error');
+      localStorage.setItem(LANGUAGE_STORAGE_KEY, 'en');
+      const user = userEvent.setup({ pointerEventsCheck: 0 });
+      render(
+        <ConfigProvider theme={{ token: { motion: false } }}>
+          <App>
+            <LangProvider>
+              <DataSourceTab />
+            </LangProvider>
+          </App>
+        </ConfigProvider>,
+      );
+
+      const row = await screen.findByRole('row', { name: /Prometheus prod/ });
+      await user.click(within(row).getByRole('button', { name: /edit/i }));
+      const previousDialog = await screen.findByRole('dialog');
+      await user.click(within(previousDialog).getByRole('button', { name: 
/test connection/i }));
+      await waitFor(() => expect(testDataSource).toHaveBeenCalledTimes(1));
+      await user.click(within(previousDialog).getByRole('button', { name: 
/cancel/i }));
+      await waitFor(() => 
expect(screen.queryByRole('dialog')).not.toBeInTheDocument());
+
+      await user.click(within(row).getByRole('button', { name: /edit/i }));
+      const currentDialog = await screen.findByRole('dialog');
+      const testButton = within(currentDialog).getByRole('button', { name: 
/test connection/i });
+      expect(testButton).not.toHaveClass('ant-btn-loading');
+      fireEvent.change(within(currentDialog).getByLabelText('URL'), {
+        target: { value: 'http://prometheus-new:9090' },
+      });
+      await user.click(testButton);
+      await waitFor(() => expect(testDataSource).toHaveBeenCalledTimes(2));
+      expect(testDataSource).toHaveBeenLastCalledWith({
+        type: 'Prometheus',
+        url: 'http://prometheus-new:9090',
+        auth: 'None',
+      });
+      expect(testButton).toHaveClass('ant-btn-loading');
+
+      await act(async () => {
+        if (outcome === 'rejection') previousTest.reject(new Error('old 
request failed'));
+        else previousTest.resolve({ success: outcome === 'success', message: 
'Old test result' });
+      });
+      expect(testButton).toHaveClass('ant-btn-loading');
+      expect(successMessage).not.toHaveBeenCalled();
+      expect(errorMessage).not.toHaveBeenCalled();
+
+      await act(async () => currentTest.resolve({ success: true, message: 
'Current test result' }));
+      await waitFor(() => 
expect(testButton).not.toHaveClass('ant-btn-loading'));
+      expect(successMessage).toHaveBeenCalledWith('Current test result');
+    },
+  );
+
+  it.each(['cancel', 'save', 'unmount'] as const)(
+    'ignores a pending modal connection test after %s Test',
+    async (action) => {
+      const pendingTest = deferred<{ success: boolean; message: string }>();
+      vi.mocked(testDataSource).mockReturnValueOnce(pendingTest.promise);
+      vi.mocked(updateDataSource).mockResolvedValueOnce(sources[0]);
+      const successMessage = vi.spyOn(message, 'success');
+      const errorMessage = vi.spyOn(message, 'error');
+      localStorage.setItem(LANGUAGE_STORAGE_KEY, 'en');
+      const user = userEvent.setup({ pointerEventsCheck: 0 });
+      const { unmount } = render(
+        <ConfigProvider theme={{ token: { motion: false } }}>
+          <App>
+            <LangProvider>
+              <DataSourceTab />
+            </LangProvider>
+          </App>
+        </ConfigProvider>,
+      );
+
+      const row = await screen.findByRole('row', { name: /Prometheus prod/ });
+      await user.click(within(row).getByRole('button', { name: /edit/i }));
+      const dialog = await screen.findByRole('dialog');
+      await user.click(within(dialog).getByRole('button', { name: /test 
connection/i }));
+      await waitFor(() => expect(testDataSource).toHaveBeenCalledTimes(1));
+
+      if (action === 'unmount') unmount();
+      else {
+        await user.click(
+          within(dialog).getByRole('button', { name: action === 'save' ? 'OK' 
: /cancel/i }),
+        );
+        await waitFor(() => 
expect(screen.queryByRole('dialog')).not.toBeInTheDocument());
+        if (action === 'save') 
expect(updateDataSource).toHaveBeenCalledTimes(1);
+      }
+      successMessage.mockClear();
+      errorMessage.mockClear();
+
+      await act(async () => {
+        if (action === 'unmount') pendingTest.reject(new Error('request failed 
after unmount'));
+        else pendingTest.resolve({ success: true, message: 'Closed modal test 
result' });
+      });
+      expect(successMessage).not.toHaveBeenCalled();
+      expect(errorMessage).not.toHaveBeenCalled();
+    },
+  );
+
   it('exports all data sources that match the active filters without secrets', 
async () => {
     vi.mocked(listAllDataSources).mockResolvedValue([
       {

Reply via email to