This is an automated email from the ASF dual-hosted git repository.
enzomartellucci pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/superset.git
The following commit(s) were added to refs/heads/master by this push:
new fe5d5fdae6 fix(chart-creation): use exact match when loading dataset
from URL parameter (#36831)
fe5d5fdae6 is described below
commit fe5d5fdae6981e3044f70cb72a9b04fb93e8cf05
Author: Enzo Martellucci <[email protected]>
AuthorDate: Wed Dec 31 13:50:03 2025 +0100
fix(chart-creation): use exact match when loading dataset from URL
parameter (#36831)
---
.../datasets/AddDataset/Footer/Footer.test.tsx | 1 +
.../features/datasets/AddDataset/Footer/index.tsx | 10 +-
.../src/pages/ChartCreation/ChartCreation.test.tsx | 227 +++++++++++++++++++++
.../src/pages/ChartCreation/index.tsx | 39 +++-
4 files changed, 265 insertions(+), 12 deletions(-)
diff --git
a/superset-frontend/src/features/datasets/AddDataset/Footer/Footer.test.tsx
b/superset-frontend/src/features/datasets/AddDataset/Footer/Footer.test.tsx
index a89c5fd6f6..b0714fc90c 100644
--- a/superset-frontend/src/features/datasets/AddDataset/Footer/Footer.test.tsx
+++ b/superset-frontend/src/features/datasets/AddDataset/Footer/Footer.test.tsx
@@ -37,6 +37,7 @@ const mockCreateResource = jest.fn();
jest.mock('src/views/CRUD/hooks', () => ({
useSingleViewResource: () => ({
createResource: mockCreateResource,
+ state: { loading: false },
}),
getDatabaseDocumentationLinks: () => ({
support:
diff --git
a/superset-frontend/src/features/datasets/AddDataset/Footer/index.tsx
b/superset-frontend/src/features/datasets/AddDataset/Footer/index.tsx
index 2b23b728ee..9e41d8876a 100644
--- a/superset-frontend/src/features/datasets/AddDataset/Footer/index.tsx
+++ b/superset-frontend/src/features/datasets/AddDataset/Footer/index.tsx
@@ -63,11 +63,10 @@ function Footer({
}: FooterProps) {
const history = useHistory();
const theme = useTheme();
- const { createResource } = useSingleViewResource<Partial<DatasetObject>>(
- 'dataset',
- t('dataset'),
- addDangerToast,
- );
+ const { createResource, state } = useSingleViewResource<
+ Partial<DatasetObject>
+ >('dataset', t('dataset'), addDangerToast);
+ const { loading } = state;
const createLogAction = (dataset: Partial<DatasetObject>) => {
let totalCount = 0;
@@ -149,6 +148,7 @@ function Footer({
<DropdownButton
type="primary"
disabled={disabledCheck}
+ loading={loading}
tooltip={!datasetObject?.table_name ? tooltipText : undefined}
onClick={() => onSave(true)}
popupRender={() => dropdownMenu}
diff --git a/superset-frontend/src/pages/ChartCreation/ChartCreation.test.tsx
b/superset-frontend/src/pages/ChartCreation/ChartCreation.test.tsx
index 5caea4df88..e67f829bab 100644
--- a/superset-frontend/src/pages/ChartCreation/ChartCreation.test.tsx
+++ b/superset-frontend/src/pages/ChartCreation/ChartCreation.test.tsx
@@ -193,3 +193,230 @@ test('double-click viz type submits with formatted URL if
datasource is selected
const formattedUrl = '/explore/?viz_type=table&datasource=table_1__table';
expect(history.push).toHaveBeenCalledWith(formattedUrl);
});
+
+test('dropdown displays matching datasets when user types a search term',
async () => {
+ fetchMock.reset();
+ fetchMock.get(/\/api\/v1\/dataset\/\?q=.*/, {
+ body: {
+ result: [
+ {
+ id: 'flights_1',
+ table_name: 'flights',
+ datasource_type: 'table',
+ database: { database_name: 'examples' },
+ schema: 'public',
+ },
+ {
+ id: 'flights_delayed_2',
+ table_name: 'flights_delayed',
+ datasource_type: 'table',
+ database: { database_name: 'examples' },
+ schema: 'public',
+ },
+ ],
+ count: 2,
+ },
+ status: 200,
+ });
+
+ await renderComponent();
+
+ const datasourceSelect = await screen.findByRole('combobox', {
+ name: 'Dataset',
+ });
+ userEvent.click(datasourceSelect);
+ userEvent.type(datasourceSelect, 'flight');
+
+ await screen.findByText('flights');
+ expect(screen.getByText('flights_delayed')).toBeInTheDocument();
+});
+
+test('handles special characters in dataset name from URL parameter', async ()
=> {
+ fetchMock.reset();
+ fetchMock.get(/\/api\/v1\/dataset\/\?q=.*/, {
+ body: {
+ result: [
+ {
+ id: 'special_1',
+ table_name: 'flightsÆ test',
+ datasource_type: 'table',
+ database: { database_name: 'test_db' },
+ schema: 'public',
+ },
+ ],
+ count: 1,
+ },
+ status: 200,
+ });
+
+ const originalLocation = window.location;
+ Object.defineProperty(window, 'location', {
+ value: {
+ ...originalLocation,
+ search: '?dataset=flights%C3%86%20test',
+ },
+ writable: true,
+ });
+
+ await renderComponent();
+
+ await screen.findByText('flightsÆ test');
+
+ Object.defineProperty(window, 'location', {
+ value: originalLocation,
+ writable: true,
+ });
+});
+
+test('pre-selects the dataset from URL parameter and shows it in dropdown',
async () => {
+ fetchMock.reset();
+ fetchMock.get(/\/api\/v1\/dataset\/\?q=.*/, {
+ body: {
+ result: [
+ {
+ id: 'flights_123',
+ table_name: 'flights',
+ datasource_type: 'table',
+ database: { database_name: 'examples' },
+ schema: 'public',
+ },
+ ],
+ count: 1,
+ },
+ status: 200,
+ });
+
+ const originalLocation = window.location;
+ Object.defineProperty(window, 'location', {
+ value: { ...originalLocation, search: '?dataset=flights' },
+ writable: true,
+ });
+
+ await renderComponent();
+
+ await screen.findByText('flights');
+
+ Object.defineProperty(window, 'location', {
+ value: originalLocation,
+ writable: true,
+ });
+});
+
+test('shows loading spinner when dataset parameter is present in URL', async
() => {
+ fetchMock.reset();
+ let resolveRequest: (value: unknown) => void;
+ const requestPromise = new Promise(resolve => {
+ resolveRequest = resolve;
+ });
+
+ fetchMock.get(/\/api\/v1\/dataset\/\?q=.*/, () =>
+ requestPromise.then(() => ({
+ body: {
+ result: [
+ {
+ id: 'flights_1',
+ table_name: 'flights',
+ datasource_type: 'table',
+ database: { database_name: 'examples' },
+ schema: 'public',
+ },
+ ],
+ count: 1,
+ },
+ status: 200,
+ })),
+ );
+
+ const originalLocation = window.location;
+ Object.defineProperty(window, 'location', {
+ value: { ...originalLocation, search: '?dataset=flights' },
+ writable: true,
+ });
+
+ render(
+ <ChartCreation
+ user={mockUser}
+ addSuccessToast={() => null}
+ theme={supersetTheme}
+ {...routeProps}
+ />,
+ {
+ useRedux: true,
+ useRouter: true,
+ },
+ );
+
+ expect(screen.getByRole('status')).toBeInTheDocument();
+
+ resolveRequest!(null);
+
+ await waitFor(() => {
+ expect(screen.queryByRole('status')).not.toBeInTheDocument();
+ });
+
+ Object.defineProperty(window, 'location', {
+ value: originalLocation,
+ writable: true,
+ });
+});
+
+test('shows only exact match when loading dataset from URL, not partial
matches', async () => {
+ fetchMock.reset();
+ fetchMock.get(/\/api\/v1\/dataset\/\?q=.*/, url => {
+ if (url.includes('opr:eq')) {
+ return {
+ body: {
+ result: [
+ {
+ id: 'flights_1',
+ table_name: 'flights',
+ datasource_type: 'table',
+ database: { database_name: 'examples' },
+ schema: 'public',
+ },
+ ],
+ count: 1,
+ },
+ status: 200,
+ };
+ }
+ return {
+ body: {
+ result: [
+ {
+ id: 'flights_1',
+ table_name: 'flights',
+ datasource_type: 'table',
+ database: { database_name: 'examples' },
+ schema: 'public',
+ },
+ {
+ id: 'flights_delayed_2',
+ table_name: 'flights_delayed',
+ datasource_type: 'table',
+ database: { database_name: 'examples' },
+ schema: 'public',
+ },
+ ],
+ count: 2,
+ },
+ status: 200,
+ };
+ });
+
+ const originalLocation = window.location;
+ Object.defineProperty(window, 'location', {
+ value: { ...originalLocation, search: '?dataset=flights' },
+ writable: true,
+ });
+
+ await renderComponent();
+
+ await screen.findByText('flights');
+ expect(screen.queryByText('flights_delayed')).not.toBeInTheDocument();
+
+ Object.defineProperty(window, 'location', {
+ value: originalLocation,
+ writable: true,
+ });
+});
diff --git a/superset-frontend/src/pages/ChartCreation/index.tsx
b/superset-frontend/src/pages/ChartCreation/index.tsx
index 118c78c3b7..3da0d7737e 100644
--- a/superset-frontend/src/pages/ChartCreation/index.tsx
+++ b/superset-frontend/src/pages/ChartCreation/index.tsx
@@ -24,7 +24,12 @@ import { withTheme, Theme } from '@emotion/react';
import { getUrlParam } from 'src/utils/urlUtils';
import { FilterPlugins, URL_PARAMS } from 'src/constants';
import { Link, withRouter, RouteComponentProps } from 'react-router-dom';
-import { AsyncSelect, Button, Steps } from '@superset-ui/core/components';
+import {
+ AsyncSelect,
+ Button,
+ Loading,
+ Steps,
+} from '@superset-ui/core/components';
import withToasts from 'src/components/MessageToasts/withToasts';
import VizTypeGallery, {
@@ -50,6 +55,7 @@ export type ChartCreationState = {
datasetName?: string | string[] | null;
vizType: string | null;
canCreateDataset: boolean;
+ loading: boolean;
};
const ESTIMATED_NAV_HEIGHT = 56;
@@ -172,6 +178,9 @@ export class ChartCreation extends PureComponent<
> {
constructor(props: ChartCreationProps) {
super(props);
+ const hasDatasetParam = new URLSearchParams(window.location.search).has(
+ 'dataset',
+ );
this.state = {
vizType: null,
canCreateDataset: findPermission(
@@ -179,6 +188,7 @@ export class ChartCreation extends PureComponent<
'Dataset',
props.user.roles,
),
+ loading: hasDatasetParam,
};
this.changeDatasource = this.changeDatasource.bind(this);
@@ -191,10 +201,14 @@ export class ChartCreation extends PureComponent<
componentDidMount() {
const params = new URLSearchParams(window.location.search).get('dataset');
if (params) {
- this.loadDatasources(params, 0, 1).then(r => {
- const datasource = r.data[0];
- this.setState({ datasource });
- });
+ this.loadDatasources(params, 0, 1, true)
+ .then(r => {
+ const datasource = r.data[0];
+ this.setState({ datasource, loading: false });
+ })
+ .catch(() => {
+ this.setState({ loading: false });
+ });
this.props.addSuccessToast(t('The dataset has been saved'));
}
}
@@ -230,7 +244,12 @@ export class ChartCreation extends PureComponent<
}
}
- loadDatasources(search: string, page: number, pageSize: number) {
+ loadDatasources(
+ search: string,
+ page: number,
+ pageSize: number,
+ exactMatch = false,
+ ) {
const query = rison.encode({
columns: [
'id',
@@ -239,7 +258,9 @@ export class ChartCreation extends PureComponent<
'database.database_name',
'schema',
],
- filters: [{ col: 'table_name', opr: 'ct', value: search }],
+ filters: [
+ { col: 'table_name', opr: exactMatch ? 'eq' : 'ct', value: search },
+ ],
page,
page_size: pageSize,
order_column: 'table_name',
@@ -301,6 +322,10 @@ export class ChartCreation extends PureComponent<
</span>
);
+ if (this.state.loading) {
+ return <Loading />;
+ }
+
return (
<StyledContainer>
<h3>{t('Create a new chart')}</h3>