gabotorresruiz commented on code in PR #43805:
URL: https://github.com/apache/superset/pull/43805#discussion_r4062846314
##########
superset-frontend/src/dashboard/components/menu/DownloadMenuItems/index.tsx:
##########
@@ -167,6 +210,101 @@ export const useDownloadMenuItems = (
}
};
+ const triggerExportDownload = (downloadUrl: string) => {
+ // Stream the file straight to disk via a hidden iframe. The endpoint sends
+ // Content-Disposition: attachment, so the browser saves it without
+ // navigating the dashboard away (fatal inside an embedded iframe) and
+ // without buffering the whole workbook in tab memory the way
fetch().blob()
+ // would. The status endpoint already confirmed the link is ready and
+ // backend-matched, so the only failure left is the narrow race where the
+ // object is removed between that check and this click; such an error
+ // response loads invisibly in the iframe and leaves the page untouched.
+ const iframe = document.createElement('iframe');
+ iframe.style.display = 'none';
+ iframe.src = downloadUrl;
+ document.body.appendChild(iframe);
+ // Remove the iframe after the browser has taken over the download; removal
+ // does not cancel a download already handed off to the browser.
+ setTimeout(() => {
+ if (iframe.parentNode) {
+ iframe.parentNode.removeChild(iframe);
+ }
+ }, EXPORT_STATUS_POLL_INTERVAL_MS);
Review Comment:
Fixed in bbc371d08dcf3f4cfecdad0039a1a7d04e7db8b8. You were right, and the
comment I had left there was wrong: removal only fails to cancel once the
response has committed, and nothing guarantees that has happened by the time
the timer fires. Reusing `EXPORT_STATUS_POLL_INTERVAL_MS` as a download handoff
deadline was the mistake, since time to first byte is unbounded.
Each frame is now held until the component unmounts and cleared in the
existing cleanup rather than on a timer. They are appended to `document.body`,
outside the React tree, so React would otherwise leave them behind.
I rebuilt your setup outside Superset to confirm both halves: an attachment
endpoint that stalls 4.5s before sending headers, and a page starting two
downloads, one whose iframe is removed at 3s and one held. Server side, in
Chromium:
```
old_removed_at_3s : CANCELLED: client closed before headers
new_held : DELIVERED: download reached the browser
```
Only the held one produced a download at all, which matches what you saw.
Added two jest tests for the lifetime contract, that the frame survives well
past the poll interval and that unmount clears it. Both fail without the change.
On the browser test with delayed attachment headers: agreed that is the test
that would actually catch a regression here, since jest cannot model the
cancellation. I have not added a Playwright case in this PR. Happy to do that
as a follow-up if you think it earns the runtime.
##########
superset/tasks/export_dashboard_excel.py:
##########
@@ -299,6 +346,13 @@ def _write_chart_sheets(
json_body["result_type"] = ChartDataResultType.FULL
json_body.pop("force", None)
+ # Guest authorization links a chart to its dashboard through
+ # ``form_data.dashboardId`` (raise_for_access); saved contexts don't carry
+ # it, so stamp it the way the browser does on interactive requests.
+ form_data = dict(json_body.get("form_data") or {})
+ form_data["dashboardId"] = dashboard_id
+ json_body["form_data"] = form_data
Review Comment:
Good find, fixed in c17e2d3026822c276e4414b52c805376b87a1c36, stamped right
next to the `dashboardId` stamp since it is the same class of gap for the same
reason.
I traced the mechanism to be sure of it. `Slice.form_data` does
`form_data.update({"slice_id": self.id, ...})`, so an interactive request
silently corrects a stale id, while the export takes
`_saved_query_context(chart.query_context)` and replays the stored JSON
verbatim. `query_context_modified` then compares `form_data["slice_id"]`
against `query_context.slice_` and treats the mismatch as a tampered payload.
When no `current_slice` is passed, `QueryContextFactory.create` instead
resolves the slice from that same stale id, which fails the dashboard to chart
linkage check. Both routes end with the copy in the skipped charts summary
while it renders normally in the dashboard.
Driving the real `query_context_modified` with the payload the task builds:
```
BEFORE: slice_id=7 -> modified=True (logs "Guest chart payload rejected
for slice 10")
AFTER: slice_id=10 -> modified=False
```
Added the copied chart case you asked for, alongside the `dashboardId`
regression test: `test_query_context_is_stamped_with_the_current_slice_id`. It
fails without the stamp.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]