diff --git a/web/src/pages/settings/__tests__/DataSourceTab.test.tsx b/web/src/pages/settings/__tests__/DataSourceTab.test.tsx
new file mode 100644
index 00000000..83af6d92
--- /dev/null
+++ b/web/src/pages/settings/__tests__/DataSourceTab.test.tsx
@@ -0,0 +1,102 @@
+/*
+ * 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
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
+import { render, screen, waitFor } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import { App } from 'antd';
+import type { DataSource } from '../../../api/settings';
+import { listDataSources, testDataSource } from '../../../api/settings';
+import { DataSourceTab } from '../index';
+
+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(),
+}));
+
+const sources: DataSource[] = [
+ {
+ key: 'prom-prod',
+ name: 'Prometheus prod',
+ type: 'Prometheus',
+ url: 'http://prometheus:9090',
+ auth: 'None',
+ status: 'healthy',
+ },
+ {
+ key: 'thanos-dr',
+ name: 'Thanos DR',
+ type: 'Thanos',
+ url: 'http://thanos:10902',
+ auth: 'Bearer Token',
+ status: 'healthy',
+ },
+];
+
+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(),
+ })),
+ });
+});
+
+describe('DataSourceTab', () => {
+ beforeEach(() => {
+ vi.mocked(listDataSources).mockResolvedValue(sources);
+ });
+
+ it('shows connection test loading only on the clicked row', async () => {
+ let resolveTest: (value: { success: boolean; message: string }) => void = () => undefined;
+ vi.mocked(testDataSource).mockReturnValue(
+ new Promise((resolve) => {
+ resolveTest = resolve;
+ }),
+ );
+
+ const user = userEvent.setup();
+ render(
+
+
+ ,
+ );
+
+ await screen.findByText('Prometheus prod');
+ const buttons = screen.getAllByRole('button', { name: /测试连接/ });
+ await user.click(buttons[0]);
+
+ await waitFor(() => {
+ expect(buttons[0]).toHaveClass('ant-btn-loading');
+ expect(buttons[1]).not.toHaveClass('ant-btn-loading');
+ });
+
+ resolveTest({ success: true, message: 'ok' });
+ });
+});
diff --git a/web/src/pages/settings/index.tsx b/web/src/pages/settings/index.tsx
index 9344eea0..434a03b4 100644
--- a/web/src/pages/settings/index.tsx
+++ b/web/src/pages/settings/index.tsx
@@ -237,13 +237,13 @@ const GeneralSettingsTab = () => {
// ─── Data Source Tab ────────────────────────────────────────────────────────
-const DataSourceTab = () => {
+export const DataSourceTab = () => {
const [dataSources, setDataSources] = useState([]);
const [loading, setLoading] = useState(true);
const [modalOpen, setModalOpen] = useState(false);
const [editingDataSource, setEditingDataSource] = useState(null);
const [dsForm] = Form.useForm();
- const [testing, setTesting] = useState(false);
+ const [testingKey, setTestingKey] = useState(null);
const [submitting, setSubmitting] = useState(false);
useEffect(() => {
@@ -264,8 +264,11 @@ const DataSourceTab = () => {
};
}, []);
- const handleTestConnection = async (data: Pick) => {
- setTesting(true);
+ const handleTestConnection = async (
+ data: Pick,
+ key: string,
+ ) => {
+ setTestingKey(key);
try {
const result = await testDataSource(data);
if (result.success) message.success(result.message);
@@ -273,7 +276,7 @@ const DataSourceTab = () => {
} catch {
message.error('连接测试失败,请稍后重试');
} finally {
- setTesting(false);
+ setTestingKey(null);
}
};
@@ -347,8 +350,8 @@ const DataSourceTab = () => {
type="link"
size="small"
icon={}
- loading={testing}
- onClick={() => void handleTestConnection(record)}
+ loading={testingKey === record.key}
+ onClick={() => void handleTestConnection(record, record.key)}
>
测试连接
@@ -447,11 +450,11 @@ const DataSourceTab = () => {
}
- loading={testing}
+ loading={testingKey === 'modal'}
onClick={() => {
void dsForm
.validateFields(['type', 'url', 'auth'])
- .then((values) => handleTestConnection(values))
+ .then((values) => handleTestConnection(values, 'modal'))
.catch(() => undefined);
}}
style={{ marginTop: 8 }}