mirror of
https://github.com/apache/superset.git
synced 2026-04-19 08:04:53 +00:00
feat(mcp): support unsaved state in Explore and Dashboard tools (#37183)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -109,6 +109,24 @@ class ChartInfo(BaseModel):
|
||||
uuid: str | None = Field(None, description="Chart UUID")
|
||||
tags: List[TagInfo] = Field(default_factory=list, description="Chart tags")
|
||||
owners: List[UserInfo] = Field(default_factory=list, description="Chart owners")
|
||||
|
||||
# Fields for unsaved state support
|
||||
form_data_key: str | None = Field(
|
||||
None,
|
||||
description=(
|
||||
"Cache key used to retrieve unsaved form_data. When present, indicates "
|
||||
"the form_data came from cache (unsaved edits) rather than the saved chart."
|
||||
),
|
||||
)
|
||||
is_unsaved_state: bool = Field(
|
||||
default=False,
|
||||
description=(
|
||||
"True if the form_data came from cache (unsaved edits) rather than the "
|
||||
"saved chart configuration. When true, the data reflects what the user "
|
||||
"sees in the Explore view, not the saved version."
|
||||
),
|
||||
)
|
||||
|
||||
model_config = ConfigDict(from_attributes=True, ser_json_timedelta="iso8601")
|
||||
|
||||
@model_serializer(mode="wrap", when_used="json")
|
||||
@@ -200,12 +218,26 @@ class VersionedResponse(BaseModel):
|
||||
|
||||
|
||||
class GetChartInfoRequest(BaseModel):
|
||||
"""Request schema for get_chart_info with support for ID or UUID."""
|
||||
"""Request schema for get_chart_info with support for ID or UUID.
|
||||
|
||||
When form_data_key is provided, the tool will retrieve the unsaved chart state
|
||||
from cache, allowing you to explain what the user actually sees (not the saved
|
||||
version). This is useful when a user edits a chart in Explore but hasn't saved yet.
|
||||
"""
|
||||
|
||||
identifier: Annotated[
|
||||
int | str,
|
||||
Field(description="Chart identifier - can be numeric ID or UUID string"),
|
||||
]
|
||||
form_data_key: str | None = Field(
|
||||
default=None,
|
||||
description=(
|
||||
"Optional cache key for retrieving unsaved chart state. When a user "
|
||||
"edits a chart in Explore but hasn't saved, the current state is stored "
|
||||
"with this key. If provided, the tool returns the current unsaved "
|
||||
"configuration instead of the saved version."
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def serialize_chart_object(chart: ChartLike | None) -> ChartInfo | None:
|
||||
@@ -788,9 +820,24 @@ class UpdateChartPreviewRequest(FormDataCacheControl):
|
||||
|
||||
|
||||
class GetChartDataRequest(QueryCacheControl):
|
||||
"""Request for chart data with cache control."""
|
||||
"""Request for chart data with cache control.
|
||||
|
||||
When form_data_key is provided, the tool will use the unsaved chart configuration
|
||||
from cache to query data, allowing you to get data for what the user actually sees
|
||||
(not the saved version). This is useful when a user edits a chart in Explore but
|
||||
hasn't saved yet.
|
||||
"""
|
||||
|
||||
identifier: int | str = Field(description="Chart identifier (ID, UUID)")
|
||||
form_data_key: str | None = Field(
|
||||
default=None,
|
||||
description=(
|
||||
"Optional cache key for retrieving unsaved chart state. When a user "
|
||||
"edits a chart in Explore but hasn't saved, the current state is stored "
|
||||
"with this key. If provided, the tool uses this configuration to query "
|
||||
"data instead of the saved chart configuration."
|
||||
),
|
||||
)
|
||||
limit: int | None = Field(
|
||||
default=None,
|
||||
description=(
|
||||
@@ -866,9 +913,24 @@ class ChartData(BaseModel):
|
||||
|
||||
|
||||
class GetChartPreviewRequest(QueryCacheControl):
|
||||
"""Request for chart preview with cache control."""
|
||||
"""Request for chart preview with cache control.
|
||||
|
||||
When form_data_key is provided, the tool will render a preview using the unsaved
|
||||
chart configuration from cache, allowing you to preview what the user actually sees
|
||||
(not the saved version). This is useful when a user edits a chart in Explore but
|
||||
hasn't saved yet.
|
||||
"""
|
||||
|
||||
identifier: int | str = Field(description="Chart identifier (ID, UUID)")
|
||||
form_data_key: str | None = Field(
|
||||
default=None,
|
||||
description=(
|
||||
"Optional cache key for retrieving unsaved chart state. When a user "
|
||||
"edits a chart in Explore but hasn't saved, the current state is stored "
|
||||
"with this key. If provided, the tool renders a preview using this "
|
||||
"configuration instead of the saved chart configuration."
|
||||
),
|
||||
)
|
||||
format: Literal["url", "ascii", "table", "vega_lite"] = Field(
|
||||
default="url",
|
||||
description=(
|
||||
|
||||
@@ -20,6 +20,7 @@ MCP tool: get_chart_data
|
||||
"""
|
||||
|
||||
import logging
|
||||
import time
|
||||
from typing import Any, Dict, List, TYPE_CHECKING
|
||||
|
||||
from fastmcp import Context
|
||||
@@ -29,6 +30,8 @@ from superset_core.mcp import tool
|
||||
if TYPE_CHECKING:
|
||||
from superset.models.slice import Slice
|
||||
|
||||
from superset.commands.exceptions import CommandException
|
||||
from superset.commands.explore.form_data.parameters import CommandParameters
|
||||
from superset.extensions import event_logger
|
||||
from superset.mcp_service.chart.schemas import (
|
||||
ChartData,
|
||||
@@ -43,6 +46,21 @@ from superset.mcp_service.utils.schema_utils import parse_request
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _get_cached_form_data(form_data_key: str) -> str | None:
|
||||
"""Retrieve form_data from cache using form_data_key.
|
||||
|
||||
Returns the JSON string of form_data if found, None otherwise.
|
||||
"""
|
||||
from superset.commands.explore.form_data.get import GetFormDataCommand
|
||||
|
||||
try:
|
||||
cmd_params = CommandParameters(key=form_data_key)
|
||||
return GetFormDataCommand(cmd_params).run()
|
||||
except (KeyError, ValueError, CommandException) as e:
|
||||
logger.warning("Failed to retrieve form_data from cache: %s", e)
|
||||
return None
|
||||
|
||||
|
||||
@tool(tags=["data"])
|
||||
@parse_request(GetChartDataRequest)
|
||||
async def get_chart_data( # noqa: C901
|
||||
@@ -57,15 +75,22 @@ async def get_chart_data( # noqa: C901
|
||||
- Multiple formats: json, csv, excel
|
||||
- Cache control: use_cache, force_refresh, cache_timeout
|
||||
- Optional row limit override (respects chart's configured limits)
|
||||
- form_data_key: retrieves data using unsaved chart configuration from Explore
|
||||
|
||||
When form_data_key is provided, the tool uses the cached (unsaved) chart
|
||||
configuration to query data, allowing you to get data for what the user
|
||||
actually sees in the Explore view (not the saved version).
|
||||
|
||||
Returns underlying data in requested format with cache status.
|
||||
"""
|
||||
await ctx.info(
|
||||
"Starting chart data retrieval: identifier=%s, format=%s, limit=%s"
|
||||
"Starting chart data retrieval: identifier=%s, format=%s, limit=%s, "
|
||||
"form_data_key=%s"
|
||||
% (
|
||||
request.identifier,
|
||||
request.format,
|
||||
request.limit,
|
||||
request.form_data_key,
|
||||
)
|
||||
)
|
||||
await ctx.debug(
|
||||
@@ -122,20 +147,111 @@ async def get_chart_data( # noqa: C901
|
||||
)
|
||||
logger.info("Getting data for chart %s: %s", chart.id, chart.slice_name)
|
||||
|
||||
import time
|
||||
|
||||
start_time = time.time()
|
||||
|
||||
# Track whether we're using unsaved state
|
||||
using_unsaved_state = False
|
||||
cached_form_data_dict = None
|
||||
|
||||
try:
|
||||
await ctx.report_progress(2, 4, "Preparing data query")
|
||||
from superset.charts.schemas import ChartDataQueryContextSchema
|
||||
from superset.commands.chart.data.get_data_command import ChartDataCommand
|
||||
|
||||
# Check if form_data_key is provided - use cached form_data instead
|
||||
if request.form_data_key:
|
||||
await ctx.info(
|
||||
"Retrieving unsaved chart state from cache: form_data_key=%s"
|
||||
% (request.form_data_key,)
|
||||
)
|
||||
if cached_form_data := _get_cached_form_data(request.form_data_key):
|
||||
try:
|
||||
parsed_form_data = utils_json.loads(cached_form_data)
|
||||
# Only use if it's actually a dict (not null, list, etc.)
|
||||
if isinstance(parsed_form_data, dict):
|
||||
cached_form_data_dict = parsed_form_data
|
||||
using_unsaved_state = True
|
||||
await ctx.info(
|
||||
"Using cached form_data from form_data_key "
|
||||
"for data query"
|
||||
)
|
||||
else:
|
||||
await ctx.warning(
|
||||
"Cached form_data is not a JSON object. "
|
||||
"Falling back to saved chart configuration."
|
||||
)
|
||||
except (TypeError, ValueError) as e:
|
||||
await ctx.warning(
|
||||
"Failed to parse cached form_data: %s. "
|
||||
"Falling back to saved chart configuration." % str(e)
|
||||
)
|
||||
else:
|
||||
await ctx.warning(
|
||||
"form_data_key provided but no cached data found. "
|
||||
"The cache may have expired. Using saved chart configuration."
|
||||
)
|
||||
|
||||
# Use the chart's saved query_context - this is the key!
|
||||
# The query_context contains all the information needed to reproduce
|
||||
# the chart's data exactly as shown in the visualization
|
||||
query_context_json = None
|
||||
if chart.query_context:
|
||||
|
||||
# If using cached form_data, we need to build query_context from it
|
||||
if using_unsaved_state and cached_form_data_dict is not None:
|
||||
# Build query context from cached form_data (unsaved state)
|
||||
from superset.common.query_context_factory import QueryContextFactory
|
||||
|
||||
factory = QueryContextFactory()
|
||||
row_limit = (
|
||||
request.limit
|
||||
or cached_form_data_dict.get("row_limit")
|
||||
or current_app.config["ROW_LIMIT"]
|
||||
)
|
||||
|
||||
# Get datasource info from cached form_data or fall back to chart
|
||||
datasource_id = cached_form_data_dict.get(
|
||||
"datasource_id", chart.datasource_id
|
||||
)
|
||||
datasource_type = cached_form_data_dict.get(
|
||||
"datasource_type", chart.datasource_type
|
||||
)
|
||||
|
||||
# Handle different chart types that have different form_data
|
||||
# structures. Some charts use "metric" (singular), not "metrics"
|
||||
# (plural): big_number, big_number_total, pop_kpi.
|
||||
# These charts also don't have groupby columns.
|
||||
cached_viz_type = cached_form_data_dict.get(
|
||||
"viz_type", chart.viz_type or ""
|
||||
)
|
||||
if cached_viz_type in ("big_number", "big_number_total", "pop_kpi"):
|
||||
metric = cached_form_data_dict.get("metric")
|
||||
cached_metrics = [metric] if metric else []
|
||||
cached_groupby: list[str] = []
|
||||
else:
|
||||
cached_metrics = cached_form_data_dict.get("metrics", [])
|
||||
cached_groupby = cached_form_data_dict.get("groupby", [])
|
||||
|
||||
query_context = factory.create(
|
||||
datasource={
|
||||
"id": datasource_id,
|
||||
"type": datasource_type,
|
||||
},
|
||||
queries=[
|
||||
{
|
||||
"filters": cached_form_data_dict.get("filters", []),
|
||||
"columns": cached_groupby,
|
||||
"metrics": cached_metrics,
|
||||
"row_limit": row_limit,
|
||||
"order_desc": cached_form_data_dict.get("order_desc", True),
|
||||
}
|
||||
],
|
||||
form_data=cached_form_data_dict,
|
||||
force=request.force_refresh,
|
||||
)
|
||||
await ctx.debug(
|
||||
"Built query_context from cached form_data (unsaved state)"
|
||||
)
|
||||
elif chart.query_context:
|
||||
try:
|
||||
query_context_json = utils_json.loads(chart.query_context)
|
||||
await ctx.debug(
|
||||
@@ -146,7 +262,7 @@ async def get_chart_data( # noqa: C901
|
||||
"Failed to parse chart query_context: %s" % str(e)
|
||||
)
|
||||
|
||||
if query_context_json is None:
|
||||
if query_context_json is None and not using_unsaved_state:
|
||||
# Fallback: Chart has no saved query_context
|
||||
# This can happen with older charts that haven't been re-saved
|
||||
await ctx.warning(
|
||||
@@ -300,7 +416,7 @@ async def get_chart_data( # noqa: C901
|
||||
form_data=form_data,
|
||||
force=request.force_refresh,
|
||||
)
|
||||
else:
|
||||
elif query_context_json is not None:
|
||||
# Apply request overrides to the saved query_context
|
||||
query_context_json["force"] = request.force_refresh
|
||||
|
||||
|
||||
@@ -24,6 +24,8 @@ import logging
|
||||
from fastmcp import Context
|
||||
from superset_core.mcp import tool
|
||||
|
||||
from superset.commands.exceptions import CommandException
|
||||
from superset.commands.explore.form_data.parameters import CommandParameters
|
||||
from superset.extensions import event_logger
|
||||
from superset.mcp_service.chart.schemas import (
|
||||
ChartError,
|
||||
@@ -37,6 +39,21 @@ from superset.mcp_service.utils.schema_utils import parse_request
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _get_cached_form_data(form_data_key: str) -> str | None:
|
||||
"""Retrieve form_data from cache using form_data_key.
|
||||
|
||||
Returns the JSON string of form_data if found, None otherwise.
|
||||
"""
|
||||
from superset.commands.explore.form_data.get import GetFormDataCommand
|
||||
|
||||
try:
|
||||
cmd_params = CommandParameters(key=form_data_key)
|
||||
return GetFormDataCommand(cmd_params).run()
|
||||
except (KeyError, ValueError, CommandException) as e:
|
||||
logger.warning("Failed to retrieve form_data from cache: %s", e)
|
||||
return None
|
||||
|
||||
|
||||
@tool(tags=["discovery"])
|
||||
@parse_request(GetChartInfoRequest)
|
||||
async def get_chart_info(
|
||||
@@ -48,6 +65,8 @@ async def get_chart_info(
|
||||
- URL field links to the chart's explore page in Superset
|
||||
- Use numeric ID or UUID string (NOT chart name)
|
||||
- To find a chart ID, use the list_charts tool first
|
||||
- When form_data_key is provided, returns the unsaved chart configuration
|
||||
(what the user sees in Explore) instead of the saved version
|
||||
|
||||
Example usage:
|
||||
```json
|
||||
@@ -63,12 +82,22 @@ async def get_chart_info(
|
||||
}
|
||||
```
|
||||
|
||||
With unsaved state (form_data_key from Explore URL):
|
||||
```json
|
||||
{
|
||||
"identifier": 123,
|
||||
"form_data_key": "abc123def456"
|
||||
}
|
||||
```
|
||||
|
||||
Returns chart details including name, type, and URL.
|
||||
"""
|
||||
from superset.daos.chart import ChartDAO
|
||||
from superset.utils import json as utils_json
|
||||
|
||||
await ctx.info(
|
||||
"Retrieving chart information: identifier=%s" % (request.identifier,)
|
||||
"Retrieving chart information: identifier=%s, form_data_key=%s"
|
||||
% (request.identifier, request.form_data_key)
|
||||
)
|
||||
|
||||
with event_logger.log_context(action="mcp.get_chart_info.lookup"):
|
||||
@@ -84,9 +113,41 @@ async def get_chart_info(
|
||||
result = tool.run_tool(request.identifier)
|
||||
|
||||
if isinstance(result, ChartInfo):
|
||||
# If form_data_key is provided, override form_data with cached version
|
||||
if request.form_data_key:
|
||||
await ctx.info(
|
||||
"Retrieving unsaved chart state from cache: form_data_key=%s"
|
||||
% (request.form_data_key,)
|
||||
)
|
||||
cached_form_data = _get_cached_form_data(request.form_data_key)
|
||||
|
||||
if cached_form_data:
|
||||
try:
|
||||
result.form_data = utils_json.loads(cached_form_data)
|
||||
result.form_data_key = request.form_data_key
|
||||
result.is_unsaved_state = True
|
||||
|
||||
# Update viz_type from cached form_data if present
|
||||
if result.form_data and "viz_type" in result.form_data:
|
||||
result.viz_type = result.form_data["viz_type"]
|
||||
|
||||
await ctx.info(
|
||||
"Chart form_data overridden with unsaved state from cache"
|
||||
)
|
||||
except (TypeError, ValueError) as e:
|
||||
await ctx.warning(
|
||||
"Failed to parse cached form_data: %s. "
|
||||
"Using saved chart configuration." % (str(e),)
|
||||
)
|
||||
else:
|
||||
await ctx.warning(
|
||||
"form_data_key provided but no cached data found. "
|
||||
"The cache may have expired. Using saved chart configuration."
|
||||
)
|
||||
|
||||
await ctx.info(
|
||||
"Chart information retrieved successfully: chart_name=%s"
|
||||
% (result.slice_name,)
|
||||
"Chart information retrieved successfully: chart_name=%s, "
|
||||
"is_unsaved_state=%s" % (result.slice_name, result.is_unsaved_state)
|
||||
)
|
||||
else:
|
||||
await ctx.warning("Chart retrieval failed: error=%s" % (str(result),))
|
||||
|
||||
@@ -270,7 +270,13 @@ class ListDashboardsRequest(MetadataCacheControl):
|
||||
|
||||
|
||||
class GetDashboardInfoRequest(MetadataCacheControl):
|
||||
"""Request schema for get_dashboard_info with support for ID, UUID, or slug."""
|
||||
"""Request schema for get_dashboard_info with support for ID, UUID, or slug.
|
||||
|
||||
When permalink_key is provided, the tool will retrieve the dashboard's filter
|
||||
state from the permalink, allowing you to see what filters the user has applied
|
||||
(not just the default filter state). This is useful when a user applies filters
|
||||
in a dashboard but the URL contains a permalink_key.
|
||||
"""
|
||||
|
||||
identifier: Annotated[
|
||||
int | str,
|
||||
@@ -278,6 +284,15 @@ class GetDashboardInfoRequest(MetadataCacheControl):
|
||||
description="Dashboard identifier - can be numeric ID, UUID string, or slug"
|
||||
),
|
||||
]
|
||||
permalink_key: str | None = Field(
|
||||
default=None,
|
||||
description=(
|
||||
"Optional permalink key for retrieving dashboard filter state. When a "
|
||||
"user applies filters in a dashboard, the state can be persisted in a "
|
||||
"permalink. If provided, the tool returns the filter configuration "
|
||||
"from that permalink."
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class DashboardInfo(BaseModel):
|
||||
@@ -320,6 +335,32 @@ class DashboardInfo(BaseModel):
|
||||
charts: List[ChartInfo] = Field(
|
||||
default_factory=list, description="Dashboard charts"
|
||||
)
|
||||
|
||||
# Fields for permalink/filter state support
|
||||
permalink_key: str | None = Field(
|
||||
None,
|
||||
description=(
|
||||
"Permalink key used to retrieve filter state. When present, indicates "
|
||||
"the filter_state came from a permalink rather than the default dashboard."
|
||||
),
|
||||
)
|
||||
filter_state: Dict[str, Any] | None = Field(
|
||||
None,
|
||||
description=(
|
||||
"Filter state from permalink. Contains dataMask (native filter values), "
|
||||
"activeTabs, anchor, and urlParams. When present, represents the actual "
|
||||
"filters the user has applied to the dashboard."
|
||||
),
|
||||
)
|
||||
is_permalink_state: bool = Field(
|
||||
default=False,
|
||||
description=(
|
||||
"True if the filter_state came from a permalink rather than the default "
|
||||
"dashboard configuration. When true, the filter_state reflects what the "
|
||||
"user sees in the dashboard, not the default filter state."
|
||||
),
|
||||
)
|
||||
|
||||
model_config = ConfigDict(from_attributes=True, ser_json_timedelta="iso8601")
|
||||
|
||||
@model_serializer(mode="wrap", when_used="json")
|
||||
|
||||
@@ -28,6 +28,8 @@ from datetime import datetime, timezone
|
||||
from fastmcp import Context
|
||||
from superset_core.mcp import tool
|
||||
|
||||
from superset.dashboards.permalink.exceptions import DashboardPermalinkGetFailedError
|
||||
from superset.dashboards.permalink.types import DashboardPermalinkValue
|
||||
from superset.extensions import event_logger
|
||||
from superset.mcp_service.dashboard.schemas import (
|
||||
dashboard_serializer,
|
||||
@@ -41,6 +43,21 @@ from superset.mcp_service.utils.schema_utils import parse_request
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _get_permalink_state(permalink_key: str) -> DashboardPermalinkValue | None:
|
||||
"""Retrieve dashboard filter state from permalink.
|
||||
|
||||
Returns the permalink value containing dashboardId and state if found,
|
||||
None otherwise.
|
||||
"""
|
||||
from superset.commands.dashboard.permalink.get import GetDashboardPermalinkCommand
|
||||
|
||||
try:
|
||||
return GetDashboardPermalinkCommand(permalink_key).run()
|
||||
except DashboardPermalinkGetFailedError as e:
|
||||
logger.warning("Failed to retrieve permalink state: %s", e)
|
||||
return None
|
||||
|
||||
|
||||
@tool(tags=["discovery"])
|
||||
@parse_request(GetDashboardInfoRequest)
|
||||
async def get_dashboard_info(
|
||||
@@ -50,8 +67,30 @@ async def get_dashboard_info(
|
||||
Get dashboard metadata by ID, UUID, or slug.
|
||||
|
||||
Returns title, charts, and layout details.
|
||||
|
||||
When permalink_key is provided, also returns the filter state from that
|
||||
permalink, allowing you to see what filters the user has applied to the
|
||||
dashboard (not just the default filter state).
|
||||
|
||||
Example usage:
|
||||
```json
|
||||
{
|
||||
"identifier": 123
|
||||
}
|
||||
```
|
||||
|
||||
With permalink (filter state from URL):
|
||||
```json
|
||||
{
|
||||
"identifier": 123,
|
||||
"permalink_key": "abc123def456"
|
||||
}
|
||||
```
|
||||
"""
|
||||
await ctx.info("Retrieving dashboard information: %s" % (request.identifier,))
|
||||
await ctx.info(
|
||||
"Retrieving dashboard information: identifier=%s, permalink_key=%s"
|
||||
% (request.identifier, request.permalink_key)
|
||||
)
|
||||
await ctx.debug(
|
||||
"Metadata cache settings: use_cache=%s, refresh_metadata=%s, force_refresh=%s"
|
||||
% (request.use_cache, request.refresh_metadata, request.force_refresh)
|
||||
@@ -73,14 +112,70 @@ async def get_dashboard_info(
|
||||
result = tool.run_tool(request.identifier)
|
||||
|
||||
if isinstance(result, DashboardInfo):
|
||||
# If permalink_key is provided, retrieve filter state
|
||||
if request.permalink_key:
|
||||
await ctx.info(
|
||||
"Retrieving filter state from permalink: permalink_key=%s"
|
||||
% (request.permalink_key,)
|
||||
)
|
||||
permalink_value = _get_permalink_state(request.permalink_key)
|
||||
|
||||
if permalink_value:
|
||||
# Verify the permalink belongs to the requested dashboard
|
||||
# dashboardId in permalink is stored as str, result.id is int
|
||||
permalink_dashboard_id = permalink_value.get("dashboardId")
|
||||
try:
|
||||
permalink_dashboard_id_int = (
|
||||
int(permalink_dashboard_id)
|
||||
if permalink_dashboard_id
|
||||
else None
|
||||
)
|
||||
except (ValueError, TypeError):
|
||||
permalink_dashboard_id_int = None
|
||||
|
||||
if (
|
||||
permalink_dashboard_id_int is not None
|
||||
and permalink_dashboard_id_int != result.id
|
||||
):
|
||||
await ctx.warning(
|
||||
"permalink_key dashboardId (%s) does not match "
|
||||
"requested dashboard id (%s); ignoring permalink "
|
||||
"filter state." % (permalink_dashboard_id, result.id)
|
||||
)
|
||||
else:
|
||||
# Extract the state from permalink value
|
||||
# Handle None or non-dict state gracefully
|
||||
raw_state = permalink_value.get("state")
|
||||
permalink_state = (
|
||||
dict(raw_state) if isinstance(raw_state, dict) else {}
|
||||
)
|
||||
result.permalink_key = request.permalink_key
|
||||
result.filter_state = permalink_state
|
||||
result.is_permalink_state = True
|
||||
|
||||
await ctx.info(
|
||||
"Filter state retrieved from permalink: "
|
||||
"has_dataMask=%s, has_activeTabs=%s"
|
||||
% (
|
||||
"dataMask" in permalink_state,
|
||||
"activeTabs" in permalink_state,
|
||||
)
|
||||
)
|
||||
else:
|
||||
await ctx.warning(
|
||||
"permalink_key provided but no permalink found. "
|
||||
"The permalink may have expired or is invalid."
|
||||
)
|
||||
|
||||
await ctx.info(
|
||||
"Dashboard information retrieved successfully: id=%s, title=%s, "
|
||||
"chart_count=%s, published=%s"
|
||||
"chart_count=%s, published=%s, is_permalink_state=%s"
|
||||
% (
|
||||
result.id,
|
||||
result.dashboard_title,
|
||||
result.chart_count,
|
||||
result.published,
|
||||
result.is_permalink_state,
|
||||
)
|
||||
)
|
||||
else:
|
||||
|
||||
Reference in New Issue
Block a user