You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by va...@apache.org on 2026/06/04 19:43:45 UTC

(superset) branch master updated: fix(dashboard-filter): Consider dashboard filters to charts not declared in the dashboard position (#40774)

This is an automated email from the ASF dual-hosted git repository.

Vitor-Avila pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/superset.git


The following commit(s) were added to refs/heads/master by this push:
     new 74060987082 fix(dashboard-filter): Consider dashboard filters to charts not declared in the dashboard position (#40774)
74060987082 is described below

commit 74060987082e115664e4b0c7b6ae35764f59cfec
Author: Vitor Avila <96...@users.noreply.github.com>
AuthorDate: Thu Jun 4 16:43:38 2026 -0300

    fix(dashboard-filter): Consider dashboard filters to charts not declared in the dashboard position (#40774)
---
 superset/charts/data/dashboard_filter_context.py   | 30 +++++---
 .../charts/test_dashboard_filter_context.py        | 80 +++++++++++++++++++++-
 2 files changed, 99 insertions(+), 11 deletions(-)

diff --git a/superset/charts/data/dashboard_filter_context.py b/superset/charts/data/dashboard_filter_context.py
index df889b52ace..50b6ef4244e 100644
--- a/superset/charts/data/dashboard_filter_context.py
+++ b/superset/charts/data/dashboard_filter_context.py
@@ -78,13 +78,21 @@ def _is_filter_in_scope_for_chart(
     position_json: dict[str, Any],
 ) -> bool:
     """
-    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.
+    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.
     """
     if (charts_in_scope := filter_config.get("chartsInScope")) is not None:
-        return chart_id in charts_in_scope
+        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", [])
@@ -93,12 +101,14 @@ def _is_filter_in_scope_for_chart(
     if chart_id in excluded:
         return False
 
-    chart_layout_item = _find_chart_layout_item(chart_id, position_json)
-    if not chart_layout_item:
-        return False
+    if chart_layout_item := _find_chart_layout_item(chart_id, position_json):
+        parents: list[str] = chart_layout_item.get("parents", [])
+        return any(parent in root_path for parent in parents)
 
-    parents: list[str] = chart_layout_item.get("parents", [])
-    return any(parent in root_path for parent in parents)
+    # If the chart doesn't exist in the dashboard layout, treat it as a
+    # root-level chart.
+    else:
+        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 9be05929f64..bfc2097f725 100644
--- a/tests/unit_tests/charts/test_dashboard_filter_context.py
+++ b/tests/unit_tests/charts/test_dashboard_filter_context.py
@@ -166,11 +166,40 @@ def test_filter_not_in_scope_different_root() -> None:
     assert _is_filter_in_scope_for_chart(flt, 10, SAMPLE_POSITION_JSON) is False
 
 
-def test_filter_in_scope_chart_not_in_layout() -> None:
+def test_filter_in_scope_chart_not_in_layout_whole_dashboard_scope() -> None:
+    """Charts not in position_json receive whole-dashboard (ROOT_ID) filters."""
     flt = _make_filter(scope_root=["ROOT_ID"])
+    assert _is_filter_in_scope_for_chart(flt, 999, SAMPLE_POSITION_JSON) is True
+
+
+def test_filter_not_in_scope_chart_not_in_layout_tab_scope() -> None:
+    """Charts not in position_json do not receive tab/section-specific filters."""
+    flt = _make_filter(scope_root=["TAB-some-tab"])
+    assert _is_filter_in_scope_for_chart(flt, 999, SAMPLE_POSITION_JSON) is False
+
+
+def test_filter_not_in_scope_chart_not_in_layout_excluded() -> None:
+    """Explicit exclusion is respected even when chart is not in layout."""
+    flt = _make_filter(scope_root=["ROOT_ID"], scope_excluded=[999])
     assert _is_filter_in_scope_for_chart(flt, 999, SAMPLE_POSITION_JSON) is False
 
 
+def test_filter_in_scope_stale_charts_in_scope_unpositioned_chart() -> None:
+    """
+    chartsInScope is a denormalized cache. When a chart is absent from both
+    chartsInScope and position_json (added to the dashboard after the filter
+    was saved), fall through to rootPath — a whole-dashboard filter applies.
+    """
+    flt = _make_filter(charts_in_scope=[10, 20], scope_root=["ROOT_ID"])
+    assert _is_filter_in_scope_for_chart(flt, 999, SAMPLE_POSITION_JSON) is True
+
+
+def test_filter_in_scope_chart_not_in_layout_empty_position_json() -> None:
+    """With empty position_json, a root-scoped filter still applies."""
+    flt = _make_filter(scope_root=["ROOT_ID"])
+    assert _is_filter_in_scope_for_chart(flt, 999, {}) is True
+
+
 # --- _extract_filter_extra_form_data ---
 
 
@@ -523,3 +552,52 @@ def test_get_dashboard_filter_context_out_of_scope_filter_excluded(
     ctx = get_dashboard_filter_context(dashboard_id=1, chart_id=10)
     assert len(ctx.filters) == 1
     assert ctx.filters[0].id == "f1"
+
+
+@patch("superset.charts.data.dashboard_filter_context._check_dashboard_access")
+@patch("superset.charts.data.dashboard_filter_context.db")
+def test_get_dashboard_filter_context_chart_not_in_layout_receives_root_filters(
+    mock_db: MagicMock,
+    mock_check_access: MagicMock,
+) -> None:
+    """
+    Charts added to a dashboard via the Save Chart dialog exist in dashboard.slices
+    but not in position_json. They should receive whole-dashboard filters and be
+    excluded from tab/section-specific filters.
+    """
+    filter_config = [
+        _make_filter(
+            flt_id="f1",
+            name="Region",
+            scope_root=["ROOT_ID"],
+            default_value=["US"],
+            target_column="region",
+        ),
+        _make_filter(
+            flt_id="f2",
+            name="Tab filter",
+            scope_root=["TAB-some-tab"],
+            default_value=["active"],
+            target_column="status",
+        ),
+    ]
+    metadata = {"native_filter_configuration": filter_config}
+
+    dashboard = MagicMock()
+    dashboard.id = 1
+    slice_obj = MagicMock()
+    slice_obj.id = 42  # chart 42 is in slices but NOT in SAMPLE_POSITION_JSON
+    dashboard.slices = [slice_obj]
+    dashboard.json_metadata = json.dumps(metadata)
+    dashboard.position_json = json.dumps(SAMPLE_POSITION_JSON)
+    (
+        mock_db.session.query.return_value.filter_by.return_value.one_or_none.return_value
+    ) = dashboard
+
+    ctx = get_dashboard_filter_context(dashboard_id=1, chart_id=42)
+
+    # Only the whole-dashboard filter (ROOT_ID) applies; tab filter does not
+    assert len(ctx.filters) == 1
+    assert ctx.filters[0].id == "f1"
+    assert ctx.filters[0].status == DashboardFilterStatus.APPLIED
+    assert ctx.extra_form_data["filters"][0]["val"] == ["US"]