mirror of
https://github.com/apache/superset.git
synced 2026-04-07 10:31:50 +00:00
committed by
GitHub
parent
91db008d72
commit
ad4ca2223e
@@ -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', () => {
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -57,6 +57,7 @@ describe('Chart', () => {
|
||||
changeFilter() {},
|
||||
setFocusedFilterField() {},
|
||||
unsetFocusedFilterField() {},
|
||||
addSuccessToast() {},
|
||||
addDangerToast() {},
|
||||
componentId: 'test',
|
||||
dashboardId: 111,
|
||||
|
||||
@@ -86,8 +86,12 @@ describe('EmbedCodeButton', () => {
|
||||
const stub = sinon
|
||||
.stub(exploreUtils, 'getURIDirectory')
|
||||
.callsFake(() => 'endpoint_url');
|
||||
const wrapper = mount(<EmbedCodeButton {...defaultProps} />);
|
||||
wrapper.setState({
|
||||
const wrapper = mount(
|
||||
<ThemeProvider theme={supersetTheme}>
|
||||
<EmbedCodeButton {...defaultProps} />
|
||||
</ThemeProvider>,
|
||||
);
|
||||
wrapper.find(EmbedCodeButton).setState({
|
||||
height: '1000',
|
||||
width: '2000',
|
||||
shortUrlId: 100,
|
||||
@@ -104,7 +108,9 @@ describe('EmbedCodeButton', () => {
|
||||
}${DashboardStandaloneMode.HIDE_NAV}&height=1000"\n` +
|
||||
`>\n` +
|
||||
`</iframe>`;
|
||||
expect(wrapper.instance().generateEmbedHTML()).toBe(embedHTML);
|
||||
expect(wrapper.find(EmbedCodeButton).instance().generateEmbedHTML()).toBe(
|
||||
embedHTML,
|
||||
);
|
||||
stub.restore();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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(<ExploreActionButtons {...defaultProps} />);
|
||||
expect(wrapper.children()).toHaveLength(5);
|
||||
it('should render 6 children/buttons', () => {
|
||||
const wrapper = shallow(
|
||||
<ExploreActionButtons {...defaultProps} store={mockStore} />,
|
||||
);
|
||||
expect(wrapper.dive().children()).toHaveLength(6);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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: <div />,
|
||||
};
|
||||
|
||||
function setup() {
|
||||
const mockStore = configureStore([]);
|
||||
const store = mockStore({});
|
||||
return shallow(
|
||||
<URLShortLinkModal store={store} {...defaultProps} />,
|
||||
).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;
|
||||
}
|
||||
@@ -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 = (
|
||||
<AntdButton
|
||||
href={disabled ? undefined : href}
|
||||
@@ -188,14 +199,13 @@ export default function Button(props: ButtonProps) {
|
||||
backgroundColor: backgroundColorDisabled,
|
||||
borderColor: borderColorDisabled,
|
||||
},
|
||||
'i:first-of-type, svg:first-of-type': {
|
||||
marginRight: theme.gridUnit * 2,
|
||||
padding: `0 ${theme.gridUnit * 2} 0 0`,
|
||||
},
|
||||
marginLeft: theme.gridUnit * 2,
|
||||
'&:first-of-type': {
|
||||
marginLeft: 0,
|
||||
},
|
||||
'& :first-of-type': {
|
||||
marginRight: firstChildMargin,
|
||||
},
|
||||
}}
|
||||
{...restProps}
|
||||
>
|
||||
|
||||
@@ -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()}
|
||||
</Tooltip>
|
||||
@@ -127,7 +111,7 @@ class CopyToClipboard extends React.Component {
|
||||
<Tooltip
|
||||
id="copy-to-clipboard-tooltip"
|
||||
placement="top"
|
||||
title={this.state.tooltipText}
|
||||
title={this.props.tooltipText}
|
||||
trigger={['hover']}
|
||||
>
|
||||
{this.getDecoratedCopyNode()}
|
||||
|
||||
@@ -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 (
|
||||
<ModalTrigger
|
||||
ref={this.setModalRef}
|
||||
triggerNode={this.props.triggerNode}
|
||||
beforeOpen={this.getCopyUrl}
|
||||
modalTitle={this.props.title || t('Share dashboard')}
|
||||
modalBody={
|
||||
<div>
|
||||
<CopyToClipboard
|
||||
text={this.state.shortUrl}
|
||||
copyNode={
|
||||
<i className="fa fa-clipboard" title={t('Copy to clipboard')} />
|
||||
}
|
||||
/>
|
||||
|
||||
<a
|
||||
href={`mailto:?Subject=${this.props.emailSubject}%20&Body=${emailBody}`}
|
||||
>
|
||||
<i className="fa fa-envelope" />
|
||||
</a>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withToasts(URLShortLinkModal);
|
||||
@@ -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 = (
|
||||
<Menu
|
||||
@@ -234,19 +241,15 @@ class HeaderActionsDropdown extends React.PureComponent {
|
||||
/>
|
||||
</Menu.Item>
|
||||
)}
|
||||
<Menu.Item key={MENU_KEYS.SHARE_DASHBOARD}>
|
||||
<URLShortLinkModal
|
||||
url={getDashboardUrl(
|
||||
window.location.pathname,
|
||||
getActiveFilters(),
|
||||
window.location.hash,
|
||||
)}
|
||||
emailSubject={emailSubject}
|
||||
emailContent={emailBody}
|
||||
addDangerToast={this.props.addDangerToast}
|
||||
triggerNode={<span>{t('Share dashboard')}</span>}
|
||||
/>
|
||||
</Menu.Item>
|
||||
<ShareMenuItems
|
||||
url={url}
|
||||
copyMenuItemTitle={t('Copy dashboard URL')}
|
||||
emailMenuItemTitle={t('Share dashboard by email')}
|
||||
emailSubject={emailSubject}
|
||||
emailBody={emailBody}
|
||||
addSuccessToast={addSuccessToast}
|
||||
addDangerToast={addDangerToast}
|
||||
/>
|
||||
<Menu.Item
|
||||
key={MENU_KEYS.REFRESH_DASHBOARD}
|
||||
data-test="refresh-dashboard-menu-item"
|
||||
|
||||
@@ -47,6 +47,7 @@ const propTypes = {
|
||||
componentId: PropTypes.string.isRequired,
|
||||
dashboardId: PropTypes.number.isRequired,
|
||||
filters: PropTypes.object.isRequired,
|
||||
addSuccessToast: PropTypes.func.isRequired,
|
||||
addDangerToast: PropTypes.func.isRequired,
|
||||
handleToggleFullSize: PropTypes.func.isRequired,
|
||||
chartStatus: PropTypes.string.isRequired,
|
||||
@@ -98,6 +99,7 @@ class SliceHeader extends React.PureComponent {
|
||||
annotationError,
|
||||
componentId,
|
||||
dashboardId,
|
||||
addSuccessToast,
|
||||
addDangerToast,
|
||||
handleToggleFullSize,
|
||||
isFullSize,
|
||||
@@ -157,6 +159,7 @@ class SliceHeader extends React.PureComponent {
|
||||
sliceCanEdit={sliceCanEdit}
|
||||
componentId={componentId}
|
||||
dashboardId={dashboardId}
|
||||
addSuccessToast={addSuccessToast}
|
||||
addDangerToast={addDangerToast}
|
||||
handleToggleFullSize={handleToggleFullSize}
|
||||
isFullSize={isFullSize}
|
||||
|
||||
@@ -21,7 +21,7 @@ import PropTypes from 'prop-types';
|
||||
import moment from 'moment';
|
||||
import { styled, t } from '@superset-ui/core';
|
||||
import { Menu, NoAnimationDropdown } from 'src/common/components';
|
||||
import URLShortLinkModal from '../../components/URLShortLinkModal';
|
||||
import ShareMenuItems from 'src/dashboard/components/menu/ShareMenuItems';
|
||||
import downloadAsImage from '../../utils/downloadAsImage';
|
||||
import getDashboardUrl from '../util/getDashboardUrl';
|
||||
import { getActiveFilters } from '../util/activeDashboardFilters';
|
||||
@@ -173,6 +173,7 @@ class SliceHeaderControls extends React.PureComponent {
|
||||
cachedDttm,
|
||||
updatedDttm,
|
||||
componentId,
|
||||
addSuccessToast,
|
||||
addDangerToast,
|
||||
isFullSize,
|
||||
} = this.props;
|
||||
@@ -193,7 +194,7 @@ class SliceHeaderControls extends React.PureComponent {
|
||||
// If all queries have same cache time we can unit them to one
|
||||
let refreshTooltip = [...new Set(refreshTooltipData)];
|
||||
refreshTooltip = refreshTooltip.map((item, index) => (
|
||||
<div>
|
||||
<div key={`tooltip-${index}`}>
|
||||
{refreshTooltip.length > 1
|
||||
? `${t('Query')} ${index + 1}: ${item}`
|
||||
: item}
|
||||
@@ -232,18 +233,18 @@ class SliceHeaderControls extends React.PureComponent {
|
||||
</Menu.Item>
|
||||
)}
|
||||
|
||||
<Menu.Item key={MENU_KEYS.SHARE_CHART}>
|
||||
<URLShortLinkModal
|
||||
url={getDashboardUrl(
|
||||
window.location.pathname,
|
||||
getActiveFilters(),
|
||||
componentId,
|
||||
)}
|
||||
addDangerToast={addDangerToast}
|
||||
title={t('Share chart')}
|
||||
triggerNode={<span>{t('Share chart')}</span>}
|
||||
/>
|
||||
</Menu.Item>
|
||||
<ShareMenuItems
|
||||
url={getDashboardUrl(
|
||||
window.location.pathname,
|
||||
getActiveFilters(),
|
||||
componentId,
|
||||
)}
|
||||
copyMenuItemTitle={t('Copy chart URL')}
|
||||
emailMenuItemTitle={t('Share chart by email')}
|
||||
emailSubject={t('Superset chart')}
|
||||
addSuccessToast={addSuccessToast}
|
||||
addDangerToast={addDangerToast}
|
||||
/>
|
||||
|
||||
<Menu.Item key={MENU_KEYS.RESIZE_LABEL}>{resizeLabel}</Menu.Item>
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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 (
|
||||
<>
|
||||
<Menu.Item key="copy-url" {...rest}>
|
||||
<div onClick={onCopyLink} role="button" tabIndex={0}>
|
||||
{copyMenuItemTitle}
|
||||
</div>
|
||||
</Menu.Item>
|
||||
<Menu.Item key="share-by-email" {...rest}>
|
||||
<div onClick={onShareByEmail} role="button" tabIndex={0}>
|
||||
{emailMenuItemTitle}
|
||||
</div>
|
||||
</Menu.Item>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ShareMenuItems;
|
||||
@@ -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,
|
||||
|
||||
@@ -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()}
|
||||
>
|
||||
<span className="btn btn-default btn-sm" data-test="embed-code-button">
|
||||
<i className="fa fa-code" />
|
||||
|
||||
</span>
|
||||
<Tooltip
|
||||
id="embed-code-tooltip"
|
||||
placement="top"
|
||||
title="Embed code"
|
||||
trigger={['hover']}
|
||||
>
|
||||
<div
|
||||
className="btn btn-default btn-sm"
|
||||
data-test="embed-code-button"
|
||||
style={{ height: 30 }}
|
||||
>
|
||||
<Icon name="code" width={15} height={15} style={{ marginTop: 1 }} />
|
||||
</div>
|
||||
</Tooltip>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
<div
|
||||
className="btn-group results"
|
||||
role="group"
|
||||
data-test="btn-group-results"
|
||||
>
|
||||
{latestQueryFormData && (
|
||||
<URLShortLinkButton
|
||||
url={getExploreLongUrl(latestQueryFormData)}
|
||||
emailSubject="Superset Chart"
|
||||
emailContent="Check out this chart: "
|
||||
/>
|
||||
)}
|
||||
|
||||
{latestQueryFormData && (
|
||||
<EmbedCodeButton latestQueryFormData={latestQueryFormData} />
|
||||
)}
|
||||
|
||||
{latestQueryFormData && (
|
||||
<div
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={doExportChart}
|
||||
className="btn btn-default btn-sm"
|
||||
title={t('Export to .json')}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<i className="fa fa-file-code-o" /> .json
|
||||
</div>
|
||||
)}
|
||||
{latestQueryFormData && (
|
||||
<div
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={doExportCSV}
|
||||
className={exportToCSVClasses}
|
||||
title={t('Export to .csv format')}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<i className="fa fa-file-text-o" /> .csv
|
||||
</div>
|
||||
)}
|
||||
<ConnectedDisplayQueryButton
|
||||
chartHeight={chartHeight}
|
||||
queryResponse={queriesResponse?.[0]}
|
||||
latestQueryFormData={latestQueryFormData}
|
||||
chartStatus={chartStatus}
|
||||
onOpenInEditor={actions.redirectSQLLab}
|
||||
onOpenPropertiesModal={actions.openPropertiesModal}
|
||||
slice={slice}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
ExploreActionButtons.propTypes = propTypes;
|
||||
@@ -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<HTMLElement>;
|
||||
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 (
|
||||
<Tooltip
|
||||
id={`${icon}-tooltip`}
|
||||
placement="top"
|
||||
title={tooltip}
|
||||
trigger={['hover']}
|
||||
onVisibleChange={onTooltipVisibilityChange}
|
||||
>
|
||||
<div
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
css={{ '&:focus, &:focus:active': { outline: 0 } }}
|
||||
className={className || 'btn btn-default btn-sm'}
|
||||
style={{ height: 30 }}
|
||||
{...rest}
|
||||
>
|
||||
{icon}
|
||||
{text && <span style={{ marginLeft: 5 }}>{text}</span>}
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
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 (
|
||||
<div
|
||||
className="btn-group results"
|
||||
role="group"
|
||||
data-test="btn-group-results"
|
||||
>
|
||||
{latestQueryFormData && (
|
||||
<>
|
||||
<ActionButton
|
||||
icon={
|
||||
<Icon
|
||||
name="link"
|
||||
width={15}
|
||||
height={15}
|
||||
style={{ marginTop: 1 }}
|
||||
/>
|
||||
}
|
||||
tooltip={copyTooltip}
|
||||
onClick={doCopyLink}
|
||||
data-test="short-link-button"
|
||||
onTooltipVisibilityChange={value =>
|
||||
!value && setTimeout(() => setCopyTooltip(copyTooltipText), 200)
|
||||
}
|
||||
/>
|
||||
<ActionButton
|
||||
icon={
|
||||
<Icon
|
||||
name="email"
|
||||
width={15}
|
||||
height={15}
|
||||
style={{ marginTop: 1 }}
|
||||
/>
|
||||
}
|
||||
tooltip={t('Share chart by email')}
|
||||
onClick={doShareEmail}
|
||||
/>
|
||||
<EmbedCodeButton latestQueryFormData={latestQueryFormData} />
|
||||
<ActionButton
|
||||
icon={<i className="fa fa-file-code-o" />}
|
||||
text=".JSON"
|
||||
tooltip={t('Export to .JSON format')}
|
||||
onClick={doExportChart}
|
||||
/>
|
||||
<ActionButton
|
||||
icon={<i className="fa fa-file-text-o" />}
|
||||
text=".CSV"
|
||||
tooltip={t('Export to .CSV format')}
|
||||
onClick={doExportCSV}
|
||||
className={exportToCSVClasses}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<ConnectedDisplayQueryButton
|
||||
chartHeight={chartHeight}
|
||||
queryResponse={queriesResponse?.[0]}
|
||||
latestQueryFormData={latestQueryFormData}
|
||||
chartStatus={chartStatus}
|
||||
onOpenInEditor={actions.redirectSQLLab}
|
||||
onOpenPropertiesModal={actions.openPropertiesModal}
|
||||
slice={slice}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default withToasts(ExploreActionButtons);
|
||||
@@ -17,8 +17,8 @@
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
const copyTextToClipboard = (text: string) =>
|
||||
new Promise((resolve, reject) => {
|
||||
const copyTextToClipboard = async (text: string) =>
|
||||
new Promise<void>((resolve, reject) => {
|
||||
const selection: Selection | null = document.getSelection();
|
||||
if (selection) {
|
||||
selection.removeAllRanges();
|
||||
|
||||
@@ -142,10 +142,10 @@ function ChartTable({
|
||||
buttons={[
|
||||
{
|
||||
name: (
|
||||
<div>
|
||||
<>
|
||||
<i className="fa fa-plus" />
|
||||
{t('Chart')}
|
||||
</div>
|
||||
</>
|
||||
),
|
||||
buttonStyle: 'tertiary',
|
||||
onClick: () => {
|
||||
|
||||
@@ -148,9 +148,10 @@ function DashboardTable({
|
||||
buttons={[
|
||||
{
|
||||
name: (
|
||||
<div>
|
||||
<i className="fa fa-plus" /> Dashboard{' '}
|
||||
</div>
|
||||
<>
|
||||
<i className="fa fa-plus" />
|
||||
Dashboard
|
||||
</>
|
||||
),
|
||||
buttonStyle: 'tertiary',
|
||||
onClick: () => {
|
||||
|
||||
@@ -267,10 +267,10 @@ const SavedQueries = ({
|
||||
buttons={[
|
||||
{
|
||||
name: (
|
||||
<div>
|
||||
<>
|
||||
<i className="fa fa-plus" />
|
||||
SQL Query{' '}
|
||||
</div>
|
||||
SQL Query
|
||||
</>
|
||||
),
|
||||
buttonStyle: 'tertiary',
|
||||
onClick: () => {
|
||||
|
||||
Reference in New Issue
Block a user