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 b79849a5c fix(consumer): refresh risk baseline after saving group
settings (#6182)
b79849a5c is described below
commit b79849a5c95ed1f5844f7253cc38c8ec9208f36e
Author: zmuxuny <[email protected]>
AuthorDate: Mon Oct 12 00:17:04 2026 -0700
fix(consumer): refresh risk baseline after saving group settings (#6182)
`consumer.tsx:532` was the only place that wrote `originalSettingsRef`, so
the risk baseline stayed frozen at whatever the modal had loaded. Enabling
consumption, saving, then disabling it inside the same session made the
comparison at `consumer.tsx:564` false and skipped the high-risk confirmation
dialog entirely; reverting a saved ordered-consumption change had the same
hole. The continuation after `updateConsumerGroupSettings` (`:588-612`) also
ran without re-checking `settingsRequ [...]
Four new cases in `ConsumerPage.test.tsx` pin this. Dropping the baseline
refresh makes `requires confirmation when disabling consumption after enabling
it in the same session` and `requires a new confirmation when reverting a saved
ordered-consumption change` see zero `Modal.confirm` calls; dropping the
failure-branch guard breaks `keeps the accepted risk baseline when a settings
save fails`; dropping the post-save request-id check breaks `does not let a
closed group save replace the [...]
---
.../pages/instance/__tests__/ConsumerPage.test.tsx | 149 +++++++++++++++++++++
web/src/pages/instance/consumer.tsx | 19 ++-
2 files changed, 165 insertions(+), 3 deletions(-)
diff --git a/web/src/pages/instance/__tests__/ConsumerPage.test.tsx
b/web/src/pages/instance/__tests__/ConsumerPage.test.tsx
index 95632fe86..f318b74df 100644
--- a/web/src/pages/instance/__tests__/ConsumerPage.test.tsx
+++ b/web/src/pages/instance/__tests__/ConsumerPage.test.tsx
@@ -1926,6 +1926,155 @@ describe('Consumer page', () => {
expect((await screen.findAllByText('消费组配置已保存')).length).toBeGreaterThan(0);
});
+ it('requires confirmation when disabling consumption after enabling it in
the same session', async () => {
+ const initialSettings = {
+ groupName: 'remote-cg',
+ retryQueueNums: 1,
+ retryMaxTimes: 16,
+ consumeEnable: false,
+ consumeMessageOrderly: false,
+ consumeBroadcastEnable: false,
+ };
+
vi.mocked(consumerService.getConsumerGroupSettings).mockResolvedValue(initialSettings);
+
vi.mocked(consumerService.updateConsumerGroupSettings).mockResolvedValueOnce({
+ ...initialSettings,
+ consumeEnable: true,
+ });
+ const confirm = vi.spyOn(Modal, 'confirm');
+ const user = userEvent.setup({ pointerEventsCheck: 0 });
+ renderWithProviders(<ConsumerPage />);
+ const row = await screen.findByRole('row', { name: /remote-cg/ });
+ await user.click(within(row).getByRole('button', { name: '详情' }));
+ const dialog = await screen.findByRole('dialog', { name: /remote-cg/ });
+ await user.click(within(dialog).getByRole('tab', { name: '配置' }));
+ const toggle = await within(dialog).findByRole('switch', { name: '启用消费' });
+ await waitFor(() =>
expect(within(dialog).getByLabelText('重试队列数')).toHaveValue('1'));
+ await user.click(toggle);
+ await user.click(within(dialog).getByRole('button', { name: /保\s*存/ }));
+ await waitFor(() =>
+
expect(consumerService.updateConsumerGroupSettings).toHaveBeenCalledTimes(1),
+ );
+ await waitFor(() =>
+ expect(within(dialog).getByRole('button', { name: /保\s*存/
})).not.toHaveClass(
+ 'ant-btn-loading',
+ ),
+ );
+ await user.click(toggle);
+ await user.click(within(dialog).getByRole('button', { name: /保\s*存/ }));
+ await waitFor(() => expect(confirm).toHaveBeenCalledTimes(1));
+
expect(consumerService.updateConsumerGroupSettings).toHaveBeenCalledTimes(1);
+ await user.click(await screen.findByRole('button', { name: /取\s*消/ }));
+
expect(consumerService.updateConsumerGroupSettings).toHaveBeenCalledTimes(1);
+ });
+
+ it('requires a new confirmation when reverting a saved ordered-consumption
change', async () => {
+ const settings = {
+ groupName: 'remote-cg',
+ retryQueueNums: 1,
+ retryMaxTimes: 16,
+ consumeEnable: true,
+ consumeMessageOrderly: false,
+ };
+
vi.mocked(consumerService.getConsumerGroupSettings).mockResolvedValue(settings);
+
vi.mocked(consumerService.updateConsumerGroupSettings).mockResolvedValueOnce({
+ ...settings,
+ consumeMessageOrderly: true,
+ });
+ const confirm = vi.spyOn(Modal, 'confirm');
+ const user = userEvent.setup({ pointerEventsCheck: 0 });
+ renderWithProviders(<ConsumerPage />);
+ const row = await screen.findByRole('row', { name: /remote-cg/ });
+ await user.click(within(row).getByRole('button', { name: '详情' }));
+ const dialog = await screen.findByRole('dialog', { name: /remote-cg/ });
+ await user.click(within(dialog).getByRole('tab', { name: '配置' }));
+ await waitFor(() =>
expect(within(dialog).getByLabelText('重试队列数')).toHaveValue('1'));
+ const toggle = within(dialog).getByRole('switch', { name: '顺序消费' });
+ await user.click(toggle);
+ await user.click(within(dialog).getByRole('button', { name: /保\s*存/ }));
+ await user.click(await screen.findByRole('button', { name: '确认修改' }));
+ await waitFor(() =>
+
expect(consumerService.updateConsumerGroupSettings).toHaveBeenCalledTimes(1),
+ );
+ await waitFor(() =>
+ expect(within(dialog).getByRole('button', { name: /保\s*存/
})).not.toHaveClass(
+ 'ant-btn-loading',
+ ),
+ );
+ await user.click(toggle);
+ await user.click(within(dialog).getByRole('button', { name: /保\s*存/ }));
+ await waitFor(() => expect(confirm).toHaveBeenCalledTimes(2));
+
expect(consumerService.updateConsumerGroupSettings).toHaveBeenCalledTimes(1);
+ });
+
+ it('keeps the accepted risk baseline when a settings save fails', async ()
=> {
+ vi.mocked(consumerService.getConsumerGroupSettings).mockResolvedValue({
+ groupName: 'remote-cg',
+ retryQueueNums: 1,
+ retryMaxTimes: 16,
+ consumeEnable: true,
+ consumeMessageOrderly: false,
+ });
+
vi.mocked(consumerService.updateConsumerGroupSettings).mockRejectedValueOnce(
+ new Error('offline'),
+ );
+ const confirm = vi.spyOn(Modal, 'confirm');
+ const user = userEvent.setup({ pointerEventsCheck: 0 });
+ renderWithProviders(<ConsumerPage />);
+ const row = await screen.findByRole('row', { name: /remote-cg/ });
+ await user.click(within(row).getByRole('button', { name: '详情' }));
+ const dialog = await screen.findByRole('dialog', { name: /remote-cg/ });
+ await user.click(within(dialog).getByRole('tab', { name: '配置' }));
+ await waitFor(() =>
expect(within(dialog).getByLabelText('重试队列数')).toHaveValue('1'));
+ await user.click(within(dialog).getByRole('switch', { name: '启用消费' }));
+ await user.click(within(dialog).getByRole('button', { name: /保\s*存/ }));
+ await user.click(await screen.findByRole('button', { name: '确认修改' }));
+ await screen.findByText('保存消费组配置失败,请稍后重试');
+ await user.click(within(dialog).getByRole('button', { name: /保\s*存/ }));
+ await waitFor(() => expect(confirm).toHaveBeenCalledTimes(2));
+
expect(consumerService.updateConsumerGroupSettings).toHaveBeenCalledTimes(1);
+ });
+
+ it('does not let a closed group save replace the next group risk baseline',
async () => {
+ const settings = {
+ groupName: 'remote-cg',
+ retryQueueNums: 1,
+ retryMaxTimes: 16,
+ consumeEnable: false,
+ };
+ const pendingSave = deferred<typeof settings>();
+ vi.mocked(consumerService.listConsumerGroupPage).mockResolvedValue(
+ groupPage([group, { ...group, name: 'other-cg' }]),
+ );
+ vi.mocked(consumerService.getConsumerGroupSettings)
+ .mockResolvedValueOnce(settings)
+ .mockResolvedValueOnce({ ...settings, groupName: 'other-cg',
consumeEnable: true });
+
vi.mocked(consumerService.updateConsumerGroupSettings).mockReturnValueOnce(pendingSave.promise);
+ const confirm = vi.spyOn(Modal, 'confirm');
+ const user = userEvent.setup({ pointerEventsCheck: 0 });
+ renderWithProviders(<ConsumerPage />);
+ const row = await screen.findByRole('row', { name: /remote-cg/ });
+ await user.click(within(row).getByRole('button', { name: '详情' }));
+ const dialog = await screen.findByRole('dialog', { name: /remote-cg/ });
+ await user.click(within(dialog).getByRole('tab', { name: '配置' }));
+ await waitFor(() =>
expect(within(dialog).getByLabelText('重试队列数')).toHaveValue('1'));
+ await user.click(within(dialog).getByRole('button', { name: /保\s*存/ }));
+ await waitFor(() =>
+
expect(consumerService.updateConsumerGroupSettings).toHaveBeenCalledTimes(1),
+ );
+ fireEvent.click(dialog.querySelector('.ant-modal-close') as HTMLElement);
+ fireEvent.click(
+ within(screen.getByRole('row', { name: /other-cg/
})).getByRole('button', { name: '详情' }),
+ );
+ const nextDialog = await screen.findByRole('dialog', { name: /other-cg/ });
+ await user.click(within(nextDialog).getByRole('tab', { name: '配置' }));
+ await waitFor(() =>
expect(within(nextDialog).getByLabelText('重试队列数')).toHaveValue('1'));
+ await act(async () => pendingSave.resolve({ ...settings, consumeEnable:
false }));
+ await user.click(within(nextDialog).getByRole('switch', { name: '启用消费' }));
+ await user.click(within(nextDialog).getByRole('button', { name: /保\s*存/
}));
+ await waitFor(() => expect(confirm).toHaveBeenCalledTimes(1));
+
expect(consumerService.updateConsumerGroupSettings).toHaveBeenCalledTimes(1);
+ });
+
it('renders an unknown (-1) lag as unavailable in the table and the lag
detail', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
vi.mocked(consumerService.listConsumerGroupPage).mockResolvedValue(
diff --git a/web/src/pages/instance/consumer.tsx
b/web/src/pages/instance/consumer.tsx
index fffe1d524..c118f45ad 100644
--- a/web/src/pages/instance/consumer.tsx
+++ b/web/src/pages/instance/consumer.tsx
@@ -558,7 +558,9 @@ const ConsumerPageContent = ({
const saveSettings = async () => {
if (!settingsGroup || !selectedInstanceId) return;
+ const requestId = settingsRequestIdRef.current;
const values = await settingsForm.validateFields();
+ if (requestId !== settingsRequestIdRef.current) return;
const original = originalSettingsRef.current;
const risks: string[] = [];
if (original && values.consumeEnable === false && original.consumeEnable
!== false) {
@@ -583,7 +585,7 @@ const ConsumerPageContent = ({
onCancel: () => resolve(false),
});
});
- if (!confirmed) return;
+ if (!confirmed || requestId !== settingsRequestIdRef.current) return;
}
setSettingsSubmitting(true);
try {
@@ -592,6 +594,13 @@ const ConsumerPageContent = ({
name: settingsGroup.name,
...values,
});
+ if (requestId !== settingsRequestIdRef.current) return;
+ // Subsequent edits must be compared with the settings the broker just
accepted,
+ // not the snapshot from when this modal was first opened.
+ originalSettingsRef.current = {
+ consumeEnable: values.consumeEnable,
+ consumeMessageOrderly: values.consumeMessageOrderly,
+ };
setGroups((current) =>
current.map((group) =>
group.name === settingsGroup.name
@@ -606,9 +615,11 @@ const ConsumerPageContent = ({
);
message.success('消费组配置已保存');
} catch {
- message.error('保存消费组配置失败,请稍后重试');
+ if (requestId === settingsRequestIdRef.current) {
+ message.error('保存消费组配置失败,请稍后重试');
+ }
} finally {
- setSettingsSubmitting(false);
+ if (requestId === settingsRequestIdRef.current)
setSettingsSubmitting(false);
}
};
@@ -1654,6 +1665,8 @@ const ConsumerPageContent = ({
setShowOnlyInconsistent(false);
setSettingsGroup(null);
setSettingsLoading(false);
+ setSettingsSubmitting(false);
+ originalSettingsRef.current = null;
settingsForm.resetFields();
}}
width={detailTab === 'progress' || detailTab === 'health' ? 1080 : 800}