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 108c5977 fix: handle partial topic batch deletion failures (#721)
108c5977 is described below

commit 108c5977364dccd3dcc75a779e2d43bbaf08cab9
Author: yx9o <[email protected]>
AuthorDate: Mon Aug 3 11:13:21 2026 +0800

    fix: handle partial topic batch deletion failures (#721)
---
 .../pages/instance/__tests__/TopicPage.test.tsx    | 24 ++++++++++
 web/src/pages/instance/topic.tsx                   | 24 +++++++---
 web/src/services/topicService.batchDelete.test.ts  | 53 ++++++++++++++++++++++
 web/src/services/topicService.ts                   | 21 +++++++--
 4 files changed, 112 insertions(+), 10 deletions(-)

diff --git a/web/src/pages/instance/__tests__/TopicPage.test.tsx 
b/web/src/pages/instance/__tests__/TopicPage.test.tsx
index 701afd10..f50a4db1 100644
--- a/web/src/pages/instance/__tests__/TopicPage.test.tsx
+++ b/web/src/pages/instance/__tests__/TopicPage.test.tsx
@@ -89,6 +89,7 @@ const getTableBody = () => {
 describe('TopicPage', () => {
   beforeEach(() => {
     topicServiceMocks.listTopics.mockResolvedValue(buildTopics(25));
+    topicServiceMocks.batchDeleteTopics.mockResolvedValue({ deleted: [], 
failed: [] });
     topicServiceMocks.getTopicRoutes.mockResolvedValue([]);
     topicServiceMocks.getTopicConsumers.mockResolvedValue([]);
   });
@@ -120,4 +121,27 @@ describe('TopicPage', () => {
     expect(within(getTableBody()).getByText('topic-21')).toBeInTheDocument();
     
expect(within(getTableBody()).queryByText('topic-01')).not.toBeInTheDocument();
   });
+
+  it('keeps failed topics selected after a partially successful batch 
deletion', async () => {
+    const user = userEvent.setup();
+    topicServiceMocks.listTopics.mockResolvedValue(buildTopics(3));
+    topicServiceMocks.batchDeleteTopics.mockResolvedValue({
+      deleted: ['topic-01', 'topic-03'],
+      failed: ['topic-02'],
+    });
+    renderWithProviders();
+
+    expect(await screen.findByText('topic-01')).toBeInTheDocument();
+    await user.click(screen.getAllByRole('checkbox')[0]);
+    await user.click(screen.getByRole('button', { name: /删除 \(3\)$/ }));
+
+    const dialog = await screen.findByRole('dialog');
+    await user.click(within(dialog).getByRole('button', { name: /删\s*除/ }));
+
+    await waitFor(() => 
expect(screen.queryByText('topic-01')).not.toBeInTheDocument());
+    expect(screen.getByText('topic-02')).toBeInTheDocument();
+    expect(screen.queryByText('topic-03')).not.toBeInTheDocument();
+    expect(screen.getByRole('button', { name: /删除 \(1\)$/ 
})).toBeInTheDocument();
+    expect(screen.getByText('已删除 2 个 Topic,1 个删除失败')).toBeInTheDocument();
+  });
 });
diff --git a/web/src/pages/instance/topic.tsx b/web/src/pages/instance/topic.tsx
index b3d9cfeb..9c0967a3 100644
--- a/web/src/pages/instance/topic.tsx
+++ b/web/src/pages/instance/topic.tsx
@@ -685,12 +685,24 @@ const TopicPage = () => {
                   onOk: async () => {
                     try {
                       const names = selectedRowKeys.map(String);
-                      await batchDeleteTopics(names);
-                      setTopics((previous) =>
-                        previous.filter((topic) => 
!names.includes(topic.name)),
-                      );
-                      message.success(`已删除 ${names.length} 个 Topic`);
-                      setSelectedRowKeys([]);
+                      const { deleted, failed } = await 
batchDeleteTopics(names);
+                      if (deleted.length > 0) {
+                        const deletedNames = new Set(deleted);
+                        setTopics((previous) =>
+                          previous.filter((topic) => 
!deletedNames.has(topic.name)),
+                        );
+                      }
+                      setSelectedRowKeys(failed);
+
+                      if (failed.length === 0) {
+                        message.success(`已删除 ${deleted.length} 个 Topic`);
+                      } else if (deleted.length > 0) {
+                        message.warning(
+                          `已删除 ${deleted.length} 个 Topic,${failed.length} 
个删除失败`,
+                        );
+                      } else {
+                        message.error(`${failed.length} 个 Topic 删除失败,请稍后重试`);
+                      }
                     } catch {
                       message.error('批量删除 Topic 失败,请稍后重试');
                     }
diff --git a/web/src/services/topicService.batchDelete.test.ts 
b/web/src/services/topicService.batchDelete.test.ts
new file mode 100644
index 00000000..49596b6c
--- /dev/null
+++ b/web/src/services/topicService.batchDelete.test.ts
@@ -0,0 +1,53 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { beforeEach, describe, expect, it, vi } from 'vitest';
+
+const metadataApiMocks = vi.hoisted(() => ({
+  deleteTopic: vi.fn(),
+}));
+
+vi.mock('../config', () => ({
+  API_BASE_URL: '/api',
+  USE_MOCK: false,
+}));
+
+vi.mock('../api/metadata', () => metadataApiMocks);
+
+import { batchDeleteTopics } from './topicService';
+
+describe('topic service batch deletion', () => {
+  beforeEach(() => {
+    vi.clearAllMocks();
+  });
+
+  it('continues deleting after a failure and reports each outcome', async () 
=> {
+    metadataApiMocks.deleteTopic.mockImplementation((name: string) =>
+      name === 'topic-02' ? Promise.reject(new Error('delete failed')) : 
Promise.resolve(),
+    );
+
+    await expect(batchDeleteTopics(['topic-01', 'topic-02', 
'topic-03'])).resolves.toEqual({
+      deleted: ['topic-01', 'topic-03'],
+      failed: ['topic-02'],
+    });
+    expect(metadataApiMocks.deleteTopic.mock.calls.map(([name]) => 
name)).toEqual([
+      'topic-01',
+      'topic-02',
+      'topic-03',
+    ]);
+  });
+});
diff --git a/web/src/services/topicService.ts b/web/src/services/topicService.ts
index 1390fda3..5db5ac48 100644
--- a/web/src/services/topicService.ts
+++ b/web/src/services/topicService.ts
@@ -64,11 +64,23 @@ export async function deleteTopic(name: string): 
Promise<void> {
   return metadataApi.deleteTopic(name);
 }
 
-// Batch delete: loop through single delete calls
-export async function batchDeleteTopics(names: string[]): Promise<void> {
+export interface BatchDeleteTopicsResult {
+  deleted: string[];
+  failed: string[];
+}
+
+// Batch delete: attempt every selected topic and report partial failures.
+export async function batchDeleteTopics(names: string[]): 
Promise<BatchDeleteTopicsResult> {
+  const result: BatchDeleteTopicsResult = { deleted: [], failed: [] };
   for (const name of names) {
-    await deleteTopic(name);
+    try {
+      await deleteTopic(name);
+      result.deleted.push(name);
+    } catch {
+      result.failed.push(name);
+    }
   }
+  return result;
 }
 
 export async function getTopicRoutes(name: string): Promise<BrokerRoute[]> {
@@ -77,7 +89,8 @@ export async function getTopicRoutes(name: string): 
Promise<BrokerRoute[]> {
 }
 
 export async function getTopicConsumers(name: string): 
Promise<ConsumerGroupInfo[]> {
-  if (USE_MOCK) return cloneConsumers((topicConsumers[name] as unknown as 
ConsumerGroupInfo[]) ?? []);
+  if (USE_MOCK)
+    return cloneConsumers((topicConsumers[name] as unknown as 
ConsumerGroupInfo[]) ?? []);
   return metadataApi.getTopicConsumers(name);
 }
 

Reply via email to