mirror of https://github.com/apache/superset.git
Pass url parameters from dashboard to charts (#8536)
* Pass url_params from dashboard to charts * Update params to form_data instead of overwriting * Add cypress tests * Add python test * Add docs * Move reserved url params to utils * Bump cypress
This commit is contained in:
parent
ff6773df4e
commit
7104b04817
|
@ -23,6 +23,7 @@ import DashboardFilterTest from './filter';
|
|||
import DashboardLoadTest from './load';
|
||||
import DashboardSaveTest from './save';
|
||||
import DashboardTabsTest from './tabs';
|
||||
import DashboardUrlParamsTest from './url_params';
|
||||
|
||||
describe('Dashboard', () => {
|
||||
DashboardControlsTest();
|
||||
|
@ -32,4 +33,5 @@ describe('Dashboard', () => {
|
|||
DashboardLoadTest();
|
||||
DashboardSaveTest();
|
||||
DashboardTabsTest();
|
||||
DashboardUrlParamsTest();
|
||||
});
|
||||
|
|
|
@ -0,0 +1,56 @@
|
|||
/**
|
||||
* 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 { WORLD_HEALTH_DASHBOARD } from './dashboard.helper';
|
||||
|
||||
export default () => describe('dashboard url params', () => {
|
||||
const urlParams = { param1: '123', param2: 'abc' };
|
||||
let sliceIds = [];
|
||||
|
||||
beforeEach(() => {
|
||||
cy.server();
|
||||
cy.login();
|
||||
|
||||
cy.visit(WORLD_HEALTH_DASHBOARD, { qs: urlParams });
|
||||
|
||||
cy.get('#app').then((data) => {
|
||||
const bootstrapData = JSON.parse(data[0].dataset.bootstrap);
|
||||
const dashboard = bootstrapData.dashboard_data;
|
||||
sliceIds = dashboard.slices.map(slice => (slice.slice_id));
|
||||
});
|
||||
});
|
||||
|
||||
it('should apply url params to slice requests', () => {
|
||||
const aliases = [];
|
||||
sliceIds
|
||||
.forEach((id) => {
|
||||
const alias = `getJson_${id}`;
|
||||
aliases.push(`@${alias}`);
|
||||
cy.route('POST', `/superset/explore_json/?form_data={"slice_id":${id}}`).as(alias);
|
||||
});
|
||||
|
||||
cy.wait(aliases).then((requests) => {
|
||||
requests.forEach((xhr) => {
|
||||
const requestFormData = xhr.request.body;
|
||||
const requestParams = JSON.parse(requestFormData.get('form_data'));
|
||||
expect(requestParams.url_params)
|
||||
.deep.eq(urlParams);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -20,7 +20,7 @@
|
|||
"clean-css": "prettier --write src/**/*.{css,less,sass,scss}",
|
||||
"cypress": "cypress",
|
||||
"cypress-debug": "cypress open --config watchForFileChanges=true",
|
||||
"install-cypress": "npm install cypress@3.4.1"
|
||||
"install-cypress": "npm install cypress@3.6.1"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
|
|
@ -50,7 +50,7 @@ import newComponentFactory from '../util/newComponentFactory';
|
|||
import { TIME_RANGE } from '../../visualizations/FilterBox/FilterBox';
|
||||
|
||||
export default function(bootstrapData) {
|
||||
const { user_id, datasources, common, editMode } = bootstrapData;
|
||||
const { user_id, datasources, common, editMode, urlParams } = bootstrapData;
|
||||
|
||||
const dashboard = { ...bootstrapData.dashboard_data };
|
||||
let preselectFilters = {};
|
||||
|
@ -113,11 +113,18 @@ export default function(bootstrapData) {
|
|||
dashboard.slices.forEach(slice => {
|
||||
const key = slice.slice_id;
|
||||
if (['separator', 'markup'].indexOf(slice.form_data.viz_type) === -1) {
|
||||
const form_data = {
|
||||
...slice.form_data,
|
||||
url_params: {
|
||||
...slice.form_data.url_params,
|
||||
...urlParams,
|
||||
},
|
||||
};
|
||||
chartQueries[key] = {
|
||||
...chart,
|
||||
id: key,
|
||||
form_data: slice.form_data,
|
||||
formData: applyDefaultFormData(slice.form_data),
|
||||
form_data,
|
||||
formData: applyDefaultFormData(form_data),
|
||||
};
|
||||
|
||||
slices[key] = {
|
||||
|
|
|
@ -56,6 +56,9 @@ def url_param(param: str, default: Optional[str] = None) -> Optional[Any]:
|
|||
parameters in the explore view as well as from the dashboard, and
|
||||
it should carry through to your queries.
|
||||
|
||||
Default values for URL parameters can be defined in chart metdata by
|
||||
adding the key-value pair `url_params: {'foo': 'bar'}`
|
||||
|
||||
:param param: the parameter to lookup
|
||||
:param default: the value to return in the absence of the parameter
|
||||
"""
|
||||
|
|
|
@ -35,7 +35,7 @@ from email.mime.text import MIMEText
|
|||
from email.utils import formatdate
|
||||
from enum import Enum
|
||||
from time import struct_time
|
||||
from typing import Iterator, List, NamedTuple, Optional, Tuple, Union
|
||||
from typing import Any, Dict, Iterator, List, NamedTuple, Optional, Tuple, Union
|
||||
from urllib.parse import unquote_plus
|
||||
|
||||
import bleach
|
||||
|
@ -906,8 +906,16 @@ def merge_extra_filters(form_data: dict):
|
|||
del form_data["extra_filters"]
|
||||
|
||||
|
||||
def merge_request_params(form_data: dict, params: dict):
|
||||
url_params = {}
|
||||
def merge_request_params(form_data: Dict[str, Any], params: Dict[str, Any]) -> None:
|
||||
"""
|
||||
Merge request parameters to the key `url_params` in form_data. Only updates
|
||||
or appends parameters to `form_data` that are defined in `params; pre-existing
|
||||
parameters not defined in params are left unchanged.
|
||||
|
||||
:param form_data: object to be updated
|
||||
:param params: request parameters received via query string
|
||||
"""
|
||||
url_params = form_data.get("url_params", {})
|
||||
for key, value in params.items():
|
||||
if key in ("form_data", "r"):
|
||||
continue
|
||||
|
@ -1234,3 +1242,13 @@ class TimeRangeEndpoint(str, Enum):
|
|||
EXCLUSIVE = "exclusive"
|
||||
INCLUSIVE = "inclusive"
|
||||
UNKNOWN = "unknown"
|
||||
|
||||
|
||||
class ReservedUrlParameters(Enum):
|
||||
"""
|
||||
Reserved URL parameters that are used internally by Superset. These will not be
|
||||
passed to chart queries, as they control the behavior of the UI.
|
||||
"""
|
||||
|
||||
STANDALONE = "standalone"
|
||||
EDIT_MODE = "edit"
|
||||
|
|
|
@ -19,7 +19,8 @@ import logging
|
|||
import re
|
||||
from contextlib import closing
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Dict, List, Optional, Union
|
||||
from enum import Enum
|
||||
from typing import List, Optional, Union
|
||||
from urllib import parse
|
||||
|
||||
import backoff
|
||||
|
@ -978,8 +979,9 @@ class Superset(BaseSupersetView):
|
|||
endpoint = "/superset/explore/?form_data={}".format(
|
||||
parse.quote(json.dumps({"slice_id": slice_id}))
|
||||
)
|
||||
if request.args.get("standalone") == "true":
|
||||
endpoint += "&standalone=true"
|
||||
param = utils.ReservedUrlParameters.STANDALONE.value
|
||||
if request.args.get(param) == "true":
|
||||
endpoint += f"&{param}=true"
|
||||
return redirect(endpoint)
|
||||
|
||||
def get_query_string_response(self, viz_obj):
|
||||
|
@ -1268,7 +1270,9 @@ class Superset(BaseSupersetView):
|
|||
datasource.name,
|
||||
)
|
||||
|
||||
standalone = request.args.get("standalone") == "true"
|
||||
standalone = (
|
||||
request.args.get(utils.ReservedUrlParameters.STANDALONE.value) == "true"
|
||||
)
|
||||
bootstrap_data = {
|
||||
"can_add": slice_add_perm,
|
||||
"can_download": slice_download_perm,
|
||||
|
@ -2178,8 +2182,12 @@ class Superset(BaseSupersetView):
|
|||
superset_can_csv = security_manager.can_access("can_csv", "Superset")
|
||||
slice_can_edit = security_manager.can_access("can_edit", "SliceModelView")
|
||||
|
||||
standalone_mode = request.args.get("standalone") == "true"
|
||||
edit_mode = request.args.get("edit") == "true"
|
||||
standalone_mode = (
|
||||
request.args.get(utils.ReservedUrlParameters.STANDALONE.value) == "true"
|
||||
)
|
||||
edit_mode = (
|
||||
request.args.get(utils.ReservedUrlParameters.EDIT_MODE.value) == "true"
|
||||
)
|
||||
|
||||
# Hack to log the dashboard_id properly, even when getting a slug
|
||||
@event_logger.log_this
|
||||
|
@ -2204,6 +2212,11 @@ class Superset(BaseSupersetView):
|
|||
"slice_can_edit": slice_can_edit,
|
||||
}
|
||||
)
|
||||
url_params = {
|
||||
key: value
|
||||
for key, value in request.args.items()
|
||||
if key not in [param.value for param in utils.ReservedUrlParameters]
|
||||
}
|
||||
|
||||
bootstrap_data = {
|
||||
"user_id": g.user.get_id(),
|
||||
|
@ -2211,6 +2224,7 @@ class Superset(BaseSupersetView):
|
|||
"datasources": {ds.uid: ds.data for ds in datasources},
|
||||
"common": self.common_bootstrap_payload(),
|
||||
"editMode": edit_mode,
|
||||
"urlParams": url_params,
|
||||
}
|
||||
|
||||
if request.args.get("json") == "true":
|
||||
|
|
|
@ -522,7 +522,7 @@ class UtilsTestCase(SupersetTestCase):
|
|||
merge_extra_filters(form_data)
|
||||
self.assertEqual(form_data, expected)
|
||||
|
||||
def test_merge_request_params(self):
|
||||
def test_merge_request_params_when_url_params_undefined(self):
|
||||
form_data = {"since": "2000", "until": "now"}
|
||||
url_params = {"form_data": form_data, "dashboard_ids": "(1,2,3,4,5)"}
|
||||
merge_request_params(form_data, url_params)
|
||||
|
@ -530,6 +530,20 @@ class UtilsTestCase(SupersetTestCase):
|
|||
self.assertIn("dashboard_ids", form_data["url_params"])
|
||||
self.assertNotIn("form_data", form_data.keys())
|
||||
|
||||
def test_merge_request_params_when_url_params_predefined(self):
|
||||
form_data = {
|
||||
"since": "2000",
|
||||
"until": "now",
|
||||
"url_params": {"abc": "123", "dashboard_ids": "(1,2,3)"},
|
||||
}
|
||||
url_params = {"form_data": form_data, "dashboard_ids": "(1,2,3,4,5)"}
|
||||
merge_request_params(form_data, url_params)
|
||||
self.assertIn("url_params", form_data.keys())
|
||||
self.assertIn("abc", form_data["url_params"])
|
||||
self.assertEquals(
|
||||
url_params["dashboard_ids"], form_data["url_params"]["dashboard_ids"]
|
||||
)
|
||||
|
||||
def test_datetime_f(self):
|
||||
self.assertEqual(
|
||||
datetime_f(datetime(1990, 9, 21, 19, 11, 19, 626096)),
|
||||
|
|
Loading…
Reference in New Issue