Compare commits

...

1 Commits

Author SHA1 Message Date
Enzo Martellucci
6d049b316e fix(reports): preserve dashboard_state urlParams in multi-tab report fan-out
When ALERT_REPORT_TABS is enabled and a Report's extra_json carries a
JSON-list anchor (multi-tab fan-out), `_get_tabs_urls` was building each
per-tab permalink with `urlParams=[["native_filters", ...]]` only —
discarding any other params from `dashboard_state.urlParams`,
including `standalone=true` which the single-tab branch explicitly
preserves (see `get_dashboard_urls` lines 290–299). It also wiped
`dataMask` and `activeTabs` to None instead of the per-tab anchor.

Align the multi-tab path with the single-tab path's merge semantics:
spread the original `dashboard_state` into each per-tab permalink, then
override only the per-tab fields (`anchor`, `activeTabs`) and merge
`native_filters` into `urlParams`.

Adds a unit test (`test_get_dashboard_urls_multitab_preserves_url_params`)
that locks in: existing urlParams survive, native_filters is appended,
each per-tab state targets exactly its anchor, and dataMask is preserved.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 18:01:56 +02:00
2 changed files with 86 additions and 6 deletions

View File

@@ -280,6 +280,7 @@ class BaseReportState:
)
urls = self._get_tabs_urls(
anchor_list,
dashboard_state=dashboard_state,
native_filter_params=native_filter_params,
user_friendly=user_friendly,
)
@@ -356,21 +357,33 @@ class BaseReportState:
def _get_tabs_urls(
self,
tab_anchors: list[str],
dashboard_state: Optional[DashboardPermalinkState] = None,
native_filter_params: Optional[str] = None,
user_friendly: bool = False,
) -> list[str]:
"""
Get multple tabs urls
Get multiple tabs urls.
Each per-tab permalink preserves the original ``dashboard_state``
``urlParams`` (e.g. ``standalone=true``) and merges in the report's
``native_filters`` with the same precedence rules as the single-tab
branch. Per-tab fields (``anchor``, ``activeTabs``) are overridden
for each tab in the fan-out.
"""
base_state: DashboardPermalinkState = dashboard_state or {}
existing_params: list[tuple[str, str]] = base_state.get("urlParams") or []
merged_params: list[list[str]] = [
list(p) for p in existing_params if p[0] != "native_filters"
]
merged_params.append(["native_filters", native_filter_params or ""])
return [
self._get_tab_url(
{
**base_state,
"anchor": tab_anchor,
"dataMask": None,
"activeTabs": None,
"urlParams": [
["native_filters", native_filter_params] # type: ignore
],
"activeTabs": [tab_anchor],
"urlParams": merged_params, # type: ignore[typeddict-item]
},
user_friendly=user_friendly,
)

View File

@@ -624,6 +624,73 @@ def test_get_tab_urls(
]
@patch("superset.commands.report.execute.CreateDashboardPermalinkCommand")
@with_feature_flags(ALERT_REPORT_TABS=True)
def test_get_dashboard_urls_multitab_preserves_url_params(
mock_permalink_cls,
mocker: MockerFixture,
app,
) -> None:
"""Multi-tab fan-out must preserve dashboard_state.urlParams (e.g. standalone)
and per-tab anchor/activeTabs, the same way the single-tab branch does."""
mock_report_schedule: ReportSchedule = mocker.Mock(spec=ReportSchedule)
mock_report_schedule.chart = False
mock_report_schedule.chart_id = None
mock_report_schedule.dashboard_id = 123
mock_report_schedule.type = "report_type"
mock_report_schedule.report_format = "report_format"
mock_report_schedule.owners = [1, 2]
mock_report_schedule.recipients = []
native_filter_rison = "(NATIVE_FILTER-1:(filterType:filter_select))"
mock_report_schedule.extra = {
"dashboard": {
"anchor": json.dumps(["TAB-1", "TAB-2"]),
"dataMask": {"NATIVE_FILTER-1": {"filterState": {"value": ["Sales"]}}},
"activeTabs": ["irrelevant"],
"urlParams": [("standalone", "true"), ("show_filters", "0")],
"nativeFilters": [ # type: ignore[typeddict-unknown-key]
{
"nativeFilterId": "NATIVE_FILTER-1",
"filterType": "filter_select",
"columnName": "department",
"filterValues": ["Sales"],
}
],
}
}
mock_report_schedule.get_native_filters_params.return_value = ( # type: ignore[attr-defined]
native_filter_rison,
[],
)
mock_permalink_cls.return_value.run.side_effect = ["key1", "key2"]
class_instance: BaseReportState = BaseReportState(
mock_report_schedule, "January 1, 2021", "execution_id_example"
)
class_instance._report_schedule = mock_report_schedule
class_instance.get_dashboard_urls()
assert mock_permalink_cls.call_count == 2
for idx, expected_anchor in enumerate(["TAB-1", "TAB-2"]):
state = mock_permalink_cls.call_args_list[idx].kwargs["state"]
# urlParams from the original dashboard_state are preserved and the
# report's native_filters is appended.
assert state["urlParams"] == [
["standalone", "true"],
["show_filters", "0"],
["native_filters", native_filter_rison],
]
# Each per-tab permalink targets exactly that tab.
assert state["anchor"] == expected_anchor
assert state["activeTabs"] == [expected_anchor]
# dataMask from the user-supplied dashboard_state is preserved
# (the previous implementation cleared it to None).
assert state["dataMask"] == {
"NATIVE_FILTER-1": {"filterState": {"value": ["Sales"]}}
}
@patch(
"superset.commands.dashboard.permalink.create.CreateDashboardPermalinkCommand.run"
)