mirror of https://github.com/apache/superset.git
fix: Explore long URL problem (#18181)
* fix: Explore long URL problem * Fixes lint problems * Fixes default value * Removes duplicated test * Fixes share menu items * Fixes tests * Debounces form_data updates * Rewrites debounce function * Moves history update outside the functional component * Mocks lodash function in tests * Fixes Cypress test * Fixes Cypress test #2
This commit is contained in:
parent
a06e043d7f
commit
4b61c76742
|
@ -21,6 +21,8 @@ describe('Advanced analytics', () => {
|
|||
cy.login();
|
||||
cy.intercept('POST', '/superset/explore_json/**').as('postJson');
|
||||
cy.intercept('GET', '/superset/explore_json/**').as('getJson');
|
||||
cy.intercept('PUT', '/api/v1/explore/**').as('putExplore');
|
||||
cy.intercept('GET', '/superset/explore/**').as('getExplore');
|
||||
});
|
||||
|
||||
it('Create custom time compare', () => {
|
||||
|
@ -40,12 +42,13 @@ describe('Advanced analytics', () => {
|
|||
|
||||
cy.get('button[data-test="run-query-button"]').click();
|
||||
cy.wait('@postJson');
|
||||
cy.wait('@putExplore');
|
||||
cy.reload();
|
||||
cy.verifySliceSuccess({
|
||||
waitAlias: '@postJson',
|
||||
chartSelector: 'svg',
|
||||
});
|
||||
|
||||
cy.wait('@getExplore');
|
||||
cy.get('.ant-collapse-header').contains('Advanced Analytics').click();
|
||||
cy.get('[data-test=time_compare]')
|
||||
.find('.ant-select-selector')
|
||||
|
|
|
@ -48,18 +48,6 @@ describe('Test explore links', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('Test if short link is generated', () => {
|
||||
cy.intercept('POST', 'r/shortner/').as('getShortUrl');
|
||||
|
||||
cy.visitChartByName('Growth Rate');
|
||||
cy.verifySliceSuccess({ waitAlias: '@chartData' });
|
||||
|
||||
cy.get('[data-test=short-link-button]').click();
|
||||
|
||||
// explicitly wait for the url response
|
||||
cy.wait('@getShortUrl');
|
||||
});
|
||||
|
||||
it('Test iframe link', () => {
|
||||
cy.visitChartByName('Growth Rate');
|
||||
cy.verifySliceSuccess({ waitAlias: '@chartData' });
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
class ResizeObserver {
|
||||
disconnect() {
|
||||
return null;
|
||||
}
|
||||
|
||||
observe() {
|
||||
return null;
|
||||
}
|
||||
|
||||
unobserve() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export { ResizeObserver };
|
|
@ -28,6 +28,7 @@ import Adapter from 'enzyme-adapter-react-16';
|
|||
import { configure as configureTranslation } from '../../packages/superset-ui-core/src/translation';
|
||||
import { Worker } from './Worker';
|
||||
import { IntersectionObserver } from './IntersectionObserver';
|
||||
import { ResizeObserver } from './ResizeObserver';
|
||||
import setupSupersetClient from './setupSupersetClient';
|
||||
import CacheStorage from './CacheStorage';
|
||||
|
||||
|
@ -51,6 +52,7 @@ g.window.location = { href: 'about:blank' };
|
|||
g.window.performance = { now: () => new Date().getTime() };
|
||||
g.window.Worker = Worker;
|
||||
g.window.IntersectionObserver = IntersectionObserver;
|
||||
g.window.ResizeObserver = ResizeObserver;
|
||||
g.URL.createObjectURL = () => '';
|
||||
g.caches = new CacheStorage();
|
||||
|
||||
|
|
|
@ -55,6 +55,10 @@ export const URL_PARAMS = {
|
|||
name: 'show_filters',
|
||||
type: 'boolean',
|
||||
},
|
||||
formDataKey: {
|
||||
name: 'form_data_key',
|
||||
type: 'string',
|
||||
},
|
||||
} as const;
|
||||
|
||||
/**
|
||||
|
|
|
@ -157,7 +157,8 @@ const createProps = () => ({
|
|||
forceRefresh: jest.fn(),
|
||||
logExploreChart: jest.fn(),
|
||||
exportCSV: jest.fn(),
|
||||
formData: {},
|
||||
onExploreChart: jest.fn(),
|
||||
formData: { slice_id: 1, datasource: '58__table' },
|
||||
});
|
||||
|
||||
test('Should render', () => {
|
||||
|
|
|
@ -58,7 +58,7 @@ const SliceHeader: FC<SliceHeaderProps> = ({
|
|||
updateSliceName = () => ({}),
|
||||
toggleExpandSlice = () => ({}),
|
||||
logExploreChart = () => ({}),
|
||||
exploreUrl = '#',
|
||||
onExploreChart,
|
||||
exportCSV = () => ({}),
|
||||
editMode = false,
|
||||
annotationQuery = {},
|
||||
|
@ -171,7 +171,7 @@ const SliceHeader: FC<SliceHeaderProps> = ({
|
|||
toggleExpandSlice={toggleExpandSlice}
|
||||
forceRefresh={forceRefresh}
|
||||
logExploreChart={logExploreChart}
|
||||
exploreUrl={exploreUrl}
|
||||
onExploreChart={onExploreChart}
|
||||
exportCSV={exportCSV}
|
||||
exportFullCSV={exportFullCSV}
|
||||
supersetCanExplore={supersetCanExplore}
|
||||
|
|
|
@ -45,6 +45,7 @@ const createProps = (viz_type = 'sunburst') => ({
|
|||
forceRefresh: jest.fn(),
|
||||
handleToggleFullSize: jest.fn(),
|
||||
toggleExpandSlice: jest.fn(),
|
||||
onExploreChart: jest.fn(),
|
||||
slice: {
|
||||
slice_id: 371,
|
||||
slice_url: '/superset/explore/?form_data=%7B%22slice_id%22%3A%20371%7D',
|
||||
|
@ -90,7 +91,7 @@ const createProps = (viz_type = 'sunburst') => ({
|
|||
chartStatus: 'rendered',
|
||||
showControls: true,
|
||||
supersetCanShare: true,
|
||||
formData: {},
|
||||
formData: { slice_id: 1, datasource: '58__table' },
|
||||
});
|
||||
|
||||
test('Should render', () => {
|
||||
|
|
|
@ -27,8 +27,6 @@ import {
|
|||
import { Menu, NoAnimationDropdown } from 'src/common/components';
|
||||
import ShareMenuItems from 'src/dashboard/components/menu/ShareMenuItems';
|
||||
import downloadAsImage from 'src/utils/downloadAsImage';
|
||||
import getDashboardUrl from 'src/dashboard/util/getDashboardUrl';
|
||||
import { getActiveFilters } from 'src/dashboard/util/activeDashboardFilters';
|
||||
import { FeatureFlag, isFeatureEnabled } from 'src/featureFlags';
|
||||
import CrossFilterScopingModal from 'src/dashboard/components/CrossFilterScopingModal/CrossFilterScopingModal';
|
||||
import Icons from 'src/components/Icons';
|
||||
|
@ -99,8 +97,8 @@ export interface SliceHeaderControlsProps {
|
|||
isExpanded?: boolean;
|
||||
updatedDttm: number | null;
|
||||
isFullSize?: boolean;
|
||||
formData: object;
|
||||
exploreUrl?: string;
|
||||
formData: { slice_id: number; datasource: string };
|
||||
onExploreChart: () => void;
|
||||
|
||||
forceRefresh: (sliceId: number, dashboardId: number) => void;
|
||||
logExploreChart?: (sliceId: number) => void;
|
||||
|
@ -215,13 +213,13 @@ class SliceHeaderControls extends React.PureComponent<
|
|||
const {
|
||||
slice,
|
||||
isFullSize,
|
||||
componentId,
|
||||
cachedDttm = [],
|
||||
updatedDttm = null,
|
||||
addSuccessToast = () => {},
|
||||
addDangerToast = () => {},
|
||||
supersetCanShare = false,
|
||||
isCached = [],
|
||||
formData,
|
||||
} = this.props;
|
||||
const crossFilterItems = getChartMetadataRegistry().items;
|
||||
const isTable = slice.viz_type === 'table';
|
||||
|
@ -283,10 +281,11 @@ class SliceHeaderControls extends React.PureComponent<
|
|||
)}
|
||||
|
||||
{this.props.supersetCanExplore && (
|
||||
<Menu.Item key={MENU_KEYS.EXPLORE_CHART}>
|
||||
<a href={this.props.exploreUrl} rel="noopener noreferrer">
|
||||
<Menu.Item
|
||||
key={MENU_KEYS.EXPLORE_CHART}
|
||||
onClick={this.props.onExploreChart}
|
||||
>
|
||||
{t('View chart in Explore')}
|
||||
</a>
|
||||
</Menu.Item>
|
||||
)}
|
||||
|
||||
|
@ -309,17 +308,13 @@ class SliceHeaderControls extends React.PureComponent<
|
|||
|
||||
{supersetCanShare && (
|
||||
<ShareMenuItems
|
||||
url={getDashboardUrl({
|
||||
pathname: window.location.pathname,
|
||||
filters: getActiveFilters(),
|
||||
hash: componentId,
|
||||
})}
|
||||
copyMenuItemTitle={t('Copy chart URL')}
|
||||
emailMenuItemTitle={t('Share chart by email')}
|
||||
emailSubject={t('Superset chart')}
|
||||
emailBody={t('Check out this chart: ')}
|
||||
addSuccessToast={addSuccessToast}
|
||||
addDangerToast={addDangerToast}
|
||||
formData={formData}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
|
|
@ -19,13 +19,10 @@
|
|||
import cx from 'classnames';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { styled } from '@superset-ui/core';
|
||||
import { styled, t, logging } from '@superset-ui/core';
|
||||
import { isEqual } from 'lodash';
|
||||
|
||||
import {
|
||||
exportChart,
|
||||
getExploreUrlFromDashboard,
|
||||
} from 'src/explore/exploreUtils';
|
||||
import { exportChart, mountExploreUrl } from 'src/explore/exploreUtils';
|
||||
import ChartContainer from 'src/chart/ChartContainer';
|
||||
import {
|
||||
LOG_ACTIONS_CHANGE_DASHBOARD_FILTER,
|
||||
|
@ -35,6 +32,8 @@ import {
|
|||
} from 'src/logger/LogUtils';
|
||||
import { areObjectsEqual } from 'src/reduxUtils';
|
||||
import { FILTER_BOX_MIGRATION_STATES } from 'src/explore/constants';
|
||||
import { postFormData } from 'src/explore/exploreUtils/formData';
|
||||
import { URL_PARAMS } from 'src/constants';
|
||||
|
||||
import SliceHeader from '../SliceHeader';
|
||||
import MissingChart from '../MissingChart';
|
||||
|
@ -241,7 +240,20 @@ export default class Chart extends React.Component {
|
|||
});
|
||||
};
|
||||
|
||||
getChartUrl = () => getExploreUrlFromDashboard(this.props.formData);
|
||||
onExploreChart = async () => {
|
||||
try {
|
||||
const key = await postFormData(
|
||||
this.props.datasource.id,
|
||||
this.props.formData,
|
||||
this.props.slice.id,
|
||||
);
|
||||
const url = mountExploreUrl(null, { [URL_PARAMS.formDataKey.name]: key });
|
||||
window.open(url, '_blank', 'noreferrer');
|
||||
} catch (error) {
|
||||
logging.error(error);
|
||||
this.props.addDangerToast(t('An error occurred while opening Explore'));
|
||||
}
|
||||
};
|
||||
|
||||
exportCSV(isFullCSV = false) {
|
||||
this.props.logEvent(LOG_ACTIONS_EXPORT_CSV_DASHBOARD_CHART, {
|
||||
|
@ -350,7 +362,7 @@ export default class Chart extends React.Component {
|
|||
editMode={editMode}
|
||||
annotationQuery={chart.annotationQuery}
|
||||
logExploreChart={this.logExploreChart}
|
||||
exploreUrl={this.getChartUrl()}
|
||||
onExploreChart={this.onExploreChart}
|
||||
exportCSV={this.exportCSV}
|
||||
exportFullCSV={this.exportFullCSV}
|
||||
updateSliceName={updateSliceName}
|
||||
|
|
|
@ -134,7 +134,7 @@ test('Click on "Copy dashboard URL" and fail', async () => {
|
|||
expect(props.addSuccessToast).toBeCalledTimes(0);
|
||||
expect(props.addDangerToast).toBeCalledTimes(1);
|
||||
expect(props.addDangerToast).toBeCalledWith(
|
||||
'Sorry, your browser does not support copying.',
|
||||
'Sorry, something went wrong. Try again later.',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -19,17 +19,19 @@
|
|||
import React from 'react';
|
||||
import { useUrlShortener } from 'src/hooks/useUrlShortener';
|
||||
import copyTextToClipboard from 'src/utils/copy';
|
||||
import { t } from '@superset-ui/core';
|
||||
import { t, logging } from '@superset-ui/core';
|
||||
import { Menu } from 'src/common/components';
|
||||
import { getUrlParam } from 'src/utils/urlUtils';
|
||||
import { postFormData } from 'src/explore/exploreUtils/formData';
|
||||
import { URL_PARAMS } from 'src/constants';
|
||||
import { mountExploreUrl } from 'src/explore/exploreUtils';
|
||||
import {
|
||||
createFilterKey,
|
||||
getFilterValue,
|
||||
} from 'src/dashboard/components/nativeFilters/FilterBar/keyValue';
|
||||
|
||||
interface ShareMenuItemProps {
|
||||
url: string;
|
||||
url?: string;
|
||||
copyMenuItemTitle: string;
|
||||
emailMenuItemTitle: string;
|
||||
emailSubject: string;
|
||||
|
@ -37,6 +39,7 @@ interface ShareMenuItemProps {
|
|||
addDangerToast: Function;
|
||||
addSuccessToast: Function;
|
||||
dashboardId?: string;
|
||||
formData?: { slice_id: number; datasource: string };
|
||||
}
|
||||
|
||||
const ShareMenuItems = (props: ShareMenuItemProps) => {
|
||||
|
@ -49,10 +52,11 @@ const ShareMenuItems = (props: ShareMenuItemProps) => {
|
|||
addDangerToast,
|
||||
addSuccessToast,
|
||||
dashboardId,
|
||||
formData,
|
||||
...rest
|
||||
} = props;
|
||||
|
||||
const getShortUrl = useUrlShortener(url);
|
||||
const getShortUrl = useUrlShortener(url || '');
|
||||
|
||||
async function getCopyUrl() {
|
||||
const risonObj = getUrlParam(URL_PARAMS.nativeFilters);
|
||||
|
@ -70,24 +74,37 @@ const ShareMenuItems = (props: ShareMenuItemProps) => {
|
|||
return `${newUrl.pathname}${newUrl.search}`;
|
||||
}
|
||||
|
||||
async function generateUrl() {
|
||||
if (formData) {
|
||||
const key = await postFormData(
|
||||
parseInt(formData.datasource.split('_')[0], 10),
|
||||
formData,
|
||||
formData.slice_id,
|
||||
);
|
||||
return `${window.location.origin}${mountExploreUrl(null, {
|
||||
[URL_PARAMS.formDataKey.name]: key,
|
||||
})}`;
|
||||
}
|
||||
const copyUrl = await getCopyUrl();
|
||||
return getShortUrl(copyUrl);
|
||||
}
|
||||
|
||||
async function onCopyLink() {
|
||||
try {
|
||||
const copyUrl = await getCopyUrl();
|
||||
const shortUrl = await getShortUrl(copyUrl);
|
||||
await copyTextToClipboard(shortUrl);
|
||||
await copyTextToClipboard(await generateUrl());
|
||||
addSuccessToast(t('Copied to clipboard!'));
|
||||
} catch (error) {
|
||||
addDangerToast(t('Sorry, your browser does not support copying.'));
|
||||
logging.error(error);
|
||||
addDangerToast(t('Sorry, something went wrong. Try again later.'));
|
||||
}
|
||||
}
|
||||
|
||||
async function onShareByEmail() {
|
||||
try {
|
||||
const copyUrl = await getCopyUrl();
|
||||
const shortUrl = await getShortUrl(copyUrl);
|
||||
const bodyWithLink = `${emailBody}${shortUrl}`;
|
||||
const bodyWithLink = `${emailBody}${await generateUrl()}`;
|
||||
window.location.href = `mailto:?Subject=${emailSubject}%20&Body=${bodyWithLink}`;
|
||||
} catch (error) {
|
||||
logging.error(error);
|
||||
addDangerToast(t('Sorry, something went wrong. Try again later.'));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,7 +17,6 @@
|
|||
* under the License.
|
||||
*/
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { t } from '@superset-ui/core';
|
||||
|
||||
import Popover from 'src/components/Popover';
|
||||
|
@ -25,13 +24,7 @@ import { FormLabel } from 'src/components/Form';
|
|||
import Icons from 'src/components/Icons';
|
||||
import { Tooltip } from 'src/components/Tooltip';
|
||||
import CopyToClipboard from 'src/components/CopyToClipboard';
|
||||
import { getShortUrl } from 'src/utils/urlUtils';
|
||||
import { URL_PARAMS } from 'src/constants';
|
||||
import { getExploreLongUrl, getURIDirectory } from '../exploreUtils';
|
||||
|
||||
const propTypes = {
|
||||
latestQueryFormData: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
export default class EmbedCodeButton extends React.Component {
|
||||
constructor(props) {
|
||||
|
@ -39,24 +32,8 @@ export default class EmbedCodeButton extends React.Component {
|
|||
this.state = {
|
||||
height: '400',
|
||||
width: '600',
|
||||
shortUrlId: 0,
|
||||
};
|
||||
this.handleInputChange = this.handleInputChange.bind(this);
|
||||
this.getCopyUrl = this.getCopyUrl.bind(this);
|
||||
this.onShortUrlSuccess = this.onShortUrlSuccess.bind(this);
|
||||
}
|
||||
|
||||
onShortUrlSuccess(shortUrl) {
|
||||
const shortUrlId = shortUrl.substring(shortUrl.indexOf('/r/') + 3);
|
||||
this.setState(() => ({
|
||||
shortUrlId,
|
||||
}));
|
||||
}
|
||||
|
||||
getCopyUrl() {
|
||||
return getShortUrl(getExploreLongUrl(this.props.latestQueryFormData))
|
||||
.then(this.onShortUrlSuccess)
|
||||
.catch(this.props.addDangerToast);
|
||||
}
|
||||
|
||||
handleInputChange(e) {
|
||||
|
@ -67,9 +44,7 @@ export default class EmbedCodeButton extends React.Component {
|
|||
}
|
||||
|
||||
generateEmbedHTML() {
|
||||
const srcLink = `${window.location.origin + getURIDirectory()}?r=${
|
||||
this.state.shortUrlId
|
||||
}&${URL_PARAMS.standalone.name}=1&height=${this.state.height}`;
|
||||
const srcLink = `${window.location.href}&${URL_PARAMS.standalone.name}=1&height=${this.state.height}`;
|
||||
return (
|
||||
'<iframe\n' +
|
||||
` width="${this.state.width}"\n` +
|
||||
|
@ -150,7 +125,6 @@ export default class EmbedCodeButton extends React.Component {
|
|||
<Popover
|
||||
trigger="click"
|
||||
placement="left"
|
||||
onClick={this.getCopyUrl}
|
||||
content={this.renderPopoverContent()}
|
||||
>
|
||||
<Tooltip
|
||||
|
@ -171,5 +145,3 @@ export default class EmbedCodeButton extends React.Component {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
EmbedCodeButton.propTypes = propTypes;
|
||||
|
|
|
@ -17,84 +17,29 @@
|
|||
* under the License.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { shallow, mount } from 'enzyme';
|
||||
import { supersetTheme, ThemeProvider } from '@superset-ui/core';
|
||||
import { shallow } from 'enzyme';
|
||||
import { styledMount as mount } from 'spec/helpers/theming';
|
||||
import Popover from 'src/components/Popover';
|
||||
import sinon from 'sinon';
|
||||
import { Provider } from 'react-redux';
|
||||
import configureStore from 'redux-mock-store';
|
||||
import fetchMock from 'fetch-mock';
|
||||
import EmbedCodeButton from 'src/explore/components/EmbedCodeButton';
|
||||
import * as exploreUtils from 'src/explore/exploreUtils';
|
||||
import * as urlUtils from 'src/utils/urlUtils';
|
||||
import { DashboardStandaloneMode } from 'src/dashboard/util/constants';
|
||||
|
||||
const ENDPOINT = 'glob:*/r/shortner/';
|
||||
|
||||
fetchMock.post(ENDPOINT, {});
|
||||
|
||||
describe('EmbedCodeButton', () => {
|
||||
const mockStore = configureStore([]);
|
||||
const store = mockStore({});
|
||||
|
||||
const defaultProps = {
|
||||
latestQueryFormData: { datasource: '107__table' },
|
||||
};
|
||||
|
||||
it('renders', () => {
|
||||
expect(React.isValidElement(<EmbedCodeButton {...defaultProps} />)).toBe(
|
||||
true,
|
||||
);
|
||||
expect(React.isValidElement(<EmbedCodeButton />)).toBe(true);
|
||||
});
|
||||
|
||||
it('renders overlay trigger', () => {
|
||||
const wrapper = shallow(<EmbedCodeButton {...defaultProps} />);
|
||||
const wrapper = shallow(<EmbedCodeButton />);
|
||||
expect(wrapper.find(Popover)).toExist();
|
||||
});
|
||||
|
||||
it('should create a short, standalone, explore url', () => {
|
||||
const spy1 = sinon.spy(exploreUtils, 'getExploreLongUrl');
|
||||
const spy2 = sinon.spy(urlUtils, 'getShortUrl');
|
||||
|
||||
const wrapper = mount(
|
||||
<ThemeProvider theme={supersetTheme}>
|
||||
<EmbedCodeButton {...defaultProps} />
|
||||
</ThemeProvider>,
|
||||
{
|
||||
wrappingComponent: Provider,
|
||||
wrappingComponentProps: {
|
||||
store,
|
||||
},
|
||||
},
|
||||
).find(EmbedCodeButton);
|
||||
wrapper.setState({
|
||||
height: '1000',
|
||||
width: '2000',
|
||||
shortUrlId: 100,
|
||||
});
|
||||
|
||||
const trigger = wrapper.find(Popover);
|
||||
trigger.simulate('click');
|
||||
expect(spy1.callCount).toBe(1);
|
||||
expect(spy2.callCount).toBe(1);
|
||||
|
||||
spy1.restore();
|
||||
spy2.restore();
|
||||
});
|
||||
|
||||
it('returns correct embed code', () => {
|
||||
const stub = sinon
|
||||
.stub(exploreUtils, 'getURIDirectory')
|
||||
.callsFake(() => 'endpoint_url');
|
||||
const wrapper = mount(
|
||||
<ThemeProvider theme={supersetTheme}>
|
||||
<EmbedCodeButton {...defaultProps} />
|
||||
</ThemeProvider>,
|
||||
);
|
||||
const href = 'http://localhost/explore?form_data_key=xxxxxxxxx';
|
||||
Object.defineProperty(window, 'location', { value: { href } });
|
||||
const wrapper = mount(<EmbedCodeButton />);
|
||||
wrapper.find(EmbedCodeButton).setState({
|
||||
height: '1000',
|
||||
width: '2000',
|
||||
shortUrlId: 100,
|
||||
});
|
||||
const embedHTML =
|
||||
`${
|
||||
|
@ -104,13 +49,12 @@ describe('EmbedCodeButton', () => {
|
|||
' seamless\n' +
|
||||
' frameBorder="0"\n' +
|
||||
' scrolling="no"\n' +
|
||||
' src="http://localhostendpoint_url?r=100&standalone='
|
||||
` src="${href}&standalone=`
|
||||
}${DashboardStandaloneMode.HIDE_NAV}&height=1000"\n` +
|
||||
`>\n` +
|
||||
`</iframe>`;
|
||||
expect(wrapper.find(EmbedCodeButton).instance().generateEmbedHTML()).toBe(
|
||||
embedHTML,
|
||||
);
|
||||
stub.restore();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -23,9 +23,8 @@ import Icons from 'src/components/Icons';
|
|||
import { Tooltip } from 'src/components/Tooltip';
|
||||
import copyTextToClipboard from 'src/utils/copy';
|
||||
import withToasts from 'src/components/MessageToasts/withToasts';
|
||||
import { useUrlShortener } from 'src/hooks/useUrlShortener';
|
||||
import EmbedCodeButton from './EmbedCodeButton';
|
||||
import { exportChart, getExploreLongUrl } from '../exploreUtils';
|
||||
import { exportChart } from '../exploreUtils';
|
||||
import ExploreAdditionalActionsMenu from './ExploreAdditionalActionsMenu';
|
||||
import { ExportToCSVDropdown } from './ExportToCSVDropdown';
|
||||
|
||||
|
@ -104,14 +103,12 @@ const ExploreActionButtons = (props: ExploreActionButtonsProps) => {
|
|||
|
||||
const copyTooltipText = t('Copy chart URL to clipboard');
|
||||
const [copyTooltip, setCopyTooltip] = useState(copyTooltipText);
|
||||
const longUrl = getExploreLongUrl(latestQueryFormData);
|
||||
const getShortUrl = useUrlShortener(longUrl);
|
||||
|
||||
const doCopyLink = async () => {
|
||||
try {
|
||||
setCopyTooltip(t('Loading...'));
|
||||
const shortUrl = await getShortUrl();
|
||||
await copyTextToClipboard(shortUrl);
|
||||
const url = window.location.href;
|
||||
await copyTextToClipboard(url);
|
||||
setCopyTooltip(t('Copied to clipboard!'));
|
||||
addSuccessToast(t('Copied to clipboard!'));
|
||||
} catch (error) {
|
||||
|
@ -123,8 +120,8 @@ const ExploreActionButtons = (props: ExploreActionButtonsProps) => {
|
|||
const doShareEmail = async () => {
|
||||
try {
|
||||
const subject = t('Superset Chart');
|
||||
const shortUrl = await getShortUrl();
|
||||
const body = t('%s%s', 'Check out this chart: ', shortUrl);
|
||||
const url = window.location.href;
|
||||
const body = encodeURIComponent(t('%s%s', 'Check out this chart: ', url));
|
||||
window.location.href = `mailto:?Subject=${subject}%20&Body=${body}`;
|
||||
} catch (error) {
|
||||
addDangerToast(t('Sorry, something went wrong. Try again later.'));
|
||||
|
@ -179,7 +176,7 @@ const ExploreActionButtons = (props: ExploreActionButtonsProps) => {
|
|||
tooltip={t('Share chart by email')}
|
||||
onClick={doShareEmail}
|
||||
/>
|
||||
<EmbedCodeButton latestQueryFormData={latestQueryFormData} />
|
||||
<EmbedCodeButton />
|
||||
<ActionButton
|
||||
prefixIcon={<Icons.FileTextOutlined iconSize="m" />}
|
||||
text=".JSON"
|
||||
|
|
|
@ -56,7 +56,6 @@ const CHART_STATUS_MAP = {
|
|||
|
||||
const propTypes = {
|
||||
actions: PropTypes.object.isRequired,
|
||||
addHistory: PropTypes.func,
|
||||
can_overwrite: PropTypes.bool.isRequired,
|
||||
can_download: PropTypes.bool.isRequired,
|
||||
dashboardId: PropTypes.number,
|
||||
|
|
|
@ -34,7 +34,6 @@ import { buildV1ChartDataPayload } from '../exploreUtils';
|
|||
|
||||
const propTypes = {
|
||||
actions: PropTypes.object.isRequired,
|
||||
addHistory: PropTypes.func,
|
||||
onQuery: PropTypes.func,
|
||||
can_overwrite: PropTypes.bool.isRequired,
|
||||
can_download: PropTypes.bool.isRequired,
|
||||
|
@ -288,7 +287,6 @@ const ExploreChartPanel = props => {
|
|||
<ConnectedExploreChartHeader
|
||||
ownState={props.ownState}
|
||||
actions={props.actions}
|
||||
addHistory={props.addHistory}
|
||||
can_overwrite={props.can_overwrite}
|
||||
can_download={props.can_download}
|
||||
dashboardId={props.dashboardId}
|
||||
|
|
|
@ -0,0 +1,112 @@
|
|||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import React from 'react';
|
||||
import fetchMock from 'fetch-mock';
|
||||
import { MemoryRouter, Route } from 'react-router-dom';
|
||||
import { render, screen, waitFor } from 'spec/helpers/testing-library';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import ExploreViewContainer from '.';
|
||||
|
||||
const reduxState = {
|
||||
explore: {
|
||||
common: { conf: { SUPERSET_WEBSERVER_TIMEOUT: 60 } },
|
||||
controls: { datasource: { value: '1__table' } },
|
||||
datasource: {
|
||||
type: 'table',
|
||||
columns: [{ is_dttm: false }],
|
||||
metrics: [{ id: 1, metric_name: 'count' }],
|
||||
},
|
||||
user: {
|
||||
userId: 1,
|
||||
},
|
||||
isStarred: false,
|
||||
},
|
||||
charts: {
|
||||
1: {
|
||||
id: 1,
|
||||
latestQueryFormData: {
|
||||
datasource: '1__table',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const key = 'aWrs7w29sd';
|
||||
|
||||
jest.mock('react-resize-detector', () => ({
|
||||
__esModule: true,
|
||||
useResizeDetector: () => ({ height: 100, width: 100 }),
|
||||
}));
|
||||
|
||||
jest.mock('lodash/debounce', () => ({
|
||||
__esModule: true,
|
||||
default: (fuc: Function) => fuc,
|
||||
}));
|
||||
|
||||
fetchMock.post('glob:*/api/v1/explore/form_data*', { key });
|
||||
fetchMock.put('glob:*/api/v1/explore/form_data*', { key });
|
||||
fetchMock.get('glob:*/api/v1/explore/form_data*', {});
|
||||
|
||||
const renderWithRouter = (withKey?: boolean) => {
|
||||
const path = '/superset/explore/';
|
||||
const search = withKey ? `?form_data_key=${key}` : '';
|
||||
return render(
|
||||
<MemoryRouter initialEntries={[`${path}${search}`]}>
|
||||
<Route path={path}>
|
||||
<ExploreViewContainer />
|
||||
</Route>
|
||||
</MemoryRouter>,
|
||||
{ useRedux: true, initialState: reduxState },
|
||||
);
|
||||
};
|
||||
|
||||
test('generates a new form_data param when none is available', async () => {
|
||||
const replaceState = jest.spyOn(window.history, 'replaceState');
|
||||
await waitFor(() => renderWithRouter());
|
||||
expect(replaceState).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
undefined,
|
||||
expect.stringMatching('form_data'),
|
||||
);
|
||||
replaceState.mockRestore();
|
||||
});
|
||||
|
||||
test('generates a different form_data param when one is provided and is mounting', async () => {
|
||||
const replaceState = jest.spyOn(window.history, 'replaceState');
|
||||
await waitFor(() => renderWithRouter(true));
|
||||
expect(replaceState).not.toHaveBeenLastCalledWith(
|
||||
0,
|
||||
expect.anything(),
|
||||
undefined,
|
||||
expect.stringMatching(key),
|
||||
);
|
||||
replaceState.mockRestore();
|
||||
});
|
||||
|
||||
test('reuses the same form_data param when updating', async () => {
|
||||
const replaceState = jest.spyOn(window.history, 'replaceState');
|
||||
const pushState = jest.spyOn(window.history, 'pushState');
|
||||
await waitFor(() => renderWithRouter());
|
||||
expect(replaceState.mock.calls.length).toBe(1);
|
||||
userEvent.click(screen.getByText('Run'));
|
||||
await waitFor(() => expect(pushState.mock.calls.length).toBe(1));
|
||||
expect(replaceState.mock.calls[0]).toEqual(pushState.mock.calls[0]);
|
||||
replaceState.mockRestore();
|
||||
pushState.mockRestore();
|
||||
});
|
|
@ -21,7 +21,7 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
|||
import PropTypes from 'prop-types';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import { connect } from 'react-redux';
|
||||
import { styled, t, css, useTheme } from '@superset-ui/core';
|
||||
import { styled, t, css, useTheme, logging } from '@superset-ui/core';
|
||||
import { debounce } from 'lodash';
|
||||
import { Resizable } from 're-resizable';
|
||||
|
||||
|
@ -37,26 +37,28 @@ import {
|
|||
LocalStorageKeys,
|
||||
} from 'src/utils/localStorageHelpers';
|
||||
import { URL_PARAMS } from 'src/constants';
|
||||
import { getUrlParam } from 'src/utils/urlUtils';
|
||||
import cx from 'classnames';
|
||||
import * as chartActions from 'src/chart/chartAction';
|
||||
import { fetchDatasourceMetadata } from 'src/dashboard/actions/datasources';
|
||||
import { chartPropShape } from 'src/dashboard/util/propShapes';
|
||||
import { mergeExtraFormData } from 'src/dashboard/components/nativeFilters/utils';
|
||||
import ExploreChartPanel from './ExploreChartPanel';
|
||||
import ConnectedControlPanelsContainer from './ControlPanelsContainer';
|
||||
import SaveModal from './SaveModal';
|
||||
import QueryAndSaveBtns from './QueryAndSaveBtns';
|
||||
import DataSourcePanel from './DatasourcePanel';
|
||||
import { getExploreLongUrl } from '../exploreUtils';
|
||||
import { areObjectsEqual } from '../../reduxUtils';
|
||||
import { getFormDataFromControls } from '../controlUtils';
|
||||
import * as exploreActions from '../actions/exploreActions';
|
||||
import * as saveModalActions from '../actions/saveModalActions';
|
||||
import * as logActions from '../../logger/actions';
|
||||
import { postFormData, putFormData } from 'src/explore/exploreUtils/formData';
|
||||
import ExploreChartPanel from '../ExploreChartPanel';
|
||||
import ConnectedControlPanelsContainer from '../ControlPanelsContainer';
|
||||
import SaveModal from '../SaveModal';
|
||||
import QueryAndSaveBtns from '../QueryAndSaveBtns';
|
||||
import DataSourcePanel from '../DatasourcePanel';
|
||||
import { mountExploreUrl } from '../../exploreUtils';
|
||||
import { areObjectsEqual } from '../../../reduxUtils';
|
||||
import { getFormDataFromControls } from '../../controlUtils';
|
||||
import * as exploreActions from '../../actions/exploreActions';
|
||||
import * as saveModalActions from '../../actions/saveModalActions';
|
||||
import * as logActions from '../../../logger/actions';
|
||||
import {
|
||||
LOG_ACTIONS_MOUNT_EXPLORER,
|
||||
LOG_ACTIONS_CHANGE_EXPLORE_CONTROLS,
|
||||
} from '../../logger/LogUtils';
|
||||
} from '../../../logger/LogUtils';
|
||||
|
||||
const propTypes = {
|
||||
...ExploreChartPanel.propTypes,
|
||||
|
@ -161,6 +163,37 @@ function useWindowSize({ delayMs = 250 } = {}) {
|
|||
return size;
|
||||
}
|
||||
|
||||
const updateHistory = debounce(
|
||||
async (formData, datasetId, isReplace, standalone, force, title) => {
|
||||
const payload = { ...formData };
|
||||
const chartId = formData.slice_id;
|
||||
|
||||
try {
|
||||
let key;
|
||||
let stateModifier;
|
||||
if (isReplace) {
|
||||
key = await postFormData(datasetId, formData, chartId);
|
||||
stateModifier = 'replaceState';
|
||||
} else {
|
||||
key = getUrlParam(URL_PARAMS.formDataKey);
|
||||
await putFormData(datasetId, key, formData, chartId);
|
||||
stateModifier = 'pushState';
|
||||
}
|
||||
const url = mountExploreUrl(
|
||||
standalone ? URL_PARAMS.standalone.name : null,
|
||||
{
|
||||
[URL_PARAMS.formDataKey.name]: key,
|
||||
},
|
||||
force,
|
||||
);
|
||||
window.history[stateModifier](payload, title, url);
|
||||
} catch (e) {
|
||||
logging.warn('Failed at altering browser history', e);
|
||||
}
|
||||
},
|
||||
1000,
|
||||
);
|
||||
|
||||
function ExploreViewContainer(props) {
|
||||
const dynamicPluginContext = usePluginContext();
|
||||
const dynamicPlugin = dynamicPluginContext.dynamicPlugins[props.vizType];
|
||||
|
@ -191,39 +224,31 @@ function ExploreViewContainer(props) {
|
|||
};
|
||||
|
||||
const addHistory = useCallback(
|
||||
({ isReplace = false, title } = {}) => {
|
||||
async ({ isReplace = false, title } = {}) => {
|
||||
const formData = props.dashboardId
|
||||
? {
|
||||
...props.form_data,
|
||||
dashboardId: props.dashboardId,
|
||||
}
|
||||
: props.form_data;
|
||||
const payload = { ...formData };
|
||||
const longUrl = getExploreLongUrl(
|
||||
formData,
|
||||
props.standalone ? URL_PARAMS.standalone.name : null,
|
||||
false,
|
||||
{},
|
||||
props.force,
|
||||
);
|
||||
const datasetId = props.datasource.id;
|
||||
|
||||
try {
|
||||
if (isReplace) {
|
||||
window.history.replaceState(payload, title, longUrl);
|
||||
} else {
|
||||
window.history.pushState(payload, title, longUrl);
|
||||
}
|
||||
} catch (e) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(
|
||||
'Failed at altering browser history',
|
||||
payload,
|
||||
updateHistory(
|
||||
formData,
|
||||
datasetId,
|
||||
isReplace,
|
||||
props.standalone,
|
||||
props.force,
|
||||
title,
|
||||
longUrl,
|
||||
);
|
||||
}
|
||||
},
|
||||
[props.form_data, props.standalone, props.force],
|
||||
[
|
||||
props.dashboardId,
|
||||
props.form_data,
|
||||
props.datasource.id,
|
||||
props.standalone,
|
||||
props.force,
|
||||
],
|
||||
);
|
||||
|
||||
const handlePopstate = useCallback(() => {
|
||||
|
@ -447,7 +472,6 @@ function ExploreViewContainer(props) {
|
|||
{...props}
|
||||
errorMessage={renderErrorMessage()}
|
||||
refreshOverlayVisible={chartIsStale}
|
||||
addHistory={addHistory}
|
||||
onQuery={onQuery}
|
||||
/>
|
||||
);
|
|
@ -22,7 +22,6 @@ import URI from 'urijs';
|
|||
import {
|
||||
buildV1ChartDataPayload,
|
||||
getExploreUrl,
|
||||
getExploreLongUrl,
|
||||
shouldUseLegacyApi,
|
||||
getSimpleSQLExpression,
|
||||
} from 'src/explore/exploreUtils';
|
||||
|
@ -35,7 +34,6 @@ describe('exploreUtils', () => {
|
|||
const formData = {
|
||||
datasource: '1__table',
|
||||
};
|
||||
const sFormData = JSON.stringify(formData);
|
||||
function compareURI(uri1, uri2) {
|
||||
expect(uri1.toString()).toBe(uri2.toString());
|
||||
}
|
||||
|
@ -191,25 +189,6 @@ describe('exploreUtils', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('getExploreLongUrl', () => {
|
||||
it('generates proper base url with form_data', () => {
|
||||
compareURI(
|
||||
URI(getExploreLongUrl(formData, 'base')),
|
||||
URI('/superset/explore/').search({ form_data: sFormData }),
|
||||
);
|
||||
});
|
||||
|
||||
it('generates url with standalone', () => {
|
||||
compareURI(
|
||||
URI(getExploreLongUrl(formData, 'standalone')),
|
||||
URI('/superset/explore/').search({
|
||||
form_data: sFormData,
|
||||
standalone: DashboardStandaloneMode.HIDE_NAV,
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('buildV1ChartDataPayload', () => {
|
||||
it('generate valid request payload despite no registered buildQuery', () => {
|
||||
const v1RequestPayload = buildV1ChartDataPayload({
|
||||
|
|
|
@ -0,0 +1,61 @@
|
|||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { SupersetClient, JsonObject } from '@superset-ui/core';
|
||||
|
||||
type Payload = {
|
||||
dataset_id: number;
|
||||
form_data: string;
|
||||
chart_id?: number;
|
||||
};
|
||||
|
||||
const assemblePayload = (
|
||||
datasetId: number,
|
||||
form_data: JsonObject,
|
||||
chartId?: number,
|
||||
) => {
|
||||
const payload: Payload = {
|
||||
dataset_id: datasetId,
|
||||
form_data: JSON.stringify(form_data),
|
||||
};
|
||||
if (chartId) {
|
||||
payload.chart_id = chartId;
|
||||
}
|
||||
return payload;
|
||||
};
|
||||
|
||||
export const postFormData = (
|
||||
datasetId: number,
|
||||
form_data: JsonObject,
|
||||
chartId?: number,
|
||||
): Promise<string> =>
|
||||
SupersetClient.post({
|
||||
endpoint: 'api/v1/explore/form_data',
|
||||
jsonPayload: assemblePayload(datasetId, form_data, chartId),
|
||||
}).then(r => r.json.key);
|
||||
|
||||
export const putFormData = (
|
||||
datasetId: number,
|
||||
key: string,
|
||||
form_data: JsonObject,
|
||||
chartId?: number,
|
||||
): Promise<string> =>
|
||||
SupersetClient.put({
|
||||
endpoint: `api/v1/explore/form_data/${key}`,
|
||||
jsonPayload: assemblePayload(datasetId, form_data, chartId),
|
||||
}).then(r => r.json.message);
|
|
@ -1,134 +0,0 @@
|
|||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { getExploreLongUrl, getExploreUrlFromDashboard } from '.';
|
||||
|
||||
const createParams = () => ({
|
||||
formData: {
|
||||
datasource: 'datasource',
|
||||
viz_type: 'viz_type',
|
||||
},
|
||||
endpointType: 'endpointType',
|
||||
allowOverflow: true,
|
||||
extraSearch: { same: 'any-string' },
|
||||
});
|
||||
|
||||
test('Should return null if formData.datasource is falsy', () => {
|
||||
const params = createParams();
|
||||
expect(
|
||||
getExploreLongUrl(
|
||||
{},
|
||||
params.endpointType,
|
||||
params.allowOverflow,
|
||||
params.extraSearch,
|
||||
),
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
test('Get url when endpointType:standalone', () => {
|
||||
const params = createParams();
|
||||
expect(
|
||||
getExploreLongUrl(
|
||||
params.formData,
|
||||
'standalone',
|
||||
params.allowOverflow,
|
||||
params.extraSearch,
|
||||
),
|
||||
).toBe(
|
||||
'/superset/explore/?same=any-string&form_data=%7B%22datasource%22%3A%22datasource%22%2C%22viz_type%22%3A%22viz_type%22%7D&standalone=1',
|
||||
);
|
||||
});
|
||||
|
||||
test('Get url when endpointType:standalone and force:true', () => {
|
||||
const params = createParams();
|
||||
expect(
|
||||
getExploreLongUrl(
|
||||
params.formData,
|
||||
'standalone',
|
||||
params.allowOverflow,
|
||||
params.extraSearch,
|
||||
true,
|
||||
),
|
||||
).toBe(
|
||||
'/superset/explore/?same=any-string&form_data=%7B%22datasource%22%3A%22datasource%22%2C%22viz_type%22%3A%22viz_type%22%7D&force=1&standalone=1',
|
||||
);
|
||||
});
|
||||
|
||||
test('Get url when endpointType:standalone and allowOverflow:false', () => {
|
||||
const params = createParams();
|
||||
expect(
|
||||
getExploreLongUrl(
|
||||
params.formData,
|
||||
params.endpointType,
|
||||
false,
|
||||
params.extraSearch,
|
||||
),
|
||||
).toBe(
|
||||
'/superset/explore/?same=any-string&form_data=%7B%22datasource%22%3A%22datasource%22%2C%22viz_type%22%3A%22viz_type%22%7D',
|
||||
);
|
||||
});
|
||||
|
||||
test('Get url when endpointType:results', () => {
|
||||
const params = createParams();
|
||||
expect(
|
||||
getExploreLongUrl(
|
||||
params.formData,
|
||||
'results',
|
||||
params.allowOverflow,
|
||||
params.extraSearch,
|
||||
),
|
||||
).toBe(
|
||||
'/superset/explore_json/?same=any-string&form_data=%7B%22datasource%22%3A%22datasource%22%2C%22viz_type%22%3A%22viz_type%22%7D',
|
||||
);
|
||||
});
|
||||
|
||||
test('Get url when endpointType:results and allowOverflow:false', () => {
|
||||
const params = createParams();
|
||||
expect(
|
||||
getExploreLongUrl(params.formData, 'results', false, params.extraSearch),
|
||||
).toBe(
|
||||
'/superset/explore_json/?same=any-string&form_data=%7B%22datasource%22%3A%22datasource%22%2C%22viz_type%22%3A%22viz_type%22%7D',
|
||||
);
|
||||
});
|
||||
|
||||
test('Get url from a dashboard', () => {
|
||||
const formData = {
|
||||
...createParams().formData,
|
||||
// these params should get filtered out
|
||||
extra_form_data: {
|
||||
filters: {
|
||||
col: 'foo',
|
||||
op: 'IN',
|
||||
val: ['bar'],
|
||||
},
|
||||
},
|
||||
dataMask: {
|
||||
'NATIVE_FILTER-bqEoUsEPe': {
|
||||
id: 'NATIVE_FILTER-bqEoUsEPe',
|
||||
lots: 'of other stuff here too',
|
||||
},
|
||||
},
|
||||
url_params: {
|
||||
native_filters: '(blah)',
|
||||
standalone: true,
|
||||
},
|
||||
};
|
||||
expect(getExploreUrlFromDashboard(formData)).toBe(
|
||||
'/superset/explore/?form_data=%7B%22datasource%22%3A%22datasource%22%2C%22viz_type%22%3A%22viz_type%22%2C%22extra_form_data%22%3A%7B%22filters%22%3A%7B%22col%22%3A%22foo%22%2C%22op%22%3A%22IN%22%2C%22val%22%3A%5B%22bar%22%5D%7D%7D%7D',
|
||||
);
|
||||
});
|
|
@ -18,7 +18,6 @@
|
|||
*/
|
||||
|
||||
import { useCallback, useEffect } from 'react';
|
||||
import { omit } from 'lodash';
|
||||
/* eslint camelcase: 0 */
|
||||
import URI from 'urijs';
|
||||
import {
|
||||
|
@ -37,8 +36,6 @@ import {
|
|||
import { DashboardStandaloneMode } from 'src/dashboard/util/constants';
|
||||
import { optionLabel } from '../../utils/common';
|
||||
|
||||
const MAX_URL_LENGTH = 8000;
|
||||
|
||||
export function getChartKey(explore) {
|
||||
const { slice } = explore;
|
||||
return slice ? slice.slice_id : 0;
|
||||
|
@ -91,64 +88,20 @@ export function getURIDirectory(endpointType = 'base') {
|
|||
return '/superset/explore/';
|
||||
}
|
||||
|
||||
/**
|
||||
* This gets the url of the explore page, with all the form data included explicitly.
|
||||
* This includes any form data overrides from the dashboard.
|
||||
*/
|
||||
export function getExploreLongUrl(
|
||||
formData,
|
||||
endpointType,
|
||||
allowOverflow = true,
|
||||
extraSearch = {},
|
||||
force = false,
|
||||
) {
|
||||
if (!formData.datasource) {
|
||||
return null;
|
||||
}
|
||||
|
||||
export function mountExploreUrl(endpointType, extraSearch = {}, force = false) {
|
||||
const uri = new URI('/');
|
||||
const directory = getURIDirectory(endpointType);
|
||||
const search = uri.search(true);
|
||||
Object.keys(extraSearch).forEach(key => {
|
||||
search[key] = extraSearch[key];
|
||||
});
|
||||
search.form_data = safeStringify(formData);
|
||||
if (endpointType === URL_PARAMS.standalone.name) {
|
||||
if (force) {
|
||||
search.force = '1';
|
||||
}
|
||||
search.standalone = DashboardStandaloneMode.HIDE_NAV;
|
||||
}
|
||||
const url = uri.directory(directory).search(search).toString();
|
||||
if (!allowOverflow && url.length > MAX_URL_LENGTH) {
|
||||
const minimalFormData = {
|
||||
datasource: formData.datasource,
|
||||
viz_type: formData.viz_type,
|
||||
};
|
||||
return getExploreLongUrl(
|
||||
minimalFormData,
|
||||
endpointType,
|
||||
false,
|
||||
{
|
||||
URL_IS_TOO_LONG_TO_SHARE: null,
|
||||
},
|
||||
force,
|
||||
);
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
export function getExploreUrlFromDashboard(formData) {
|
||||
// remove formData params that we don't need in the explore url.
|
||||
// These are present when generating explore urls from the dashboard page.
|
||||
// This should be superseded by some sort of "exploration context" system
|
||||
// where form data and other context is referenced by id.
|
||||
const trimmedFormData = omit(formData, [
|
||||
'dataMask',
|
||||
'url_params',
|
||||
'label_colors',
|
||||
]);
|
||||
return getExploreLongUrl(trimmedFormData, null, false);
|
||||
return uri.directory(directory).search(search).toString();
|
||||
}
|
||||
|
||||
export function getChartDataUri({ path, qs, allowDomainSharding = false }) {
|
||||
|
|
|
@ -87,6 +87,8 @@ from superset.exceptions import (
|
|||
SupersetSecurityException,
|
||||
SupersetTimeoutException,
|
||||
)
|
||||
from superset.explore.form_data.commands.get import GetFormDataCommand
|
||||
from superset.explore.form_data.commands.parameters import CommandParameters
|
||||
from superset.extensions import async_query_manager, cache_manager
|
||||
from superset.jinja_context import get_template_processor
|
||||
from superset.models.core import Database, FavStar, Log
|
||||
|
@ -730,7 +732,19 @@ class Superset(BaseSupersetView): # pylint: disable=too-many-public-methods
|
|||
def explore(
|
||||
self, datasource_type: Optional[str] = None, datasource_id: Optional[int] = None
|
||||
) -> FlaskResponse:
|
||||
form_data, slc = get_form_data(use_slice_data=True)
|
||||
initial_form_data = {}
|
||||
|
||||
form_data_key = request.args.get("form_data_key")
|
||||
if form_data_key:
|
||||
parameters = CommandParameters(actor=g.user, key=form_data_key,)
|
||||
value = GetFormDataCommand(parameters).run()
|
||||
if value:
|
||||
initial_form_data = json.loads(value)
|
||||
|
||||
form_data, slc = get_form_data(
|
||||
use_slice_data=True, initial_form_data=initial_form_data
|
||||
)
|
||||
|
||||
query_context = request.form.get("query_context")
|
||||
# Flash the SIP-15 message if the slice is owned by the current user and has not
|
||||
# been updated, i.e., is not using the [start, end) interval.
|
||||
|
|
|
@ -137,9 +137,11 @@ def loads_request_json(request_json_data: str) -> Dict[Any, Any]:
|
|||
|
||||
|
||||
def get_form_data( # pylint: disable=too-many-locals
|
||||
slice_id: Optional[int] = None, use_slice_data: bool = False
|
||||
slice_id: Optional[int] = None,
|
||||
use_slice_data: bool = False,
|
||||
initial_form_data: Optional[Dict[str, Any]] = None,
|
||||
) -> Tuple[Dict[str, Any], Optional[Slice]]:
|
||||
form_data: Dict[str, Any] = {}
|
||||
form_data: Dict[str, Any] = initial_form_data or {}
|
||||
|
||||
if has_request_context(): # type: ignore
|
||||
# chart data API requests are JSON
|
||||
|
|
Loading…
Reference in New Issue