diff --git a/superset-frontend/src/constants.ts b/superset-frontend/src/constants.ts index e27022fe63ca..4444e8ea1f4a 100644 --- a/superset-frontend/src/constants.ts +++ b/superset-frontend/src/constants.ts @@ -59,6 +59,14 @@ export const URL_PARAMS = { name: 'form_data_key', type: 'string', }, + sliceId: { + name: 'slice_id', + type: 'string', + }, + datasetId: { + name: 'dataset_id', + type: 'string', + }, } as const; /** diff --git a/superset-frontend/src/dashboard/components/gridComponents/Chart.jsx b/superset-frontend/src/dashboard/components/gridComponents/Chart.jsx index 70679600dffd..ba455188f70c 100644 --- a/superset-frontend/src/dashboard/components/gridComponents/Chart.jsx +++ b/superset-frontend/src/dashboard/components/gridComponents/Chart.jsx @@ -245,9 +245,12 @@ export default class Chart extends React.Component { const key = await postFormData( this.props.datasource.id, this.props.formData, - this.props.slice.id, + this.props.slice.slice_id, ); - const url = mountExploreUrl(null, { [URL_PARAMS.formDataKey.name]: key }); + const url = mountExploreUrl(null, { + [URL_PARAMS.formDataKey.name]: key, + [URL_PARAMS.sliceId.name]: this.props.slice.slice_id, + }); window.open(url, '_blank', 'noreferrer'); } catch (error) { logging.error(error); diff --git a/superset-frontend/src/dashboard/components/menu/ShareMenuItems/ShareMenuItems.test.tsx b/superset-frontend/src/dashboard/components/menu/ShareMenuItems/ShareMenuItems.test.tsx index 43b4188cd906..bdf22009efda 100644 --- a/superset-frontend/src/dashboard/components/menu/ShareMenuItems/ShareMenuItems.test.tsx +++ b/superset-frontend/src/dashboard/components/menu/ShareMenuItems/ShareMenuItems.test.tsx @@ -159,7 +159,7 @@ test('Click on "Share dashboard by email" and succeed', async () => { await waitFor(() => { expect(props.addDangerToast).toBeCalledTimes(0); expect(window.location.href).toBe( - 'mailto:?Subject=Superset dashboard COVID Vaccine Dashboard%20&Body=Check out this dashboard: http://localhost:8088/r/3', + 'mailto:?Subject=Superset%20dashboard%20COVID%20Vaccine%20Dashboard%20&Body=Check%20out%20this%20dashboard%3A%20http%3A%2F%2Flocalhost%3A8088%2Fr%2F3', ); }); }); diff --git a/superset-frontend/src/dashboard/components/menu/ShareMenuItems/index.tsx b/superset-frontend/src/dashboard/components/menu/ShareMenuItems/index.tsx index ed77628bd46e..9518e3a4d62d 100644 --- a/superset-frontend/src/dashboard/components/menu/ShareMenuItems/index.tsx +++ b/superset-frontend/src/dashboard/components/menu/ShareMenuItems/index.tsx @@ -83,6 +83,7 @@ const ShareMenuItems = (props: ShareMenuItemProps) => { ); return `${window.location.origin}${mountExploreUrl(null, { [URL_PARAMS.formDataKey.name]: key, + [URL_PARAMS.sliceId.name]: formData.slice_id, })}`; } const copyUrl = await getCopyUrl(); @@ -101,8 +102,11 @@ const ShareMenuItems = (props: ShareMenuItemProps) => { async function onShareByEmail() { try { - const bodyWithLink = `${emailBody}${await generateUrl()}`; - window.location.href = `mailto:?Subject=${emailSubject}%20&Body=${bodyWithLink}`; + const encodedBody = encodeURIComponent( + `${emailBody}${await generateUrl()}`, + ); + const encodedSubject = encodeURIComponent(emailSubject); + window.location.href = `mailto:?Subject=${encodedSubject}%20&Body=${encodedBody}`; } catch (error) { logging.error(error); addDangerToast(t('Sorry, something went wrong. Try again later.')); diff --git a/superset-frontend/src/explore/components/ExploreViewContainer/ExploreViewContainer.test.tsx b/superset-frontend/src/explore/components/ExploreViewContainer/ExploreViewContainer.test.tsx index 527c5ffe7c8b..da815ca7dc32 100644 --- a/superset-frontend/src/explore/components/ExploreViewContainer/ExploreViewContainer.test.tsx +++ b/superset-frontend/src/explore/components/ExploreViewContainer/ExploreViewContainer.test.tsx @@ -28,6 +28,7 @@ const reduxState = { common: { conf: { SUPERSET_WEBSERVER_TIMEOUT: 60 } }, controls: { datasource: { value: '1__table' } }, datasource: { + id: 1, type: 'table', columns: [{ is_dttm: false }], metrics: [{ id: 1, metric_name: 'count' }], @@ -65,7 +66,7 @@ fetchMock.get('glob:*/api/v1/explore/form_data*', {}); const renderWithRouter = (withKey?: boolean) => { const path = '/superset/explore/'; - const search = withKey ? `?form_data_key=${key}` : ''; + const search = withKey ? `?form_data_key=${key}&dataset_id=1` : ''; return render( @@ -82,7 +83,12 @@ test('generates a new form_data param when none is available', async () => { expect(replaceState).toHaveBeenCalledWith( expect.anything(), undefined, - expect.stringMatching('form_data'), + expect.stringMatching('form_data_key'), + ); + expect(replaceState).toHaveBeenCalledWith( + expect.anything(), + undefined, + expect.stringMatching('dataset_id'), ); replaceState.mockRestore(); }); @@ -96,6 +102,11 @@ test('generates a different form_data param when one is provided and is mounting undefined, expect.stringMatching(key), ); + expect(replaceState).toHaveBeenCalledWith( + expect.anything(), + undefined, + expect.stringMatching('dataset_id'), + ); replaceState.mockRestore(); }); diff --git a/superset-frontend/src/explore/components/ExploreViewContainer/index.jsx b/superset-frontend/src/explore/components/ExploreViewContainer/index.jsx index 780217bb664f..07c93253849a 100644 --- a/superset-frontend/src/explore/components/ExploreViewContainer/index.jsx +++ b/superset-frontend/src/explore/components/ExploreViewContainer/index.jsx @@ -167,6 +167,12 @@ const updateHistory = debounce( async (formData, datasetId, isReplace, standalone, force, title) => { const payload = { ...formData }; const chartId = formData.slice_id; + const additionalParam = {}; + if (chartId) { + additionalParam[URL_PARAMS.sliceId.name] = chartId; + } else { + additionalParam[URL_PARAMS.datasetId.name] = datasetId; + } try { let key; @@ -183,6 +189,7 @@ const updateHistory = debounce( standalone ? URL_PARAMS.standalone.name : null, { [URL_PARAMS.formDataKey.name]: key, + ...additionalParam, }, force, ); diff --git a/superset-frontend/src/explore/components/controls/DatasourceControl/index.jsx b/superset-frontend/src/explore/components/controls/DatasourceControl/index.jsx index c78683b9980d..c0dc8bb01000 100644 --- a/superset-frontend/src/explore/components/controls/DatasourceControl/index.jsx +++ b/superset-frontend/src/explore/components/controls/DatasourceControl/index.jsx @@ -20,6 +20,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import { t, styled, supersetTheme } from '@superset-ui/core'; +import { getUrlParam } from 'src/utils/urlUtils'; import { Dropdown, Menu } from 'src/common/components'; import { Tooltip } from 'src/components/Tooltip'; @@ -32,6 +33,7 @@ import { postForm } from 'src/explore/exploreUtils'; import Button from 'src/components/Button'; import ErrorAlert from 'src/components/ErrorMessage/ErrorAlert'; import WarningIconWithTooltip from 'src/components/WarningIconWithTooltip'; +import { URL_PARAMS } from 'src/constants'; const propTypes = { actions: PropTypes.object.isRequired, @@ -182,6 +184,14 @@ class DatasourceControl extends React.PureComponent { const { showChangeDatasourceModal, showEditDatasourceModal } = this.state; const { datasource, onChange } = this.props; const isMissingDatasource = datasource.id == null; + let isMissingParams = false; + if (isMissingDatasource) { + const datasetId = getUrlParam(URL_PARAMS.datasetId); + const sliceId = getUrlParam(URL_PARAMS.sliceId); + if (!datasetId && !sliceId) { + isMissingParams = true; + } + } const isSqlSupported = datasource.type === 'table'; @@ -244,7 +254,25 @@ class DatasourceControl extends React.PureComponent { {/* missing dataset */} - {isMissingDatasource && ( + {isMissingDatasource && isMissingParams && ( +
+ +

+ {t( + 'The URL is missing the dataset_id or slice_id parameters.', + )} +

+ + } + /> +
+ )} + {isMissingDatasource && !isMissingParams && (