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 b30035027 fix(web): surface failed mutations instead of swallowing the 
rejection (#7105)
b30035027 is described below

commit b300350278ef72a1240fad13738ddc599a2a2eb0
Author: messere <[email protected]>
AuthorDate: Mon Oct 26 20:15:41 2026 +0800

    fix(web): surface failed mutations instead of swallowing the rejection 
(#7105)
    
    Integration batch of three same-pattern fixes by messere1: a rejected bulk 
rule delete (#7045), a rejected consumer group delete (#7046) and a rejected 
proxy restart (#7105) all disappeared into antd's confirm wrapper, leaving the 
operator with no outcome. Each handler now reports the failure with its own 
message and keeps the selection armed where a retry makes sense. Commits keep 
their original author; #7045 and #7046 are closed as folded into this carrier.
---
 web/src/i18n/translations.ts                       | 12 +++++++++++
 .../pages/cluster/__tests__/ClusterPage.test.tsx   | 17 +++++++++++++++
 web/src/pages/cluster/index.tsx                    | 13 +++++++++---
 .../pages/instance/__tests__/ConsumerPage.test.tsx | 24 ++++++++++++++++++++++
 web/src/pages/instance/consumer.tsx                | 14 +++++++++----
 web/src/pages/ops/__tests__/AlertsPage.test.tsx    | 23 +++++++++++++++++++++
 web/src/pages/ops/alerts.tsx                       |  5 +++++
 7 files changed, 101 insertions(+), 7 deletions(-)

diff --git a/web/src/i18n/translations.ts b/web/src/i18n/translations.ts
index 5b73c6a49..4fe25240c 100644
--- a/web/src/i18n/translations.ts
+++ b/web/src/i18n/translations.ts
@@ -1430,6 +1430,10 @@ const translations: Record<string, Record<Lang, string>> 
= {
     zh: '删除完成:成功 {success},失败 {failed}',
     en: 'Delete completed: {success} succeeded, {failed} failed',
   },
+  'alerts.bulkDeleteFailed': {
+    zh: '{count} 条告警规则删除失败',
+    en: '{count} alert rules failed to delete',
+  },
   'alerts.bulkEnableSuccess': {
     zh: '已启用 {count} 条告警规则',
     en: 'Enabled {count} alert rules',
@@ -2560,6 +2564,10 @@ const translations: Record<string, Record<Lang, string>> 
= {
     en: 'All configurations and progress will be removed. This cannot be 
undone.',
   },
   'consumer.deleted': { zh: '消费组 {name} 已删除', en: 'Consumer group {name} 
deleted' },
+  'consumer.deleteFailed': {
+    zh: '删除消费组失败,请稍后重试',
+    en: 'Failed to delete consumer group. Please try again later.',
+  },
   'consumer.searchPlaceholder': { zh: '搜索 Group 名称或 Topic', en: 'Search group 
name or topic' },
   'consumer.allModes': { zh: '全部模式', en: 'All Modes' },
   'consumer.push': { zh: 'Push', en: 'Push' },
@@ -3018,6 +3026,10 @@ const translations: Record<string, Record<Lang, string>> 
= {
     zh: 'Proxy 重启已提交: {addr}',
     en: 'Proxy restart submitted: {addr}',
   },
+  'cluster.restartProxyFailed': {
+    zh: '重启 Proxy "{addr}" 失败,请稍后重试',
+    en: 'Failed to restart Proxy "{addr}". Please try again later.',
+  },
   'cluster.restartBrokerConfirm': {
     zh: '确定要重启 Broker "{name}" 吗?',
     en: 'Are you sure to restart Broker "{name}"?',
diff --git a/web/src/pages/cluster/__tests__/ClusterPage.test.tsx 
b/web/src/pages/cluster/__tests__/ClusterPage.test.tsx
index d582f726e..951325d8a 100644
--- a/web/src/pages/cluster/__tests__/ClusterPage.test.tsx
+++ b/web/src/pages/cluster/__tests__/ClusterPage.test.tsx
@@ -386,6 +386,23 @@ describe('Cluster page', () => {
     expect(within(dialog).getByText('8080')).toBeInTheDocument();
   });
 
+  it('reports a failed proxy restart instead of staying silent', async () => {
+    const user = userEvent.setup({ pointerEventsCheck: 0 });
+    renderWithProviders(<ClusterPage />);
+
+    await user.click(screen.getByRole('tab', { name: /Proxy 管理/ }));
+    const proxyRow = await screen.findByRole('row', { name: 
/10\.101\.2\.21:8081/ });
+    clusterServiceMocks.restartProxy.mockRejectedValueOnce(new Error('proxy 
unreachable'));
+    await user.click(within(proxyRow).getByRole('button', { name: /重\s*启/ }));
+
+    await user.click(await screen.findByRole('button', { name: /确\s*认/ }));
+    // A rejected onOk is dropped by antd's confirm wrapper: without a catch 
the operator gets
+    // no outcome at all — no toast, and the dialog never signals what 
happened.
+    expect(await screen.findByText(/重启 Proxy "10\.101\.2\.21:8081" 
失败/)).toBeInTheDocument();
+    expect(screen.queryByText(/Proxy 重启已提交/)).not.toBeInTheDocument();
+    expect(clusterServiceMocks.restartProxy).toHaveBeenCalledTimes(1);
+  });
+
   it('centers the broker status column', async () => {
     renderWithProviders(<ClusterPage />);
     const brokerRow = await screen.findByRole('row', { name: 
/10\.101\.2\.11:10911/ });
diff --git a/web/src/pages/cluster/index.tsx b/web/src/pages/cluster/index.tsx
index cd21b3137..2dfa24696 100644
--- a/web/src/pages/cluster/index.tsx
+++ b/web/src/pages/cluster/index.tsx
@@ -1823,9 +1823,16 @@ const ClusterPage = () => {
                   okText: t('common.confirm'),
                   cancelText: t('common.cancel'),
                   onOk: async () => {
-                    await restartProxy({ clusterId: record.clusterId, addr: 
record.addr });
-                    await requestRefresh('operation');
-                    message.success(t('cluster.restartProxySubmitted', { addr: 
record.addr }));
+                    // antd's confirm wrapper drops a rejected onOk: without 
this catch a failed
+                    // restart leaves the operator with no outcome at all — no 
toast, and a dialog
+                    // that resets its spinner as if nothing had been 
requested.
+                    try {
+                      await restartProxy({ clusterId: record.clusterId, addr: 
record.addr });
+                      await requestRefresh('operation');
+                      message.success(t('cluster.restartProxySubmitted', { 
addr: record.addr }));
+                    } catch {
+                      message.error(t('cluster.restartProxyFailed', { addr: 
record.addr }));
+                    }
                   },
                 });
               }}
diff --git a/web/src/pages/instance/__tests__/ConsumerPage.test.tsx 
b/web/src/pages/instance/__tests__/ConsumerPage.test.tsx
index 4b2bba94b..365c28a70 100644
--- a/web/src/pages/instance/__tests__/ConsumerPage.test.tsx
+++ b/web/src/pages/instance/__tests__/ConsumerPage.test.tsx
@@ -471,6 +471,30 @@ describe('Consumer page', () => {
     confirmSpy.mockRestore();
   });
 
+  it('reports a failed consumer group deletion instead of staying silent', 
async () => {
+    const user = userEvent.setup({ pointerEventsCheck: 0 });
+    const confirmSpy = vi.spyOn(Modal, 'confirm').mockImplementation((config) 
=> {
+      void config.onOk?.();
+      return { destroy: vi.fn(), update: vi.fn() } as unknown as 
ReturnType<typeof Modal.confirm>;
+    });
+    vi.mocked(consumerService.deleteConsumerGroup).mockRejectedValue(new 
Error('network error'));
+    
vi.mocked(consumerService.listConsumerGroupPage).mockResolvedValue(groupPage([group]));
+    renderWithProviders(<ConsumerPage />);
+
+    const row = await screen.findByRole('row', { name: /remote-cg/ });
+    await user.click(within(row).getByRole('button', { name: /删除/ }));
+
+    await waitFor(() =>
+      
expect(consumerService.deleteConsumerGroup).toHaveBeenCalledWith('remote-cg', 
'instance-1'),
+    );
+    // The delete was rejected, so the operator must be told instead of the 
request
+    // failing with no feedback at all.
+    expect(await screen.findByText('删除消费组失败,请稍后重试')).toBeInTheDocument();
+    // The group still exists on the server, so the row must still be listed.
+    expect(await screen.findByRole('row', { name: /remote-cg/ 
})).toBeInTheDocument();
+    confirmSpy.mockRestore();
+  });
+
   it('moves back from an emptied last consumer group page after batch 
deletion', async () => {
     const user = userEvent.setup({ pointerEventsCheck: 0 });
     const confirmSpy = vi.spyOn(Modal, 'confirm').mockImplementation((config) 
=> {
diff --git a/web/src/pages/instance/consumer.tsx 
b/web/src/pages/instance/consumer.tsx
index 8578a65dc..92b59ba5b 100644
--- a/web/src/pages/instance/consumer.tsx
+++ b/web/src/pages/instance/consumer.tsx
@@ -1108,10 +1108,16 @@ const ConsumerPageContent = ({
                 okButtonProps: { danger: true },
                 cancelText: '取消',
                 onOk: async () => {
-                  await deleteConsumerGroup(record.name, selectedInstanceId || 
undefined);
-                  await reloadConsumerGroupPage();
-                  setSelectedRowKeys((prev) => prev.filter((key) => key !== 
record.name));
-                  message.success(`消费组 ${record.name} 已删除`);
+                  try {
+                    await deleteConsumerGroup(record.name, selectedInstanceId 
|| undefined);
+                    await reloadConsumerGroupPage();
+                    setSelectedRowKeys((prev) => prev.filter((key) => key !== 
record.name));
+                    message.success(`消费组 ${record.name} 已删除`);
+                  } catch {
+                    // A failed delete must tell the operator: the row still 
exists on the
+                    // server, and a silent rejection leaves the dialog open 
with no outcome.
+                    message.error(t('consumer.deleteFailed'));
+                  }
                 },
               });
             }}
diff --git a/web/src/pages/ops/__tests__/AlertsPage.test.tsx 
b/web/src/pages/ops/__tests__/AlertsPage.test.tsx
index 94bdedf5e..85f219945 100644
--- a/web/src/pages/ops/__tests__/AlertsPage.test.tsx
+++ b/web/src/pages/ops/__tests__/AlertsPage.test.tsx
@@ -1297,6 +1297,29 @@ describe('AlertsPage', () => {
     expect(await screen.findByText('所选告警规则已删除')).toBeInTheDocument();
   });
 
+  it('shows a failure message and keeps the selection when the bulk delete 
request fails', async () => {
+    vi.mocked(bulkDeleteAlertRules).mockRejectedValue(new Error('network 
error'));
+    const user = userEvent.setup();
+    renderPage();
+    await screen.findByText('Broker disk usage');
+    await user.click(within(getRuleRow('Broker disk 
usage')).getByRole('checkbox'));
+    await user.click(within(getRuleRow('Consumer lag')).getByRole('checkbox'));
+    await user.click(screen.getByRole('button', { name: '批量删除' }));
+    await user.click(await screen.findByRole('button', { name: 'OK' }));
+
+    await waitFor(() => expect(bulkDeleteAlertRules).toHaveBeenCalledWith([1, 
2]));
+    expect(await screen.findByText('2 条告警规则删除失败')).toBeInTheDocument();
+    // A request-level failure must not clear the selection: every selected 
rule still
+    // exists on the server, so the operator can retry without re-checking 
rows.
+    expect(within(getRuleRow('Broker disk 
usage')).getByRole('checkbox')).toBeChecked();
+    expect(within(getRuleRow('Consumer 
lag')).getByRole('checkbox')).toBeChecked();
+    // The confirmation dialog closes once the outcome (even a failed one) is 
reported,
+    // instead of sitting open with no feedback the way a rejected onOk does.
+    await waitFor(() =>
+      expect(screen.queryByRole('button', { name: 'OK' 
})).not.toBeInTheDocument(),
+    );
+  });
+
   it('stays on the page after a bulk delete clears a full page while more 
pages remain', async () => {
     // 45 rules in total: page 2 holds a full page of 20 rules (ids 21-40). 
Deleting that full
     // page leaves 25 rules across 2 pages, so page 2 is still valid and the 
view must refresh
diff --git a/web/src/pages/ops/alerts.tsx b/web/src/pages/ops/alerts.tsx
index 4a77a1a3b..266474041 100644
--- a/web/src/pages/ops/alerts.tsx
+++ b/web/src/pages/ops/alerts.tsx
@@ -678,6 +678,11 @@ const AlertsPageContent = ({ domain = 'CLUSTER' }: 
AlertsPageProps) => {
                 failed: failedIds.length,
               }),
             );
+        } catch {
+          // A request-level failure (network error, 5xx) deletes nothing, so 
the selection
+          // stays armed for a retry. Without this branch the rejected onOk is 
swallowed by
+          // antd and the dialog just stays open with no outcome at all.
+          message.error(t('alerts.bulkDeleteFailed', { count: targetIds.length 
}));
         } finally {
           setBulkAction(null);
         }

Reply via email to