[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
This commit is contained in:
parent
98ac72074c
commit
adebd40d30
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Reference in New Issue