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 aa377cf41 fix(consumer): require loaded settings before saving (#6764)
aa377cf41 is described below
commit aa377cf4196e8cc8a96b2ce7d412c0647df37d6a
Author: zmuxuny <[email protected]>
AuthorDate: Tue Oct 27 03:26:18 2026 -0700
fix(consumer): require loaded settings before saving (#6764)
The consumer group settings drawer let an operator press Save before the
initial
read had landed, so the form submitted its empty defaults over the group's
real
configuration - a write that bypassed the confirmation the loaded values are
supposed to give.
Track the initial read with settingsReady / settingsFailed: the Save and
Reset
buttons stay disabled until it succeeds, saveSettings() returns early
unless the
state is ready and not loading, a failed read renders an error with a retry
that
re-arms the form, and closing the drawer clears both flags plus the captured
original values so the next group cannot inherit them. The hardcoded Chinese
failure toast moves to the consumer.settingsLoadFailed i18n key.
The tests pin the button-level disabling; the early return in
saveSettings() is
defence in depth and is not covered by an assertion of its own.
---
web/src/i18n/translations.ts | 4 +
.../pages/instance/__tests__/ConsumerPage.test.tsx | 151 +++++++++++++++++++++
web/src/pages/instance/consumer.tsx | 36 ++++-
3 files changed, 188 insertions(+), 3 deletions(-)
diff --git a/web/src/i18n/translations.ts b/web/src/i18n/translations.ts
index b00b05ac9..3088a7b38 100644
--- a/web/src/i18n/translations.ts
+++ b/web/src/i18n/translations.ts
@@ -2609,6 +2609,10 @@ const translations: Record<string, Record<Lang, string>>
= {
en: 'Failed to load consume progress for consumer group {name}',
},
'consumer.createFailed': { zh: '消费组创建失败', en: 'Failed to create consumer
group' },
+ 'consumer.settingsLoadFailed': {
+ zh: '加载消费组配置失败,请稍后重试',
+ en: 'Failed to load consumer group settings. Please retry.',
+ },
'consumer.resetFailed': { zh: '消费位点重置失败', en: 'Failed to reset consume
offset' },
// ─── Home Page (additional) ───
diff --git a/web/src/pages/instance/__tests__/ConsumerPage.test.tsx
b/web/src/pages/instance/__tests__/ConsumerPage.test.tsx
index 038661ddf..312796132 100644
--- a/web/src/pages/instance/__tests__/ConsumerPage.test.tsx
+++ b/web/src/pages/instance/__tests__/ConsumerPage.test.tsx
@@ -2193,6 +2193,157 @@ describe('Consumer page', () => {
expect(cells[protocolIndex].querySelector('.ant-tag')).toBeNull();
});
+ it('doesNotSubmitConsumerSettingsAfterTheInitialReadFailsTest', async () => {
+ vi.mocked(consumerService.getConsumerGroupSettings).mockRejectedValueOnce(
+ new Error('read failed'),
+ );
+ vi.mocked(consumerService.updateConsumerGroupSettings).mockResolvedValue({
+ groupName: 'remote-cg',
+ retryQueueNums: 1,
+ retryMaxTimes: 16,
+ consumeEnable: false,
+ });
+ 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 screen.findAllByText('加载消费组配置失败,请稍后重试');
+ await user.type(within(dialog).getByLabelText('重试队列数'), '1');
+ await user.type(within(dialog).getByLabelText('最大重试次数'), '16');
+ await user.click(within(dialog).getByRole('switch', { name: '启用消费' }));
+ await user.click(within(dialog).getByRole('switch', { name: '启用消费' }));
+ await user.click(within(dialog).getByRole('button', { name: /保\s*存/ }));
+
+ expect(consumerService.updateConsumerGroupSettings).not.toHaveBeenCalled();
+ expect(within(dialog).getByRole('button', { name: /保\s*存/
})).toBeDisabled();
+ expect(within(dialog).getByLabelText('重试队列数')).toBeDisabled();
+ expect(within(dialog).getByRole('switch', { name: '启用消费'
})).toBeDisabled();
+ });
+
+ it('blocksSettingsSavesWhileTheInitialReadIsPendingTest', async () => {
+ const pendingSettings = deferred<{
+ groupName: string;
+ retryQueueNums: number;
+ retryMaxTimes: number;
+ consumeEnable: boolean;
+ }>();
+ vi.mocked(consumerService.getConsumerGroupSettings).mockReturnValueOnce(
+ pendingSettings.promise,
+ );
+ 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(consumerService.getConsumerGroupSettings).toHaveBeenCalledTimes(1));
+ expect(within(dialog).getByRole('button', { name: /保\s*存/
})).toBeDisabled();
+ expect(within(dialog).getByLabelText('重试队列数')).toBeDisabled();
+ expect(within(dialog).getByLabelText('最大重试次数')).toBeDisabled();
+ expect(within(dialog).getByRole('switch', { name: '启用消费'
})).toBeDisabled();
+ await user.click(within(dialog).getByRole('button', { name: /保\s*存/ }));
+ expect(consumerService.updateConsumerGroupSettings).not.toHaveBeenCalled();
+ await act(async () =>
+ pendingSettings.resolve({
+ groupName: 'remote-cg',
+ retryQueueNums: 1,
+ retryMaxTimes: 16,
+ consumeEnable: true,
+ }),
+ );
+ await waitFor(() =>
+ expect(within(dialog).getByRole('button', { name: /保\s*存/
})).toBeEnabled(),
+ );
+ expect(within(dialog).getByRole('switch', { name: '启用消费' })).toBeChecked();
+ });
+
+ it('retriesAFailedInitialSettingsReadBeforeAllowingConfirmedChangesTest',
async () => {
+ vi.mocked(consumerService.getConsumerGroupSettings)
+ .mockRejectedValueOnce(new Error('read failed'))
+ .mockResolvedValueOnce({
+ groupName: 'remote-cg',
+ retryQueueNums: 1,
+ retryMaxTimes: 16,
+ consumeEnable: true,
+ });
+ vi.mocked(consumerService.updateConsumerGroupSettings).mockResolvedValue({
+ groupName: 'remote-cg',
+ retryQueueNums: 1,
+ retryMaxTimes: 16,
+ consumeEnable: false,
+ });
+ 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 screen.findAllByText('加载消费组配置失败,请稍后重试');
+ await user.click(within(dialog).getByRole('button', { name: /重\s*试/ }));
+ await waitFor(() =>
expect(within(dialog).getByLabelText('重试队列数')).toHaveValue('1'));
+ expect(consumerService.getConsumerGroupSettings).toHaveBeenCalledTimes(2);
+ expect(within(dialog).getByRole('switch', { name: '启用消费' })).toBeChecked();
+ await user.click(within(dialog).getByRole('switch', { name: '启用消费' }));
+ await user.click(within(dialog).getByRole('button', { name: /保\s*存/ }));
+ await waitFor(() => expect(confirm).toHaveBeenCalledTimes(1));
+ expect(consumerService.updateConsumerGroupSettings).not.toHaveBeenCalled();
+ await user.click(await screen.findByRole('button', { name: '确认修改' }));
+ await waitFor(() =>
+ expect(consumerService.updateConsumerGroupSettings).toHaveBeenCalledWith(
+ expect.objectContaining({
+ instanceId: 'instance-1',
+ name: 'remote-cg',
+ retryQueueNums: 1,
+ retryMaxTimes: 16,
+ consumeEnable: false,
+ }),
+ ),
+ );
+ });
+
+ it('doesNotUnlockAnotherGroupsSettingsWhenAClosedReadCompletesTest', async
() => {
+ const settings = { groupName: 'remote-cg', retryQueueNums: 1,
retryMaxTimes: 16 };
+ const previousRead = deferred<typeof settings>();
+ const currentRead = deferred<typeof settings>();
+ vi.mocked(consumerService.listConsumerGroupPage).mockResolvedValue(
+ groupPage([group, { ...group, name: 'other-cg' }]),
+ );
+ vi.mocked(consumerService.refreshConsumerGroup).mockImplementation(async
(name) => ({
+ ...group,
+ name,
+ }));
+ vi.mocked(consumerService.getConsumerGroupSettings)
+ .mockReturnValueOnce(previousRead.promise)
+ .mockReturnValueOnce(currentRead.promise);
+ 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 firstDialog = await screen.findByRole('dialog', { name: /remote-cg/
});
+ await user.click(within(firstDialog).getByRole('tab', { name: '配置' }));
+ await waitFor(() =>
expect(consumerService.getConsumerGroupSettings).toHaveBeenCalledTimes(1));
+ fireEvent.click(within(firstDialog).getByRole('button', { name: 'Close'
}));
+ fireEvent.click(
+ within(screen.getByRole('row', { name: /other-cg/
})).getByRole('button', { name: '详情' }),
+ );
+ const currentDialog = await screen.findByRole('dialog', { name: /other-cg/
});
+ await user.click(within(currentDialog).getByRole('tab', { name: '配置' }));
+ await waitFor(() =>
expect(consumerService.getConsumerGroupSettings).toHaveBeenCalledTimes(2));
+ await act(async () => previousRead.resolve(settings));
+ expect(within(currentDialog).getByRole('button', { name: /保\s*存/
})).toBeDisabled();
+ expect(within(currentDialog).getByLabelText('重试队列数')).toHaveValue('');
+ await act(async () =>
+ currentRead.resolve({ ...settings, groupName: 'other-cg',
retryQueueNums: 3 }),
+ );
+ await waitFor(() =>
+ expect(within(currentDialog).getByRole('button', { name: /保\s*存/
})).toBeEnabled(),
+ );
+ expect(within(currentDialog).getByLabelText('重试队列数')).toHaveValue('3');
+ });
+
it('edits retry settings from the detail modal settings tab', async () => {
vi.mocked(consumerService.getConsumerGroupSettings).mockResolvedValue({
groupName: 'remote-cg',
diff --git a/web/src/pages/instance/consumer.tsx
b/web/src/pages/instance/consumer.tsx
index fbc314ece..bede02a48 100644
--- a/web/src/pages/instance/consumer.tsx
+++ b/web/src/pages/instance/consumer.tsx
@@ -273,6 +273,8 @@ const ConsumerPageContent = ({
const [selectedGroup, setSelectedGroup] = useState<ConsumerGroup |
null>(null);
const [settingsGroup, setSettingsGroup] = useState<ConsumerGroup |
null>(null);
const [settingsLoading, setSettingsLoading] = useState(false);
+ const [settingsReady, setSettingsReady] = useState(false);
+ const [settingsFailed, setSettingsFailed] = useState(false);
const [settingsSubmitting, setSettingsSubmitting] = useState(false);
const [settingsForm] = Form.useForm<{
retryQueueNums: number;
@@ -564,6 +566,9 @@ const ConsumerPageContent = ({
const requestId = ++settingsRequestIdRef.current;
setSettingsGroup(group);
setSettingsLoading(true);
+ setSettingsReady(false);
+ setSettingsFailed(false);
+ originalSettingsRef.current = null;
try {
const settings = await getConsumerGroupSettings(group.name,
selectedInstanceId);
if (requestId === settingsRequestIdRef.current) {
@@ -572,10 +577,12 @@ const ConsumerPageContent = ({
consumeEnable: settings.consumeEnable,
consumeMessageOrderly: settings.consumeMessageOrderly,
};
+ setSettingsReady(true);
}
} catch {
if (requestId === settingsRequestIdRef.current) {
- message.error('加载消费组配置失败,请稍后重试');
+ setSettingsFailed(true);
+ message.error(t('consumer.settingsLoadFailed'));
}
} finally {
if (requestId === settingsRequestIdRef.current) {
@@ -596,7 +603,7 @@ const ConsumerPageContent = ({
};
const saveSettings = async () => {
- if (!settingsGroup || !selectedInstanceId) return;
+ if (!settingsGroup || !selectedInstanceId || !settingsReady ||
settingsLoading) return;
const requestId = settingsRequestIdRef.current;
const values = await settingsForm.validateFields();
if (requestId !== settingsRequestIdRef.current) return;
@@ -1727,6 +1734,8 @@ const ConsumerPageContent = ({
setShowOnlyInconsistent(false);
setSettingsGroup(null);
setSettingsLoading(false);
+ setSettingsReady(false);
+ setSettingsFailed(false);
setSettingsSubmitting(false);
originalSettingsRef.current = null;
settingsForm.resetFields();
@@ -2260,7 +2269,27 @@ const ConsumerPageContent = ({
disabled: isCloudInstance,
children: (
<Spin spinning={settingsLoading}>
- <Form form={settingsForm} layout="vertical">
+ {settingsFailed && (
+ <Alert
+ type="error"
+ showIcon
+ message={t('consumer.settingsLoadFailed')}
+ style={{ marginBottom: 16 }}
+ action={
+ <Button
+ size="small"
+ onClick={() => void
loadGroupSettings(selectedGroup)}
+ >
+ {t('common.retry')}
+ </Button>
+ }
+ />
+ )}
+ <Form
+ form={settingsForm}
+ layout="vertical"
+ disabled={!settingsReady || settingsLoading}
+ >
<Row gutter={16}>
<Col span={8}>
<Form.Item label="Group 名称">
@@ -2315,6 +2344,7 @@ const ConsumerPageContent = ({
<Button
type="primary"
loading={settingsSubmitting}
+ disabled={!settingsReady || settingsLoading}
onClick={() => void saveSettings()}
>
保存