fix(ui): remove makeUrl() double-prefix bugs under subdirectory deployment (#39503)

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Vitor Avila <96086495+Vitor-Avila@users.noreply.github.com>
This commit is contained in:
Maxime Beauchemin
2026-05-07 11:39:38 -07:00
committed by GitHub
parent 8c80caefa3
commit aa710672ed
5 changed files with 64 additions and 12 deletions

View File

@@ -35,7 +35,6 @@ import { CheckboxChangeEvent } from '@superset-ui/core/components/Checkbox/types
import { useHistory } from 'react-router-dom';
import { setItem, LocalStorageKeys } from 'src/utils/localStorageHelpers';
import { makeUrl } from 'src/utils/pathUtils';
import Tabs from '@superset-ui/core/components/Tabs';
import {
Button,
@@ -1824,7 +1823,9 @@ const DatabaseModal: FunctionComponent<DatabaseModalProps> = ({
onClick={() => {
setLoading(true);
fetchAndSetDB();
redirectURL(makeUrl(`/sqllab?db=true`));
// redirectURL() delegates to history.push; React Router's basename
// already prefixes the application root, so pass a relative path.
redirectURL('/sqllab?db=true');
}}
>
{t('Query data in SQL Lab')}

View File

@@ -24,7 +24,6 @@ import { TableTab } from 'src/views/CRUD/types';
import { t } from '@apache-superset/core/translation';
import { styled } from '@apache-superset/core/theme';
import { navigateTo } from 'src/utils/navigationUtils';
import { makeUrl } from 'src/utils/pathUtils';
import { WelcomeTable } from './types';
const EmptyContainer = styled.div`
@@ -59,7 +58,9 @@ const REDIRECTS = {
create: {
[WelcomeTable.Charts]: '/chart/add',
[WelcomeTable.Dashboards]: '/dashboard/new',
[WelcomeTable.SavedQueries]: makeUrl('/sqllab?new=true'),
// navigateTo() applies the application root internally; keep this
// relative so the prefix isn't added twice.
[WelcomeTable.SavedQueries]: '/sqllab?new=true',
},
viewAll: {
[WelcomeTable.Charts]: '/chart/list',

View File

@@ -44,7 +44,7 @@ import {
TelemetryPixel,
} from '@superset-ui/core/components';
import type { ItemType, MenuItem } from '@superset-ui/core/components/Menu';
import { ensureAppRoot, makeUrl } from 'src/utils/pathUtils';
import { ensureAppRoot } from 'src/utils/pathUtils';
import { isEmbedded } from 'src/dashboard/util/isEmbedded';
import { findPermission } from 'src/utils/findPermission';
import { isUserAdmin } from 'src/dashboard/util/permissionUtils';
@@ -213,7 +213,10 @@ const RightMenu = ({
},
{
label: t('SQL query'),
url: makeUrl('/sqllab?new=true'),
// Keep the URL relative so isFrontendRoute() matches and Link navigates
// via React Router; the <Typography.Link> fallback applies ensureAppRoot
// exactly once for non-frontend routes.
url: '/sqllab?new=true',
icon: <Icons.SearchOutlined data-test={`menu-item-${t('SQL query')}`} />,
perm: 'can_sqllab',
view: 'Superset',

View File

@@ -25,11 +25,20 @@ import {
fireEvent,
waitFor,
} from 'spec/helpers/testing-library';
import { MemoryRouter } from 'react-router-dom';
import { MemoryRouter, useLocation } from 'react-router-dom';
import { QueryParamProvider } from 'use-query-params';
import { ReactRouter5Adapter } from 'use-query-params/adapters/react-router-5';
import * as getBootstrapData from 'src/utils/getBootstrapData';
import SavedQueryList from '.';
// Renders the current router pathname+search so tests can assert navigation.
function LocationDisplay() {
const location = useLocation();
return (
<div data-test="location-display">{`${location.pathname}${location.search}`}</div>
);
}
// Increase default timeout
jest.setTimeout(30000);
@@ -88,6 +97,7 @@ const renderList = (props = {}, storeOverrides = {}) =>
<MemoryRouter>
<QueryParamProvider adapter={ReactRouter5Adapter}>
<SavedQueryList user={mockUser} {...props} />
<LocationDisplay />
</QueryParamProvider>
</MemoryRouter>,
{
@@ -242,4 +252,39 @@ describe('SavedQueryList', () => {
// Verify delete buttons are not shown
expect(screen.queryByTestId('delete-action')).not.toBeInTheDocument();
});
test('"+ Query" button pushes a router-relative path (subdirectory deployment)', async () => {
// Simulate SUPERSET_APP_ROOT=/superset. ensureAppRoot/makeUrl read
// applicationRoot() dynamically, so mocking it here makes the buggy code
// path (makeUrl() around history.push) produce '/superset/sqllab?new=true'
// instead of being a no-op. React Router's <Router basename> prefixes the
// app root on its own, so history.push MUST receive a path without the
// app-root prefix — otherwise navigation lands at /superset/superset/sqllab
// and shows a blank page (sc-103661).
const applicationRootSpy = jest
.spyOn(getBootstrapData, 'applicationRoot')
.mockReturnValue('/superset');
try {
renderList();
await screen.findByTestId('saved_query-list-view');
const queryButton = await screen.findByRole('button', {
name: /query/i,
});
fireEvent.click(queryButton);
await waitFor(() => {
// The MemoryRouter in renderList uses the default ('/') basename, so
// useLocation reflects exactly what history.push received. A correct
// router-relative push produces '/sqllab?new=true'; a buggy push that
// re-applied the app root would produce '/superset/sqllab?new=true'.
const location = screen.getByTestId('location-display').textContent;
expect(location).toBe('/sqllab?new=true');
});
} finally {
applicationRootSpy.mockRestore();
}
});
});

View File

@@ -223,7 +223,9 @@ function SavedQueryList({
name: t('Query'),
buttonStyle: 'primary',
onClick: () => {
history.push(makeUrl('/sqllab?new=true'));
// React Router's basename already includes the application root; passing
// a relative path ensures correct navigation under subdirectory deployments.
history.push('/sqllab?new=true');
},
});
@@ -245,7 +247,9 @@ function SavedQueryList({
if (openInNewWindow) {
window.open(makeUrl(`/sqllab?savedQueryId=${id}`));
} else {
history.push(makeUrl(`/sqllab?savedQueryId=${id}`));
// React Router's basename already includes the application root; passing
// a relative path ensures correct navigation under subdirectory deployments.
history.push(`/sqllab?savedQueryId=${id}`);
}
};
@@ -338,9 +342,7 @@ function SavedQueryList({
row: {
original: { id, label },
},
}: any) => (
<Link to={makeUrl(`/sqllab?savedQueryId=${id}`)}>{label}</Link>
),
}: any) => <Link to={`/sqllab?savedQueryId=${id}`}>{label}</Link>,
id: 'label',
},
{