mirror of
https://github.com/apache/superset.git
synced 2026-07-20 21:55:46 +00:00
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:
@@ -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');
|
||||
});
|
||||
});
|
||||
@@ -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')}
|
||||
/>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user