This is an automated email from the ASF dual-hosted git repository. eschutho pushed a commit to branch fix-dashboard-position-non-dict-filter-scope-2 in repository https://gitbox.apache.org/repos/asf/superset.git
commit 24a244589bdc740c2344ed065f19d12001a6c5c9 Author: Elizabeth Thompson <[email protected]> AuthorDate: Wed Sep 23 15:12:32 2026 +0000 fix(dashboard): don't 500 deriving filter scopes from a non-mapping layout (SC-121722) Dashboard.position returns whatever position_json parses to, so a layout stored as a JSON string or array reaches build_chart_layout_items, which called .values() on it and made GET /api/v1/dashboard/<id> raise AttributeError. Treat a non-mapping layout as placing no charts, the same tolerance Dashboard.tabs already applies to this case. Fixes SUPERSET-PYTHON-17F0 Co-Authored-By: Claude <[email protected]> --- superset/dashboards/filter_scope.py | 12 ++++- tests/unit_tests/dashboards/filter_scope_test.py | 58 ++++++++++++++++++++++++ 2 files changed, 69 insertions(+), 1 deletion(-) diff --git a/superset/dashboards/filter_scope.py b/superset/dashboards/filter_scope.py index 9454fc7d35f..eedcca28974 100644 --- a/superset/dashboards/filter_scope.py +++ b/superset/dashboards/filter_scope.py @@ -36,6 +36,7 @@ and ``getCrossFiltersConfiguration``. from __future__ import annotations +import logging import re from typing import Any, TYPE_CHECKING @@ -44,6 +45,8 @@ from superset.utils import json if TYPE_CHECKING: from superset.models.dashboard import Dashboard +logger = logging.getLogger(__name__) + CHART_TYPE = "CHART" TAB_TYPE = "TAB" NATIVE_FILTER_DIVIDER_PREFIX = "NATIVE_FILTER_DIVIDER-" @@ -57,8 +60,15 @@ ChartLayoutItems = dict[int, list[dict[str, Any]]] def build_chart_layout_items(position_data: dict[str, Any]) -> ChartLayoutItems: - """Map each chart id in the layout to the layout items that render it.""" + """Map each chart id in the layout to the layout items that render it. + + A layout that is not a mapping (``position_json`` holding a JSON string or + array) places no charts, so every derived scope comes out empty. + """ chart_layout_items: ChartLayoutItems = {} + if not isinstance(position_data, dict): + logger.warning("Dashboard layout is not a mapping; no charts are in scope") + return chart_layout_items for item in position_data.values(): if not isinstance(item, dict) or item.get("type") != CHART_TYPE: continue diff --git a/tests/unit_tests/dashboards/filter_scope_test.py b/tests/unit_tests/dashboards/filter_scope_test.py index 3e95cf28d19..f523e9102f3 100644 --- a/tests/unit_tests/dashboards/filter_scope_test.py +++ b/tests/unit_tests/dashboards/filter_scope_test.py @@ -15,10 +15,14 @@ # specific language governing permissions and limitations # under the License. +import logging from types import SimpleNamespace from typing import Any +import pytest + from superset.dashboards.filter_scope import ( + build_chart_layout_items, derive_json_metadata, derive_metadata_scopes, derive_scopes, @@ -231,3 +235,57 @@ def test_derive_json_metadata_passes_through_unparsable_metadata() -> None: assert derive_json_metadata(dashboard, "not json") == "not json" # type: ignore[arg-type] assert derive_json_metadata(dashboard, "[]") == "[]" # type: ignore[arg-type] + + +def test_derive_scopes_when_layout_is_not_a_mapping( + caplog: pytest.LogCaptureFixture, +) -> None: + """Valid JSON that is not an object yields empty scopes instead of raising. + + ``Dashboard.position`` hands back whatever ``position_json`` parses to, so a + layout stored as a JSON string or array reaches the derivation as-is. + """ + caplog.set_level(logging.WARNING, logger="superset.dashboards.filter_scope") + metadata = { + "native_filter_configuration": [ + { + "id": "NATIVE_FILTER-1", + "scope": {"rootPath": ["ROOT_ID"], "excluded": []}, + "chartsInScope": [1, 2], + "tabsInScope": ["TAB-1"], + } + ], + } + + position_data: Any + for position_data in ("not a layout", [], None): + assert build_chart_layout_items(position_data) == {} + derived = derive_scopes(metadata, position_data, CHART_IDS) + native_filter = derived["native_filter_configuration"][0] + assert native_filter["chartsInScope"] == [] + assert native_filter["tabsInScope"] == [] + + assert caplog.text.count("layout is not a mapping") == 6 + + +def test_derive_json_metadata_when_layout_is_not_a_mapping() -> None: + """The ``GET /api/v1/dashboard/{id}`` path for a string ``position_json``.""" + dashboard = SimpleNamespace( + position=json.loads(json.dumps("not a layout")), + slices=[SimpleNamespace(id=1)], + ) + stored = json.dumps( + { + "native_filter_configuration": [ + { + "id": "NATIVE_FILTER-1", + "scope": {"rootPath": ["ROOT_ID"], "excluded": []}, + "chartsInScope": [1], + } + ] + } + ) + + derived = json.loads(derive_json_metadata(dashboard, stored)) # type: ignore[arg-type] + + assert derived["native_filter_configuration"][0]["chartsInScope"] == []
