From 494af990e48c0b567820c002a5cb4ffc6e29aa95 Mon Sep 17 00:00:00 2001 From: Vitor Avila Date: Thu, 18 Jun 2026 19:48:57 -0300 Subject: [PATCH] fix(chart API): apply dashboard filters by live scope, not stale chartsInScope --- .../charts/data/dashboard_filter_context.py | 20 +++++------------- .../charts/test_dashboard_filter_context.py | 21 +++++++++++++------ 2 files changed, 20 insertions(+), 21 deletions(-) diff --git a/superset/charts/data/dashboard_filter_context.py b/superset/charts/data/dashboard_filter_context.py index fa4227a9c049..b7d745b49bcb 100644 --- a/superset/charts/data/dashboard_filter_context.py +++ b/superset/charts/data/dashboard_filter_context.py @@ -80,20 +80,11 @@ def _is_filter_in_scope_for_chart( """ Determines whether a native filter applies to a given chart. - When chartsInScope is present on the filter config, uses that directly. - Otherwise falls back to scope.rootPath and scope.excluded with the - dashboard layout. Also considers charts that were added to the dashboard - but were not instantiated in the layout. + A chart is in scope when one of its layout ancestors is in ``rootPath`` and + it's not excluded. The persisted ``chartsInScope`` is intentionally NOT used + here (it's a denormalized cache that the frontend recomputes from ``scope`` + on every load, so it can be stale). """ - if (charts_in_scope := filter_config.get("chartsInScope")) is not None: - if chart_id in charts_in_scope: - return True - - # If the chart is found in position_json and not in chartsInScope, - # it was explicitly excluded by the filter scope config. - if _find_chart_layout_item(chart_id, position_json) is not None: - return False - scope = filter_config.get("scope", {}) root_path: list[str] = scope.get("rootPath", []) excluded: list[int] = scope.get("excluded", []) @@ -107,8 +98,7 @@ def _is_filter_in_scope_for_chart( # If the chart doesn't exist in the dashboard layout, treat it as a # root-level chart. - else: - return "ROOT_ID" in root_path + return "ROOT_ID" in root_path def _find_chart_layout_item( diff --git a/tests/unit_tests/charts/test_dashboard_filter_context.py b/tests/unit_tests/charts/test_dashboard_filter_context.py index bfc2097f7253..3bc062330bfb 100644 --- a/tests/unit_tests/charts/test_dashboard_filter_context.py +++ b/tests/unit_tests/charts/test_dashboard_filter_context.py @@ -140,15 +140,24 @@ def test_filter_in_scope_via_charts_in_scope() -> None: assert _is_filter_in_scope_for_chart(flt, 10, SAMPLE_POSITION_JSON) is True -def test_filter_not_in_scope_via_charts_in_scope() -> None: - flt = _make_filter(charts_in_scope=[20, 30]) - assert _is_filter_in_scope_for_chart(flt, 10, SAMPLE_POSITION_JSON) is False +def test_filter_in_scope_ignores_stale_charts_in_scope() -> None: + """ + Regression: a chart present in the layout and within scope.rootPath is in + scope even when the (stale) chartsInScope cache omits it. chartsInScope is a + denormalized cache the frontend recomputes from scope on load (persisted + only on save). + """ + flt = _make_filter(charts_in_scope=[20, 30], scope_root=["ROOT_ID"]) + assert _is_filter_in_scope_for_chart(flt, 10, SAMPLE_POSITION_JSON) is True -def test_filter_empty_charts_in_scope_not_in_scope() -> None: - """Empty chartsInScope means in scope for no charts; do not fall back to rootPath""" +def test_filter_in_scope_ignores_empty_charts_in_scope() -> None: + """ + An empty (stale) chartsInScope must not exclude a chart that scope.rootPath + includes; scope is the source of truth. + """ flt = _make_filter(charts_in_scope=[], scope_root=["ROOT_ID"]) - assert _is_filter_in_scope_for_chart(flt, 10, SAMPLE_POSITION_JSON) is False + assert _is_filter_in_scope_for_chart(flt, 10, SAMPLE_POSITION_JSON) is True def test_filter_in_scope_via_root_path() -> None: