diff --git a/superset-frontend/src/features/databases/DatabaseModal/index.tsx b/superset-frontend/src/features/databases/DatabaseModal/index.tsx index 0919941938a..1fa38c5e152 100644 --- a/superset-frontend/src/features/databases/DatabaseModal/index.tsx +++ b/superset-frontend/src/features/databases/DatabaseModal/index.tsx @@ -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 = ({ 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')} diff --git a/superset-frontend/src/features/home/EmptyState.tsx b/superset-frontend/src/features/home/EmptyState.tsx index 119fd6596ce..436f8181f5b 100644 --- a/superset-frontend/src/features/home/EmptyState.tsx +++ b/superset-frontend/src/features/home/EmptyState.tsx @@ -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', diff --git a/superset-frontend/src/features/home/RightMenu.tsx b/superset-frontend/src/features/home/RightMenu.tsx index 28c25498642..6462c1c588b 100644 --- a/superset-frontend/src/features/home/RightMenu.tsx +++ b/superset-frontend/src/features/home/RightMenu.tsx @@ -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 fallback applies ensureAppRoot + // exactly once for non-frontend routes. + url: '/sqllab?new=true', icon: , perm: 'can_sqllab', view: 'Superset', diff --git a/superset-frontend/src/pages/SavedQueryList/SavedQueryList.test.tsx b/superset-frontend/src/pages/SavedQueryList/SavedQueryList.test.tsx index 12cf7e6aba7..9fcb399e7e2 100644 --- a/superset-frontend/src/pages/SavedQueryList/SavedQueryList.test.tsx +++ b/superset-frontend/src/pages/SavedQueryList/SavedQueryList.test.tsx @@ -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 ( +
{`${location.pathname}${location.search}`}
+ ); +} + // Increase default timeout jest.setTimeout(30000); @@ -88,6 +97,7 @@ const renderList = (props = {}, storeOverrides = {}) => + , { @@ -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 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(); + } + }); }); diff --git a/superset-frontend/src/pages/SavedQueryList/index.tsx b/superset-frontend/src/pages/SavedQueryList/index.tsx index 65797e69b96..1010585eb37 100644 --- a/superset-frontend/src/pages/SavedQueryList/index.tsx +++ b/superset-frontend/src/pages/SavedQueryList/index.tsx @@ -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) => ( - {label} - ), + }: any) => {label}, id: 'label', }, {