From 393ee25d6efab2e6fd498b3f0bbe7261f63a86dc Mon Sep 17 00:00:00 2001 From: liuhy Date: Sat, 25 Jul 2026 18:04:39 -0700 Subject: [PATCH] fix: scope data source test loading by row --- .../settings/__tests__/DataSourceTab.test.tsx | 102 ++++++++++++++++++ web/src/pages/settings/index.tsx | 21 ++-- 2 files changed, 114 insertions(+), 9 deletions(-) create mode 100644 web/src/pages/settings/__tests__/DataSourceTab.test.tsx 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 = () => {