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 7449801a [ISSUE #1615] Prevent duplicate general settings saves (#1625)
7449801a is described below
commit 7449801ac9cc1299d0ef39487b9d543c4fb1fe91
Author: 0 <[email protected]>
AuthorDate: Tue Aug 11 20:31:34 2026 +0800
[ISSUE #1615] Prevent duplicate general settings saves (#1625)
---
.../settings/__tests__/GeneralSettingsTab.test.tsx | 75 ++++++++++++++++++++++
web/src/pages/settings/index.tsx | 8 ++-
2 files changed, 81 insertions(+), 2 deletions(-)
diff --git a/web/src/pages/settings/__tests__/GeneralSettingsTab.test.tsx
b/web/src/pages/settings/__tests__/GeneralSettingsTab.test.tsx
new file mode 100644
index 00000000..f9ceed58
--- /dev/null
+++ b/web/src/pages/settings/__tests__/GeneralSettingsTab.test.tsx
@@ -0,0 +1,75 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0.
+ */
+
+import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
+import { fireEvent, render, screen, waitFor } from '@testing-library/react';
+import { App } from 'antd';
+import { getGeneralSettings, saveGeneralSettings } from
'../../../api/settings';
+import { GeneralSettingsTab } from '../index';
+
+beforeAll(() => {
+ Object.defineProperty(window, 'matchMedia', {
+ writable: true,
+ value: vi.fn().mockImplementation((query: string) => ({
+ matches: false,
+ media: query,
+ onchange: null,
+ addListener: vi.fn(),
+ removeListener: vi.fn(),
+ addEventListener: vi.fn(),
+ removeEventListener: vi.fn(),
+ dispatchEvent: vi.fn(),
+ })),
+ });
+});
+
+vi.mock('../../../api/settings', () => ({
+ createDataSource: vi.fn(),
+ deleteDataSource: vi.fn(),
+ getGeneralSettings: vi.fn(),
+ listDataSources: vi.fn(),
+ saveGeneralSettings: vi.fn(),
+ testDataSource: vi.fn(),
+ updateDataSource: vi.fn(),
+}));
+
+describe('GeneralSettingsTab', () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ vi.mocked(getGeneralSettings).mockResolvedValue({
+ theme: 'system',
+ compact: false,
+ desktopNotify: false,
+ notifySound: false,
+ sessionTimeout: 30,
+ requireLogin: true,
+ llmProvider: 'openai',
+ apiKeyConfigured: false,
+ model: 'test-model',
+ baseUrl: 'https://example.test/v1',
+ });
+ });
+
+ it('ignores a duplicate submit while a save is in flight', async () => {
+ vi.mocked(saveGeneralSettings).mockImplementation(() => new Promise(() =>
{}));
+ render(
+ <App>
+ <GeneralSettingsTab />
+ </App>,
+ );
+
+ const saveButton = await screen.findByRole('button', { name: '保存设置' });
+ await waitFor(() => expect(saveButton).toBeEnabled());
+ const form = saveButton.closest('form');
+ expect(form).not.toBeNull();
+
+ fireEvent.submit(form!);
+ fireEvent.submit(form!);
+
+ await waitFor(() => expect(saveGeneralSettings).toHaveBeenCalledTimes(1));
+ });
+});
diff --git a/web/src/pages/settings/index.tsx b/web/src/pages/settings/index.tsx
index 1cb408bb..eb1384d4 100644
--- a/web/src/pages/settings/index.tsx
+++ b/web/src/pages/settings/index.tsx
@@ -15,7 +15,7 @@
* limitations under the License.
*/
-import { useEffect, useState } from 'react';
+import { useEffect, useRef, useState } from 'react';
import {
Button,
Checkbox,
@@ -108,10 +108,11 @@ const withoutSecrets = (values: DataSourceFormValues):
Partial<DataSource> => {
// ─── General Settings Tab ───────────────────────────────────────────────────
-const GeneralSettingsTab = () => {
+export const GeneralSettingsTab = () => {
const [form] = Form.useForm<GeneralSettingsUpdate>();
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
+ const saveInFlightRef = useRef(false);
const [apiKeyConfigured, setApiKeyConfigured] = useState(false);
const clearApiKey = Form.useWatch('clearApiKey', form);
@@ -137,6 +138,8 @@ const GeneralSettingsTab = () => {
}, [form]);
const handleFinish = async (values: GeneralSettingsUpdate) => {
+ if (saveInFlightRef.current) return;
+ saveInFlightRef.current = true;
setSaving(true);
try {
await saveGeneralSettings(values);
@@ -148,6 +151,7 @@ const GeneralSettingsTab = () => {
} catch {
message.error('设置保存失败,请稍后重试');
} finally {
+ saveInFlightRef.current = false;
setSaving(false);
}
};