Compare commits

...

1 Commits

Author SHA1 Message Date
Maxime Beauchemin
1b570c1d9a fix(explore): skip re-fetch when navigating away from /explore (sc-104553)
The history.listen in Chart/index.tsx was firing loadExploreData for every
PUSH/POP, including navigations that leave the /explore route (e.g. "Save &
go to dashboard" pushes to /superset/dashboard/:id/). That kicks off an
/api/v1/explore/ request with the destination's URL params while the Explore
page is unmounting, either racing the unmount abort or surfacing a spurious
"Failed to load chart data" toast on the destination dashboard.

Guard the listener so it only re-fetches for navigations that stay inside
the routes where ChartPage is mounted (/explore/* and /superset/explore/p).
Post-save REPLACE with a saveAction is still handled.

Also adds a regression test that navigates from /explore to
/superset/dashboard/5/ and asserts no extra /api/v1/explore/ call is made.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-21 05:11:57 +00:00
2 changed files with 56 additions and 5 deletions

View File

@@ -256,13 +256,13 @@ describe('ChartPage', () => {
window.history.pushState(
{},
'',
`/?${URL_PARAMS.dashboardPageId.name}=${dashboardPageId}`,
`/explore/?${URL_PARAMS.dashboardPageId.name}=${dashboardPageId}`,
);
const { getByTestId } = render(
<>
<Link
to={{
pathname: '/',
pathname: '/explore/',
search: `?${URL_PARAMS.dashboardPageId.name}=${dashboardPageId}`,
state: { saveAction: 'overwrite' },
}}
@@ -319,7 +319,7 @@ describe('ChartPage', () => {
});
render(
<>
<Link to="/?slice_id=99">Navigate away</Link>
<Link to="/explore/?slice_id=99">Navigate away</Link>
<ChartPage />
</>,
{
@@ -358,6 +358,44 @@ describe('ChartPage', () => {
JSON.stringify({ show_cell_bars: true }).slice(1, -1),
);
});
test('does not re-fetch explore data when navigating off the /explore route (e.g. "Save & go to dashboard")', async () => {
// Regression test for sc-104553: "Save & go to dashboard" calls
// history.push('/superset/dashboard/:id/'). The history.listen in
// ChartPage previously fired loadExploreData for that PUSH, starting a
// bogus /api/v1/explore/ request with the dashboard URL's params while
// the page was in the middle of unmounting.
const exploreApiRoute = 'glob:*/api/v1/explore/*';
const exploreFormData = getExploreFormData({
viz_type: VizType.Table,
show_cell_bars: true,
});
fetchMock.get(exploreApiRoute, {
result: { dataset: { id: 1 }, form_data: exploreFormData },
});
render(
<>
<Link to="/superset/dashboard/5/">Go to dashboard</Link>
<ChartPage />
</>,
{
useRouter: true,
useRedux: true,
useDnd: true,
},
);
// Initial mount fetches once.
await waitFor(() =>
expect(fetchMock.callHistory.calls(exploreApiRoute).length).toBe(1),
);
fireEvent.click(screen.getByText('Go to dashboard'));
// A PUSH to a non-/explore path must not trigger another explore fetch.
// Give any stray async work a chance to run before asserting.
await new Promise(resolve => setTimeout(resolve, 0));
expect(fetchMock.callHistory.calls(exploreApiRoute).length).toBe(1);
});
});
test('does not show error toast when request is aborted on unmount', async () => {
@@ -409,7 +447,7 @@ describe('ChartPage', () => {
render(
<>
<Link to="/?slice_id=99">Navigate</Link>
<Link to="/explore/?slice_id=99">Navigate</Link>
<ChartPage />
</>,
{

View File

@@ -300,15 +300,28 @@ export default function ExplorePage() {
// PUSH/POP: full reload (unmount + re-fetch).
// REPLACE with saveAction state: re-fetch without unmount (keeps chart visible).
// Other REPLACE: ignored (URL sync from updateHistory).
// Navigations that leave the /explore route (e.g. "Save & go to dashboard"
// pushes /superset/dashboard/:id/) must not trigger a re-fetch here: the
// Explore page is about to unmount, and fetching /api/v1/explore/ with the
// destination's params starts a request that either races the unmount abort
// or surfaces a spurious error toast on the destination page.
useEffect(() => {
const unlisten = history.listen((loc: Location, action: Action) => {
// ChartPage is mounted at /explore/ and /superset/explore/p in
// routes.tsx; only re-fetch for navigations that stay inside those.
const isExploreRoute =
loc.pathname.startsWith('/explore') ||
loc.pathname.startsWith('/superset/explore');
const saveAction = (loc.state as Record<string, unknown>)?.saveAction as
| SaveActionType
| undefined;
if (action === 'PUSH' || action === 'POP') {
if (!isExploreRoute) {
return;
}
setIsLoaded(false);
loadExploreData(loc, saveAction);
} else if (saveAction) {
} else if (saveAction && isExploreRoute) {
loadExploreData(loc, saveAction);
}
});