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
 

Reply via email to