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:
Ville Brofeldt 2019-11-21 07:07:08 +02:00 committed by GitHub
parent ff6773df4e
commit 7104b04817
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 128 additions and 14 deletions

View File

@ -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();
});

View File

@ -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);
});
});
});
});

View File

@ -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",

View File

@ -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] = {

View File

@ -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
"""

View File

@ -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"

View File

@ -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":

View File

@ -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)),