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);
}