This is an automated email from the ASF dual-hosted git repository.
rusackas 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 4a9aecda4aa fix(dashboard-import): remap chartsInScope on import
(#26338) (#40140)
4a9aecda4aa is described below
commit 4a9aecda4aaeebf4c95c6625a4b86dbd14468a38
Author: Evan Rusackas <[email protected]>
AuthorDate: Wed May 20 13:41:14 2026 -0700
fix(dashboard-import): remap chartsInScope on import (#26338) (#40140)
Co-authored-by: Claude Code <[email protected]>
Co-authored-by: Claude <[email protected]>
---
superset/commands/dashboard/importers/v1/utils.py | 27 ++++-
.../dashboards/commands/importers/v1/utils_test.py | 130 +++++++++++++++++++++
2 files changed, 156 insertions(+), 1 deletion(-)
diff --git a/superset/commands/dashboard/importers/v1/utils.py
b/superset/commands/dashboard/importers/v1/utils.py
index 2d9b6bf1188..01739047ede 100644
--- a/superset/commands/dashboard/importers/v1/utils.py
+++ b/superset/commands/dashboard/importers/v1/utils.py
@@ -62,6 +62,21 @@ def build_uuid_to_id_map(position: dict[str, Any]) ->
dict[str, int]:
}
+def _remap_charts_in_scope(container: dict[str, Any], id_map: dict[int, int])
-> None:
+ """Remap source-env chart IDs in ``container["chartsInScope"]`` in place.
+
+ ``chartsInScope`` is a denormalized cache of the charts a filter (native
+ or cross-filter) currently applies to. Both surfaces share this contract,
+ so they share this remap. Unresolvable IDs are dropped rather than
+ passed through, matching the convention used for ``scope.excluded``.
+ """
+ charts_in_scope = container.get("chartsInScope")
+ if isinstance(charts_in_scope, list):
+ container["chartsInScope"] = [
+ id_map[old_id] for old_id in charts_in_scope if old_id in id_map
+ ]
+
+
def update_id_refs( # pylint: disable=too-many-locals # noqa: C901
config: dict[str, Any],
chart_ids: dict[str, int],
@@ -145,6 +160,8 @@ def update_id_refs( # pylint: disable=too-many-locals #
noqa: C901
id_map[old_id] for old_id in scope_excluded if old_id in id_map
]
+ _remap_charts_in_scope(native_filter, id_map)
+
# fix display control dataset references
for customization in (
fixed.get("metadata", {}).get("chart_customization_config") or []
@@ -170,7 +187,7 @@ def update_id_refs( # pylint: disable=too-many-locals #
noqa: C901
return fixed
-def update_cross_filter_scoping(
+def update_cross_filter_scoping( # noqa: C901
config: dict[str, Any], id_map: dict[int, int]
) -> dict[str, Any]:
# fix cross filter references
@@ -185,6 +202,9 @@ def update_cross_filter_scoping(
id_map[old_id] for old_id in scope_excluded if old_id in id_map
]
+ # Global cross-filter chartsInScope mirrors the native-filter case.
+ _remap_charts_in_scope(cross_filter_global_config, id_map)
+
if "chart_configuration" in (metadata := fixed.get("metadata", {})):
# Build remapped configuration in a single pass for
clarity/readability.
new_chart_configuration: dict[str, Any] = {}
@@ -212,6 +232,11 @@ def update_cross_filter_scoping(
if old_id in id_map
]
+ # Cross-filter chartsInScope mirrors the native-filter case.
+ cross_filters = chart_config.get("crossFilters")
+ if isinstance(cross_filters, dict):
+ _remap_charts_in_scope(cross_filters, id_map)
+
new_chart_configuration[str(new_id)] = chart_config
metadata["chart_configuration"] = new_chart_configuration
diff --git a/tests/unit_tests/dashboards/commands/importers/v1/utils_test.py
b/tests/unit_tests/dashboards/commands/importers/v1/utils_test.py
index 6fe39c1cc3d..60fba73ac9c 100644
--- a/tests/unit_tests/dashboards/commands/importers/v1/utils_test.py
+++ b/tests/unit_tests/dashboards/commands/importers/v1/utils_test.py
@@ -238,6 +238,136 @@ def
test_update_native_filter_config_default_rootpath_preserved():
assert scope["excluded"] == []
+def test_update_id_refs_remaps_charts_in_scope():
+ """
+ Regression for #26338: ``chartsInScope`` on a native filter holds chart
+ IDs and must be remapped from source-env IDs to destination-env IDs
+ during import.
+
+ The export side already converts ``chartsInScope`` IDs to UUIDs (see
+ ``export_example.py:325``). The import side must symmetrically convert
+ them back to the destination environment's chart IDs. Without that
+ remap, the field carries stale source IDs into the imported dashboard
+ and breaks ``filtersInScope`` / ``filtersOutScope`` computation —
+ filters end up applied to the wrong charts (or none at all).
+ """
+ from superset.commands.dashboard.importers.v1.utils import update_id_refs
+
+ config: dict[str, Any] = {
+ "position": {
+ "CHART1": {
+ "id": "CHART1",
+ "meta": {"chartId": 101, "uuid": "uuid1"},
+ "type": "CHART",
+ },
+ "CHART2": {
+ "id": "CHART2",
+ "meta": {"chartId": 102, "uuid": "uuid2"},
+ "type": "CHART",
+ },
+ },
+ "metadata": {
+ "native_filter_configuration": [
+ {
+ "id": "NATIVE_FILTER-region",
+ "scope": {"rootPath": ["ROOT_ID"], "excluded": []},
+ # chartsInScope contains source-env chart IDs.
+ "chartsInScope": [101, 102, 103],
+ }
+ ],
+ },
+ }
+ chart_ids = {"uuid1": 1, "uuid2": 2}
+ dataset_info: dict[str, dict[str, Any]] = {}
+
+ fixed = update_id_refs(config, chart_ids, dataset_info)
+ filter_config = fixed["metadata"]["native_filter_configuration"][0]
+
+ # Resolved IDs are remapped; unknown IDs (103) are dropped rather than
+ # left to silently bind to whatever chart owns that integer in the
+ # destination environment.
+ assert filter_config["chartsInScope"] == [1, 2]
+
+
+def test_update_id_refs_remaps_cross_filter_charts_in_scope():
+ """
+ Companion to test_update_id_refs_remaps_charts_in_scope. Cross-filter
+ config also stores ``chartsInScope`` (under ``crossFilters`` per chart)
+ and must be remapped on import for the same reason.
+ """
+ from superset.commands.dashboard.importers.v1.utils import update_id_refs
+
+ config: dict[str, Any] = {
+ "position": {
+ "CHART1": {
+ "id": "CHART1",
+ "meta": {"chartId": 101, "uuid": "uuid1"},
+ "type": "CHART",
+ },
+ "CHART2": {
+ "id": "CHART2",
+ "meta": {"chartId": 102, "uuid": "uuid2"},
+ "type": "CHART",
+ },
+ },
+ "metadata": {
+ "chart_configuration": {
+ "101": {
+ "id": 101,
+ "crossFilters": {
+ "scope": {"rootPath": ["ROOT_ID"], "excluded": []},
+ "chartsInScope": [101, 102, 103],
+ },
+ },
+ },
+ },
+ }
+ chart_ids = {"uuid1": 1, "uuid2": 2}
+ dataset_info: dict[str, dict[str, Any]] = {}
+
+ fixed = update_id_refs(config, chart_ids, dataset_info)
+ cross_filters =
fixed["metadata"]["chart_configuration"]["1"]["crossFilters"]
+
+ assert cross_filters["chartsInScope"] == [1, 2]
+
+
+def test_update_id_refs_remaps_global_chart_configuration_charts_in_scope():
+ """
+ Per-chart and native-filter ``chartsInScope`` are remapped by their own
+ branches; ``global_chart_configuration.chartsInScope`` lives next to
+ ``global_chart_configuration.scope.excluded`` and needs the same treatment
+ so the global cross-filter scope cache doesn't keep stale source-env IDs.
+ """
+ from superset.commands.dashboard.importers.v1.utils import update_id_refs
+
+ config: dict[str, Any] = {
+ "position": {
+ "CHART1": {
+ "id": "CHART1",
+ "meta": {"chartId": 101, "uuid": "uuid1"},
+ "type": "CHART",
+ },
+ "CHART2": {
+ "id": "CHART2",
+ "meta": {"chartId": 102, "uuid": "uuid2"},
+ "type": "CHART",
+ },
+ },
+ "metadata": {
+ "global_chart_configuration": {
+ "scope": {"rootPath": ["ROOT_ID"], "excluded": []},
+ "chartsInScope": [101, 102, 103],
+ },
+ },
+ }
+ chart_ids = {"uuid1": 1, "uuid2": 2}
+ dataset_info: dict[str, dict[str, Any]] = {}
+
+ fixed = update_id_refs(config, chart_ids, dataset_info)
+
+ assert fixed["metadata"]["global_chart_configuration"]["chartsInScope"] ==
[1, 2]
+
+
def
test_update_id_refs_cross_filter_chart_configuration_key_and_excluded_mapping():
from superset.commands.dashboard.importers.v1.utils import update_id_refs