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 3a18bab0 [ISSUE #1505] Ignore stale cloud catalog responses (#1506)
3a18bab0 is described below

commit 3a18bab02fbea1afd9c84d5cf923dc6d57218047
Author: youngkermit8-coder <[email protected]>
AuthorDate: Tue Aug 11 20:19:02 2026 +0800

    [ISSUE #1505] Ignore stale cloud catalog responses (#1506)
    
    * [ISSUE #1505] Ignore stale cloud catalog responses
    
    Signed-off-by: youngkermit8-coder <[email protected]>
    
    * [ISSUE #1505] Clear cloud loading state on vendor changes
    
    Signed-off-by: youngkermit8-coder <[email protected]>
    
    ---------
    
    Signed-off-by: youngkermit8-coder <[email protected]>
---
 .../pages/instance/__tests__/InstancePage.test.tsx | 316 +++++++++++++++++++++
 web/src/pages/instance/index.tsx                   |  95 ++++++-
 2 files changed, 396 insertions(+), 15 deletions(-)

diff --git a/web/src/pages/instance/__tests__/InstancePage.test.tsx 
b/web/src/pages/instance/__tests__/InstancePage.test.tsx
index 5e8daeb3..2fd2442b 100644
--- a/web/src/pages/instance/__tests__/InstancePage.test.tsx
+++ b/web/src/pages/instance/__tests__/InstancePage.test.tsx
@@ -20,11 +20,27 @@ import { act, fireEvent, render, screen, waitFor, within } 
from '@testing-librar
 import userEvent from '@testing-library/user-event';
 import { MemoryRouter } from 'react-router-dom';
 import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
+import * as aliyunCatalogApi from '../../../api/aliyunCatalog';
+import * as cloudCredentialApi from '../../../api/cloudCredential';
 import type { Instance } from '../../../api/instance';
 import { LangProvider } from '../../../i18n/LangContext';
 import * as instanceService from '../../../services/instanceService';
 import InstancePage from '../index';
 
+vi.mock('../../../api/aliyunCatalog', () => ({
+  listAliyunInstances: vi.fn(),
+  listAliyunRegions: vi.fn(),
+}));
+
+vi.mock('../../../api/cloudCredential', () => ({
+  listCloudCredentials: vi.fn(),
+}));
+
+vi.mock('../../../api/tencentCatalog', () => ({
+  listTencentInstances: vi.fn(),
+  listTencentRegions: vi.fn(),
+}));
+
 vi.mock('../../../services/instanceService', () => ({
   createInstance: vi.fn(),
   deleteInstance: vi.fn(),
@@ -76,9 +92,22 @@ const renderPage = () =>
     </App>,
   );
 
+const deferred = <T,>() => {
+  let resolve!: (value: T) => void;
+  let reject!: (reason?: unknown) => void;
+  const promise = new Promise<T>((resolvePromise, rejectPromise) => {
+    resolve = resolvePromise;
+    reject = rejectPromise;
+  });
+  return { promise, reject, resolve };
+};
+
 describe('InstancePage', () => {
   beforeEach(() => {
     vi.clearAllMocks();
+    vi.mocked(cloudCredentialApi.listCloudCredentials).mockResolvedValue([]);
+    vi.mocked(aliyunCatalogApi.listAliyunRegions).mockResolvedValue([]);
+    vi.mocked(aliyunCatalogApi.listAliyunInstances).mockResolvedValue([]);
     vi.mocked(instanceService.listInstances).mockResolvedValue([
       instance('proxy-1', 'production-proxy'),
       instance('direct-1', 'development-direct', 'DIRECT'),
@@ -211,6 +240,293 @@ describe('InstancePage', () => {
     expect(within(dialog).getByText(/云凭据与云上实例完成接入/)).toBeInTheDocument();
   });
 
+  it('ignores a stale region response after the cloud credential changes', 
async () => {
+    const user = userEvent.setup();
+    const oldRegions = deferred<Array<{ regionId: string; regionName: string 
}>>();
+    const latestRegions = deferred<Array<{ regionId: string; regionName: 
string }>>();
+    vi.mocked(cloudCredentialApi.listCloudCredentials).mockResolvedValue([
+      {
+        id: 'cred-old',
+        name: 'old-account',
+        vendor: 'ALIYUN',
+        accessKey: 'LTAI-old',
+        createdAt: '2026-01-01T00:00:00Z',
+      },
+      {
+        id: 'cred-latest',
+        name: 'latest-account',
+        vendor: 'ALIYUN',
+        accessKey: 'LTAI-latest',
+        createdAt: '2026-01-01T00:00:00Z',
+      },
+    ]);
+    vi.mocked(aliyunCatalogApi.listAliyunRegions)
+      .mockReturnValueOnce(oldRegions.promise)
+      .mockReturnValueOnce(latestRegions.promise);
+
+    renderPage();
+    expect(await screen.findByText('production-proxy')).toBeInTheDocument();
+    await user.click(screen.getByRole('button', { name: /添加实例/ }));
+    const dialog = await screen.findByRole('dialog');
+    await user.click(within(dialog).getByRole('tab', { name: /Aliyun 版/ }));
+    await waitFor(() => 
expect(cloudCredentialApi.listCloudCredentials).toHaveBeenCalled());
+
+    const credentialSelect = within(dialog).getAllByRole('combobox')[0];
+    fireEvent.mouseDown(credentialSelect.parentElement!);
+    await user.click(
+      await screen.findByText(/old-account/, { selector: 
'.ant-select-item-option-content' }),
+    );
+    await waitFor(() =>
+      
expect(aliyunCatalogApi.listAliyunRegions).toHaveBeenCalledWith('cred-old'),
+    );
+
+    fireEvent.mouseDown(credentialSelect.parentElement!);
+    await user.click(
+      await screen.findByText(/latest-account/, { selector: 
'.ant-select-item-option-content' }),
+    );
+    await waitFor(() =>
+      
expect(aliyunCatalogApi.listAliyunRegions).toHaveBeenCalledWith('cred-latest'),
+    );
+
+    await act(async () =>
+      latestRegions.resolve([{ regionId: 'cn-shanghai', regionName: 'Shanghai' 
}]),
+    );
+    await act(async () =>
+      oldRegions.resolve([{ regionId: 'cn-hangzhou-old', regionName: 'Old 
Hangzhou' }]),
+    );
+
+    const regionSelect = within(dialog).getAllByRole('combobox')[1];
+    fireEvent.mouseDown(regionSelect.parentElement!);
+    expect(
+      await screen.findByText(/Shanghai/, { selector: 
'.ant-select-item-option-content' }),
+    ).toBeInTheDocument();
+    expect(
+      screen.queryByText(/Old Hangzhou/, { selector: 
'.ant-select-item-option-content' }),
+    ).not.toBeInTheDocument();
+  });
+
+  it('ignores a stale instance response after the cloud region changes', async 
() => {
+    const user = userEvent.setup();
+    const oldInstances =
+      deferred<
+        Array<{ instanceId: string; instanceName: string; status: string; 
regionId: string }>
+      >();
+    const latestInstances =
+      deferred<
+        Array<{ instanceId: string; instanceName: string; status: string; 
regionId: string }>
+      >();
+    vi.mocked(cloudCredentialApi.listCloudCredentials).mockResolvedValue([
+      {
+        id: 'cred-1',
+        name: 'cloud-account',
+        vendor: 'ALIYUN',
+        accessKey: 'LTAI-one',
+        createdAt: '2026-01-01T00:00:00Z',
+      },
+    ]);
+    vi.mocked(aliyunCatalogApi.listAliyunRegions).mockResolvedValue([
+      { regionId: 'cn-beijing', regionName: 'Beijing' },
+      { regionId: 'cn-shanghai', regionName: 'Shanghai' },
+    ]);
+    vi.mocked(aliyunCatalogApi.listAliyunInstances)
+      .mockReturnValueOnce(oldInstances.promise)
+      .mockReturnValueOnce(latestInstances.promise);
+
+    renderPage();
+    expect(await screen.findByText('production-proxy')).toBeInTheDocument();
+    await user.click(screen.getByRole('button', { name: /添加实例/ }));
+    const dialog = await screen.findByRole('dialog');
+    await user.click(within(dialog).getByRole('tab', { name: /Aliyun 版/ }));
+    await waitFor(() => 
expect(cloudCredentialApi.listCloudCredentials).toHaveBeenCalled());
+
+    const selects = within(dialog).getAllByRole('combobox');
+    fireEvent.mouseDown(selects[0].parentElement!);
+    await user.click(
+      await screen.findByText(/cloud-account/, { selector: 
'.ant-select-item-option-content' }),
+    );
+    await waitFor(() => 
expect(aliyunCatalogApi.listAliyunRegions).toHaveBeenCalledWith('cred-1'));
+
+    fireEvent.mouseDown(selects[1].parentElement!);
+    await user.click(
+      await screen.findByText(/Beijing/, { selector: 
'.ant-select-item-option-content' }),
+    );
+    await waitFor(() =>
+      
expect(aliyunCatalogApi.listAliyunInstances).toHaveBeenCalledWith('cred-1', 
'cn-beijing'),
+    );
+
+    fireEvent.mouseDown(selects[1].parentElement!);
+    await user.click(
+      await screen.findByText(/Shanghai/, { selector: 
'.ant-select-item-option-content' }),
+    );
+    await waitFor(() =>
+      
expect(aliyunCatalogApi.listAliyunInstances).toHaveBeenCalledWith('cred-1', 
'cn-shanghai'),
+    );
+
+    await act(async () =>
+      latestInstances.resolve([
+        {
+          instanceId: 'rmq-latest',
+          instanceName: 'latest-instance',
+          status: 'RUNNING',
+          regionId: 'cn-shanghai',
+        },
+      ]),
+    );
+    await act(async () =>
+      oldInstances.resolve([
+        {
+          instanceId: 'rmq-old',
+          instanceName: 'old-instance',
+          status: 'RUNNING',
+          regionId: 'cn-beijing',
+        },
+      ]),
+    );
+
+    fireEvent.mouseDown(selects[2].parentElement!);
+    expect(
+      await screen.findByText(/latest-instance/, { selector: 
'.ant-select-item-option-content' }),
+    ).toBeInTheDocument();
+    expect(
+      screen.queryByText(/old-instance/, { selector: 
'.ant-select-item-option-content' }),
+    ).not.toBeInTheDocument();
+  });
+
+  it('clears a pending region load when the cloud vendor changes', async () => 
{
+    const user = userEvent.setup();
+    const pendingRegions = deferred<Array<{ regionId: string; regionName: 
string }>>();
+    vi.mocked(cloudCredentialApi.listCloudCredentials).mockResolvedValue([
+      {
+        id: 'cred-aliyun',
+        name: 'aliyun-account',
+        vendor: 'ALIYUN',
+        accessKey: 'LTAI-one',
+        createdAt: '2026-01-01T00:00:00Z',
+      },
+      {
+        id: 'cred-tencent',
+        name: 'tencent-account',
+        vendor: 'TENCENT',
+        accessKey: 'AKID-one',
+        createdAt: '2026-01-01T00:00:00Z',
+      },
+    ]);
+    
vi.mocked(aliyunCatalogApi.listAliyunRegions).mockReturnValue(pendingRegions.promise);
+
+    renderPage();
+    expect(await screen.findByText('production-proxy')).toBeInTheDocument();
+    await user.click(screen.getByRole('button', { name: /添加实例/ }));
+    const dialog = await screen.findByRole('dialog');
+    await user.click(within(dialog).getByRole('tab', { name: /Aliyun/ }));
+    await waitFor(() => 
expect(cloudCredentialApi.listCloudCredentials).toHaveBeenCalled());
+
+    const credentialSelect = within(dialog).getAllByRole('combobox')[0];
+    fireEvent.mouseDown(credentialSelect.parentElement!);
+    await user.click(
+      await screen.findByText(/aliyun-account/, { selector: 
'.ant-select-item-option-content' }),
+    );
+    await waitFor(() =>
+      
expect(aliyunCatalogApi.listAliyunRegions).toHaveBeenCalledWith('cred-aliyun'),
+    );
+    await waitFor(() =>
+      
expect(within(dialog).getAllByRole('combobox')[1].closest('.ant-select')).toHaveClass(
+        'ant-select-loading',
+      ),
+    );
+
+    await user.click(within(dialog).getByRole('tab', { name: /Tencent/ }));
+
+    const switchedSelects = within(dialog).getAllByRole('combobox');
+    expect(switchedSelects[0]).toHaveValue('');
+    expect(switchedSelects[1]).toHaveValue('');
+    expect(switchedSelects[2]).toHaveValue('');
+    expect(switchedSelects[1]).toBeDisabled();
+    expect(switchedSelects[2]).toBeDisabled();
+    
expect(switchedSelects[1].closest('.ant-select')).not.toHaveClass('ant-select-loading');
+
+    await act(async () =>
+      pendingRegions.resolve([{ regionId: 'cn-hangzhou-old', regionName: 'Old 
Hangzhou' }]),
+    );
+    expect(switchedSelects[1]).toBeDisabled();
+  });
+
+  it('clears a pending instance load when the cloud vendor changes', async () 
=> {
+    const user = userEvent.setup();
+    const pendingInstances =
+      deferred<
+        Array<{ instanceId: string; instanceName: string; status: string; 
regionId: string }>
+      >();
+    vi.mocked(cloudCredentialApi.listCloudCredentials).mockResolvedValue([
+      {
+        id: 'cred-aliyun',
+        name: 'aliyun-account',
+        vendor: 'ALIYUN',
+        accessKey: 'LTAI-one',
+        createdAt: '2026-01-01T00:00:00Z',
+      },
+      {
+        id: 'cred-tencent',
+        name: 'tencent-account',
+        vendor: 'TENCENT',
+        accessKey: 'AKID-one',
+        createdAt: '2026-01-01T00:00:00Z',
+      },
+    ]);
+    vi.mocked(aliyunCatalogApi.listAliyunRegions).mockResolvedValue([
+      { regionId: 'cn-beijing', regionName: 'Beijing' },
+    ]);
+    
vi.mocked(aliyunCatalogApi.listAliyunInstances).mockReturnValue(pendingInstances.promise);
+
+    renderPage();
+    expect(await screen.findByText('production-proxy')).toBeInTheDocument();
+    await user.click(screen.getByRole('button', { name: /添加实例/ }));
+    const dialog = await screen.findByRole('dialog');
+    await user.click(within(dialog).getByRole('tab', { name: /Aliyun/ }));
+    await waitFor(() => 
expect(cloudCredentialApi.listCloudCredentials).toHaveBeenCalled());
+
+    const selects = within(dialog).getAllByRole('combobox');
+    fireEvent.mouseDown(selects[0].parentElement!);
+    await user.click(
+      await screen.findByText(/aliyun-account/, { selector: 
'.ant-select-item-option-content' }),
+    );
+    await waitFor(() => 
expect(aliyunCatalogApi.listAliyunRegions).toHaveBeenCalled());
+    fireEvent.mouseDown(selects[1].parentElement!);
+    await user.click(
+      await screen.findByText(/Beijing/, { selector: 
'.ant-select-item-option-content' }),
+    );
+    await waitFor(() =>
+      expect(aliyunCatalogApi.listAliyunInstances).toHaveBeenCalledWith(
+        'cred-aliyun',
+        'cn-beijing',
+      ),
+    );
+    await waitFor(() =>
+      
expect(selects[2].closest('.ant-select')).toHaveClass('ant-select-loading'),
+    );
+
+    await user.click(within(dialog).getByRole('tab', { name: /Tencent/ }));
+
+    const switchedSelects = within(dialog).getAllByRole('combobox');
+    expect(switchedSelects[0]).toHaveValue('');
+    expect(switchedSelects[1]).toHaveValue('');
+    expect(switchedSelects[2]).toHaveValue('');
+    expect(switchedSelects[1]).toBeDisabled();
+    expect(switchedSelects[2]).toBeDisabled();
+    
expect(switchedSelects[2].closest('.ant-select')).not.toHaveClass('ant-select-loading');
+
+    await act(async () =>
+      pendingInstances.resolve([
+        {
+          instanceId: 'rmq-old',
+          instanceName: 'old-instance',
+          status: 'RUNNING',
+          regionId: 'cn-beijing',
+        },
+      ]),
+    );
+    expect(switchedSelects[2]).toBeDisabled();
+  });
+
   it('sorts and renders instances without remarks', async () => {
     const user = userEvent.setup();
     vi.mocked(instanceService.listInstances).mockResolvedValue([
diff --git a/web/src/pages/instance/index.tsx b/web/src/pages/instance/index.tsx
index 824d9297..4eb346cd 100644
--- a/web/src/pages/instance/index.tsx
+++ b/web/src/pages/instance/index.tsx
@@ -141,22 +141,41 @@ const InstancePage = () => {
 
   useEffect(() => {
     if (!cloudVendor || !addModalOpen) {
-      return;
+      const timer = window.setTimeout(() => setCredentialsLoading(false), 0);
+      return () => window.clearTimeout(timer);
     }
+    let active = true;
     const timer = window.setTimeout(() => {
       setCredentialsLoading(true);
       listCloudCredentials()
-        .then((items) => setCredentials(items.filter((item) => item.vendor === 
vendor)))
-        .catch(() => message.error('云凭据列表加载失败'))
-        .finally(() => setCredentialsLoading(false));
+        .then((items) => {
+          if (active) {
+            setCredentials(items.filter((item) => item.vendor === vendor));
+          }
+        })
+        .catch(() => {
+          if (active) {
+            message.error('云凭据列表加载失败');
+          }
+        })
+        .finally(() => {
+          if (active) {
+            setCredentialsLoading(false);
+          }
+        });
     }, 0);
-    return () => window.clearTimeout(timer);
+    return () => {
+      active = false;
+      window.clearTimeout(timer);
+    };
   }, [vendor, cloudVendor, addModalOpen]);
 
   useEffect(() => {
     if (!cloudVendor || !addCredentialId) {
-      return;
+      const timer = window.setTimeout(() => setRegionsLoading(false), 0);
+      return () => window.clearTimeout(timer);
     }
+    let active = true;
     const timer = window.setTimeout(() => {
       setRegionsLoading(true);
       const request =
@@ -165,6 +184,9 @@ const InstancePage = () => {
           : listTencentRegions(addCredentialId);
       request
         .then((items) => {
+          if (!active) {
+            return;
+          }
           setRegions(items);
           if (!addForm.getFieldValue('regionId')) {
             const preferred = items.find(
@@ -175,16 +197,29 @@ const InstancePage = () => {
             }
           }
         })
-        .catch(() => message.error('云地域列表加载失败'))
-        .finally(() => setRegionsLoading(false));
+        .catch(() => {
+          if (active) {
+            message.error('云地域列表加载失败');
+          }
+        })
+        .finally(() => {
+          if (active) {
+            setRegionsLoading(false);
+          }
+        });
     }, 0);
-    return () => window.clearTimeout(timer);
+    return () => {
+      active = false;
+      window.clearTimeout(timer);
+    };
   }, [vendor, cloudVendor, addCredentialId, addForm]);
 
   useEffect(() => {
     if (!cloudVendor || !addCredentialId || !addRegionId) {
-      return;
+      const timer = window.setTimeout(() => setCloudInstancesLoading(false), 
0);
+      return () => window.clearTimeout(timer);
     }
+    let active = true;
     const timer = window.setTimeout(() => {
       setCloudInstancesLoading(true);
       const request =
@@ -192,13 +227,43 @@ const InstancePage = () => {
           ? listAliyunInstances(addCredentialId, addRegionId)
           : listTencentInstances(addCredentialId, addRegionId);
       request
-        .then(setCloudInstances)
-        .catch(() => message.error('云实例列表加载失败'))
-        .finally(() => setCloudInstancesLoading(false));
+        .then((items) => {
+          if (active) {
+            setCloudInstances(items);
+          }
+        })
+        .catch(() => {
+          if (active) {
+            message.error('云实例列表加载失败');
+          }
+        })
+        .finally(() => {
+          if (active) {
+            setCloudInstancesLoading(false);
+          }
+        });
     }, 0);
-    return () => window.clearTimeout(timer);
+    return () => {
+      active = false;
+      window.clearTimeout(timer);
+    };
   }, [vendor, cloudVendor, addCredentialId, addRegionId]);
 
+  const handleVendorChange = (nextVendor: string) => {
+    setVendor(nextVendor as InstanceVendor);
+    setCredentials([]);
+    setRegions([]);
+    setCloudInstances([]);
+    setCredentialsLoading(false);
+    setRegionsLoading(false);
+    setCloudInstancesLoading(false);
+    addForm.setFieldsValue({
+      credentialId: undefined,
+      regionId: undefined,
+      cloudInstanceId: undefined,
+    });
+  };
+
   const handleCredentialChange = () => {
     setRegions([]);
     setCloudInstances([]);
@@ -499,7 +564,7 @@ const InstancePage = () => {
         <Tabs
           type="card"
           activeKey={vendor}
-          onChange={(key) => setVendor(key as InstanceVendor)}
+          onChange={handleVendorChange}
           style={{ marginTop: 8, marginBottom: 4 }}
           items={VENDOR_OPTIONS.map((option) => ({
             key: option.key,

Reply via email to