diff --git a/superset-frontend/package-lock.json b/superset-frontend/package-lock.json index ac8ebeb1f3fe..8c21954efbeb 100644 --- a/superset-frontend/package-lock.json +++ b/superset-frontend/package-lock.json @@ -6781,6 +6781,16 @@ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, + "node_modules/@jest/source-map/node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/@jest/test-result": { "version": "30.0.2", "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-30.0.2.tgz", @@ -20795,15 +20805,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/camel-case": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-4.1.2.tgz", @@ -44945,6 +44946,15 @@ "node": ">=6" } }, + "node_modules/parent-module/node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/parse-conflict-json": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/parse-conflict-json/-/parse-conflict-json-3.0.1.tgz", diff --git a/superset-frontend/src/SqlLab/components/ResultSet/index.tsx b/superset-frontend/src/SqlLab/components/ResultSet/index.tsx index 643e4a712c7e..a4fed2ccbdc7 100644 --- a/superset-frontend/src/SqlLab/components/ResultSet/index.tsx +++ b/superset-frontend/src/SqlLab/components/ResultSet/index.tsx @@ -64,8 +64,7 @@ import { SaveDatasetModal, } from 'src/SqlLab/components/SaveDatasetModal'; import { EXPLORE_CHART_DEFAULT, SqlLabRootState } from 'src/SqlLab/types'; -import { mountExploreUrl } from 'src/explore/exploreUtils'; -import { postFormData } from 'src/explore/exploreUtils/formData'; +import { generateExploreUrl } from 'src/explore/exploreUtils/formData'; import ProgressBar from '@superset-ui/core/components/ProgressBar'; import { addDangerToast } from 'src/components/MessageToasts/actions'; import { prepareCopyToClipboardTabularData } from 'src/utils/common'; @@ -78,7 +77,6 @@ import { reFetchQueryResults, reRunQuery, } from 'src/SqlLab/actions/sqlLab'; -import { URL_PARAMS } from 'src/constants'; import useLogAction from 'src/logger/useLogAction'; import { LOG_ACTIONS_SQLLAB_COPY_RESULT_TO_CLIPBOARD, @@ -277,16 +275,13 @@ const ResultSet = ({ const openInNewWindow = clickEvent.metaKey; logAction(LOG_ACTIONS_SQLLAB_CREATE_CHART, {}); if (results?.query_id) { - const key = await postFormData(results.query_id, 'query', { + const url = await generateExploreUrl(results.query_id, 'query', { ...EXPLORE_CHART_DEFAULT, datasource: `${results.query_id}__query`, ...{ all_columns: results.columns.map(column => column.column_name), }, }); - const url = mountExploreUrl(null, { - [URL_PARAMS.formDataKey.name]: key, - }); if (openInNewWindow) { window.open(url, '_blank', 'noreferrer'); } else { diff --git a/superset-frontend/src/components/Chart/DrillBy/DrillByModal.tsx b/superset-frontend/src/components/Chart/DrillBy/DrillByModal.tsx index f118d34c7de1..7491d9f2a7b0 100644 --- a/superset-frontend/src/components/Chart/DrillBy/DrillByModal.tsx +++ b/superset-frontend/src/components/Chart/DrillBy/DrillByModal.tsx @@ -43,7 +43,7 @@ import { } from '@superset-ui/core/components'; import { RootState } from 'src/dashboard/types'; import { DashboardPageIdContext } from 'src/dashboard/containers/DashboardPage'; -import { postFormData } from 'src/explore/exploreUtils/formData'; +import { generateExploreUrl } from 'src/explore/exploreUtils/formData'; import { simpleFilterToAdhoc } from 'src/utils/simpleFilterToAdhoc'; import { useDatasetMetadataBar } from 'src/features/datasets/metadataBar/useDatasetMetadataBar'; import { useToasts } from 'src/components/MessageToasts/withToasts'; @@ -96,11 +96,12 @@ const ModalFooter = ({ formData, closeModal }: ModalFooterProps) => { useEffect(() => { // short circuit if the user is embedded as explore is not available if (isEmbedded()) return; - postFormData(Number(datasource_id), datasource_type, formData, 0) - .then(key => { - setUrl( - `/explore/?form_data_key=${key}&dashboard_page_id=${dashboardPageId}`, - ); + generateExploreUrl(Number(datasource_id), datasource_type, formData, { + chartId: 0, + dashboardPageId, + }) + .then(url => { + setUrl(url); }) .catch(() => { addDangerToast(t('Failed to generate chart edit URL')); diff --git a/superset-frontend/src/components/Chart/DrillDetail/DrillDetailModal.test.tsx b/superset-frontend/src/components/Chart/DrillDetail/DrillDetailModal.test.tsx index b63c7c2c866b..a31eb1e9c028 100644 --- a/superset-frontend/src/components/Chart/DrillDetail/DrillDetailModal.test.tsx +++ b/superset-frontend/src/components/Chart/DrillDetail/DrillDetailModal.test.tsx @@ -32,6 +32,14 @@ jest.mock('react-router-dom', () => ({ }), })); +jest.mock('src/explore/exploreUtils', () => ({ + ...jest.requireActual('src/explore/exploreUtils'), + getExploreUrl: jest.fn( + ({ formData }) => + `/explore/?dashboard_page_id=&slice_id=${formData.slice_id}`, + ), +})); + const { id: chartId, form_data: formData } = chartQueries[sliceId]; const { slice_name: chartName } = formData; const store = getMockStoreWithNativeFilters(); @@ -43,7 +51,10 @@ const drillToDetailModalState = { }, }; -const renderModal = async (overrideState: Record = {}) => { +const renderModal = async ( + overrideState: Record = {}, + dataset?: any, +) => { const DrillDetailModalWrapper = () => { const [showModal, setShowModal] = useState(false); return ( @@ -57,6 +68,7 @@ const renderModal = async (overrideState: Record = {}) => { initialFilters={[]} showModal={showModal} onHideModal={() => setShowModal(false)} + dataset={dataset} /> ); @@ -80,11 +92,21 @@ test('should render the title', async () => { expect(screen.getByText(`Drill to detail: ${chartName}`)).toBeInTheDocument(); }); -test('should render the button', async () => { +test('should not render Explore button when no drill-through chart is configured', async () => { await renderModal(); expect( - screen.getByRole('button', { name: 'Edit chart' }), - ).toBeInTheDocument(); + screen.queryByRole('button', { name: 'Explore' }), + ).not.toBeInTheDocument(); + expect(screen.getAllByRole('button', { name: 'Close' })).toHaveLength(2); +}); + +test('should render Explore button when drill-through chart is configured', async () => { + const datasetWithDrillThrough = { + drill_through_chart_id: 123, + id: 456, // Required for URL generation + }; + await renderModal({}, datasetWithDrillThrough); + expect(screen.getByRole('button', { name: 'Explore' })).toBeInTheDocument(); expect(screen.getAllByRole('button', { name: 'Close' })).toHaveLength(2); }); @@ -95,20 +117,19 @@ test('should close the modal', async () => { expect(screen.queryByRole('dialog')).not.toBeInTheDocument(); }); -test('should forward to Explore', async () => { - await renderModal(); - userEvent.click(screen.getByRole('button', { name: 'Edit chart' })); - expect(mockHistoryPush).toHaveBeenCalledWith( - `/explore/?dashboard_page_id=&slice_id=${sliceId}`, - ); -}); - -test('should render "Edit chart" as disabled without can_explore permission', async () => { - await renderModal({ - user: { - ...drillToDetailModalState.user, - roles: { Admin: [['invalid_permission', 'Superset']] }, +test('should render "Explore" as disabled without can_explore permission', async () => { + const datasetWithDrillThrough = { + drill_through_chart_id: 123, + id: 456, // Required for URL generation + }; + await renderModal( + { + user: { + ...drillToDetailModalState.user, + roles: { Admin: [['invalid_permission', 'Superset']] }, + }, }, - }); - expect(screen.getByRole('button', { name: 'Edit chart' })).toBeDisabled(); + datasetWithDrillThrough, + ); + expect(screen.getByRole('button', { name: 'Explore' })).toBeDisabled(); }); diff --git a/superset-frontend/src/components/Chart/DrillDetail/DrillDetailModal.tsx b/superset-frontend/src/components/Chart/DrillDetail/DrillDetailModal.tsx index 6f627fc6c6ca..cffb8a55d981 100644 --- a/superset-frontend/src/components/Chart/DrillDetail/DrillDetailModal.tsx +++ b/superset-frontend/src/components/Chart/DrillDetail/DrillDetailModal.tsx @@ -17,8 +17,7 @@ * under the License. */ -import { useCallback, useContext, useMemo } from 'react'; -import { useHistory } from 'react-router-dom'; +import { useContext, useMemo, useState } from 'react'; import { BinaryQueryObjectFilterClause, css, @@ -33,37 +32,46 @@ import { isEmbedded } from 'src/dashboard/util/isEmbedded'; import { Slice } from 'src/types/Chart'; import { RootState } from 'src/dashboard/types'; import { findPermission } from 'src/utils/findPermission'; +import { useToasts } from 'src/components/MessageToasts/withToasts'; +import { getFormDataWithDashboardContext } from 'src/explore/controlUtils/getFormDataWithDashboardContext'; +import { useDashboardFormData } from 'src/dashboard/hooks/useDashboardFormData'; +import { generateExploreUrl } from 'src/explore/exploreUtils/formData'; import { Dataset } from '../types'; import DrillDetailPane from './DrillDetailPane'; interface ModalFooterProps { canExplore: boolean; closeModal?: () => void; - exploreChart: () => void; + showEditButton: boolean; + onExploreClick?: (event: React.MouseEvent) => void; + isGeneratingUrl: boolean; } const ModalFooter = ({ canExplore, closeModal, - exploreChart, + showEditButton, + onExploreClick, + isGeneratingUrl, }: ModalFooterProps) => { const theme = useTheme(); return ( <> - {!isEmbedded() && ( + {!isEmbedded() && showEditButton && ( )}