Skip to content

Commit

Permalink
Pass url parameters from dashboard to charts (apache#8536)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
villebro authored Nov 21, 2019
1 parent ff6773d commit 7104b04
Show file tree
Hide file tree
Showing 8 changed files with 128 additions and 14 deletions.
2 changes: 2 additions & 0 deletions superset/assets/cypress/integration/dashboard/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -32,4 +33,5 @@ describe('Dashboard', () => {
DashboardLoadTest();
DashboardSaveTest();
DashboardTabsTest();
DashboardUrlParamsTest();
});
56 changes: 56 additions & 0 deletions superset/assets/cypress/integration/dashboard/url_params.js
Original file line number Diff line number Diff line change
@@ -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);
});
});
});
});
2 changes: 1 addition & 1 deletion superset/assets/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
13 changes: 10 additions & 3 deletions superset/assets/src/dashboard/reducers/getInitialState.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {};
Expand Down Expand Up @@ -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] = {
Expand Down
3 changes: 3 additions & 0 deletions superset/jinja_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
"""
Expand Down
24 changes: 21 additions & 3 deletions superset/utils/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
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
Expand Down Expand Up @@ -906,8 +906,16 @@ def get_filter_key(f):
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
Expand Down Expand Up @@ -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"
26 changes: 20 additions & 6 deletions superset/views/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@
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
Expand Down Expand Up @@ -978,8 +979,9 @@ def slice(self, slice_id):
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):
Expand Down Expand Up @@ -1268,7 +1270,9 @@ def explore(self, datasource_type=None, datasource_id=None):
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,
Expand Down Expand Up @@ -2178,8 +2182,12 @@ def dashboard(self, dashboard_id):
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
Expand All @@ -2204,13 +2212,19 @@ def dashboard(**kwargs):
"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(),
"dashboard_data": dashboard_data,
"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":
Expand Down
16 changes: 15 additions & 1 deletion tests/utils_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -522,14 +522,28 @@ def test_merge_extra_filters_adds_unequal_lists(self):
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)
self.assertIn("url_params", form_data.keys())
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)),
Expand Down

0 comments on commit 7104b04

Please sign in to comment.