This is an automated email from the ASF dual-hosted git repository. graceguo pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/incubator-superset.git
The following commit(s) were added to refs/heads/master by this push: new adebd40 [cache warm_up] warm_up slice with dashboard default_filters (#9311) adebd40 is described below commit adebd40d300fb86a53220ee99b453fb853fb2ef5 Author: Grace Guo <grace....@airbnb.com> AuthorDate: Wed Mar 18 08:21:10 2020 -0700 [cache warm_up] warm_up slice with dashboard default_filters (#9311) * [cache warm_up] warm_up slice with dashboard default_filters * update Celery warmup tasks * fix code review comments * add try catch and type checking for parsed dash metadata * extra code review fix --- superset/tasks/cache.py | 31 +++--- superset/views/core.py | 6 ++ superset/views/utils.py | 88 ++++++++++++++++ tests/strategy_tests.py | 22 ++++ tests/utils_tests.py | 265 ++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 396 insertions(+), 16 deletions(-) diff --git a/superset/tasks/cache.py b/superset/tasks/cache.py index ae95f20..67c366b 100644 --- a/superset/tasks/cache.py +++ b/superset/tasks/cache.py @@ -18,6 +18,7 @@ import json import logging +from typing import Any, Dict, Optional from urllib import request from urllib.error import URLError @@ -31,6 +32,7 @@ from superset.models.dashboard import Dashboard from superset.models.slice import Slice from superset.models.tags import Tag, TaggedObject from superset.utils.core import parse_human_datetime +from superset.views.utils import build_extra_filters logger = get_task_logger(__name__) logger.setLevel(logging.INFO) @@ -54,27 +56,23 @@ def get_form_data(chart_id, dashboard=None): if not default_filters: return form_data - # do not apply filters if chart is immune to them - immune_fields = [] filter_scopes = json_metadata.get("filter_scopes", {}) - if filter_scopes: - for scopes in filter_scopes.values(): - for (field, scope) in scopes.items(): - if chart_id in scope.get("immune", []): - immune_fields.append(field) - - extra_filters = [] - for filters in default_filters.values(): - for col, val in filters.items(): - if col not in immune_fields: - extra_filters.append({"col": col, "op": "in", "val": val}) + layout = json.loads(dashboard.position_json or "{}") + if ( + isinstance(layout, dict) + and isinstance(filter_scopes, dict) + and isinstance(default_filters, dict) + ): + extra_filters = build_extra_filters( + layout, filter_scopes, default_filters, chart_id + ) if extra_filters: form_data["extra_filters"] = extra_filters return form_data -def get_url(chart): +def get_url(chart, extra_filters: Optional[Dict[str, Any]] = None): """Return external URL for warming up a given chart/table cache.""" with app.test_request_context(): baseurl = ( @@ -82,7 +80,7 @@ def get_url(chart): "{SUPERSET_WEBSERVER_ADDRESS}:" "{SUPERSET_WEBSERVER_PORT}".format(**app.config) ) - return f"{baseurl}{chart.url}" + return f"{baseurl}{chart.get_explore_url(overrides=extra_filters)}" class Strategy: @@ -181,7 +179,8 @@ class TopNDashboardsStrategy(Strategy): dashboards = session.query(Dashboard).filter(Dashboard.id.in_(dash_ids)).all() for dashboard in dashboards: for chart in dashboard.slices: - urls.append(get_url(chart)) + form_data_with_filters = get_form_data(chart.id, dashboard) + urls.append(get_url(chart, form_data_with_filters)) return urls diff --git a/superset/views/core.py b/superset/views/core.py index ff6dcbf..447461e 100755 --- a/superset/views/core.py +++ b/superset/views/core.py @@ -90,6 +90,7 @@ from superset.utils.dashboard_filter_scopes_converter import copy_filter_scopes from superset.utils.dates import now_as_float from superset.utils.decorators import etag_cache, stats_timing from superset.views.database.filters import DatabaseFilter +from superset.views.utils import get_dashboard_extra_filters from .base import ( api, @@ -1651,6 +1652,7 @@ class Superset(BaseSupersetView): slices = None session = db.session() slice_id = request.args.get("slice_id") + dashboard_id = request.args.get("dashboard_id") table_name = request.args.get("table_name") db_name = request.args.get("db_name") @@ -1696,6 +1698,10 @@ class Superset(BaseSupersetView): for slc in slices: try: form_data = get_form_data(slc.id, use_slice_data=True)[0] + if dashboard_id: + form_data["extra_filters"] = get_dashboard_extra_filters( + slc.id, dashboard_id + ) obj = get_viz( datasource_type=slc.datasource.type, datasource_id=slc.datasource.id, diff --git a/superset/views/utils.py b/superset/views/utils.py index 9bcfc6e..40de71e 100644 --- a/superset/views/utils.py +++ b/superset/views/utils.py @@ -27,6 +27,7 @@ from superset import app, db, viz from superset.connectors.connector_registry import ConnectorRegistry from superset.exceptions import SupersetException from superset.legacy import update_time_range +from superset.models.dashboard import Dashboard from superset.models.slice import Slice from superset.utils.core import QueryStatus, TimeRangeEndpoint @@ -262,3 +263,90 @@ def get_time_range_endpoints( return (TimeRangeEndpoint(start), TimeRangeEndpoint(end)) return (TimeRangeEndpoint.INCLUSIVE, TimeRangeEndpoint.EXCLUSIVE) + + +# see all dashboard components type in +# /superset-frontend/src/dashboard/util/componentTypes.js +CONTAINER_TYPES = ["COLUMN", "GRID", "TABS", "TAB", "ROW"] + + +def get_dashboard_extra_filters( + slice_id: int, dashboard_id: int +) -> List[Dict[str, Any]]: + session = db.session() + dashboard = session.query(Dashboard).filter_by(id=dashboard_id).one_or_none() + + # is chart in this dashboard? + if ( + dashboard is None + or not dashboard.json_metadata + or not dashboard.slices + or not any([slc for slc in dashboard.slices if slc.id == slice_id]) + ): + return [] + + try: + # does this dashboard have default filters? + json_metadata = json.loads(dashboard.json_metadata) + default_filters = json.loads(json_metadata.get("default_filters", "null")) + if not default_filters: + return [] + + # are default filters applicable to the given slice? + filter_scopes = json_metadata.get("filter_scopes", {}) + layout = json.loads(dashboard.position_json or "{}") + + if ( + isinstance(layout, dict) + and isinstance(filter_scopes, dict) + and isinstance(default_filters, dict) + ): + return build_extra_filters(layout, filter_scopes, default_filters, slice_id) + except json.JSONDecodeError: + pass + + return [] + + +def build_extra_filters( + layout: Dict, + filter_scopes: Dict, + default_filters: Dict[str, Dict[str, List]], + slice_id: int, +) -> List[Dict[str, Any]]: + extra_filters = [] + + # do not apply filters if chart is not in filter's scope or + # chart is immune to the filter + for filter_id, columns in default_filters.items(): + scopes_by_filter_field = filter_scopes.get(filter_id, {}) + for col, val in columns.items(): + current_field_scopes = scopes_by_filter_field.get(col, {}) + scoped_container_ids = current_field_scopes.get("scope", ["ROOT_ID"]) + immune_slice_ids = current_field_scopes.get("immune", []) + + for container_id in scoped_container_ids: + if slice_id not in immune_slice_ids and is_slice_in_container( + layout, container_id, slice_id + ): + extra_filters.append({"col": col, "op": "in", "val": val}) + + return extra_filters + + +def is_slice_in_container(layout: Dict, container_id: str, slice_id: int) -> bool: + if container_id == "ROOT_ID": + return True + + node = layout[container_id] + node_type = node.get("type") + if node_type == "CHART" and node.get("meta", {}).get("chartId") == slice_id: + return True + + if node_type in CONTAINER_TYPES: + children = node.get("children", []) + return any( + is_slice_in_container(layout, child_id, slice_id) for child_id in children + ) + + return False diff --git a/tests/strategy_tests.py b/tests/strategy_tests.py index 646931f..08d25b2 100644 --- a/tests/strategy_tests.py +++ b/tests/strategy_tests.py @@ -33,6 +33,22 @@ from .base_tests import SupersetTestCase URL_PREFIX = "http://0.0.0.0:8081" +mock_positions = { + "DASHBOARD_VERSION_KEY": "v2", + "DASHBOARD_CHART_TYPE-1": { + "type": "CHART", + "id": "DASHBOARD_CHART_TYPE-1", + "children": [], + "meta": {"width": 4, "height": 50, "chartId": 1}, + }, + "DASHBOARD_CHART_TYPE-2": { + "type": "CHART", + "id": "DASHBOARD_CHART_TYPE-2", + "children": [], + "meta": {"width": 4, "height": 50, "chartId": 2}, + }, +} + class CacheWarmUpTests(SupersetTestCase): def __init__(self, *args, **kwargs): @@ -48,6 +64,7 @@ class CacheWarmUpTests(SupersetTestCase): chart_id = 1 dashboard = MagicMock() dashboard.json_metadata = None + dashboard.position_json = json.dumps(mock_positions) result = get_form_data(chart_id, dashboard) expected = {"slice_id": chart_id} self.assertEqual(result, expected) @@ -56,6 +73,7 @@ class CacheWarmUpTests(SupersetTestCase): chart_id = 1 filter_box_id = 2 dashboard = MagicMock() + dashboard.position_json = json.dumps(mock_positions) dashboard.json_metadata = json.dumps( { "filter_scopes": { @@ -76,6 +94,7 @@ class CacheWarmUpTests(SupersetTestCase): chart_id = 1 dashboard = MagicMock() dashboard.json_metadata = json.dumps({}) + dashboard.position_json = json.dumps(mock_positions) result = get_form_data(chart_id, dashboard) expected = {"slice_id": chart_id} self.assertEqual(result, expected) @@ -84,6 +103,7 @@ class CacheWarmUpTests(SupersetTestCase): chart_id = 1 filter_box_id = 2 dashboard = MagicMock() + dashboard.position_json = json.dumps(mock_positions) dashboard.json_metadata = json.dumps( { "default_filters": json.dumps( @@ -112,6 +132,7 @@ class CacheWarmUpTests(SupersetTestCase): chart_id = 1 filter_box_id = 2 dashboard = MagicMock() + dashboard.position_json = json.dumps(mock_positions) dashboard.json_metadata = json.dumps( { "default_filters": json.dumps( @@ -132,6 +153,7 @@ class CacheWarmUpTests(SupersetTestCase): chart_id = 1 filter_box_id = 2 dashboard = MagicMock() + dashboard.position_json = json.dumps(mock_positions) dashboard.json_metadata = json.dumps( { "default_filters": json.dumps( diff --git a/tests/utils_tests.py b/tests/utils_tests.py index cc19c48..5332007 100644 --- a/tests/utils_tests.py +++ b/tests/utils_tests.py @@ -56,6 +56,7 @@ from superset.utils.core import ( zlib_decompress, ) from superset.views.utils import get_time_range_endpoints +from superset.views.utils import build_extra_filters from tests.base_tests import SupersetTestCase @@ -956,3 +957,267 @@ class UtilsTestCase(SupersetTestCase): self.assertListEqual(get_iterable(123), [123]) self.assertListEqual(get_iterable([123]), [123]) self.assertListEqual(get_iterable("foo"), ["foo"]) + + def test_build_extra_filters(self): + layout = { + "CHART-2ee52f30": { + "children": [], + "id": "CHART-2ee52f30", + "meta": { + "chartId": 1020, + "height": 38, + "sliceName": "Chart 927", + "width": 6, + }, + "parents": [ + "ROOT_ID", + "TABS-Qq4sdkANSY", + "TAB-VrhTX2WUlO", + "TABS-N1zN4CIZP0", + "TAB-asWdJzKmTN", + "ROW-i_sG4ccXE", + ], + "type": "CHART", + }, + "CHART-36bfc934": { + "children": [], + "id": "CHART-36bfc934", + "meta": { + "chartId": 1018, + "height": 26, + "sliceName": "Region Filter", + "width": 2, + }, + "parents": [ + "ROOT_ID", + "TABS-Qq4sdkANSY", + "TAB-W62P60D88", + "ROW-1e064e3c", + "COLUMN-fe3914b8", + ], + "type": "CHART", + }, + "CHART-E_y2cuNHTv": { + "children": [], + "id": "CHART-E_y2cuNHTv", + "meta": {"chartId": 998, "height": 55, "sliceName": "MAP", "width": 6}, + "parents": [ + "ROOT_ID", + "TABS-Qq4sdkANSY", + "TAB-W62P60D88", + "ROW-1e064e3c", + ], + "type": "CHART", + }, + "CHART-JNxDOsAfEb": { + "children": [], + "id": "CHART-JNxDOsAfEb", + "meta": { + "chartId": 1015, + "height": 27, + "sliceName": "Population", + "width": 4, + }, + "parents": [ + "ROOT_ID", + "TABS-Qq4sdkANSY", + "TAB-W62P60D88", + "ROW-1e064e3c", + "COLUMN-fe3914b8", + ], + "type": "CHART", + }, + "CHART-KoOwqalV80": { + "children": [], + "id": "CHART-KoOwqalV80", + "meta": { + "chartId": 927, + "height": 20, + "sliceName": "Chart 927", + "width": 4, + }, + "parents": [ + "ROOT_ID", + "TABS-Qq4sdkANSY", + "TAB-VrhTX2WUlO", + "TABS-N1zN4CIZP0", + "TAB-cHNWcBZC9", + "ROW-9b9vrWKPY", + ], + "type": "CHART", + }, + "CHART-YCQAPVK7mQ": { + "children": [], + "id": "CHART-YCQAPVK7mQ", + "meta": { + "chartId": 1023, + "height": 38, + "sliceName": "World's Population", + "width": 4, + }, + "parents": [ + "ROOT_ID", + "TABS-Qq4sdkANSY", + "TAB-VrhTX2WUlO", + "ROW-UfxFT36oV5", + ], + "type": "CHART", + }, + "COLUMN-fe3914b8": { + "children": ["CHART-36bfc934", "CHART-JNxDOsAfEb"], + "id": "COLUMN-fe3914b8", + "meta": {"background": "BACKGROUND_TRANSPARENT", "width": 6}, + "parents": [ + "ROOT_ID", + "TABS-Qq4sdkANSY", + "TAB-W62P60D88", + "ROW-1e064e3c", + ], + "type": "COLUMN", + }, + "DASHBOARD_VERSION_KEY": "v2", + "GRID_ID": { + "children": [], + "id": "GRID_ID", + "parents": ["ROOT_ID"], + "type": "GRID", + }, + "HEADER_ID": { + "id": "HEADER_ID", + "meta": {"text": "Test warmup 1023"}, + "type": "HEADER", + }, + "ROOT_ID": { + "children": ["TABS-Qq4sdkANSY"], + "id": "ROOT_ID", + "type": "ROOT", + }, + "ROW-1e064e3c": { + "children": ["COLUMN-fe3914b8", "CHART-E_y2cuNHTv"], + "id": "ROW-1e064e3c", + "meta": {"background": "BACKGROUND_TRANSPARENT"}, + "parents": ["ROOT_ID", "TABS-Qq4sdkANSY", "TAB-W62P60D88"], + "type": "ROW", + }, + "ROW-9b9vrWKPY": { + "children": ["CHART-KoOwqalV80"], + "id": "ROW-9b9vrWKPY", + "meta": {"background": "BACKGROUND_TRANSPARENT"}, + "parents": [ + "ROOT_ID", + "TABS-Qq4sdkANSY", + "TAB-VrhTX2WUlO", + "TABS-N1zN4CIZP0", + "TAB-cHNWcBZC9", + ], + "type": "ROW", + }, + "ROW-UfxFT36oV5": { + "children": ["CHART-YCQAPVK7mQ"], + "id": "ROW-UfxFT36oV5", + "meta": {"background": "BACKGROUND_TRANSPARENT"}, + "parents": ["ROOT_ID", "TABS-Qq4sdkANSY", "TAB-VrhTX2WUlO"], + "type": "ROW", + }, + "ROW-i_sG4ccXE": { + "children": ["CHART-2ee52f30"], + "id": "ROW-i_sG4ccXE", + "meta": {"background": "BACKGROUND_TRANSPARENT"}, + "parents": [ + "ROOT_ID", + "TABS-Qq4sdkANSY", + "TAB-VrhTX2WUlO", + "TABS-N1zN4CIZP0", + "TAB-asWdJzKmTN", + ], + "type": "ROW", + }, + "TAB-VrhTX2WUlO": { + "children": ["ROW-UfxFT36oV5", "TABS-N1zN4CIZP0"], + "id": "TAB-VrhTX2WUlO", + "meta": {"text": "New Tab"}, + "parents": ["ROOT_ID", "TABS-Qq4sdkANSY"], + "type": "TAB", + }, + "TAB-W62P60D88": { + "children": ["ROW-1e064e3c"], + "id": "TAB-W62P60D88", + "meta": {"text": "Tab 2"}, + "parents": ["ROOT_ID", "TABS-Qq4sdkANSY"], + "type": "TAB", + }, + "TAB-asWdJzKmTN": { + "children": ["ROW-i_sG4ccXE"], + "id": "TAB-asWdJzKmTN", + "meta": {"text": "nested tab 1"}, + "parents": [ + "ROOT_ID", + "TABS-Qq4sdkANSY", + "TAB-VrhTX2WUlO", + "TABS-N1zN4CIZP0", + ], + "type": "TAB", + }, + "TAB-cHNWcBZC9": { + "children": ["ROW-9b9vrWKPY"], + "id": "TAB-cHNWcBZC9", + "meta": {"text": "test2d tab 2"}, + "parents": [ + "ROOT_ID", + "TABS-Qq4sdkANSY", + "TAB-VrhTX2WUlO", + "TABS-N1zN4CIZP0", + ], + "type": "TAB", + }, + "TABS-N1zN4CIZP0": { + "children": ["TAB-asWdJzKmTN", "TAB-cHNWcBZC9"], + "id": "TABS-N1zN4CIZP0", + "meta": {}, + "parents": ["ROOT_ID", "TABS-Qq4sdkANSY", "TAB-VrhTX2WUlO"], + "type": "TABS", + }, + "TABS-Qq4sdkANSY": { + "children": ["TAB-VrhTX2WUlO", "TAB-W62P60D88"], + "id": "TABS-Qq4sdkANSY", + "meta": {}, + "parents": ["ROOT_ID"], + "type": "TABS", + }, + } + filter_scopes = { + "1018": { + "region": {"scope": ["TAB-W62P60D88"], "immune": [998]}, + "country_name": {"scope": ["ROOT_ID"], "immune": [927, 998]}, + } + } + default_filters = { + "1018": {"region": ["North America"], "country_name": ["United States"]} + } + + # immune to all filters + slice_id = 998 + extra_filters = build_extra_filters( + layout, filter_scopes, default_filters, slice_id + ) + expected = [] + self.assertEqual(extra_filters, expected) + + # in scope + slice_id = 1015 + extra_filters = build_extra_filters( + layout, filter_scopes, default_filters, slice_id + ) + expected = [ + {"col": "region", "op": "in", "val": ["North America"]}, + {"col": "country_name", "op": "in", "val": ["United States"]}, + ] + self.assertEqual(extra_filters, expected) + + # not in scope + slice_id = 927 + extra_filters = build_extra_filters( + layout, filter_scopes, default_filters, slice_id + ) + expected = [] + self.assertEqual(extra_filters, expected)