You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by ru...@apache.org on 2026/09/09 03:54:01 UTC
(superset) branch fix/issue-36053-suggested-open-pr created (now 462eb52ba87)
This is an automated email from the ASF dual-hosted git repository.
rusackas pushed a change to branch fix/issue-36053-suggested-open-pr
in repository https://gitbox.apache.org/repos/asf/superset.git
at 462eb52ba87 fix(dashboard): include native filter names in filter_state GET response
This branch includes the following new commits:
new 462eb52ba87 fix(dashboard): include native filter names in filter_state GET response
The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails. The revisions
listed as "add" were already present in the repository and have only
been added to this reference.
(superset) 01/01: fix(dashboard): include native filter names in filter_state GET response
Posted by ru...@apache.org.
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 <ev...@rusackas.com>
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 <no...@anthropic.com>
---
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}")