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 aa0b16f6 [ISSUE #1619] Prevent duplicate Topic create requests (#1629)
aa0b16f6 is described below

commit aa0b16f66e37317b8684097e35689905cb8479eb
Author: 0 <[email protected]>
AuthorDate: Tue Aug 11 20:32:49 2026 +0800

    [ISSUE #1619] Prevent duplicate Topic create requests (#1629)
---
 .../pages/instance/__tests__/TopicPage.test.tsx    | 22 ++++++++++++++++++++--
 web/src/pages/instance/topic.tsx                   | 15 +++++++++++++--
 2 files changed, 33 insertions(+), 4 deletions(-)

diff --git a/web/src/pages/instance/__tests__/TopicPage.test.tsx 
b/web/src/pages/instance/__tests__/TopicPage.test.tsx
index 0750eeb3..9144c18f 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 { render, screen, waitFor, within } from '@testing-library/react';
+import { 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 } from 'antd';
@@ -154,6 +154,24 @@ describe('TopicPage', () => {
     vi.clearAllMocks();
   });
 
+  it('ignores duplicate Topic creates while the first request is pending', 
async () => {
+    topicServiceMocks.createTopic.mockImplementation(() => new Promise(() => 
{}));
+    const user = userEvent.setup();
+    renderWithProviders();
+
+    expect(await screen.findByText('topic-01')).toBeInTheDocument();
+    await user.click(screen.getByRole('button', { name: /创建 Topic/ }));
+    const dialog = await screen.findByRole('dialog');
+    await user.type(within(dialog).getByLabelText('Topic 名称'), 'new-topic');
+    const create = within(dialog).getByRole('button', { name: /创\s*建/ });
+
+    fireEvent.click(create);
+    fireEvent.click(create);
+
+    await waitFor(() => 
expect(topicServiceMocks.createTopic).toHaveBeenCalledTimes(1));
+    expect(create).toHaveClass('ant-btn-loading');
+  });
+
   it('downloads the currently filtered topics when exporting', async () => {
     const user = userEvent.setup();
     const clickSpy = vi.spyOn(HTMLAnchorElement.prototype, 
'click').mockImplementation(vi.fn());
@@ -422,7 +440,7 @@ describe('TopicPage', () => {
 
     expect(await screen.findByText('共 0 个 Topic')).toBeInTheDocument();
     expect(topicServiceMocks.listTopics).not.toHaveBeenCalled();
-    expect(document.querySelector('.ant-spin-spinning')).toBeNull();
+    await waitFor(() => 
expect(document.querySelector('.ant-spin-spinning')).toBeNull());
     expect(screen.getByRole('button', { name: /导入/ })).toBeDisabled();
     expect(screen.getByRole('button', { name: /创建 Topic/ })).toBeDisabled();
   });
diff --git a/web/src/pages/instance/topic.tsx b/web/src/pages/instance/topic.tsx
index c34428b1..45c28a74 100644
--- a/web/src/pages/instance/topic.tsx
+++ b/web/src/pages/instance/topic.tsx
@@ -291,6 +291,7 @@ const TopicPage = () => {
   const [rebuilding, setRebuilding] = useState(false);
   const [selectedTopic, setSelectedTopic] = useState<Topic | null>(null);
   const [modalOpen, setModalOpen] = useState(false);
+  const [creating, setCreating] = useState(false);
   const [form] = Form.useForm();
   const [sendModalOpen, setSendModalOpen] = useState(false);
   const [sendTopic, setSendTopic] = useState<Topic | null>(null);
@@ -306,6 +307,7 @@ const TopicPage = () => {
   const [importing, setImporting] = useState(false);
 
   const topicRequestIdRef = useRef(0);
+  const createInFlightRef = useRef(false);
 
   useEffect(() => {
     if (!selectedInstanceId) {
@@ -682,10 +684,13 @@ const TopicPage = () => {
 
   // ─── Create modal submit ──────────────────────────────────────
   const handleCreate = async () => {
+    if (createInFlightRef.current) return;
     if (!selectedInstanceId) {
       message.error('请先选择实例');
       return;
     }
+    createInFlightRef.current = true;
+    setCreating(true);
     try {
       const values = await form.validateFields();
       const created = await createTopic({
@@ -696,8 +701,13 @@ const TopicPage = () => {
       message.success(`Topic「${created.name}」创建成功`);
       setModalOpen(false);
       form.resetFields();
-    } catch {
-      message.error('创建 Topic 失败,请稍后重试');
+    } catch (error) {
+      if (!(error && typeof error === 'object' && 'errorFields' in error)) {
+        message.error('创建 Topic 失败,请稍后重试');
+      }
+    } finally {
+      createInFlightRef.current = false;
+      setCreating(false);
     }
   };
 
@@ -1117,6 +1127,7 @@ const TopicPage = () => {
           form.resetFields();
         }}
         onOk={handleCreate}
+        confirmLoading={creating}
         okText="创建"
         cancelText="取消"
         width={560}

Reply via email to