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 571f6d393 fix(settings): lock credential submissions (#2658)
571f6d393 is described below
commit 571f6d393b571ef3be5fc0642b0430587ed1bc2b
Author: btlqql <[email protected]>
AuthorDate: Wed Sep 2 17:09:26 2026 +0800
fix(settings): lock credential submissions (#2658)
---
web/src/pages/settings/CloudCredentialTab.tsx | 18 ++++++++--
.../settings/__tests__/CloudCredentialTab.test.tsx | 40 +++++++++++++++++++++-
2 files changed, 54 insertions(+), 4 deletions(-)
diff --git a/web/src/pages/settings/CloudCredentialTab.tsx
b/web/src/pages/settings/CloudCredentialTab.tsx
index 077e08355..295d519f0 100644
--- a/web/src/pages/settings/CloudCredentialTab.tsx
+++ b/web/src/pages/settings/CloudCredentialTab.tsx
@@ -74,6 +74,7 @@ export const CloudCredentialTab = () => {
const [form] = Form.useForm<CredentialFormValues>();
const [submitting, setSubmitting] = useState(false);
const requestSeqRef = useRef(0);
+ const submitInFlightRef = useRef(false);
useEffect(() => {
const timer = window.setTimeout(() => setDebouncedSearch(search.trim()),
300);
@@ -133,12 +134,16 @@ export const CloudCredentialTab = () => {
setPage(1);
};
- const closeModal = () => {
+ const resetModal = () => {
setModalOpen(false);
setEditingCredential(null);
form.resetFields();
};
+ const closeModal = () => {
+ if (!submitInFlightRef.current) resetModal();
+ };
+
const openCreateModal = () => {
setEditingCredential(null);
form.resetFields();
@@ -156,9 +161,11 @@ export const CloudCredentialTab = () => {
};
const handleSubmit = async () => {
+ if (submitInFlightRef.current) return;
+ submitInFlightRef.current = true;
+ setSubmitting(true);
try {
const values = await form.validateFields();
- setSubmitting(true);
if (editingCredential) {
const saved = await updateCloudCredential({
id: editingCredential.id,
@@ -180,13 +187,14 @@ export const CloudCredentialTab = () => {
message.success(t('settings.credentialAdded'));
}
await loadCredentials();
- closeModal();
+ resetModal();
} catch (error) {
if (error && typeof error === 'object' && 'errorFields' in error) {
return; // validation failure; antd already shows field-level errors
}
message.error(t('settings.credentialSaveFailed'));
} finally {
+ submitInFlightRef.current = false;
setSubmitting(false);
}
};
@@ -312,6 +320,10 @@ export const CloudCredentialTab = () => {
onCancel={closeModal}
onOk={() => void handleSubmit()}
confirmLoading={submitting}
+ closable={!submitting}
+ maskClosable={!submitting}
+ keyboard={!submitting}
+ cancelButtonProps={{ disabled: submitting }}
destroyOnHidden
>
{editingCredential && (
diff --git a/web/src/pages/settings/__tests__/CloudCredentialTab.test.tsx
b/web/src/pages/settings/__tests__/CloudCredentialTab.test.tsx
index f205e049d..cc58753ab 100644
--- a/web/src/pages/settings/__tests__/CloudCredentialTab.test.tsx
+++ b/web/src/pages/settings/__tests__/CloudCredentialTab.test.tsx
@@ -16,7 +16,7 @@
*/
import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
-import { render, screen, waitFor, within } from '@testing-library/react';
+import { fireEvent, render, screen, waitFor, within } from
'@testing-library/react';
import userEvent from '@testing-library/user-event';
import { App } from 'antd';
import type { CloudCredentialPage } from '../../../api/cloudCredential';
@@ -200,6 +200,44 @@ describe('CloudCredentialTab', () => {
);
});
+ it('submits a credential only once and keeps the editor open while saving',
async () => {
+ localStorage.setItem(LANGUAGE_STORAGE_KEY, 'en');
+ const save = deferred<CloudCredentialPage['items'][number]>();
+ vi.mocked(createCloudCredential).mockReturnValue(save.promise);
+ const user = userEvent.setup({ pointerEventsCheck: 0 });
+ renderTab();
+ await screen.findByText('aliyun-test');
+ await user.click(screen.getByRole('button', { name: /Add credential/ }));
+
+ const dialog = await screen.findByRole('dialog');
+ await user.type(
+ within(dialog).getByPlaceholderText('Example: Aliyun test account'),
+ 'tencent-prod',
+ );
+ await user.click(within(dialog).getAllByRole('combobox')[0]);
+ await user.click(await screen.findByText('Tencent Cloud'));
+ await user.type(screen.getByPlaceholderText('LTAI...'),
'AKID000000009999');
+ await user.type(screen.getByPlaceholderText('Enter a SecretKey'),
'secret-9999');
+ const okButton = within(dialog).getByRole('button', { name: 'OK' });
+
+ fireEvent.click(okButton);
+ fireEvent.click(okButton);
+
+ await waitFor(() =>
expect(createCloudCredential).toHaveBeenCalledTimes(1));
+ expect(within(dialog).getByRole('button', { name: 'Cancel'
})).toBeDisabled();
+ expect(screen.getByRole('dialog')).toBeInTheDocument();
+
+ save.resolve({
+ id: 2,
+ name: 'tencent-prod',
+ vendor: 'TENCENT',
+ accessKey: 'AKID****9999',
+ gmtCreate: '2026-08-18T11:00:00',
+ });
+ await waitFor(() => expect(listCloudCredentials).toHaveBeenCalledTimes(2));
+ expect(createCloudCredential).toHaveBeenCalledTimes(1);
+ });
+
it('updates name and remark while keeping the secret unchanged when blank',
async () => {
vi.mocked(updateCloudCredential).mockResolvedValue({
...credentials.items[0],