From 66a4c94a1ed542e69fe6399bab4c01d4540486cf Mon Sep 17 00:00:00 2001 From: Ville Brofeldt <33317356+villebro@users.noreply.github.com> Date: Fri, 7 May 2021 21:07:44 +0300 Subject: [PATCH] fix(chart-data): handle url_params in csv export and native filters (#14526) --- .../src/dashboard/actions/hydrate.js | 31 ++--------- .../components/nativeFilters/utils.ts | 3 +- .../dashboard/util/extractUrlParams.test.ts | 53 +++++++++++++++++++ .../src/dashboard/util/extractUrlParams.ts | 49 +++++++++++++++++ superset/views/utils.py | 10 +++- tests/utils_tests.py | 20 +++++++ 6 files changed, 137 insertions(+), 29 deletions(-) create mode 100644 superset-frontend/src/dashboard/util/extractUrlParams.test.ts create mode 100644 superset-frontend/src/dashboard/util/extractUrlParams.ts diff --git a/superset-frontend/src/dashboard/actions/hydrate.js b/superset-frontend/src/dashboard/actions/hydrate.js index 9e4e2b051..64eaeca14 100644 --- a/superset-frontend/src/dashboard/actions/hydrate.js +++ b/superset-frontend/src/dashboard/actions/hydrate.js @@ -24,7 +24,6 @@ import { CategoricalColorNamespace, getChartMetadataRegistry, } from '@superset-ui/core'; -import querystring from 'query-string'; import { chart } from 'src/chart/chartReducer'; import { initSliceEntities } from 'src/dashboard/reducers/sliceEntities'; @@ -55,27 +54,7 @@ import getLocationHash from 'src/dashboard/util/getLocationHash'; import newComponentFactory from 'src/dashboard/util/newComponentFactory'; import { TIME_RANGE } from 'src/visualizations/FilterBox/FilterBox'; import { FeatureFlag, isFeatureEnabled } from '../../featureFlags'; - -const reservedQueryParams = new Set(['standalone', 'edit']); - -/** - * Returns the url params that are used to customize queries - * in datasets built using sql lab. - * We may want to extract this to some kind of util in the future. - */ -const extractUrlParams = queryParams => - Object.entries(queryParams).reduce((acc, [key, value]) => { - if (reservedQueryParams.has(key)) return acc; - // if multiple url params share the same key (?foo=bar&foo=baz), they will appear as an array. - // Only one value can be used for a given query param, so we just take the first one. - if (Array.isArray(value)) { - return { - ...acc, - [key]: value[0], - }; - } - return { ...acc, [key]: value }; - }, {}); +import extractUrlParams from '../util/extractUrlParams'; export const HYDRATE_DASHBOARD = 'HYDRATE_DASHBOARD'; @@ -85,9 +64,9 @@ export const hydrateDashboard = (dashboardData, chartData, datasourcesData) => ( ) => { const { user, common } = getState(); let { metadata } = dashboardData; - const queryParams = querystring.parse(window.location.search); - const urlParams = extractUrlParams(queryParams); - const editMode = queryParams.edit === 'true'; + const regularUrlParams = extractUrlParams('regular'); + const reservedUrlParams = extractUrlParams('reserved'); + const editMode = reservedUrlParams.edit === 'true'; let preselectFilters = {}; @@ -154,7 +133,7 @@ export const hydrateDashboard = (dashboardData, chartData, datasourcesData) => ( ...slice.form_data, url_params: { ...slice.form_data.url_params, - ...urlParams, + ...regularUrlParams, }, }; chartQueries[key] = { diff --git a/superset-frontend/src/dashboard/components/nativeFilters/utils.ts b/superset-frontend/src/dashboard/components/nativeFilters/utils.ts index a42540ac2..a674a2767 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/utils.ts +++ b/superset-frontend/src/dashboard/components/nativeFilters/utils.ts @@ -28,6 +28,7 @@ import { import { Charts } from 'src/dashboard/types'; import { RefObject } from 'react'; import { DataMaskStateWithId } from 'src/dataMask/types'; +import extractUrlParams from 'src/dashboard/util/extractUrlParams'; import { Filter } from './types'; export const getFormData = ({ @@ -76,7 +77,7 @@ export const getFormData = ({ defaultValue: defaultDataMask?.filterState?.value, time_range, time_range_endpoints: ['inclusive', 'exclusive'], - url_params: {}, + url_params: extractUrlParams('regular'), viz_type: filterType, inputRef, }; diff --git a/superset-frontend/src/dashboard/util/extractUrlParams.test.ts b/superset-frontend/src/dashboard/util/extractUrlParams.test.ts new file mode 100644 index 000000000..dd81c10d5 --- /dev/null +++ b/superset-frontend/src/dashboard/util/extractUrlParams.test.ts @@ -0,0 +1,53 @@ +/** + * 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 extractUrlParams from './extractUrlParams'; + +const originalWindowLocation = window.location; + +describe('extractUrlParams', () => { + beforeAll(() => { + // @ts-ignore + delete window.location; + // @ts-ignore + window.location = { search: '?edit=true&abc=123' }; + }); + + afterAll(() => { + window.location = originalWindowLocation; + }); + + it('returns all urlParams', () => { + expect(extractUrlParams('all')).toEqual({ + edit: 'true', + abc: '123', + }); + }); + + it('returns reserved urlParams', () => { + expect(extractUrlParams('reserved')).toEqual({ + edit: 'true', + }); + }); + + it('returns regular urlParams', () => { + expect(extractUrlParams('regular')).toEqual({ + abc: '123', + }); + }); +}); diff --git a/superset-frontend/src/dashboard/util/extractUrlParams.ts b/superset-frontend/src/dashboard/util/extractUrlParams.ts new file mode 100644 index 000000000..7ef7dd9fa --- /dev/null +++ b/superset-frontend/src/dashboard/util/extractUrlParams.ts @@ -0,0 +1,49 @@ +/** + * 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 querystring from 'query-string'; +import { JsonObject } from '@superset-ui/core'; + +const reservedQueryParams = new Set(['standalone', 'edit']); + +export type UrlParamType = 'reserved' | 'regular' | 'all'; + +/** + * Returns the url params that are used to customize queries + */ +export default function extractUrlParams( + urlParamType: UrlParamType, +): JsonObject { + const queryParams = querystring.parse(window.location.search); + return Object.entries(queryParams).reduce((acc, [key, value]) => { + if ( + (urlParamType === 'regular' && reservedQueryParams.has(key)) || + (urlParamType === 'reserved' && !reservedQueryParams.has(key)) + ) + return acc; + // if multiple url params share the same key (?foo=bar&foo=baz), they will appear as an array. + // Only one value can be used for a given query param, so we just take the first one. + if (Array.isArray(value)) { + return { + ...acc, + [key]: value[0], + }; + } + return { ...acc, [key]: value }; + }, {}); +} diff --git a/superset/views/utils.py b/superset/views/utils.py index b0fea9703..767490ca7 100644 --- a/superset/views/utils.py +++ b/superset/views/utils.py @@ -124,7 +124,7 @@ def loads_request_json(request_json_data: str) -> Dict[Any, Any]: return {} -def get_form_data( +def get_form_data( # pylint: disable=too-many-locals slice_id: Optional[int] = None, use_slice_data: bool = False ) -> Tuple[Dict[str, Any], Optional[Slice]]: form_data = {} @@ -139,7 +139,13 @@ def get_form_data( if request_json_data: form_data.update(request_json_data) if request_form_data: - form_data.update(loads_request_json(request_form_data)) + parsed_form_data = loads_request_json(request_form_data) + # some chart data api requests are form_data + queries = parsed_form_data.get("queries") + if isinstance(queries, list): + form_data.update(queries[0]) + else: + form_data.update(parsed_form_data) # request params can overwrite the body if request_args_data: form_data.update(loads_request_json(request_args_data)) diff --git a/tests/utils_tests.py b/tests/utils_tests.py index c0546245a..5781965ae 100644 --- a/tests/utils_tests.py +++ b/tests/utils_tests.py @@ -1015,6 +1015,26 @@ class TestUtils(SupersetTestCase): self.assertEqual(slc, None) + def test_get_form_data_request_form_with_queries(self) -> None: + # the CSV export uses for requests, even when sending requests to + # /api/v1/chart/data + with app.test_request_context( + data={ + "form_data": json.dumps({"queries": [{"url_params": {"foo": "bar"}}]}) + } + ): + form_data, slc = get_form_data() + + self.assertEqual( + form_data, + { + "url_params": {"foo": "bar"}, + "time_range_endpoints": get_time_range_endpoints(form_data={}), + }, + ) + + self.assertEqual(slc, None) + def test_get_form_data_request_args_and_form(self) -> None: with app.test_request_context( data={"form_data": json.dumps({"foo": "bar"})},