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

rusackas pushed a commit to branch fix/issue-36053-suggested-open-pr
in repository https://gitbox.apache.org/repos/asf/superset.git

commit 462eb52ba87cab01ea62cad6711341b4204c91d0
Author: rusackas <[email protected]>
AuthorDate: Tue Sep 8 20:53:08 2026 -0700

    fix(dashboard): include native filter names in filter_state GET response
    
    The cached filter_state value is a DataMaskStateWithId keyed by filter id,
    not a flat object, so a client can't reliably read a "name"/label out of
    it. Cross-reference the filter ids present in the cached value against the
    dashboard's native_filter_configuration and return a `names` map (filter
    id -> label) alongside `value`.
    
    Fixes #36053
    
    Co-Authored-By: Claude Sonnet 5 <[email protected]>
---
 superset/commands/dashboard/filter_state/get.py    | 49 +++++++++++++++++++++-
 superset/dashboards/filter_state/api.py            | 28 ++++++++++++-
 .../dashboards/filter_state/api_tests.py           | 39 +++++++++++++++++
 3 files changed, 113 insertions(+), 3 deletions(-)

diff --git a/superset/commands/dashboard/filter_state/get.py 
b/superset/commands/dashboard/filter_state/get.py
index 1fee32e1b01..c832f230535 100644
--- a/superset/commands/dashboard/filter_state/get.py
+++ b/superset/commands/dashboard/filter_state/get.py
@@ -14,15 +14,17 @@
 # KIND, either express or implied.  See the License for the
 # specific language governing permissions and limitations
 # under the License.
-from typing import Optional
+from typing import Any, Optional
 
 from flask import current_app as app
 
 from superset.commands.dashboard.filter_state.utils import check_access
 from superset.commands.temporary_cache.get import GetTemporaryCacheCommand
 from superset.commands.temporary_cache.parameters import CommandParameters
+from superset.daos.dashboard import DashboardDAO
 from superset.extensions import cache_manager
 from superset.temporary_cache.utils import cache_key
+from superset.utils import json
 
 
 class GetFilterStateCommand(GetTemporaryCacheCommand):
@@ -40,3 +42,48 @@ class GetFilterStateCommand(GetTemporaryCacheCommand):
         if entry and self._refresh_timeout:
             cache_manager.filter_state_cache.set(key, entry)
         return entry.get("value")
+
+    @staticmethod
+    def get_filter_names(resource_id: int, value: Optional[str]) -> dict[str, 
str]:
+        """
+        Cross-reference the filter ids present in a cached filter_state
+        ``value`` (a ``DataMaskStateWithId``, i.e. a map keyed by filter id)
+        against the dashboard's ``native_filter_configuration`` to build a
+        map of filter id -> human-readable filter label.
+
+        The cached blob itself has no notion of a filter's label: that only
+        lives in the dashboard's filter configuration metadata (#36053).
+        """
+        if not value:
+            return {}
+        try:
+            parsed_value = json.loads(value)
+        except (json.JSONDecodeError, TypeError):
+            return {}
+        if not isinstance(parsed_value, dict):
+            return {}
+
+        dashboard = DashboardDAO.get_by_id_or_slug(str(resource_id))
+        try:
+            metadata = json.loads(dashboard.json_metadata or "{}")
+        except (json.JSONDecodeError, TypeError):
+            return {}
+        if not isinstance(metadata, dict):
+            return {}
+
+        native_filters: list[dict[str, Any]] = metadata.get(
+            "native_filter_configuration", []
+        )
+        id_to_name = {
+            native_filter["id"]: native_filter["name"]
+            for native_filter in native_filters
+            if isinstance(native_filter, dict)
+            and native_filter.get("id") is not None
+            and native_filter.get("name") is not None
+        }
+
+        return {
+            filter_id: id_to_name[filter_id]
+            for filter_id in parsed_value
+            if filter_id in id_to_name
+        }
diff --git a/superset/dashboards/filter_state/api.py 
b/superset/dashboards/filter_state/api.py
index 25fb7ad2095..dff9e8071bd 100644
--- a/superset/dashboards/filter_state/api.py
+++ b/superset/dashboards/filter_state/api.py
@@ -23,8 +23,13 @@ from superset.commands.dashboard.filter_state.create import 
CreateFilterStateCom
 from superset.commands.dashboard.filter_state.delete import 
