mirror of
https://github.com/apache/superset.git
synced 2026-09-09 08:44:32 +00:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
462eb52ba8 | ||
|
|
daf41bdf04 |
@@ -212,7 +212,13 @@ jobs:
|
||||
- name: Setup Python
|
||||
uses: $/.github/actions/setup-backend/
|
||||
- name: Install dependencies
|
||||
uses: ./.github/actions/cached-dependencies
|
||||
# cached-dependencies is a git submodule (not a plain directory), and
|
||||
# the $/ self-repository syntax resolves action files directly from
|
||||
# the repository without performing a real (submodule-aware)
|
||||
# checkout, so it can't see into a submodule's gitlink. Keep this one
|
||||
# on the workspace-relative ./ form, consistent with every other
|
||||
# workflow in the repo that references this action.
|
||||
uses: ./.github/actions/cached-dependencies # zizmor: ignore[self-repository] - $/ cannot resolve an action that lives in a submodule; ./ is required here
|
||||
with:
|
||||
run: |
|
||||
# sqlite needs this working directory
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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}")
|
||||
|
||||
Reference in New Issue
Block a user