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],

Reply via email to