DeleteFilterStateCommand
 from superset.commands.dashboard.filter_state.get import GetFilterStateCommand
 from superset.commands.dashboard.filter_state.update import 
UpdateFilterStateCommand
+from superset.commands.temporary_cache.exceptions import (
+    TemporaryCacheAccessDeniedError,
+    TemporaryCacheResourceNotFoundError,
+)
+from superset.commands.temporary_cache.parameters import CommandParameters
 from superset.extensions import event_logger
-from superset.temporary_cache.api import TemporaryCacheRestApi
+from superset.temporary_cache.api import CODEC, TemporaryCacheRestApi
 
 logger = logging.getLogger(__name__)
 
@@ -256,6 +261,14 @@ class DashboardFilterStateRestApi(TemporaryCacheRestApi):
                       value:
                         type: string
                         description: The stored value
+                      names:
+                        type: object
+                        description: >-
+                          A map of native filter id to that filter's
+                          human-readable label, for the filter ids present in
+                          `value`. Cross-referenced from the dashboard's
+                          native filter configuration, since the cached
+                          `value` itself has no notion of a filter's label.
             400:
               $ref: '#/components/responses/400'
             401:
@@ -267,7 +280,18 @@ class DashboardFilterStateRestApi(TemporaryCacheRestApi):
             500:
               $ref: '#/components/responses/500'
         """
-        return super().get(pk, key)
+        try:
+            args = CommandParameters(resource_id=pk, key=key, codec=CODEC)
+            command = self.get_get_command()(args)
+            value = command.run()
+            if not value:
+                return self.response_404()
+            names = command.get_filter_names(pk, value)
+            return self.response(200, value=value, names=names)
+        except TemporaryCacheAccessDeniedError as ex:
+            return self.response(403, message=str(ex))
+        except TemporaryCacheResourceNotFoundError as ex:
+            return self.response(404, message=str(ex))
 
     @expose("/<int:pk>/filter_state/<string:key>", methods=("DELETE",))
     @protect()
diff --git a/tests/integration_tests/dashboards/filter_state/api_tests.py 
b/tests/integration_tests/dashboards/filter_state/api_tests.py
index ef081efd4e1..6cdf40b350b 100644
--- a/tests/integration_tests/dashboards/filter_state/api_tests.py
+++ b/tests/integration_tests/dashboards/filter_state/api_tests.py
@@ -317,6 +317,45 @@ def test_get_dashboard_filter_state(test_client, 
login_as_admin, dashboard_id: i
     assert INITIAL_VALUE == data.get("value")
 
 
+def test_get_dashboard_filter_state_includes_names(
+    test_client, login_as_admin, dashboard_id: int, admin_id: int
+):
+    """
+    The cached filter state is a DataMaskStateWithId, i.e. a map keyed by
+    filter id, not a flat object with a top-level "name" (#36053). The
+    human-readable label for each filter id lives in the dashboard's
+    native_filter_configuration, so the GET response should cross-reference
+    it and return a `names` map of filter id -> label alongside `value`.
+    """
+    dashboard = db.session.query(Dashboard).filter_by(id=dashboard_id).one()
+    original_json_metadata = dashboard.json_metadata
+    filter_id = "NATIVE_FILTER-abc123"
+    try:
+        metadata = json.loads(dashboard.json_metadata or "{}")
+        metadata["native_filter_configuration"] = [
+            {"id": filter_id, "name": "My Filter Label"}
+        ]
+        dashboard.json_metadata = json.dumps(metadata)
+        db.session.commit()
+
+        filter_state_value = json.dumps(
+            {filter_id: {"id": filter_id, "extraFormData": {}}}
+        )
+        cache_manager.filter_state_cache.set(
+            cache_key(dashboard_id, KEY),
+            {"owner": admin_id, "value": filter_state_value},
+        )
+
+        resp = 
test_client.get(f"api/v1/dashboard/{dashboard_id}/filter_state/{KEY}")
+        assert resp.status_code == 200
+        data = json.loads(resp.data.decode("utf-8"))
+        assert data.get("value") == filter_state_value
+        assert data.get("names") == {filter_id: "My Filter Label"}
+    finally:
+        dashboard.json_metadata = original_json_metadata
+        db.session.commit()
+
+
 def test_get_access_denied(test_client, login_as, dashboard_id):
     login_as("gamma")
     resp = 
test_client.get(f"api/v1/dashboard/{dashboard_id}/filter_state/{KEY}")

Reply via email to