diff --git a/superset-frontend/cypress-base/cypress/integration/explore/link.test.ts b/superset-frontend/cypress-base/cypress/integration/explore/link.test.ts index fdbf6f5f983..f825fe01d09 100644 --- a/superset-frontend/cypress-base/cypress/integration/explore/link.test.ts +++ b/superset-frontend/cypress-base/cypress/integration/explore/link.test.ts @@ -48,7 +48,7 @@ describe('Test explore links', () => { }); }); - it('Visit short link', () => { + it('Test if short link is generated', () => { cy.intercept('POST', 'r/shortner/').as('getShortUrl'); cy.visitChartByName('Growth Rate'); @@ -58,13 +58,6 @@ describe('Test explore links', () => { // explicitly wait for the url response cy.wait('@getShortUrl'); - - cy.get('#shorturl-popover [data-test="short-url"]') - .invoke('text') - .then(text => { - cy.visit(text); - }); - cy.verifySliceSuccess({ waitAlias: '@chartData' }); }); it('Test iframe link', () => { diff --git a/superset-frontend/spec/javascripts/dashboard/components/HeaderActionsDropdown_spec.jsx b/superset-frontend/spec/javascripts/dashboard/components/HeaderActionsDropdown_spec.jsx index 17bc7bc3c4e..ea3b65ca28e 100644 --- a/superset-frontend/spec/javascripts/dashboard/components/HeaderActionsDropdown_spec.jsx +++ b/superset-frontend/spec/javascripts/dashboard/components/HeaderActionsDropdown_spec.jsx @@ -20,11 +20,11 @@ import React from 'react'; import { shallow } from 'enzyme'; import { Menu, NoAnimationDropdown } from 'src/common/components'; import RefreshIntervalModal from 'src/dashboard/components/RefreshIntervalModal'; -import URLShortLinkModal from 'src/components/URLShortLinkModal'; import HeaderActionsDropdown from 'src/dashboard/components/HeaderActionsDropdown'; import SaveModal from 'src/dashboard/components/SaveModal'; import CssEditor from 'src/dashboard/components/CssEditor'; import fetchMock from 'fetch-mock'; +import ShareMenuItems from 'src/dashboard/components/menu/ShareMenuItems'; fetchMock.get('glob:*/csstemplateasyncmodelview/api/read', {}); @@ -79,9 +79,9 @@ describe('HeaderActionsDropdown', () => { expect(menu.find(SaveModal)).not.toExist(); }); - it('should render five Menu items', () => { + it('should render available Menu items', () => { const { menu } = setup(overrideProps); - expect(menu.find(Menu.Item)).toHaveLength(5); + expect(menu.find(Menu.Item)).toHaveLength(4); }); it('should render the RefreshIntervalModal', () => { @@ -89,9 +89,9 @@ describe('HeaderActionsDropdown', () => { expect(menu.find(RefreshIntervalModal)).toExist(); }); - it('should render the URLShortLinkModal', () => { + it('should render the ShareMenuItems', () => { const { menu } = setup(overrideProps); - expect(menu.find(URLShortLinkModal)).toExist(); + expect(menu.find(ShareMenuItems)).toExist(); }); it('should not render the CssEditor', () => { @@ -113,9 +113,9 @@ describe('HeaderActionsDropdown', () => { expect(menu.find(SaveModal)).toExist(); }); - it('should render six Menu items', () => { + it('should render available Menu items', () => { const { menu } = setup(overrideProps); - expect(menu.find(Menu.Item)).toHaveLength(6); + expect(menu.find(Menu.Item)).toHaveLength(5); }); it('should render the RefreshIntervalModal', () => { @@ -123,9 +123,9 @@ describe('HeaderActionsDropdown', () => { expect(menu.find(RefreshIntervalModal)).toExist(); }); - it('should render the URLShortLinkModal', () => { + it('should render the ShareMenuItems', () => { const { menu } = setup(overrideProps); - expect(menu.find(URLShortLinkModal)).toExist(); + expect(menu.find(ShareMenuItems)).toExist(); }); it('should not render the CssEditor', () => { @@ -147,9 +147,9 @@ describe('HeaderActionsDropdown', () => { expect(menu.find(SaveModal)).toExist(); }); - it('should render seven MenuItems', () => { + it('should render available MenuItems', () => { const { menu } = setup(overrideProps); - expect(menu.find(Menu.Item)).toHaveLength(7); + expect(menu.find(Menu.Item)).toHaveLength(6); }); it('should render the RefreshIntervalModal', () => { @@ -157,9 +157,9 @@ describe('HeaderActionsDropdown', () => { expect(menu.find(RefreshIntervalModal)).toExist(); }); - it('should render the URLShortLinkModal', () => { + it('should render the ShareMenuItems', () => { const { menu } = setup(overrideProps); - expect(menu.find(URLShortLinkModal)).toExist(); + expect(menu.find(ShareMenuItems)).toExist(); }); it('should render the CssEditor', () => { diff --git a/superset-frontend/spec/javascripts/dashboard/components/gridComponents/Chart_spec.jsx b/superset-frontend/spec/javascripts/dashboard/components/gridComponents/Chart_spec.jsx index 52dc41c2e18..513fe4923fc 100644 --- a/superset-frontend/spec/javascripts/dashboard/components/gridComponents/Chart_spec.jsx +++ b/superset-frontend/spec/javascripts/dashboard/components/gridComponents/Chart_spec.jsx @@ -57,6 +57,7 @@ describe('Chart', () => { changeFilter() {}, setFocusedFilterField() {}, unsetFocusedFilterField() {}, + addSuccessToast() {}, addDangerToast() {}, componentId: 'test', dashboardId: 111, diff --git a/superset-frontend/spec/javascripts/explore/components/EmbedCodeButton_spec.jsx b/superset-frontend/spec/javascripts/explore/components/EmbedCodeButton_spec.jsx index 74c0d21e38c..2c61cceaaee 100644 --- a/superset-frontend/spec/javascripts/explore/components/EmbedCodeButton_spec.jsx +++ b/superset-frontend/spec/javascripts/explore/components/EmbedCodeButton_spec.jsx @@ -86,8 +86,12 @@ describe('EmbedCodeButton', () => { const stub = sinon .stub(exploreUtils, 'getURIDirectory') .callsFake(() => 'endpoint_url'); - const wrapper = mount(); - wrapper.setState({ + const wrapper = mount( + + + , + ); + wrapper.find(EmbedCodeButton).setState({ height: '1000', width: '2000', shortUrlId: 100, @@ -104,7 +108,9 @@ describe('EmbedCodeButton', () => { }${DashboardStandaloneMode.HIDE_NAV}&height=1000"\n` + `>\n` + ``; - expect(wrapper.instance().generateEmbedHTML()).toBe(embedHTML); + expect(wrapper.find(EmbedCodeButton).instance().generateEmbedHTML()).toBe( + embedHTML, + ); stub.restore(); }); }); diff --git a/superset-frontend/spec/javascripts/explore/components/ExploreActionButtons_spec.jsx b/superset-frontend/spec/javascripts/explore/components/ExploreActionButtons_spec.jsx index 9e07f0e4006..5a405b6c5e9 100644 --- a/superset-frontend/spec/javascripts/explore/components/ExploreActionButtons_spec.jsx +++ b/superset-frontend/spec/javascripts/explore/components/ExploreActionButtons_spec.jsx @@ -18,6 +18,7 @@ */ import React from 'react'; import { shallow } from 'enzyme'; +import { mockStore } from 'spec/fixtures/mockStore'; import ExploreActionButtons from 'src/explore/components/ExploreActionButtons'; describe('ExploreActionButtons', () => { @@ -35,8 +36,10 @@ describe('ExploreActionButtons', () => { ).toBe(true); }); - it('should render 5 children/buttons', () => { - const wrapper = shallow(); - expect(wrapper.children()).toHaveLength(5); + it('should render 6 children/buttons', () => { + const wrapper = shallow( + , + ); + expect(wrapper.dive().children()).toHaveLength(6); }); }); diff --git a/superset-frontend/spec/javascripts/components/URLShortLinkModal_spec.jsx b/superset-frontend/src/common/hooks/useUrlShortener.ts similarity index 52% rename from superset-frontend/spec/javascripts/components/URLShortLinkModal_spec.jsx rename to superset-frontend/src/common/hooks/useUrlShortener.ts index 41ff9565d67..f8d9f815111 100644 --- a/superset-frontend/spec/javascripts/components/URLShortLinkModal_spec.jsx +++ b/superset-frontend/src/common/hooks/useUrlShortener.ts @@ -16,31 +16,24 @@ * specific language governing permissions and limitations * under the License. */ -import React from 'react'; -import configureStore from 'redux-mock-store'; -import { shallow } from 'enzyme'; +import { useState, useEffect } from 'react'; +import { getShortUrl as getShortUrlUtil } from 'src/utils/urlUtils'; -import URLShortLinkModal from 'src/components/URLShortLinkModal'; -import ModalTrigger from 'src/components/ModalTrigger'; +export function useUrlShortener(url: string): Function { + const [update, setUpdate] = useState(false); + const [shortUrl, setShortUrl] = useState(''); -describe('URLShortLinkModal', () => { - const defaultProps = { - url: 'mockURL', - emailSubject: 'Mock Subject', - emailContent: 'mock content', - triggerNode:
, - }; - - function setup() { - const mockStore = configureStore([]); - const store = mockStore({}); - return shallow( - , - ).dive(); + async function getShortUrl() { + if (update) { + const newShortUrl = await getShortUrlUtil(url); + setShortUrl(newShortUrl); + setUpdate(false); + return newShortUrl; + } + return shortUrl; } - it('renders ModalTrigger', () => { - const wrapper = setup(); - expect(wrapper.find(ModalTrigger)).toExist(); - }); -}); + useEffect(() => setUpdate(true), [url]); + + return getShortUrl; +} diff --git a/superset-frontend/src/components/Button/index.tsx b/superset-frontend/src/components/Button/index.tsx index cfbf6418f81..cb500cccb0a 100644 --- a/superset-frontend/src/components/Button/index.tsx +++ b/superset-frontend/src/components/Button/index.tsx @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import React, { CSSProperties } from 'react'; +import React, { CSSProperties, Children, ReactElement } from 'react'; import { kebabCase } from 'lodash'; import { mix } from 'polished'; import cx from 'classnames'; @@ -144,6 +144,17 @@ export default function Button(props: ButtonProps) { colorHover = primary.base; } + const element = children as ReactElement; + + let renderedChildren = []; + if (element && element.type === React.Fragment) { + renderedChildren = Children.toArray(element.props.children); + } else { + renderedChildren = Children.toArray(children); + } + + const firstChildMargin = renderedChildren.length > 1 ? theme.gridUnit * 2 : 0; + const button = ( diff --git a/superset-frontend/src/components/CopyToClipboard.jsx b/superset-frontend/src/components/CopyToClipboard.jsx index 5caf9d610cf..bded61fd69f 100644 --- a/superset-frontend/src/components/CopyToClipboard.jsx +++ b/superset-frontend/src/components/CopyToClipboard.jsx @@ -32,6 +32,7 @@ const propTypes = { wrapped: PropTypes.bool, tooltipText: PropTypes.string, addDangerToast: PropTypes.func.isRequired, + addSuccessToast: PropTypes.func.isRequired, }; const defaultProps = { @@ -45,22 +46,12 @@ const defaultProps = { class CopyToClipboard extends React.Component { constructor(props) { super(props); - this.state = { - tooltipText: this.props.tooltipText, - }; // bindings this.copyToClipboard = this.copyToClipboard.bind(this); - this.resetTooltipText = this.resetTooltipText.bind(this); - this.onMouseOut = this.onMouseOut.bind(this); this.onClick = this.onClick.bind(this); } - onMouseOut() { - // delay to avoid flash of text change on tooltip - setTimeout(this.resetTooltipText, 200); - } - onClick() { if (this.props.getText) { this.props.getText(d => { @@ -75,18 +66,13 @@ class CopyToClipboard extends React.Component { return React.cloneElement(this.props.copyNode, { style: { cursor: 'pointer' }, onClick: this.onClick, - onMouseOut: this.onMouseOut, }); } - resetTooltipText() { - this.setState({ tooltipText: this.props.tooltipText }); - } - copyToClipboard(textToCopy) { copyTextToClipboard(textToCopy) .then(() => { - this.setState({ tooltipText: t('Copied!') }); + this.props.addSuccessToast(t('Copied to clipboard!')); }) .catch(() => { this.props.addDangerToast( @@ -106,10 +92,8 @@ class CopyToClipboard extends React.Component { id="copy-to-clipboard-tooltip" placement="top" style={{ cursor: 'pointer' }} - title={this.state.tooltipText} + title={this.props.tooltipText} trigger={['hover']} - onClick={this.onClick} - onMouseOut={this.onMouseOut} > {this.getDecoratedCopyNode()} @@ -127,7 +111,7 @@ class CopyToClipboard extends React.Component { {this.getDecoratedCopyNode()} diff --git a/superset-frontend/src/components/URLShortLinkModal.tsx b/superset-frontend/src/components/URLShortLinkModal.tsx deleted file mode 100644 index 1dee4213377..00000000000 --- a/superset-frontend/src/components/URLShortLinkModal.tsx +++ /dev/null @@ -1,105 +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 React from 'react'; -import { t } from '@superset-ui/core'; -import CopyToClipboard from './CopyToClipboard'; -import { getShortUrl } from '../utils/urlUtils'; -import withToasts from '../messageToasts/enhancers/withToasts'; -import ModalTrigger from './ModalTrigger'; - -type URLShortLinkModalProps = { - url: string; - emailSubject: string; - emailContent: string; - title?: string; - addDangerToast: (msg: string) => void; - triggerNode: JSX.Element; -}; - -type URLShortLinkModalState = { - shortUrl: string; -}; - -class URLShortLinkModal extends React.Component< - URLShortLinkModalProps, - URLShortLinkModalState -> { - static defaultProps = { - url: window.location.href.substring(window.location.origin.length), - emailSubject: '', - emailContent: '', - }; - - modal: ModalTrigger | null; - - constructor(props: URLShortLinkModalProps) { - super(props); - this.state = { - shortUrl: '', - }; - this.modal = null; - this.setModalRef = this.setModalRef.bind(this); - this.onShortUrlSuccess = this.onShortUrlSuccess.bind(this); - this.getCopyUrl = this.getCopyUrl.bind(this); - } - - onShortUrlSuccess(shortUrl: string) { - this.setState(() => ({ shortUrl })); - } - - setModalRef(ref: ModalTrigger | null) { - this.modal = ref; - } - - getCopyUrl() { - getShortUrl(this.props.url) - .then(this.onShortUrlSuccess) - .catch(this.props.addDangerToast); - } - - render() { - const emailBody = t('%s%s', this.props.emailContent, this.state.shortUrl); - return ( - - - } - /> -    - - - -
- } - /> - ); - } -} - -export default withToasts(URLShortLinkModal); diff --git a/superset-frontend/src/dashboard/components/HeaderActionsDropdown.jsx b/superset-frontend/src/dashboard/components/HeaderActionsDropdown.jsx index a2b6b024870..5d27f1f7f59 100644 --- a/superset-frontend/src/dashboard/components/HeaderActionsDropdown.jsx +++ b/superset-frontend/src/dashboard/components/HeaderActionsDropdown.jsx @@ -24,12 +24,12 @@ import { styled, SupersetClient, t } from '@superset-ui/core'; import { Menu, NoAnimationDropdown } from 'src/common/components'; import Icon from 'src/components/Icon'; import { URL_PARAMS } from 'src/constants'; +import ShareMenuItems from 'src/dashboard/components/menu/ShareMenuItems'; import CssEditor from './CssEditor'; import RefreshIntervalModal from './RefreshIntervalModal'; import SaveModal from './SaveModal'; import injectCustomCss from '../util/injectCustomCss'; import { SAVE_TYPE_NEWDASHBOARD } from '../util/constants'; -import URLShortLinkModal from '../../components/URLShortLinkModal'; import FilterScopeModal from './filterscope/FilterScopeModal'; import downloadAsImage from '../../utils/downloadAsImage'; import getDashboardUrl from '../util/getDashboardUrl'; @@ -197,11 +197,18 @@ class HeaderActionsDropdown extends React.PureComponent { refreshLimit, refreshWarning, lastModifiedTime, + addSuccessToast, + addDangerToast, } = this.props; const emailTitle = t('Superset dashboard'); const emailSubject = `${emailTitle} ${dashboardTitle}`; const emailBody = t('Check out this dashboard: '); + const url = getDashboardUrl( + window.location.pathname, + getActiveFilters(), + window.location.hash, + ); const menu = ( )} - - {t('Share dashboard')}} - /> - + ( -
+
{refreshTooltip.length > 1 ? `${t('Query')} ${index + 1}: ${item}` : item} @@ -232,18 +233,18 @@ class SliceHeaderControls extends React.PureComponent { )} - - {t('Share chart')}} - /> - + {resizeLabel} diff --git a/superset-frontend/src/dashboard/components/gridComponents/Chart.jsx b/superset-frontend/src/dashboard/components/gridComponents/Chart.jsx index 7dd01e97873..b02897782b7 100644 --- a/superset-frontend/src/dashboard/components/gridComponents/Chart.jsx +++ b/superset-frontend/src/dashboard/components/gridComponents/Chart.jsx @@ -67,6 +67,7 @@ const propTypes = { supersetCanExplore: PropTypes.bool.isRequired, supersetCanCSV: PropTypes.bool.isRequired, sliceCanEdit: PropTypes.bool.isRequired, + addSuccessToast: PropTypes.func.isRequired, addDangerToast: PropTypes.func.isRequired, }; @@ -256,6 +257,7 @@ export default class Chart extends React.Component { supersetCanExplore, supersetCanCSV, sliceCanEdit, + addSuccessToast, addDangerToast, handleToggleFullSize, isFullSize, @@ -306,6 +308,7 @@ export default class Chart extends React.Component { componentId={componentId} dashboardId={dashboardId} filters={filters} + addSuccessToast={addSuccessToast} addDangerToast={addDangerToast} handleToggleFullSize={handleToggleFullSize} isFullSize={isFullSize} diff --git a/superset-frontend/src/dashboard/components/menu/ShareMenuItems.tsx b/superset-frontend/src/dashboard/components/menu/ShareMenuItems.tsx new file mode 100644 index 00000000000..2152dc75357 --- /dev/null +++ b/superset-frontend/src/dashboard/components/menu/ShareMenuItems.tsx @@ -0,0 +1,85 @@ +/** + * 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 { useUrlShortener } from 'src/common/hooks/useUrlShortener'; +import copyTextToClipboard from 'src/utils/copy'; +import { t } from '@superset-ui/core'; +import { Menu } from 'src/common/components'; + +interface ShareMenuItemProps { + url: string; + copyMenuItemTitle: string; + emailMenuItemTitle: string; + emailSubject: string; + emailBody: string; + addDangerToast: Function; + addSuccessToast: Function; +} + +const ShareMenuItems = (props: ShareMenuItemProps) => { + const { + url, + copyMenuItemTitle, + emailMenuItemTitle, + emailSubject, + emailBody, + addDangerToast, + addSuccessToast, + ...rest + } = props; + + const getShortUrl = useUrlShortener(url); + + async function onCopyLink() { + try { + const shortUrl = await getShortUrl(); + await copyTextToClipboard(shortUrl); + addSuccessToast(t('Copied to clipboard!')); + } catch (error) { + addDangerToast(t('Sorry, your browser does not support copying.')); + } + } + + async function onShareByEmail() { + try { + const shortUrl = await getShortUrl(); + const bodyWithLink = `${emailBody}${shortUrl}`; + window.location.href = `mailto:?Subject=${emailSubject}%20&Body=${bodyWithLink}`; + } catch (error) { + addDangerToast(t('Sorry, something went wrong. Try again later.')); + } + } + + return ( + <> + +
+ {copyMenuItemTitle} +
+
+ +
+ {emailMenuItemTitle} +
+
+ + ); +}; + +export default ShareMenuItems; diff --git a/superset-frontend/src/dashboard/containers/Chart.jsx b/superset-frontend/src/dashboard/containers/Chart.jsx index 7f74657380b..19f4c9adb44 100644 --- a/superset-frontend/src/dashboard/containers/Chart.jsx +++ b/superset-frontend/src/dashboard/containers/Chart.jsx @@ -26,7 +26,7 @@ import { } from '../actions/dashboardState'; import { updateComponents } from '../actions/dashboardLayout'; import { changeFilter } from '../actions/dashboardFilters'; -import { addDangerToast } from '../../messageToasts/actions'; +import { addSuccessToast, addDangerToast } from '../../messageToasts/actions'; import { refreshChart } from '../../chart/chartAction'; import { logEvent } from '../../logger/actions'; import { @@ -89,6 +89,7 @@ function mapDispatchToProps(dispatch) { return bindActionCreators( { updateComponents, + addSuccessToast, addDangerToast, toggleExpandSlice, changeFilter, diff --git a/superset-frontend/src/explore/components/EmbedCodeButton.jsx b/superset-frontend/src/explore/components/EmbedCodeButton.jsx index 0ff7afce959..b65ba756aef 100644 --- a/superset-frontend/src/explore/components/EmbedCodeButton.jsx +++ b/superset-frontend/src/explore/components/EmbedCodeButton.jsx @@ -22,6 +22,8 @@ import { t } from '@superset-ui/core'; import Popover from 'src/common/components/Popover'; import FormLabel from 'src/components/FormLabel'; +import Icon from 'src/components/Icon'; +import { Tooltip } from 'src/common/components/Tooltip'; import CopyToClipboard from 'src/components/CopyToClipboard'; import { getShortUrl } from 'src/utils/urlUtils'; import { URL_PARAMS } from 'src/constants'; @@ -150,10 +152,20 @@ export default class EmbedCodeButton extends React.Component { onClick={this.getCopyUrl} content={this.renderPopoverContent()} > - - -   - + +
+ +
+
); } diff --git a/superset-frontend/src/explore/components/ExploreActionButtons.jsx b/superset-frontend/src/explore/components/ExploreActionButtons.jsx deleted file mode 100644 index 23035beb039..00000000000 --- a/superset-frontend/src/explore/components/ExploreActionButtons.jsx +++ /dev/null @@ -1,120 +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 React from 'react'; -import PropTypes from 'prop-types'; -import cx from 'classnames'; -import { t } from '@superset-ui/core'; - -import URLShortLinkButton from '../../components/URLShortLinkButton'; -import EmbedCodeButton from './EmbedCodeButton'; -import ConnectedDisplayQueryButton from './DisplayQueryButton'; -import { exportChart, getExploreLongUrl } from '../exploreUtils'; - -const propTypes = { - actions: PropTypes.object.isRequired, - canDownload: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]) - .isRequired, - chartStatus: PropTypes.string, - chartHeight: PropTypes.string.isRequired, - latestQueryFormData: PropTypes.object, - queriesResponse: PropTypes.arrayOf(PropTypes.object), - slice: PropTypes.object, -}; - -export default function ExploreActionButtons({ - actions, - canDownload, - chartHeight, - chartStatus, - latestQueryFormData, - queriesResponse, - slice, -}) { - const exportToCSVClasses = cx('btn btn-default btn-sm', { - disabled: !canDownload, - }); - const doExportCSV = exportChart.bind(this, { - formData: latestQueryFormData, - resultType: 'results', - resultFormat: 'csv', - }); - const doExportChart = exportChart.bind(this, { - formData: latestQueryFormData, - resultType: 'results', - resultFormat: 'json', - }); - - return ( -
- {latestQueryFormData && ( - - )} - - {latestQueryFormData && ( - - )} - - {latestQueryFormData && ( -
- .json -
- )} - {latestQueryFormData && ( -
- .csv -
- )} - -
- ); -} - -ExploreActionButtons.propTypes = propTypes; diff --git a/superset-frontend/src/explore/components/ExploreActionButtons.tsx b/superset-frontend/src/explore/components/ExploreActionButtons.tsx new file mode 100644 index 00000000000..60f7301bbb8 --- /dev/null +++ b/superset-frontend/src/explore/components/ExploreActionButtons.tsx @@ -0,0 +1,204 @@ +/** + * 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, { useState } from 'react'; +import cx from 'classnames'; +import { t } from '@superset-ui/core'; +import Icon from 'src/components/Icon'; +import { Tooltip } from 'src/common/components/Tooltip'; +import copyTextToClipboard from 'src/utils/copy'; +import withToasts from 'src/messageToasts/enhancers/withToasts'; +import { useUrlShortener } from 'src/common/hooks/useUrlShortener'; +import EmbedCodeButton from './EmbedCodeButton'; +import ConnectedDisplayQueryButton from './DisplayQueryButton'; +import { exportChart, getExploreLongUrl } from '../exploreUtils'; + +type ActionButtonProps = { + icon: React.ReactElement; + text?: string; + tooltip: string; + className?: string; + onClick: React.MouseEventHandler; + onTooltipVisibilityChange?: (visible: boolean) => void; + 'data-test'?: string; +}; + +type ExploreActionButtonsProps = { + actions: { redirectSQLLab: Function; openPropertiesModal: Function }; + canDownload: boolean; + chartHeight: number; + chartStatus: string; + latestQueryFormData: {}; + queriesResponse: {}; + slice: { slice_name: string }; + addDangerToast: Function; +}; + +const ActionButton = (props: ActionButtonProps) => { + const { + icon, + text, + tooltip, + className, + onTooltipVisibilityChange, + ...rest + } = props; + return ( + +
+ {icon} + {text && {text}} +
+
+ ); +}; + +const ExploreActionButtons = (props: ExploreActionButtonsProps) => { + const { + actions, + canDownload, + chartHeight, + chartStatus, + latestQueryFormData, + queriesResponse, + slice, + addDangerToast, + } = props; + + 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); + setCopyTooltip(t('Copied to clipboard!')); + } catch (error) { + setCopyTooltip(t('Sorry, your browser does not support copying.')); + } + }; + + const doShareEmail = async () => { + try { + const subject = t('Superset Chart'); + const shortUrl = await getShortUrl(); + const body = t('%s%s', 'Check out this chart: ', shortUrl); + window.location.href = `mailto:?Subject=${subject}%20&Body=${body}`; + } catch (error) { + addDangerToast(t('Sorry, something went wrong. Try again later.')); + } + }; + + const doExportCSV = exportChart.bind(this, { + formData: latestQueryFormData, + resultType: 'results', + resultFormat: 'csv', + }); + + const doExportChart = exportChart.bind(this, { + formData: latestQueryFormData, + resultType: 'results', + resultFormat: 'json', + }); + + const exportToCSVClasses = cx('btn btn-default btn-sm', { + disabled: !canDownload, + }); + + return ( +
+ {latestQueryFormData && ( + <> + + } + tooltip={copyTooltip} + onClick={doCopyLink} + data-test="short-link-button" + onTooltipVisibilityChange={value => + !value && setTimeout(() => setCopyTooltip(copyTooltipText), 200) + } + /> + + } + tooltip={t('Share chart by email')} + onClick={doShareEmail} + /> + + } + text=".JSON" + tooltip={t('Export to .JSON format')} + onClick={doExportChart} + /> + } + text=".CSV" + tooltip={t('Export to .CSV format')} + onClick={doExportCSV} + className={exportToCSVClasses} + /> + + )} + +
+ ); +}; + +export default withToasts(ExploreActionButtons); diff --git a/superset-frontend/src/utils/copy.ts b/superset-frontend/src/utils/copy.ts index bab69d1d487..7db289c0403 100644 --- a/superset-frontend/src/utils/copy.ts +++ b/superset-frontend/src/utils/copy.ts @@ -17,8 +17,8 @@ * under the License. */ -const copyTextToClipboard = (text: string) => - new Promise((resolve, reject) => { +const copyTextToClipboard = async (text: string) => + new Promise((resolve, reject) => { const selection: Selection | null = document.getSelection(); if (selection) { selection.removeAllRanges(); diff --git a/superset-frontend/src/views/CRUD/welcome/ChartTable.tsx b/superset-frontend/src/views/CRUD/welcome/ChartTable.tsx index bafc38315eb..8eebf1f631e 100644 --- a/superset-frontend/src/views/CRUD/welcome/ChartTable.tsx +++ b/superset-frontend/src/views/CRUD/welcome/ChartTable.tsx @@ -142,10 +142,10 @@ function ChartTable({ buttons={[ { name: ( -
+ <> {t('Chart')} -
+ ), buttonStyle: 'tertiary', onClick: () => { diff --git a/superset-frontend/src/views/CRUD/welcome/DashboardTable.tsx b/superset-frontend/src/views/CRUD/welcome/DashboardTable.tsx index 04fc116ebf0..5b8f933ba9e 100644 --- a/superset-frontend/src/views/CRUD/welcome/DashboardTable.tsx +++ b/superset-frontend/src/views/CRUD/welcome/DashboardTable.tsx @@ -148,9 +148,10 @@ function DashboardTable({ buttons={[ { name: ( -
- Dashboard{' '} -
+ <> + + Dashboard + ), buttonStyle: 'tertiary', onClick: () => { diff --git a/superset-frontend/src/views/CRUD/welcome/SavedQueries.tsx b/superset-frontend/src/views/CRUD/welcome/SavedQueries.tsx index f86b0b82c3a..68705ffde03 100644 --- a/superset-frontend/src/views/CRUD/welcome/SavedQueries.tsx +++ b/superset-frontend/src/views/CRUD/welcome/SavedQueries.tsx @@ -267,10 +267,10 @@ const SavedQueries = ({ buttons={[ { name: ( -
+ <> - SQL Query{' '} -
+ SQL Query + ), buttonStyle: 'tertiary', onClick: () => {