fix(dashboard): fix dataset search in filter config modal (#35488)

Co-authored-by: Claude <noreply@anthropic.com>
(cherry picked from commit 53687ae659)
This commit is contained in:
Rafael Benitez
2025-11-03 18:29:39 -03:00
committed by Joe Li
parent c48b5ad65e
commit f7ebdd71e7
4 changed files with 222 additions and 8 deletions

View File

@@ -0,0 +1,203 @@
/**
* 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 {
render,
screen,
userEvent,
waitFor,
within,
} from 'spec/helpers/testing-library';
import fetchMock from 'fetch-mock';
import DatasetSelect from './DatasetSelect';
const DATASETS = [
{
id: 1,
table_name: 'birth_names',
database: { database_name: 'examples' },
schema: 'public',
},
{
id: 2,
table_name: 'energy_usage',
database: { database_name: 'examples' },
schema: 'public',
},
{
id: 3,
table_name: 'flights',
database: { database_name: 'examples' },
schema: 'main',
},
{
id: 4,
table_name: 'customers',
database: { database_name: 'sales_db' },
schema: 'dbo',
},
];
const mockOnChange = jest.fn();
afterEach(() => {
fetchMock.restore();
jest.clearAllMocks();
});
const getSelect = () => screen.getByRole('combobox', { name: /dataset/i });
const openSelect = () => {
userEvent.click(getSelect());
};
const typeIntoSelect = async (text: string) => {
const select = getSelect();
userEvent.clear(select);
return userEvent.type(select, text, { delay: 10 });
};
const findOption = (text: string) =>
waitFor(() => {
// eslint-disable-next-line testing-library/no-node-access
const virtualList = document.querySelector('.rc-virtual-list');
if (!virtualList) {
throw new Error('Virtual list not found');
}
return within(virtualList as HTMLElement).getByText(text);
});
test('renders the dataset select component', () => {
fetchMock.get('glob:*/api/v1/dataset/*', {
result: [],
count: 0,
});
render(<DatasetSelect onChange={mockOnChange} />);
expect(getSelect()).toBeInTheDocument();
});
test('loads and displays datasets when opened', async () => {
fetchMock.get('glob:*/api/v1/dataset/*', {
result: DATASETS,
count: DATASETS.length,
});
render(<DatasetSelect onChange={mockOnChange} />);
openSelect();
expect(await findOption('birth_names')).toBeInTheDocument();
expect(await findOption('energy_usage')).toBeInTheDocument();
});
test('searches for datasets by table_name locally in loaded options', async () => {
fetchMock.get('glob:*/api/v1/dataset/*', {
result: DATASETS,
count: DATASETS.length,
});
render(<DatasetSelect onChange={mockOnChange} />);
openSelect();
// Wait for all options to load
await findOption('flights');
// Now type to filter locally
await typeIntoSelect('flight');
// Should filter to show only flights
expect(await findOption('flights')).toBeInTheDocument();
});
test('uses optionFilterProps to enable table_name filtering', async () => {
// This test verifies that the optionFilterProps includes table_name
// which enables client-side filtering on that field
fetchMock.get('glob:*/api/v1/dataset/*', {
result: DATASETS,
count: DATASETS.length,
});
render(<DatasetSelect onChange={mockOnChange} />);
openSelect();
// Load all options
await findOption('energy_usage');
// Search by table_name substring
await typeIntoSelect('energy');
// Should find the dataset by table_name
expect(await findOption('energy_usage')).toBeInTheDocument();
});
test('filters options case-insensitively on table_name', async () => {
fetchMock.get('glob:*/api/v1/dataset/*', {
result: DATASETS,
count: DATASETS.length,
});
render(<DatasetSelect onChange={mockOnChange} />);
openSelect();
// Load options
await findOption('birth_names');
// Type in uppercase
await typeIntoSelect('BIRTH');
// Should still find it (case insensitive)
expect(await findOption('birth_names')).toBeInTheDocument();
});
test('calls onChange when a dataset is selected', async () => {
fetchMock.get('glob:*/api/v1/dataset/*', {
result: DATASETS,
count: DATASETS.length,
});
render(<DatasetSelect onChange={mockOnChange} />);
openSelect();
const option = await findOption('birth_names');
userEvent.click(option);
await waitFor(() => {
expect(mockOnChange).toHaveBeenCalled();
const callArg = mockOnChange.mock.calls[0][0];
expect(callArg).toEqual({ key: 1, label: expect.anything(), value: 1 });
});
});
test('includes table_name field in option data structure', async () => {
fetchMock.get('glob:*/api/v1/dataset/*', {
result: [DATASETS[0]],
count: 1,
});
render(<DatasetSelect onChange={mockOnChange} />);
openSelect();
const option = await findOption('birth_names');
userEvent.click(option);
await waitFor(() => {
expect(mockOnChange).toHaveBeenCalled();
const callArg = mockOnChange.mock.calls[0][1];
expect(callArg).toHaveProperty('table_name', 'birth_names');
});
});

View File

@@ -68,9 +68,12 @@ const DatasetSelect = ({ onChange, value }: DatasetSelectProps) => {
const list: {
label: string | ReactNode;
value: string | number;
table_name: string;
}[] = response.json.result.map((item: Dataset) => ({
...item,
label: DatasetSelectLabel(item),
value: item.id,
table_name: item.table_name,
}));
return {
data: list,
@@ -89,6 +92,7 @@ const DatasetSelect = ({ onChange, value }: DatasetSelectProps) => {
value={value}
options={loadDatasetOptions}
onChange={onChange}
optionFilterProps={['table_name']}
notFoundContent={t('No compatible datasets found')}
placeholder={t('Select a dataset')}
/>

View File

@@ -334,18 +334,24 @@ test.skip('validates the default value', async () => {
test('validates the pre-filter value', async () => {
jest.useFakeTimers();
try {
defaultRender();
defaultRender();
await userEvent.click(screen.getByText(FILTER_SETTINGS_REGEX));
await userEvent.click(getCheckbox(PRE_FILTER_REGEX));
userEvent.click(screen.getByText(FILTER_SETTINGS_REGEX));
userEvent.click(getCheckbox(PRE_FILTER_REGEX));
jest.runAllTimers();
} finally {
jest.useRealTimers();
}
jest.runOnlyPendingTimers();
jest.useRealTimers();
expect(
await screen.findByText(PRE_FILTER_REQUIRED_REGEX),
).toBeInTheDocument();
// Wait for validation to complete after timer switch
await waitFor(() => {
expect(screen.getByText(PRE_FILTER_REQUIRED_REGEX)).toBeInTheDocument();
});
}, 50000); // Slow-running test, increase timeout to 50 seconds.
// eslint-disable-next-line jest/no-disabled-tests

View File

@@ -257,11 +257,12 @@ export class ChartCreation extends PureComponent<
id: number;
label: string | ReactNode;
value: string;
table_name: string;
}[] = response.json.result.map((item: Dataset) => ({
id: item.id,
value: `${item.id}__${item.datasource_type}`,
label: DatasetSelectLabel(item),
customLabel: item.table_name,
table_name: item.table_name,
}));
return {
data: list,
@@ -320,7 +321,7 @@ export class ChartCreation extends PureComponent<
name="select-datasource"
onChange={this.changeDatasource}
options={this.loadDatasources}
optionFilterProps={['id', 'customLabel']}
optionFilterProps={['id', 'table_name']}
placeholder={t('Choose a dataset')}
showSearch
value={this.state.datasource}