mirror of
https://github.com/apache/superset.git
synced 2026-04-19 08:04:53 +00:00
feat(explore): Move chart actions into dropdown (#19446)
* feat(explore): Move chart actions to a dropdown menu * Fix tests and add some new ones * Add background color to embed code button * Fix cypress tests * Move copy permalink to actions menu * Remove unused function * Move share by email above embed code * Fix test * Fix test * Fix test * Fix test * Fix test
This commit is contained in:
committed by
GitHub
parent
85e330e94b
commit
1a1322d3d9
@@ -38,7 +38,7 @@ describe('Test explore links', () => {
|
||||
cy.visitChartByName('Growth Rate');
|
||||
cy.verifySliceSuccess({ waitAlias: '@chartData' });
|
||||
|
||||
cy.get('div#query').click();
|
||||
cy.get('[aria-label="Menu actions trigger"]').click();
|
||||
cy.get('span').contains('View query').parent().click();
|
||||
cy.wait('@chartData').then(() => {
|
||||
cy.get('code');
|
||||
@@ -52,7 +52,12 @@ describe('Test explore links', () => {
|
||||
cy.visitChartByName('Growth Rate');
|
||||
cy.verifySliceSuccess({ waitAlias: '@chartData' });
|
||||
|
||||
cy.get('[data-test=embed-code-button]').click();
|
||||
cy.get('[aria-label="Menu actions trigger"]').click();
|
||||
cy.get('div[title="Share"]').trigger('mouseover');
|
||||
// need to use [id= syntax, otherwise error gets triggered because of special character in id
|
||||
cy.get('[id="share_submenu$Menu"]').within(() => {
|
||||
cy.contains('Embed code').parent().click();
|
||||
});
|
||||
cy.get('#embed-code-popover').within(() => {
|
||||
cy.get('textarea[name=embedCode]').contains('iframe');
|
||||
});
|
||||
|
||||
@@ -39,6 +39,7 @@ const propTypes = {
|
||||
resizableConfig: PropTypes.object,
|
||||
draggable: PropTypes.bool,
|
||||
draggableConfig: PropTypes.object,
|
||||
destroyOnClose: PropTypes.bool,
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
@@ -89,6 +90,7 @@ export default class ModalTrigger extends React.Component {
|
||||
resizableConfig={this.props.resizableConfig}
|
||||
draggable={this.props.draggable}
|
||||
draggableConfig={this.props.draggableConfig}
|
||||
destroyOnClose={this.props.destroyOnClose}
|
||||
>
|
||||
{this.props.modalBody}
|
||||
</Modal>
|
||||
|
||||
@@ -26,8 +26,7 @@ import React, {
|
||||
} from 'react';
|
||||
import { t, SupersetTheme } from '@superset-ui/core';
|
||||
import { getClientErrorObject } from 'src/utils/getClientErrorObject';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import { connect, useDispatch, useSelector } from 'react-redux';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { addReport, editReport } from 'src/reports/actions/reports';
|
||||
import { AlertObject } from 'src/views/CRUD/alert/types';
|
||||
|
||||
@@ -85,6 +84,7 @@ interface ChartObject {
|
||||
chartUpdateEndTime: number;
|
||||
chartUpdateStartTime: number;
|
||||
latestQueryFormData: object;
|
||||
sliceFormData: Record<string, any>;
|
||||
queryController: { abort: () => {} };
|
||||
queriesResponse: object;
|
||||
triggerQuery: boolean;
|
||||
@@ -92,7 +92,6 @@ interface ChartObject {
|
||||
}
|
||||
|
||||
interface ReportProps {
|
||||
addReport: (report?: ReportObject) => {};
|
||||
onHide: () => {};
|
||||
onReportAdd: (report?: ReportObject) => {};
|
||||
addDangerToast: (msg: string) => void;
|
||||
@@ -102,7 +101,6 @@ interface ReportProps {
|
||||
dashboardId?: number;
|
||||
chart?: ChartObject;
|
||||
creationMethod: string;
|
||||
props: any;
|
||||
}
|
||||
|
||||
interface ReportPayloadType {
|
||||
@@ -189,8 +187,8 @@ const ReportModal: FunctionComponent<ReportProps> = ({
|
||||
show = false,
|
||||
...props
|
||||
}) => {
|
||||
const vizType = props.props.chart?.sliceFormData?.viz_type;
|
||||
const isChart = !!props.props.chart;
|
||||
const vizType = props.chart?.sliceFormData?.viz_type;
|
||||
const isChart = !!props.chart;
|
||||
const defaultNotificationFormat =
|
||||
isChart && TEXT_BASED_VISUALIZATION_TYPES.includes(vizType)
|
||||
? NOTIFICATION_FORMATS.TEXT
|
||||
@@ -226,19 +224,19 @@ const ReportModal: FunctionComponent<ReportProps> = ({
|
||||
// Create new Report
|
||||
const newReportValues: Partial<ReportObject> = {
|
||||
crontab: currentReport?.crontab,
|
||||
dashboard: props.props.dashboardId,
|
||||
chart: props.props.chart?.id,
|
||||
dashboard: props.dashboardId,
|
||||
chart: props.chart?.id,
|
||||
description: currentReport?.description,
|
||||
name: currentReport?.name,
|
||||
owners: [props.props.userId],
|
||||
owners: [props.userId],
|
||||
recipients: [
|
||||
{
|
||||
recipient_config_json: { target: props.props.userEmail },
|
||||
recipient_config_json: { target: props.userEmail },
|
||||
type: 'Email',
|
||||
},
|
||||
],
|
||||
type: 'Report',
|
||||
creation_method: props.props.creationMethod,
|
||||
creation_method: props.creationMethod,
|
||||
active: true,
|
||||
report_format: currentReport?.report_format || defaultNotificationFormat,
|
||||
timezone: currentReport?.timezone,
|
||||
@@ -416,7 +414,4 @@ const ReportModal: FunctionComponent<ReportProps> = ({
|
||||
);
|
||||
};
|
||||
|
||||
const mapDispatchToProps = (dispatch: any) =>
|
||||
bindActionCreators({ addReport, editReport }, dispatch);
|
||||
|
||||
export default connect(null, mapDispatchToProps)(withToasts(ReportModal));
|
||||
export default withToasts(ReportModal);
|
||||
|
||||
@@ -662,12 +662,10 @@ class Header extends React.PureComponent {
|
||||
<ReportModal
|
||||
show={this.state.showingReportModal}
|
||||
onHide={this.hideReportModal}
|
||||
props={{
|
||||
userId: user.userId,
|
||||
userEmail: user.email,
|
||||
dashboardId: dashboardInfo.id,
|
||||
creationMethod: 'dashboards',
|
||||
}}
|
||||
userId={user.userId}
|
||||
userEmail={user.email}
|
||||
dashboardId={dashboardInfo.id}
|
||||
creationMethod="dashboards"
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@@ -1,168 +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 Popover from 'src/components/Popover';
|
||||
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 { URL_PARAMS } from 'src/constants';
|
||||
import { getChartPermalink } from 'src/utils/urlUtils';
|
||||
|
||||
export default class EmbedCodeButton extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
height: '400',
|
||||
width: '600',
|
||||
url: '',
|
||||
errorMessage: '',
|
||||
};
|
||||
this.handleInputChange = this.handleInputChange.bind(this);
|
||||
this.updateUrl = this.updateUrl.bind(this);
|
||||
}
|
||||
|
||||
handleInputChange(e) {
|
||||
const { value, name } = e.currentTarget;
|
||||
const data = {};
|
||||
data[name] = value;
|
||||
this.setState(data);
|
||||
}
|
||||
|
||||
updateUrl() {
|
||||
this.setState({ url: '' });
|
||||
getChartPermalink(this.props.formData)
|
||||
.then(url => this.setState({ errorMessage: '', url }))
|
||||
.catch(() => {
|
||||
this.setState({ errorMessage: t('Error') });
|
||||
this.props.addDangerToast(
|
||||
t('Sorry, something went wrong. Try again later.'),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
generateEmbedHTML() {
|
||||
if (!this.state.url) return '';
|
||||
const srcLink = `${this.state.url}?${URL_PARAMS.standalone.name}=1&height=${this.state.height}`;
|
||||
return (
|
||||
'<iframe\n' +
|
||||
` width="${this.state.width}"\n` +
|
||||
` height="${this.state.height}"\n` +
|
||||
' seamless\n' +
|
||||
' frameBorder="0"\n' +
|
||||
' scrolling="no"\n' +
|
||||
` src="${srcLink}"\n` +
|
||||
'>\n' +
|
||||
'</iframe>'
|
||||
);
|
||||
}
|
||||
|
||||
renderPopoverContent() {
|
||||
const html = this.generateEmbedHTML();
|
||||
const text =
|
||||
this.state.errorMessage || html || t('Generating link, please wait..');
|
||||
return (
|
||||
<div id="embed-code-popover" data-test="embed-code-popover">
|
||||
<div className="row">
|
||||
<div className="col-sm-10">
|
||||
<textarea
|
||||
data-test="embed-code-textarea"
|
||||
name="embedCode"
|
||||
disabled={!html}
|
||||
value={text}
|
||||
rows="4"
|
||||
readOnly
|
||||
className="form-control input-sm"
|
||||
style={{ resize: 'vertical' }}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-sm-2">
|
||||
<CopyToClipboard
|
||||
shouldShowText={false}
|
||||
text={html}
|
||||
copyNode={
|
||||
<i className="fa fa-clipboard" title={t('Copy to clipboard')} />
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<br />
|
||||
<div className="row">
|
||||
<div className="col-md-6 col-sm-12">
|
||||
<div className="form-group">
|
||||
<small>
|
||||
<FormLabel htmlFor="embed-height">{t('Height')}</FormLabel>
|
||||
</small>
|
||||
<input
|
||||
className="form-control input-sm"
|
||||
type="text"
|
||||
defaultValue={this.state.height}
|
||||
name="height"
|
||||
onChange={this.handleInputChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-md-6 col-sm-12">
|
||||
<div className="form-group">
|
||||
<small>
|
||||
<FormLabel htmlFor="embed-width">{t('Width')}</FormLabel>
|
||||
</small>
|
||||
<input
|
||||
className="form-control input-sm"
|
||||
type="text"
|
||||
defaultValue={this.state.width}
|
||||
name="width"
|
||||
onChange={this.handleInputChange}
|
||||
id="embed-width"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Popover
|
||||
trigger="click"
|
||||
placement="left"
|
||||
onClick={this.updateUrl}
|
||||
content={this.renderPopoverContent()}
|
||||
>
|
||||
<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={{ display: 'flex', alignItems: 'center', height: 30 }}
|
||||
>
|
||||
<Icons.Code iconSize="l" />
|
||||
</div>
|
||||
</Tooltip>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,62 +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 { shallow } from 'enzyme';
|
||||
import { styledMount as mount } from 'spec/helpers/theming';
|
||||
import Popover from 'src/components/Popover';
|
||||
import EmbedCodeButton from 'src/explore/components/EmbedCodeButton';
|
||||
import { DashboardStandaloneMode } from 'src/dashboard/util/constants';
|
||||
|
||||
describe('EmbedCodeButton', () => {
|
||||
it('renders', () => {
|
||||
expect(React.isValidElement(<EmbedCodeButton />)).toBe(true);
|
||||
});
|
||||
|
||||
it('renders overlay trigger', () => {
|
||||
const wrapper = shallow(<EmbedCodeButton />);
|
||||
expect(wrapper.find(Popover)).toExist();
|
||||
});
|
||||
|
||||
it('returns correct embed code', () => {
|
||||
const wrapper = mount(
|
||||
<EmbedCodeButton formData={{}} addDangerToast={() => {}} />,
|
||||
);
|
||||
const url = 'http://localhost/explore/p/100';
|
||||
wrapper.find(EmbedCodeButton).setState({
|
||||
height: '1000',
|
||||
width: '2000',
|
||||
url,
|
||||
});
|
||||
const embedHTML =
|
||||
`${
|
||||
'<iframe\n' +
|
||||
' width="2000"\n' +
|
||||
' height="1000"\n' +
|
||||
' seamless\n' +
|
||||
' frameBorder="0"\n' +
|
||||
' scrolling="no"\n' +
|
||||
` src="${url}?standalone=`
|
||||
}${DashboardStandaloneMode.HIDE_NAV}&height=1000"\n` +
|
||||
`>\n` +
|
||||
`</iframe>`;
|
||||
expect(wrapper.find(EmbedCodeButton).instance().generateEmbedHTML()).toBe(
|
||||
embedHTML,
|
||||
);
|
||||
});
|
||||
});
|
||||
153
superset-frontend/src/explore/components/EmbedCodeContent.jsx
Normal file
153
superset-frontend/src/explore/components/EmbedCodeContent.jsx
Normal file
@@ -0,0 +1,153 @@
|
||||
/**
|
||||
* 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, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { css, styled, t } from '@superset-ui/core';
|
||||
import { Input, TextArea } from 'src/components/Input';
|
||||
import CopyToClipboard from 'src/components/CopyToClipboard';
|
||||
import { URL_PARAMS } from 'src/constants';
|
||||
import { getChartPermalink } from 'src/utils/urlUtils';
|
||||
import { CopyButton } from './DataTableControl';
|
||||
|
||||
const CopyButtonEmbedCode = styled(CopyButton)`
|
||||
&& {
|
||||
margin: 0 0 ${({ theme }) => theme.gridUnit}px;
|
||||
}
|
||||
`;
|
||||
|
||||
const EmbedCodeContent = ({ formData, addDangerToast }) => {
|
||||
const [height, setHeight] = useState('400');
|
||||
const [width, setWidth] = useState('600');
|
||||
const [url, setUrl] = useState('');
|
||||
const [errorMessage, setErrorMessage] = useState('');
|
||||
|
||||
const handleInputChange = useCallback(e => {
|
||||
const { value, name } = e.currentTarget;
|
||||
if (name === 'width') {
|
||||
setWidth(value);
|
||||
}
|
||||
if (name === 'height') {
|
||||
setHeight(value);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const updateUrl = useCallback(() => {
|
||||
setUrl('');
|
||||
getChartPermalink(formData)
|
||||
.then(url => {
|
||||
setUrl(url);
|
||||
setErrorMessage('');
|
||||
})
|
||||
.catch(() => {
|
||||
setErrorMessage(t('Error'));
|
||||
addDangerToast(t('Sorry, something went wrong. Try again later.'));
|
||||
});
|
||||
}, [addDangerToast, formData]);
|
||||
|
||||
useEffect(() => {
|
||||
updateUrl();
|
||||
}, []);
|
||||
|
||||
const html = useMemo(() => {
|
||||
if (!url) return '';
|
||||
const srcLink = `${url}?${URL_PARAMS.standalone.name}=1&height=${height}`;
|
||||
return (
|
||||
'<iframe\n' +
|
||||
` width="${width}"\n` +
|
||||
` height="${height}"\n` +
|
||||
' seamless\n' +
|
||||
' frameBorder="0"\n' +
|
||||
' scrolling="no"\n' +
|
||||
` src="${srcLink}"\n` +
|
||||
'>\n' +
|
||||
'</iframe>'
|
||||
);
|
||||
}, [height, url, width]);
|
||||
|
||||
const text = errorMessage || html || t('Generating link, please wait..');
|
||||
return (
|
||||
<div id="embed-code-popover" data-test="embed-code-popover">
|
||||
<div
|
||||
css={css`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
`}
|
||||
>
|
||||
<CopyToClipboard
|
||||
shouldShowText={false}
|
||||
text={html}
|
||||
copyNode={
|
||||
<CopyButtonEmbedCode buttonSize="xsmall">
|
||||
<i className="fa fa-clipboard" />
|
||||
</CopyButtonEmbedCode>
|
||||
}
|
||||
/>
|
||||
<TextArea
|
||||
data-test="embed-code-textarea"
|
||||
name="embedCode"
|
||||
disabled={!html}
|
||||
value={text}
|
||||
rows="4"
|
||||
readOnly
|
||||
css={theme => css`
|
||||
resize: vertical;
|
||||
padding: ${theme.gridUnit * 2}px;
|
||||
font-size: ${theme.typography.sizes.s}px;
|
||||
border-radius: 4px;
|
||||
background-color: ${theme.colors.secondary.light5};
|
||||
`}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
css={theme => css`
|
||||
display: flex;
|
||||
margin-top: ${theme.gridUnit * 4}px;
|
||||
& > div {
|
||||
margin-right: ${theme.gridUnit * 2}px;
|
||||
}
|
||||
& > div:last-of-type {
|
||||
margin-right: 0;
|
||||
margin-left: ${theme.gridUnit * 2}px;
|
||||
}
|
||||
`}
|
||||
>
|
||||
<div>
|
||||
<label htmlFor="embed-height">{t('Chart height')}</label>
|
||||
<Input
|
||||
type="text"
|
||||
defaultValue={height}
|
||||
name="height"
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="embed-width">{t('Chart width')}</label>
|
||||
<Input
|
||||
type="text"
|
||||
defaultValue={width}
|
||||
name="width"
|
||||
onChange={handleInputChange}
|
||||
id="embed-width"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default EmbedCodeContent;
|
||||
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
* 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 { render, screen } from 'spec/helpers/testing-library';
|
||||
import EmbedCodeContent from 'src/explore/components/EmbedCodeContent';
|
||||
|
||||
const url = 'http://localhost/explore/p/100';
|
||||
fetchMock.post('glob:*/api/v1/explore/permalink', { url });
|
||||
|
||||
describe('EmbedCodeButton', () => {
|
||||
it('renders', () => {
|
||||
expect(React.isValidElement(<EmbedCodeContent />)).toBe(true);
|
||||
});
|
||||
|
||||
it('returns correct embed code', async () => {
|
||||
render(<EmbedCodeContent />, { useRedux: true });
|
||||
expect(await screen.findByText('iframe', { exact: false })).toBeVisible();
|
||||
expect(await screen.findByText('/iframe', { exact: false })).toBeVisible();
|
||||
expect(
|
||||
await screen.findByText('width="600"', { exact: false }),
|
||||
).toBeVisible();
|
||||
expect(
|
||||
await screen.findByText('height="400"', { exact: false }),
|
||||
).toBeVisible();
|
||||
expect(
|
||||
await screen.findByText(`src="${url}?standalone=1&height=400"`, {
|
||||
exact: false,
|
||||
}),
|
||||
).toBeVisible();
|
||||
});
|
||||
});
|
||||
@@ -1,117 +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 { shallow, mount } from 'enzyme';
|
||||
import { mockStore } from 'spec/fixtures/mockStore';
|
||||
import ExploreActionButtons from 'src/explore/components/ExploreActionButtons';
|
||||
import * as exploreUtils from 'src/explore/exploreUtils';
|
||||
|
||||
import { supersetTheme, ThemeProvider } from '@superset-ui/core';
|
||||
import { Provider } from 'react-redux';
|
||||
import sinon from 'sinon';
|
||||
|
||||
describe('ExploreActionButtons', () => {
|
||||
const defaultProps = {
|
||||
actions: {},
|
||||
canDownloadCSV: 'True',
|
||||
latestQueryFormData: {},
|
||||
queryEndpoint: 'localhost',
|
||||
chartHeight: '30px',
|
||||
};
|
||||
|
||||
it('renders', () => {
|
||||
expect(
|
||||
React.isValidElement(<ExploreActionButtons {...defaultProps} />),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('should render 6 children/buttons', () => {
|
||||
const wrapper = shallow(
|
||||
<ExploreActionButtons {...defaultProps} store={mockStore} />,
|
||||
);
|
||||
expect(wrapper.dive().children()).toHaveLength(6);
|
||||
});
|
||||
|
||||
describe('ExploreActionButtons and no permission to download CSV', () => {
|
||||
let wrapper;
|
||||
const defaultProps = {
|
||||
actions: {},
|
||||
canDownloadCSV: false,
|
||||
latestQueryFormData: {},
|
||||
queryEndpoint: 'localhost',
|
||||
chartHeight: '30px',
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
wrapper = mount(
|
||||
<ThemeProvider theme={supersetTheme}>
|
||||
<ExploreActionButtons {...defaultProps} />
|
||||
</ThemeProvider>,
|
||||
{
|
||||
wrappingComponent: Provider,
|
||||
wrappingComponentProps: {
|
||||
store: mockStore,
|
||||
},
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it('should render csv buttons but is disabled and not clickable', () => {
|
||||
const spyExportChart = sinon.spy(exploreUtils, 'exportChart');
|
||||
|
||||
const csvButton = wrapper.find('div.disabled');
|
||||
expect(wrapper).toHaveLength(1);
|
||||
csvButton.simulate('click');
|
||||
expect(spyExportChart.callCount).toBe(0);
|
||||
spyExportChart.restore();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Dropdown csv button when viz type is pivot table', () => {
|
||||
let wrapper;
|
||||
const defaultProps = {
|
||||
actions: {},
|
||||
canDownloadCSV: false,
|
||||
latestQueryFormData: { viz_type: 'pivot_table_v2' },
|
||||
queryEndpoint: 'localhost',
|
||||
chartHeight: '30px',
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
wrapper = mount(
|
||||
<ThemeProvider theme={supersetTheme}>
|
||||
<ExploreActionButtons {...defaultProps} />
|
||||
</ThemeProvider>,
|
||||
{
|
||||
wrappingComponent: Provider,
|
||||
wrappingComponentProps: {
|
||||
store: mockStore,
|
||||
},
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it('should render a dropdown button when viz type is pivot table', () => {
|
||||
const csvTrigger = wrapper.find(
|
||||
'div[role="button"] span[aria-label="caret-down"]',
|
||||
);
|
||||
expect(csvTrigger).toExist();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,234 +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, { ReactElement, useState } from 'react';
|
||||
import cx from 'classnames';
|
||||
import { QueryFormData, t } from '@superset-ui/core';
|
||||
import Icons from 'src/components/Icons';
|
||||
import { Tooltip } from 'src/components/Tooltip';
|
||||
import { Slice } from 'src/types/Chart';
|
||||
import copyTextToClipboard from 'src/utils/copy';
|
||||
import { getChartPermalink } from 'src/utils/urlUtils';
|
||||
import withToasts from 'src/components/MessageToasts/withToasts';
|
||||
import EmbedCodeButton from './EmbedCodeButton';
|
||||
import { exportChart } from '../exploreUtils';
|
||||
import ExploreAdditionalActionsMenu from './ExploreAdditionalActionsMenu';
|
||||
import { ExportToCSVDropdown } from './ExportToCSVDropdown';
|
||||
|
||||
type ActionButtonProps = {
|
||||
prefixIcon: React.ReactElement;
|
||||
suffixIcon?: React.ReactElement;
|
||||
text?: string | ReactElement;
|
||||
tooltip: string;
|
||||
className?: string;
|
||||
onClick?: React.MouseEventHandler<HTMLElement>;
|
||||
onTooltipVisibilityChange?: (visible: boolean) => void;
|
||||
'data-test'?: string;
|
||||
};
|
||||
|
||||
type ExploreActionButtonsProps = {
|
||||
actions: { redirectSQLLab: () => void; openPropertiesModal: () => void };
|
||||
canDownloadCSV: boolean;
|
||||
chartStatus: string;
|
||||
latestQueryFormData: QueryFormData;
|
||||
queriesResponse: {};
|
||||
slice: Slice;
|
||||
addDangerToast: Function;
|
||||
addSuccessToast: Function;
|
||||
};
|
||||
|
||||
const VIZ_TYPES_PIVOTABLE = ['pivot_table', 'pivot_table_v2'];
|
||||
|
||||
const ActionButton = (props: ActionButtonProps) => {
|
||||
const {
|
||||
prefixIcon,
|
||||
suffixIcon,
|
||||
text,
|
||||
tooltip,
|
||||
className,
|
||||
onTooltipVisibilityChange,
|
||||
...rest
|
||||
} = props;
|
||||
return (
|
||||
<Tooltip
|
||||
id={`${prefixIcon}-tooltip`}
|
||||
placement="top"
|
||||
title={tooltip}
|
||||
trigger={['hover']}
|
||||
onVisibleChange={onTooltipVisibilityChange}
|
||||
>
|
||||
<div
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
css={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
'&:focus, &:focus:active': { outline: 0 },
|
||||
}}
|
||||
className={className || 'btn btn-default btn-sm'}
|
||||
style={{ height: 30 }}
|
||||
{...rest}
|
||||
>
|
||||
{prefixIcon}
|
||||
{text && <span style={{ marginLeft: 5 }}>{text}</span>}
|
||||
{suffixIcon}
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
const ExploreActionButtons = (props: ExploreActionButtonsProps) => {
|
||||
const {
|
||||
actions,
|
||||
canDownloadCSV,
|
||||
chartStatus,
|
||||
latestQueryFormData,
|
||||
slice,
|
||||
addDangerToast,
|
||||
addSuccessToast,
|
||||
} = props;
|
||||
|
||||
const copyTooltipText = t('Copy permalink to clipboard');
|
||||
const [copyTooltip, setCopyTooltip] = useState(copyTooltipText);
|
||||
|
||||
const doCopyLink = async () => {
|
||||
try {
|
||||
setCopyTooltip(t('Loading...'));
|
||||
const url = await getChartPermalink(latestQueryFormData);
|
||||
await copyTextToClipboard(url);
|
||||
setCopyTooltip(t('Copied to clipboard!'));
|
||||
addSuccessToast(t('Copied to clipboard!'));
|
||||
} catch (error) {
|
||||
setCopyTooltip(t('Copying permalink failed.'));
|
||||
addDangerToast(t('Sorry, something went wrong. Try again later.'));
|
||||
}
|
||||
};
|
||||
|
||||
const doShareEmail = async () => {
|
||||
try {
|
||||
const subject = t('Superset Chart');
|
||||
const url = await getChartPermalink(latestQueryFormData);
|
||||
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.'));
|
||||
}
|
||||
};
|
||||
|
||||
const doExportCSV = canDownloadCSV
|
||||
? exportChart.bind(this, {
|
||||
formData: latestQueryFormData,
|
||||
resultType: 'full',
|
||||
resultFormat: 'csv',
|
||||
})
|
||||
: null;
|
||||
|
||||
const doExportCSVPivoted = canDownloadCSV
|
||||
? exportChart.bind(this, {
|
||||
formData: latestQueryFormData,
|
||||
resultType: 'post_processed',
|
||||
resultFormat: 'csv',
|
||||
})
|
||||
: null;
|
||||
|
||||
const doExportJson = exportChart.bind(this, {
|
||||
formData: latestQueryFormData,
|
||||
resultType: 'results',
|
||||
resultFormat: 'json',
|
||||
});
|
||||
|
||||
const exportToCSVClasses = cx('btn btn-default btn-sm', {
|
||||
disabled: !canDownloadCSV,
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
className="btn-group results"
|
||||
role="group"
|
||||
data-test="btn-group-results"
|
||||
>
|
||||
{latestQueryFormData && (
|
||||
<>
|
||||
<ActionButton
|
||||
prefixIcon={<Icons.Link iconSize="l" />}
|
||||
tooltip={copyTooltip}
|
||||
onClick={doCopyLink}
|
||||
data-test="short-link-button"
|
||||
onTooltipVisibilityChange={value =>
|
||||
!value && setTimeout(() => setCopyTooltip(copyTooltipText), 200)
|
||||
}
|
||||
/>
|
||||
<ActionButton
|
||||
prefixIcon={<Icons.Email iconSize="l" />}
|
||||
tooltip={t('Share permalink by email')}
|
||||
onClick={doShareEmail}
|
||||
/>
|
||||
<EmbedCodeButton
|
||||
formData={latestQueryFormData}
|
||||
addDangerToast={addDangerToast}
|
||||
/>
|
||||
<ActionButton
|
||||
prefixIcon={<Icons.FileTextOutlined iconSize="m" />}
|
||||
text=".JSON"
|
||||
tooltip={t('Export to .JSON format')}
|
||||
onClick={doExportJson}
|
||||
/>
|
||||
{VIZ_TYPES_PIVOTABLE.includes(latestQueryFormData.viz_type) ? (
|
||||
<ExportToCSVDropdown
|
||||
exportCSVOriginal={doExportCSV}
|
||||
exportCSVPivoted={doExportCSVPivoted}
|
||||
>
|
||||
<ActionButton
|
||||
prefixIcon={<Icons.FileExcelOutlined iconSize="m" />}
|
||||
suffixIcon={
|
||||
<Icons.CaretDown
|
||||
iconSize="l"
|
||||
css={theme => `
|
||||
margin-left: ${theme.gridUnit}px;
|
||||
margin-right: ${-theme.gridUnit}px;
|
||||
`}
|
||||
/>
|
||||
}
|
||||
text=".CSV"
|
||||
tooltip={t('Export to .CSV format')}
|
||||
className={exportToCSVClasses}
|
||||
/>
|
||||
</ExportToCSVDropdown>
|
||||
) : (
|
||||
<ActionButton
|
||||
prefixIcon={<Icons.FileExcelOutlined iconSize="m" />}
|
||||
text=".CSV"
|
||||
tooltip={t('Export to .CSV format')}
|
||||
onClick={doExportCSV}
|
||||
className={exportToCSVClasses}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<ExploreAdditionalActionsMenu
|
||||
latestQueryFormData={latestQueryFormData}
|
||||
chartStatus={chartStatus}
|
||||
onOpenInEditor={actions.redirectSQLLab}
|
||||
onOpenPropertiesModal={actions.openPropertiesModal}
|
||||
slice={slice}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default withToasts(ExploreActionButtons);
|
||||
@@ -1,60 +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 { shallow } from 'enzyme';
|
||||
import { styledMount as mount } from 'spec/helpers/theming';
|
||||
import thunk from 'redux-thunk';
|
||||
import configureStore from 'redux-mock-store';
|
||||
import { AntdDropdown } from 'src/components';
|
||||
import { Menu } from 'src/components/Menu';
|
||||
import ExploreAdditionalActionsMenu from 'src/explore/components/ExploreAdditionalActionsMenu';
|
||||
|
||||
const mockStore = configureStore([thunk]);
|
||||
const store = mockStore({});
|
||||
|
||||
describe('ExploreAdditionalActionsMenu', () => {
|
||||
const defaultProps = {
|
||||
animation: false,
|
||||
queryResponse: {
|
||||
query: 'SELECT * FROM foo',
|
||||
language: 'sql',
|
||||
},
|
||||
chartStatus: 'success',
|
||||
queryEndpoint: 'localhost',
|
||||
latestQueryFormData: {
|
||||
datasource: '1__table',
|
||||
},
|
||||
chartHeight: '30px',
|
||||
};
|
||||
|
||||
it('is valid', () => {
|
||||
expect(
|
||||
React.isValidElement(<ExploreAdditionalActionsMenu {...defaultProps} />),
|
||||
).toBe(true);
|
||||
});
|
||||
it('renders a dropdown with 3 items', () => {
|
||||
const wrapper = mount(
|
||||
<ExploreAdditionalActionsMenu store={store} {...defaultProps} />,
|
||||
);
|
||||
const dropdown = wrapper.find(AntdDropdown);
|
||||
const menu = shallow(<div>{dropdown.prop('overlay')}</div>);
|
||||
const menuItems = menu.find(Menu.Item);
|
||||
expect(menuItems).toHaveLength(3);
|
||||
});
|
||||
});
|
||||
@@ -18,11 +18,13 @@
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { render, screen, waitFor } from 'spec/helpers/testing-library';
|
||||
import fetchMock from 'fetch-mock';
|
||||
import sinon from 'sinon';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { render, screen, waitFor } from 'spec/helpers/testing-library';
|
||||
import * as chartAction from 'src/components/Chart/chartAction';
|
||||
import * as downloadAsImage from 'src/utils/downloadAsImage';
|
||||
import fetchMock from 'fetch-mock';
|
||||
import * as exploreUtils from 'src/explore/exploreUtils';
|
||||
import ExploreAdditionalActionsMenu from '.';
|
||||
|
||||
const createProps = () => ({
|
||||
@@ -78,6 +80,8 @@ const createProps = () => ({
|
||||
chartStatus: 'rendered',
|
||||
onOpenPropertiesModal: jest.fn(),
|
||||
onOpenInEditor: jest.fn(),
|
||||
canAddReports: false,
|
||||
canDownloadCSV: false,
|
||||
});
|
||||
|
||||
fetchMock.post(
|
||||
@@ -106,28 +110,105 @@ test('Should open a menu', () => {
|
||||
expect(props.onOpenInEditor).toBeCalledTimes(0);
|
||||
expect(props.onOpenPropertiesModal).toBeCalledTimes(0);
|
||||
|
||||
expect(
|
||||
screen.getByRole('menuitem', { name: 'Edit properties' }),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByRole('menuitem', { name: 'View query' }),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByRole('menuitem', { name: 'Run in SQL Lab' }),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByRole('menuitem', { name: 'Download as image' }),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByText('Edit chart properties')).toBeInTheDocument();
|
||||
expect(screen.getByText('Download')).toBeInTheDocument();
|
||||
expect(screen.getByText('Share')).toBeInTheDocument();
|
||||
expect(screen.getByText('View query')).toBeInTheDocument();
|
||||
expect(screen.getByText('Run in SQL Lab')).toBeInTheDocument();
|
||||
|
||||
expect(screen.queryByText('Set up an email report')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('Manage email report')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('Should call onOpenPropertiesModal when click on "Edit properties"', () => {
|
||||
test('Menu has email report item if user can add report', () => {
|
||||
const props = createProps();
|
||||
props.canAddReports = true;
|
||||
render(<ExploreAdditionalActionsMenu {...props} />, {
|
||||
useRedux: true,
|
||||
});
|
||||
|
||||
userEvent.click(screen.getByRole('button'));
|
||||
expect(screen.queryByText('Manage email report')).not.toBeInTheDocument();
|
||||
expect(screen.getByText('Set up an email report')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('Should open download submenu', async () => {
|
||||
const props = createProps();
|
||||
render(<ExploreAdditionalActionsMenu {...props} />, {
|
||||
useRedux: true,
|
||||
});
|
||||
|
||||
userEvent.click(screen.getByRole('button'));
|
||||
|
||||
expect(screen.queryByText('Export to .CSV')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('Export to .JSON')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('Download as image')).not.toBeInTheDocument();
|
||||
|
||||
expect(screen.getByText('Download')).toBeInTheDocument();
|
||||
userEvent.hover(screen.getByText('Download'));
|
||||
expect(await screen.findByText('Export to .CSV')).toBeInTheDocument();
|
||||
expect(await screen.findByText('Export to .JSON')).toBeInTheDocument();
|
||||
expect(await screen.findByText('Download as image')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('Should open share submenu', async () => {
|
||||
const props = createProps();
|
||||
render(<ExploreAdditionalActionsMenu {...props} />, {
|
||||
useRedux: true,
|
||||
});
|
||||
|
||||
userEvent.click(screen.getByRole('button'));
|
||||
|
||||
expect(
|
||||
screen.queryByText('Copy permalink to clipboard'),
|
||||
).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('Embed code')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('Share chart by email')).not.toBeInTheDocument();
|
||||
|
||||
expect(screen.getByText('Share')).toBeInTheDocument();
|
||||
userEvent.hover(screen.getByText('Share'));
|
||||
expect(
|
||||
await screen.findByText('Copy permalink to clipboard'),
|
||||
).toBeInTheDocument();
|
||||
expect(await screen.findByText('Embed code')).toBeInTheDocument();
|
||||
expect(await screen.findByText('Share chart by email')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('Should open report submenu if report exists', async () => {
|
||||
const props = createProps();
|
||||
props.canAddReports = true;
|
||||
render(<ExploreAdditionalActionsMenu {...props} />, {
|
||||
useRedux: true,
|
||||
initialState: {
|
||||
reports: {
|
||||
'1': { name: 'Test report' },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
userEvent.click(screen.getByRole('button'));
|
||||
|
||||
expect(screen.queryByText('Email reports active')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('Edit email report')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('Download as image')).not.toBeInTheDocument();
|
||||
|
||||
expect(screen.getByText('Manage email report')).toBeInTheDocument();
|
||||
userEvent.hover(screen.getByText('Manage email report'));
|
||||
expect(await screen.findByText('Email reports active')).toBeInTheDocument();
|
||||
expect(await screen.findByText('Edit email report')).toBeInTheDocument();
|
||||
expect(await screen.findByText('Delete email report')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('Should call onOpenPropertiesModal when click on "Edit chart properties"', () => {
|
||||
const props = createProps();
|
||||
render(<ExploreAdditionalActionsMenu {...props} />, {
|
||||
useRedux: true,
|
||||
});
|
||||
expect(props.onOpenInEditor).toBeCalledTimes(0);
|
||||
userEvent.click(screen.getByRole('button'));
|
||||
userEvent.click(screen.getByRole('menuitem', { name: 'Edit properties' }));
|
||||
userEvent.click(
|
||||
screen.getByRole('menuitem', { name: 'Edit chart properties' }),
|
||||
);
|
||||
expect(props.onOpenPropertiesModal).toBeCalledTimes(1);
|
||||
});
|
||||
|
||||
@@ -162,18 +243,89 @@ test('Should call onOpenInEditor when click on "Run in SQL Lab"', () => {
|
||||
expect(props.onOpenInEditor).toBeCalledTimes(1);
|
||||
});
|
||||
|
||||
test('Should call downloadAsImage when click on "Download as image"', () => {
|
||||
const props = createProps();
|
||||
const spy = jest.spyOn(downloadAsImage, 'default');
|
||||
render(<ExploreAdditionalActionsMenu {...props} />, {
|
||||
useRedux: true,
|
||||
describe('Download', () => {
|
||||
let spyDownloadAsImage = sinon.spy();
|
||||
let spyExportChart = sinon.spy();
|
||||
|
||||
beforeEach(() => {
|
||||
spyDownloadAsImage = sinon.spy(downloadAsImage, 'default');
|
||||
spyExportChart = sinon.spy(exploreUtils, 'exportChart');
|
||||
});
|
||||
afterEach(() => {
|
||||
spyDownloadAsImage.restore();
|
||||
spyExportChart.restore();
|
||||
});
|
||||
test('Should call downloadAsImage when click on "Download as image"', async () => {
|
||||
const props = createProps();
|
||||
const spy = jest.spyOn(downloadAsImage, 'default');
|
||||
render(<ExploreAdditionalActionsMenu {...props} />, {
|
||||
useRedux: true,
|
||||
});
|
||||
|
||||
expect(spy).toBeCalledTimes(0);
|
||||
userEvent.click(screen.getByRole('button'));
|
||||
expect(spy).toBeCalledTimes(0);
|
||||
|
||||
userEvent.hover(screen.getByText('Download'));
|
||||
const downloadAsImageElement = await screen.findByText('Download as image');
|
||||
userEvent.click(downloadAsImageElement);
|
||||
|
||||
expect(spy).toBeCalledTimes(1);
|
||||
});
|
||||
|
||||
expect(spy).toBeCalledTimes(0);
|
||||
userEvent.click(screen.getByRole('button'));
|
||||
expect(spy).toBeCalledTimes(0);
|
||||
test('Should not export to CSV if canDownloadCSV=false', async () => {
|
||||
const props = createProps();
|
||||
render(<ExploreAdditionalActionsMenu {...props} />, {
|
||||
useRedux: true,
|
||||
});
|
||||
userEvent.click(screen.getByRole('button'));
|
||||
userEvent.hover(screen.getByText('Download'));
|
||||
const exportCSVElement = await screen.findByText('Export to .CSV');
|
||||
userEvent.click(exportCSVElement);
|
||||
expect(spyExportChart.callCount).toBe(0);
|
||||
spyExportChart.restore();
|
||||
});
|
||||
|
||||
userEvent.click(screen.getByRole('menuitem', { name: 'Download as image' }));
|
||||
test('Should export to CSV if canDownloadCSV=true', async () => {
|
||||
const props = createProps();
|
||||
props.canDownloadCSV = true;
|
||||
render(<ExploreAdditionalActionsMenu {...props} />, {
|
||||
useRedux: true,
|
||||
});
|
||||
|
||||
expect(spy).toBeCalledTimes(1);
|
||||
userEvent.click(screen.getByRole('button'));
|
||||
userEvent.hover(screen.getByText('Download'));
|
||||
const exportCSVElement = await screen.findByText('Export to .CSV');
|
||||
userEvent.click(exportCSVElement);
|
||||
expect(spyExportChart.callCount).toBe(1);
|
||||
spyExportChart.restore();
|
||||
});
|
||||
|
||||
test('Should export to JSON', async () => {
|
||||
const props = createProps();
|
||||
render(<ExploreAdditionalActionsMenu {...props} />, {
|
||||
useRedux: true,
|
||||
});
|
||||
|
||||
userEvent.click(screen.getByRole('button'));
|
||||
userEvent.hover(screen.getByText('Download'));
|
||||
const exportJsonElement = await screen.findByText('Export to .JSON');
|
||||
userEvent.click(exportJsonElement);
|
||||
expect(spyExportChart.callCount).toBe(1);
|
||||
});
|
||||
|
||||
test('Should export to pivoted CSV if canDownloadCSV=true and viz_type=pivot_table_v2', async () => {
|
||||
const props = createProps();
|
||||
props.canDownloadCSV = true;
|
||||
props.latestQueryFormData.viz_type = 'pivot_table_v2';
|
||||
render(<ExploreAdditionalActionsMenu {...props} />, {
|
||||
useRedux: true,
|
||||
});
|
||||
|
||||
userEvent.click(screen.getByRole('button'));
|
||||
userEvent.hover(screen.getByText('Download'));
|
||||
const exportCSVElement = await screen.findByText('Export to pivoted .CSV');
|
||||
userEvent.click(exportCSVElement);
|
||||
expect(spyExportChart.callCount).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
/**
|
||||
* 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, { useCallback } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import pick from 'lodash/pick';
|
||||
import { t } from '@superset-ui/core';
|
||||
import ReportModal from 'src/components/ReportModal';
|
||||
import { ExplorePageState } from 'src/explore/reducers/getInitialState';
|
||||
import DeleteModal from 'src/components/DeleteModal';
|
||||
import { deleteActiveReport } from 'src/reports/actions/reports';
|
||||
import { ChartState } from 'src/explore/types';
|
||||
|
||||
type ReportMenuItemsProps = {
|
||||
report: Record<string, any>;
|
||||
isVisible: boolean;
|
||||
onHide: () => void;
|
||||
isDeleting: boolean;
|
||||
setIsDeleting: (isDeleting: boolean) => void;
|
||||
};
|
||||
export const ExploreReport = ({
|
||||
report,
|
||||
isVisible,
|
||||
onHide,
|
||||
isDeleting,
|
||||
setIsDeleting,
|
||||
}: ReportMenuItemsProps) => {
|
||||
const dispatch = useDispatch();
|
||||
const chart = useSelector<ExplorePageState, ChartState | undefined>(state => {
|
||||
if (!state.charts) {
|
||||
return undefined;
|
||||
}
|
||||
const charts = Object.values(state.charts);
|
||||
if (charts.length > 0) {
|
||||
return charts[0];
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
const { userId, email } = useSelector<
|
||||
ExplorePageState,
|
||||
{ userId: number; email: string }
|
||||
>(state => pick(state.explore.user, ['userId', 'email']));
|
||||
|
||||
const handleReportDelete = useCallback(() => {
|
||||
dispatch(deleteActiveReport(report));
|
||||
setIsDeleting(false);
|
||||
}, [dispatch, report, setIsDeleting]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ReportModal
|
||||
show={isVisible}
|
||||
onHide={onHide}
|
||||
userId={userId}
|
||||
userEmail={email}
|
||||
chart={chart}
|
||||
creationMethod="charts"
|
||||
/>
|
||||
{isDeleting && (
|
||||
<DeleteModal
|
||||
description={t(
|
||||
'This action will permanently delete %s.',
|
||||
report.name,
|
||||
)}
|
||||
onConfirm={() => {
|
||||
if (report) {
|
||||
handleReportDelete();
|
||||
}
|
||||
}}
|
||||
onHide={() => setIsDeleting(false)}
|
||||
open
|
||||
title={t('Delete Report?')}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -16,114 +16,430 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
import { t } from '@superset-ui/core';
|
||||
import { FileOutlined, FileImageOutlined } from '@ant-design/icons';
|
||||
import { css, styled, t, useTheme } from '@superset-ui/core';
|
||||
import { AntdDropdown } from 'src/components';
|
||||
import { Menu } from 'src/components/Menu';
|
||||
import downloadAsImage from 'src/utils/downloadAsImage';
|
||||
import Icons from 'src/components/Icons';
|
||||
import ModalTrigger from 'src/components/ModalTrigger';
|
||||
import { sliceUpdated } from 'src/explore/actions/exploreActions';
|
||||
import Button from 'src/components/Button';
|
||||
import withToasts from 'src/components/MessageToasts/withToasts';
|
||||
import Checkbox from 'src/components/Checkbox';
|
||||
import { exportChart } from 'src/explore/exploreUtils';
|
||||
import downloadAsImage from 'src/utils/downloadAsImage';
|
||||
import { noOp } from 'src/utils/common';
|
||||
import { getChartPermalink } from 'src/utils/urlUtils';
|
||||
import { toggleActive } from 'src/reports/actions/reports';
|
||||
import ViewQueryModal from '../controls/ViewQueryModal';
|
||||
import EmbedCodeContent from '../EmbedCodeContent';
|
||||
import { ExploreReport } from './ExploreReport';
|
||||
import copyTextToClipboard from '../../../utils/copy';
|
||||
|
||||
const propTypes = {
|
||||
onOpenPropertiesModal: PropTypes.func,
|
||||
onOpenInEditor: PropTypes.func,
|
||||
chartStatus: PropTypes.string,
|
||||
latestQueryFormData: PropTypes.object.isRequired,
|
||||
slice: PropTypes.object,
|
||||
};
|
||||
|
||||
const MENU_KEYS = {
|
||||
EDIT_PROPERTIES: 'edit_properties',
|
||||
RUN_IN_SQL_LAB: 'run_in_sql_lab',
|
||||
DOWNLOAD_SUBMENU: 'download_submenu',
|
||||
EXPORT_TO_CSV: 'export_to_csv',
|
||||
EXPORT_TO_CSV_PIVOTED: 'export_to_csv_pivoted',
|
||||
EXPORT_TO_JSON: 'export_to_json',
|
||||
DOWNLOAD_AS_IMAGE: 'download_as_image',
|
||||
SHARE_SUBMENU: 'share_submenu',
|
||||
COPY_PERMALINK: 'copy_permalink',
|
||||
EMBED_CODE: 'embed_code',
|
||||
SHARE_BY_EMAIL: 'share_by_email',
|
||||
REPORT_SUBMENU: 'report_submenu',
|
||||
SET_UP_REPORT: 'set_up_report',
|
||||
SET_REPORT_ACTIVE: 'set_report_active',
|
||||
EDIT_REPORT: 'edit_report',
|
||||
DELETE_REPORT: 'delete_report',
|
||||
VIEW_QUERY: 'view_query',
|
||||
RUN_IN_SQL_LAB: 'run_in_sql_lab',
|
||||
};
|
||||
|
||||
const ExploreAdditionalActionsMenu = props => {
|
||||
const { datasource } = props.latestQueryFormData;
|
||||
const sqlSupported = datasource && datasource.split('__')[1] === 'table';
|
||||
const handleMenuClick = ({ key, domEvent }) => {
|
||||
const { slice, onOpenInEditor, latestQueryFormData } = props;
|
||||
switch (key) {
|
||||
case MENU_KEYS.EDIT_PROPERTIES:
|
||||
props.onOpenPropertiesModal();
|
||||
break;
|
||||
case MENU_KEYS.RUN_IN_SQL_LAB:
|
||||
onOpenInEditor(latestQueryFormData);
|
||||
break;
|
||||
case MENU_KEYS.DOWNLOAD_AS_IMAGE:
|
||||
downloadAsImage(
|
||||
'.panel-body > .chart-container',
|
||||
// eslint-disable-next-line camelcase
|
||||
slice?.slice_name ?? t('New chart'),
|
||||
{},
|
||||
true,
|
||||
)(domEvent);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
};
|
||||
const VIZ_TYPES_PIVOTABLE = ['pivot_table', 'pivot_table_v2'];
|
||||
|
||||
const { slice } = props;
|
||||
return (
|
||||
<AntdDropdown
|
||||
trigger="click"
|
||||
data-test="query-dropdown"
|
||||
overlay={
|
||||
<Menu onClick={handleMenuClick} selectable={false}>
|
||||
{slice && (
|
||||
<Menu.Item key={MENU_KEYS.EDIT_PROPERTIES}>
|
||||
{t('Edit properties')}
|
||||
</Menu.Item>
|
||||
)}
|
||||
<Menu.Item key={MENU_KEYS.VIEW_QUERY}>
|
||||
<ModalTrigger
|
||||
triggerNode={
|
||||
<span data-test="view-query-menu-item">{t('View query')}</span>
|
||||
}
|
||||
modalTitle={t('View query')}
|
||||
modalBody={
|
||||
<ViewQueryModal
|
||||
latestQueryFormData={props.latestQueryFormData}
|
||||
/>
|
||||
}
|
||||
draggable
|
||||
resizable
|
||||
responsive
|
||||
/>
|
||||
</Menu.Item>
|
||||
{sqlSupported && (
|
||||
<Menu.Item key={MENU_KEYS.RUN_IN_SQL_LAB}>
|
||||
{t('Run in SQL Lab')}
|
||||
</Menu.Item>
|
||||
)}
|
||||
<Menu.Item key={MENU_KEYS.DOWNLOAD_AS_IMAGE}>
|
||||
{t('Download as image')}
|
||||
</Menu.Item>
|
||||
</Menu>
|
||||
const MenuItemWithCheckboxContainer = styled.div`
|
||||
${({ theme }) => css`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
& svg {
|
||||
width: ${theme.gridUnit * 3}px;
|
||||
height: ${theme.gridUnit * 3}px;
|
||||
}
|
||||
|
||||
& span[role='checkbox'] {
|
||||
display: inline-flex;
|
||||
margin-right: ${theme.gridUnit}px;
|
||||
}
|
||||
`}
|
||||
`;
|
||||
|
||||
const MenuTrigger = styled(Button)`
|
||||
${({ theme }) => css`
|
||||
width: ${theme.gridUnit * 6}px;
|
||||
height: ${theme.gridUnit * 6}px;
|
||||
padding: 0;
|
||||
border: 1px solid ${theme.colors.primary.dark2};
|
||||
|
||||
&.ant-btn > span.anticon {
|
||||
line-height: 0;
|
||||
transition: inherit;
|
||||
}
|
||||
|
||||
&:hover:not(:focus) > span.anticon {
|
||||
color: ${theme.colors.primary.light1};
|
||||
}
|
||||
`}
|
||||
`;
|
||||
|
||||
const ExploreAdditionalActionsMenu = ({
|
||||
latestQueryFormData,
|
||||
canDownloadCSV,
|
||||
addDangerToast,
|
||||
addSuccessToast,
|
||||
slice,
|
||||
onOpenInEditor,
|
||||
onOpenPropertiesModal,
|
||||
canAddReports,
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const [isDropdownVisible, setIsDropdownVisible] = useState(false);
|
||||
const [openSubmenus, setOpenSubmenus] = useState([]);
|
||||
const [showReportModal, setShowReportModal] = useState(false);
|
||||
const [showDeleteReportModal, setShowDeleteReportModal] = useState(false);
|
||||
const dispatch = useDispatch();
|
||||
const report = useSelector(state => {
|
||||
if (!state.reports) {
|
||||
return undefined;
|
||||
}
|
||||
const reports = Object.values(state?.reports);
|
||||
if (reports.length > 0) {
|
||||
return reports[0];
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
const isReportActive = report?.active;
|
||||
const { datasource } = latestQueryFormData;
|
||||
const sqlSupported = datasource && datasource.split('__')[1] === 'table';
|
||||
|
||||
const shareByEmail = useCallback(async () => {
|
||||
try {
|
||||
const subject = t('Superset Chart');
|
||||
const url = await getChartPermalink(latestQueryFormData);
|
||||
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.'));
|
||||
}
|
||||
}, [addDangerToast, latestQueryFormData]);
|
||||
|
||||
const exportCSV = useCallback(
|
||||
() =>
|
||||
canDownloadCSV
|
||||
? exportChart({
|
||||
formData: latestQueryFormData,
|
||||
resultType: 'full',
|
||||
resultFormat: 'csv',
|
||||
})
|
||||
: null,
|
||||
[canDownloadCSV, latestQueryFormData],
|
||||
);
|
||||
|
||||
const exportCSVPivoted = useCallback(
|
||||
() =>
|
||||
canDownloadCSV
|
||||
? exportChart({
|
||||
formData: latestQueryFormData,
|
||||
resultType: 'post_processed',
|
||||
resultFormat: 'csv',
|
||||
})
|
||||
: null,
|
||||
[canDownloadCSV, latestQueryFormData],
|
||||
);
|
||||
|
||||
const exportJson = useCallback(
|
||||
() =>
|
||||
exportChart({
|
||||
formData: latestQueryFormData,
|
||||
resultType: 'results',
|
||||
resultFormat: 'json',
|
||||
}),
|
||||
[latestQueryFormData],
|
||||
);
|
||||
|
||||
const copyLink = useCallback(async () => {
|
||||
try {
|
||||
if (!latestQueryFormData) {
|
||||
throw new Error();
|
||||
}
|
||||
>
|
||||
<div
|
||||
role="button"
|
||||
id="query"
|
||||
tabIndex={0}
|
||||
className="btn btn-default btn-sm"
|
||||
const url = await getChartPermalink(latestQueryFormData);
|
||||
await copyTextToClipboard(url);
|
||||
addSuccessToast(t('Copied to clipboard!'));
|
||||
} catch (error) {
|
||||
addDangerToast(t('Sorry, something went wrong. Try again later.'));
|
||||
}
|
||||
}, [addDangerToast, addSuccessToast, latestQueryFormData]);
|
||||
|
||||
const handleMenuClick = useCallback(
|
||||
({ key, domEvent }) => {
|
||||
switch (key) {
|
||||
case MENU_KEYS.EDIT_PROPERTIES:
|
||||
onOpenPropertiesModal();
|
||||
setIsDropdownVisible(false);
|
||||
break;
|
||||
case MENU_KEYS.EXPORT_TO_CSV:
|
||||
exportCSV();
|
||||
setIsDropdownVisible(false);
|
||||
setOpenSubmenus([]);
|
||||
break;
|
||||
case MENU_KEYS.EXPORT_TO_CSV_PIVOTED:
|
||||
exportCSVPivoted();
|
||||
setIsDropdownVisible(false);
|
||||
setOpenSubmenus([]);
|
||||
break;
|
||||
case MENU_KEYS.EXPORT_TO_JSON:
|
||||
exportJson();
|
||||
setIsDropdownVisible(false);
|
||||
setOpenSubmenus([]);
|
||||
|
||||
break;
|
||||
case MENU_KEYS.DOWNLOAD_AS_IMAGE:
|
||||
downloadAsImage(
|
||||
'.panel-body > .chart-container',
|
||||
// eslint-disable-next-line camelcase
|
||||
slice?.slice_name ?? t('New chart'),
|
||||
{},
|
||||
true,
|
||||
)(domEvent);
|
||||
setIsDropdownVisible(false);
|
||||
setOpenSubmenus([]);
|
||||
break;
|
||||
case MENU_KEYS.COPY_PERMALINK:
|
||||
copyLink();
|
||||
setIsDropdownVisible(false);
|
||||
setOpenSubmenus([]);
|
||||
break;
|
||||
case MENU_KEYS.EMBED_CODE:
|
||||
setIsDropdownVisible(false);
|
||||
setOpenSubmenus([]);
|
||||
break;
|
||||
case MENU_KEYS.SHARE_BY_EMAIL:
|
||||
shareByEmail();
|
||||
setIsDropdownVisible(false);
|
||||
setOpenSubmenus([]);
|
||||
break;
|
||||
case MENU_KEYS.SET_UP_REPORT:
|
||||
setShowReportModal(true);
|
||||
setIsDropdownVisible(false);
|
||||
break;
|
||||
case MENU_KEYS.SET_REPORT_ACTIVE:
|
||||
dispatch(toggleActive(report, !isReportActive));
|
||||
break;
|
||||
case MENU_KEYS.EDIT_REPORT:
|
||||
setShowReportModal(true);
|
||||
setIsDropdownVisible(false);
|
||||
setOpenSubmenus([]);
|
||||
break;
|
||||
case MENU_KEYS.DELETE_REPORT:
|
||||
setShowDeleteReportModal(true);
|
||||
setIsDropdownVisible(false);
|
||||
setOpenSubmenus([]);
|
||||
break;
|
||||
case MENU_KEYS.VIEW_QUERY:
|
||||
setIsDropdownVisible(false);
|
||||
break;
|
||||
case MENU_KEYS.RUN_IN_SQL_LAB:
|
||||
onOpenInEditor(latestQueryFormData);
|
||||
setIsDropdownVisible(false);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
},
|
||||
[
|
||||
copyLink,
|
||||
dispatch,
|
||||
exportCSV,
|
||||
exportCSVPivoted,
|
||||
exportJson,
|
||||
isReportActive,
|
||||
latestQueryFormData,
|
||||
onOpenInEditor,
|
||||
onOpenPropertiesModal,
|
||||
report,
|
||||
shareByEmail,
|
||||
slice?.slice_name,
|
||||
],
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<AntdDropdown
|
||||
trigger="click"
|
||||
data-test="query-dropdown"
|
||||
visible={isDropdownVisible}
|
||||
onVisibleChange={setIsDropdownVisible}
|
||||
overlay={
|
||||
<Menu
|
||||
onClick={handleMenuClick}
|
||||
selectable={false}
|
||||
openKeys={openSubmenus}
|
||||
onOpenChange={setOpenSubmenus}
|
||||
>
|
||||
{slice && (
|
||||
<>
|
||||
<Menu.Item key={MENU_KEYS.EDIT_PROPERTIES}>
|
||||
{t('Edit chart properties')}
|
||||
</Menu.Item>
|
||||
<Menu.Divider />
|
||||
</>
|
||||
)}
|
||||
<Menu.SubMenu
|
||||
title={t('Download')}
|
||||
key={MENU_KEYS.DOWNLOAD_SUBMENU}
|
||||
>
|
||||
{VIZ_TYPES_PIVOTABLE.includes(latestQueryFormData.viz_type) ? (
|
||||
<>
|
||||
<Menu.Item
|
||||
key={MENU_KEYS.EXPORT_TO_CSV}
|
||||
icon={<FileOutlined />}
|
||||
disabled={!canDownloadCSV}
|
||||
>
|
||||
{t('Export to original .CSV')}
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
key={MENU_KEYS.EXPORT_TO_CSV_PIVOTED}
|
||||
icon={<FileOutlined />}
|
||||
disabled={!canDownloadCSV}
|
||||
>
|
||||
{t('Export to pivoted .CSV')}
|
||||
</Menu.Item>
|
||||
</>
|
||||
) : (
|
||||
<Menu.Item
|
||||
key={MENU_KEYS.EXPORT_TO_CSV}
|
||||
icon={<FileOutlined />}
|
||||
disabled={!canDownloadCSV}
|
||||
>
|
||||
{t('Export to .CSV')}
|
||||
</Menu.Item>
|
||||
)}
|
||||
<Menu.Item key={MENU_KEYS.EXPORT_TO_JSON} icon={<FileOutlined />}>
|
||||
{t('Export to .JSON')}
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
key={MENU_KEYS.DOWNLOAD_AS_IMAGE}
|
||||
icon={<FileImageOutlined />}
|
||||
>
|
||||
{t('Download as image')}
|
||||
</Menu.Item>
|
||||
</Menu.SubMenu>
|
||||
<Menu.SubMenu title={t('Share')} key={MENU_KEYS.SHARE_SUBMENU}>
|
||||
<Menu.Item key={MENU_KEYS.COPY_PERMALINK}>
|
||||
{t('Copy permalink to clipboard')}
|
||||
</Menu.Item>
|
||||
<Menu.Item key={MENU_KEYS.SHARE_BY_EMAIL}>
|
||||
{t('Share chart by email')}
|
||||
</Menu.Item>
|
||||
<Menu.Item key={MENU_KEYS.EMBED_CODE}>
|
||||
<ModalTrigger
|
||||
triggerNode={
|
||||
<span data-test="embed-code-button">{t('Embed code')}</span>
|
||||
}
|
||||
modalTitle={t('Embed code')}
|
||||
modalBody={
|
||||
<EmbedCodeContent
|
||||
formData={latestQueryFormData}
|
||||
addDangerToast={addDangerToast}
|
||||
/>
|
||||
}
|
||||
maxWidth={`${theme.gridUnit * 100}px`}
|
||||
destroyOnClose
|
||||
responsive
|
||||
/>
|
||||
</Menu.Item>
|
||||
</Menu.SubMenu>
|
||||
<Menu.Divider />
|
||||
{canAddReports &&
|
||||
(report ? (
|
||||
<Menu.SubMenu
|
||||
title={t('Manage email report')}
|
||||
key={MENU_KEYS.REPORT_SUBMENU}
|
||||
>
|
||||
<Menu.Item key={MENU_KEYS.SET_REPORT_ACTIVE}>
|
||||
<MenuItemWithCheckboxContainer>
|
||||
<Checkbox checked={isReportActive} onChange={noOp} />
|
||||
{t('Email reports active')}
|
||||
</MenuItemWithCheckboxContainer>
|
||||
</Menu.Item>
|
||||
<Menu.Item key={MENU_KEYS.EDIT_REPORT}>
|
||||
{t('Edit email report')}
|
||||
</Menu.Item>
|
||||
<Menu.Item key={MENU_KEYS.DELETE_REPORT}>
|
||||
{t('Delete email report')}
|
||||
</Menu.Item>
|
||||
</Menu.SubMenu>
|
||||
) : (
|
||||
<Menu.Item key={MENU_KEYS.SET_UP_REPORT}>
|
||||
{t('Set up an email report')}
|
||||
</Menu.Item>
|
||||
))}
|
||||
<Menu.Divider />
|
||||
<Menu.Item key={MENU_KEYS.VIEW_QUERY}>
|
||||
<ModalTrigger
|
||||
triggerNode={
|
||||
<span data-test="view-query-menu-item">
|
||||
{t('View query')}
|
||||
</span>
|
||||
}
|
||||
modalTitle={t('View query')}
|
||||
modalBody={
|
||||
<ViewQueryModal latestQueryFormData={latestQueryFormData} />
|
||||
}
|
||||
draggable
|
||||
resizable
|
||||
responsive
|
||||
/>
|
||||
</Menu.Item>
|
||||
{sqlSupported && (
|
||||
<Menu.Item key={MENU_KEYS.RUN_IN_SQL_LAB}>
|
||||
{t('Run in SQL Lab')}
|
||||
</Menu.Item>
|
||||
)}
|
||||
</Menu>
|
||||
}
|
||||
>
|
||||
<i role="img" className="fa fa-bars" />
|
||||
</div>
|
||||
</AntdDropdown>
|
||||
<MenuTrigger
|
||||
buttonStyle="tertiary"
|
||||
aria-label={t('Menu actions trigger')}
|
||||
>
|
||||
<Icons.MoreHoriz
|
||||
iconColor={theme.colors.primary.dark2}
|
||||
iconSize={theme.typography.sizes.m}
|
||||
/>
|
||||
</MenuTrigger>
|
||||
</AntdDropdown>
|
||||
<ExploreReport
|
||||
report={report}
|
||||
isVisible={showReportModal}
|
||||
onHide={() => setShowReportModal(false)}
|
||||
isDeleting={showDeleteReportModal}
|
||||
setIsDeleting={setShowDeleteReportModal}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
ExploreAdditionalActionsMenu.propTypes = propTypes;
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return bindActionCreators({ sliceUpdated }, dispatch);
|
||||
}
|
||||
|
||||
export default connect(null, mapDispatchToProps)(ExploreAdditionalActionsMenu);
|
||||
export default withToasts(ExploreAdditionalActionsMenu);
|
||||
|
||||
@@ -96,14 +96,11 @@ const createProps = () => ({
|
||||
test('Cancelling changes to the properties should reset previous properties', () => {
|
||||
const props = createProps();
|
||||
render(<ExploreHeader {...props} />, { useRedux: true });
|
||||
|
||||
const openModal = screen.getByRole('button', {
|
||||
name: 'Edit chart properties',
|
||||
});
|
||||
const newChartName = 'New chart name';
|
||||
const prevChartName = props.slice_name;
|
||||
|
||||
userEvent.click(openModal);
|
||||
userEvent.click(screen.getByLabelText('Menu actions trigger'));
|
||||
userEvent.click(screen.getByText('Edit chart properties'));
|
||||
|
||||
const nameInput = screen.getByRole('textbox', { name: 'Name' });
|
||||
|
||||
@@ -114,7 +111,8 @@ test('Cancelling changes to the properties should reset previous properties', ()
|
||||
|
||||
userEvent.click(screen.getByRole('button', { name: 'Cancel' }));
|
||||
|
||||
userEvent.click(openModal);
|
||||
userEvent.click(screen.getByLabelText('Menu actions trigger'));
|
||||
userEvent.click(screen.getByText('Edit chart properties'));
|
||||
|
||||
expect(screen.getByDisplayValue(prevChartName)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
@@ -20,22 +20,18 @@ import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import PropTypes from 'prop-types';
|
||||
import Icons from 'src/components/Icons';
|
||||
import {
|
||||
CategoricalColorNamespace,
|
||||
SupersetClient,
|
||||
styled,
|
||||
t,
|
||||
} from '@superset-ui/core';
|
||||
import { Tooltip } from 'src/components/Tooltip';
|
||||
import ReportModal from 'src/components/ReportModal';
|
||||
import {
|
||||
fetchUISpecificReport,
|
||||
toggleActive,
|
||||
deleteActiveReport,
|
||||
} from 'src/reports/actions/reports';
|
||||
import { isFeatureEnabled, FeatureFlag } from 'src/featureFlags';
|
||||
import HeaderReportActionsDropdown from 'src/components/ReportModal/HeaderReportActionsDropdown';
|
||||
import { chartPropShape } from 'src/dashboard/util/propShapes';
|
||||
import EditableTitle from 'src/components/EditableTitle';
|
||||
import AlteredSliceTag from 'src/components/AlteredSliceTag';
|
||||
@@ -45,8 +41,9 @@ import CachedLabel from 'src/components/CachedLabel';
|
||||
import PropertiesModal from 'src/explore/components/PropertiesModal';
|
||||
import { sliceUpdated } from 'src/explore/actions/exploreActions';
|
||||
import CertifiedBadge from 'src/components/CertifiedBadge';
|
||||
import ExploreActionButtons from '../ExploreActionButtons';
|
||||
import withToasts from 'src/components/MessageToasts/withToasts';
|
||||
import RowCountLabel from '../RowCountLabel';
|
||||
import ExploreAdditionalActionsMenu from '../ExploreAdditionalActionsMenu';
|
||||
|
||||
const CHART_STATUS_MAP = {
|
||||
failed: 'danger',
|
||||
@@ -113,13 +110,9 @@ export class ExploreChartHeader extends React.PureComponent {
|
||||
super(props);
|
||||
this.state = {
|
||||
isPropertiesModalOpen: false,
|
||||
showingReportModal: false,
|
||||
};
|
||||
this.openPropertiesModal = this.openPropertiesModal.bind(this);
|
||||
this.closePropertiesModal = this.closePropertiesModal.bind(this);
|
||||
this.showReportModal = this.showReportModal.bind(this);
|
||||
this.hideReportModal = this.hideReportModal.bind(this);
|
||||
this.renderReportModal = this.renderReportModal.bind(this);
|
||||
this.fetchChartDashboardData = this.fetchChartDashboardData.bind(this);
|
||||
}
|
||||
|
||||
@@ -209,38 +202,6 @@ export class ExploreChartHeader extends React.PureComponent {
|
||||
});
|
||||
}
|
||||
|
||||
showReportModal() {
|
||||
this.setState({ showingReportModal: true });
|
||||
}
|
||||
|
||||
hideReportModal() {
|
||||
this.setState({ showingReportModal: false });
|
||||
}
|
||||
|
||||
renderReportModal() {
|
||||
const attachedReportExists = !!Object.keys(this.props.reports).length;
|
||||
return attachedReportExists ? (
|
||||
<HeaderReportActionsDropdown
|
||||
showReportModal={this.showReportModal}
|
||||
hideReportModal={this.hideReportModal}
|
||||
toggleActive={this.props.toggleActive}
|
||||
deleteActiveReport={this.props.deleteActiveReport}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<span
|
||||
role="button"
|
||||
title={t('Schedule email report')}
|
||||
tabIndex={0}
|
||||
className="action-button"
|
||||
onClick={this.showReportModal}
|
||||
>
|
||||
<Icons.Calendar />
|
||||
</span>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
canAddReports() {
|
||||
if (!isFeatureEnabled(FeatureFlag.ALERT_REPORTS)) {
|
||||
return false;
|
||||
@@ -315,20 +276,6 @@ export class ExploreChartHeader extends React.PureComponent {
|
||||
slice={this.props.slice}
|
||||
/>
|
||||
)}
|
||||
<Tooltip
|
||||
id="edit-desc-tooltip"
|
||||
title={t('Edit chart properties')}
|
||||
>
|
||||
<span
|
||||
aria-label={t('Edit chart properties')}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
className="edit-desc-icon"
|
||||
onClick={this.openPropertiesModal}
|
||||
>
|
||||
<i className="fa fa-edit" />
|
||||
</span>
|
||||
</Tooltip>
|
||||
{this.props.chart.sliceFormData && (
|
||||
<AlteredSliceTag
|
||||
className="altered"
|
||||
@@ -358,27 +305,13 @@ export class ExploreChartHeader extends React.PureComponent {
|
||||
isRunning={chartStatus === 'loading'}
|
||||
status={CHART_STATUS_MAP[chartStatus]}
|
||||
/>
|
||||
{this.canAddReports() && this.renderReportModal()}
|
||||
<ReportModal
|
||||
show={this.state.showingReportModal}
|
||||
onHide={this.hideReportModal}
|
||||
props={{
|
||||
userId: this.props.user.userId,
|
||||
userEmail: this.props.user.email,
|
||||
chart: this.props.chart,
|
||||
creationMethod: 'charts',
|
||||
}}
|
||||
/>
|
||||
<ExploreActionButtons
|
||||
actions={{
|
||||
...this.props.actions,
|
||||
openPropertiesModal: this.openPropertiesModal,
|
||||
}}
|
||||
<ExploreAdditionalActionsMenu
|
||||
onOpenInEditor={this.props.actions.redirectSQLLab}
|
||||
onOpenPropertiesModal={this.openPropertiesModal}
|
||||
slice={this.props.slice}
|
||||
canDownloadCSV={this.props.can_download}
|
||||
chartStatus={chartStatus}
|
||||
latestQueryFormData={latestQueryFormData}
|
||||
queryResponse={queryResponse}
|
||||
canAddReports={this.canAddReports()}
|
||||
/>
|
||||
</div>
|
||||
</StyledHeader>
|
||||
@@ -395,4 +328,7 @@ function mapDispatchToProps(dispatch) {
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(null, mapDispatchToProps)(ExploreChartHeader);
|
||||
export default connect(
|
||||
null,
|
||||
mapDispatchToProps,
|
||||
)(withToasts(ExploreChartHeader));
|
||||
|
||||
Reference in New Issue
Block a user