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 DashboardLoadTest from './load';
|
||||||
import DashboardSaveTest from './save';
|
import DashboardSaveTest from './save';
|
||||||
import DashboardTabsTest from './tabs';
|
import DashboardTabsTest from './tabs';
|
||||||
|
import DashboardUrlParamsTest from './url_params';
|
||||||
|
|
||||||
describe('Dashboard', () => {
|
describe('Dashboard', () => {
|
||||||
DashboardControlsTest();
|
DashboardControlsTest();
|
||||||
|
@ -32,4 +33,5 @@ describe('Dashboard', () => {
|
||||||
DashboardLoadTest();
|
DashboardLoadTest();
|
||||||
DashboardSaveTest();
|
DashboardSaveTest();
|
||||||
DashboardTabsTest();
|
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}",
|
"clean-css": "prettier --write src/**/*.{css,less,sass,scss}",
|
||||||
"cypress": "cypress",
|
"cypress": "cypress",
|
||||||
"cypress-debug": "cypress open --config watchForFileChanges=true",
|
"cypress-debug": "cypress open --config watchForFileChanges=true",
|
||||||
"install-cypress": "npm install cypress@3.4.1"
|
"install-cypress": "npm install cypress@3.6.1"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
|
|
@ -50,7 +50,7 @@ import newComponentFactory from '../util/newComponentFactory';
|
||||||
import { TIME_RANGE } from '../../visualizations/FilterBox/FilterBox';
|
import { TIME_RANGE } from '../../visualizations/FilterBox/FilterBox';
|
||||||
|
|
||||||
export default function(bootstrapData) {
|
export default function(bootstrapData) {
|
||||||
const { user_id, datasources, common, editMode } = bootstrapData;
|
const { user_id, datasources, common, editMode, urlParams } = bootstrapData;
|
||||||
|
|
||||||
const dashboard = { ...bootstrapData.dashboard_data };
|
const dashboard = { ...bootstrapData.dashboard_data };
|
||||||
let preselectFilters = {};
|
let preselectFilters = {};
|
||||||
|
@ -113,11 +113,18 @@ export default function(bootstrapData) {
|
||||||
dashboard.slices.forEach(slice => {
|
dashboard.slices.forEach(slice => {
|
||||||
const key = slice.slice_id;
|
const key = slice.slice_id;
|
||||||
if (['separator', 'markup'].indexOf(slice.form_data.viz_type) === -1) {
|
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] = {
|
chartQueries[key] = {
|
||||||
...chart,
|
...chart,
|
||||||
id: key,
|
id: key,
|
||||||
form_data: slice.form_data,
|
form_data,
|
||||||
formData: applyDefaultFormData(slice.form_data),
|
formData: applyDefaultFormData(form_data),
|
||||||
};
|
};
|
||||||
|
|
||||||
slices[key] = {
|
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
|
parameters in the explore view as well as from the dashboard, and
|
||||||
it should carry through to your queries.
|
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 param: the parameter to lookup
|
||||||
:param default: the value to return in the absence of the parameter
|
: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 email.utils import formatdate
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from time import struct_time
|
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
|
from urllib.parse import unquote_plus
|
||||||
|
|
||||||
import bleach
|
import bleach
|
||||||
|
@ -906,8 +906,16 @@ def merge_extra_filters(form_data: dict):
|
||||||
del form_data["extra_filters"]
|
del form_data["extra_filters"]
|
||||||
|
|
||||||
|
|
||||||
def merge_request_params(form_data: dict, params: dict):
|
def merge_request_params(form_data: Dict[str, Any], params: Dict[str, Any]) -> None:
|
||||||
url_params = {}
|
"""
|
||||||
|
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():
|
for key, value in params.items():
|
||||||
if key in ("form_data", "r"):
|
if key in ("form_data", "r"):
|
||||||
continue
|
continue
|
||||||
|
@ -1234,3 +1242,13 @@ class TimeRangeEndpoint(str, Enum):
|
||||||
EXCLUSIVE = "exclusive"
|
EXCLUSIVE = "exclusive"
|
||||||
INCLUSIVE = "inclusive"
|
INCLUSIVE = "inclusive"
|
||||||
UNKNOWN = "unknown"
|
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
|
import re
|
||||||
from contextlib import closing
|
from contextlib import closing
|
||||||
from datetime import datetime, timedelta
|
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
|
from urllib import parse
|
||||||
|
|
||||||
import backoff
|
import backoff
|
||||||
|
@ -978,8 +979,9 @@ class Superset(BaseSupersetView):
|
||||||
endpoint = "/superset/explore/?form_data={}".format(
|
endpoint = "/superset/explore/?form_data={}".format(
|
||||||
parse.quote(json.dumps({"slice_id": slice_id}))
|
parse.quote(json.dumps({"slice_id": slice_id}))
|
||||||
)
|
)
|
||||||
if request.args.get("standalone") == "true":
|
param = utils.ReservedUrlParameters.STANDALONE.value
|
||||||
endpoint += "&standalone=true"
|
if request.args.get(param) == "true":
|
||||||
|
endpoint += f"&{param}=true"
|
||||||
return redirect(endpoint)
|
return redirect(endpoint)
|
||||||
|
|
||||||
def get_query_string_response(self, viz_obj):
|
def get_query_string_response(self, viz_obj):
|
||||||
|
@ -1268,7 +1270,9 @@ class Superset(BaseSupersetView):
|
||||||
datasource.name,
|
datasource.name,
|
||||||
)
|
)
|
||||||
|
|
||||||
standalone = request.args.get("standalone") == "true"
|
standalone = (
|
||||||
|
request.args.get(utils.ReservedUrlParameters.STANDALONE.value) == "true"
|
||||||
|
)
|
||||||
bootstrap_data = {
|
bootstrap_data = {
|
||||||
"can_add": slice_add_perm,
|
"can_add": slice_add_perm,
|
||||||
"can_download": slice_download_perm,
|
"can_download": slice_download_perm,
|
||||||
|
@ -2178,8 +2182,12 @@ class Superset(BaseSupersetView):
|
||||||
superset_can_csv = security_manager.can_access("can_csv", "Superset")
|
superset_can_csv = security_manager.can_access("can_csv", "Superset")
|
||||||
slice_can_edit = security_manager.can_access("can_edit", "SliceModelView")
|
slice_can_edit = security_manager.can_access("can_edit", "SliceModelView")
|
||||||
|
|
||||||
standalone_mode = request.args.get("standalone") == "true"
|
standalone_mode = (
|
||||||
edit_mode = request.args.get("edit") == "true"
|
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
|
# Hack to log the dashboard_id properly, even when getting a slug
|
||||||
@event_logger.log_this
|
@event_logger.log_this
|
||||||
|
@ -2204,6 +2212,11 @@ class Superset(BaseSupersetView):
|
||||||
"slice_can_edit": slice_can_edit,
|
"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 = {
|
bootstrap_data = {
|
||||||
"user_id": g.user.get_id(),
|
"user_id": g.user.get_id(),
|
||||||
|
@ -2211,6 +2224,7 @@ class Superset(BaseSupersetView):
|
||||||
"datasources": {ds.uid: ds.data for ds in datasources},
|
"datasources": {ds.uid: ds.data for ds in datasources},
|
||||||
"common": self.common_bootstrap_payload(),
|
"common": self.common_bootstrap_payload(),
|
||||||
"editMode": edit_mode,
|
"editMode": edit_mode,
|
||||||
|
"urlParams": url_params,
|
||||||
}
|
}
|
||||||
|
|
||||||
if request.args.get("json") == "true":
|
if request.args.get("json") == "true":
|
||||||
|
|
|
@ -522,7 +522,7 @@ class UtilsTestCase(SupersetTestCase):
|
||||||
merge_extra_filters(form_data)
|
merge_extra_filters(form_data)
|
||||||
self.assertEqual(form_data, expected)
|
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"}
|
form_data = {"since": "2000", "until": "now"}
|
||||||
url_params = {"form_data": form_data, "dashboard_ids": "(1,2,3,4,5)"}
|
url_params = {"form_data": form_data, "dashboard_ids": "(1,2,3,4,5)"}
|
||||||
merge_request_params(form_data, url_params)
|
merge_request_params(form_data, url_params)
|
||||||
|
@ -530,6 +530,20 @@ class UtilsTestCase(SupersetTestCase):
|
||||||
self.assertIn("dashboard_ids", form_data["url_params"])
|
self.assertIn("dashboard_ids", form_data["url_params"])
|
||||||
self.assertNotIn("form_data", form_data.keys())
|
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):
|
def test_datetime_f(self):
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
datetime_f(datetime(1990, 9, 21, 19, 11, 19, 626096)),
|
datetime_f(datetime(1990, 9, 21, 19, 11, 19, 626096)),
|
||||||
|
|
Loading…
Reference in New Issue