You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by kg...@apache.org on 2022/03/31 18:44:00 UTC
[superset] branch master updated: feat(explore): Move chart actions into dropdown (#19446)
This is an automated email from the ASF dual-hosted git repository.
kgabryje pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/superset.git
The following commit(s) were added to refs/heads/master by this push:
new 1a1322d feat(explore): Move chart actions into dropdown (#19446)
1a1322d is described below
commit 1a1322d3d92960416a4874997b53aec3bd59eff6
Author: Kamil Gabryjelski <ka...@gmail.com>
AuthorDate: Thu Mar 31 20:41:15 2022 +0200
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
---
.../cypress/integration/explore/link.test.ts | 9 +-
.../src/components/ModalTrigger/index.jsx | 2 +
.../src/components/ReportModal/index.tsx | 25 +-
.../src/dashboard/components/Header/index.jsx | 10 +-
.../src/explore/components/EmbedCodeButton.jsx | 168 --------
.../explore/components/EmbedCodeButton.test.jsx | 62 ---
.../src/explore/components/EmbedCodeContent.jsx | 153 +++++++
.../explore/components/EmbedCodeContent.test.jsx | 48 +++
.../components/ExploreActionButtons.test.jsx | 117 -----
.../explore/components/ExploreActionButtons.tsx | 234 ----------
.../ExploreAdditionalActionsMenu.test.jsx | 60 ---
.../ExploreAdditionalActionsMenu.test.tsx | 198 ++++++++-
.../ExploreAdditionalActionsMenu/ExploreReport.tsx | 92 ++++
.../ExploreAdditionalActionsMenu/index.jsx | 476 +++++++++++++++++----
.../ExploreChartHeader/ExploreChartHeader.test.tsx | 10 +-
.../components/ExploreChartHeader/index.jsx | 84 +---
16 files changed, 901 insertions(+), 847 deletions(-)
diff --git a/superset-frontend/cypress-base/cypress/integration/explore/link.test.ts b/superset-frontend/cypress-base/cypress/integration/explore/link.test.ts
index 027326e..9f07e9c 100644
--- a/superset-frontend/cypress-base/cypress/integration/explore/link.test.ts
+++ b/superset-frontend/cypress-base/cypress/integration/explore/link.test.ts
@@ -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');
});
diff --git a/superset-frontend/src/components/ModalTrigger/index.jsx b/superset-frontend/src/components/ModalTrigger/index.jsx
index e01e659..b15000d 100644
--- a/superset-frontend/src/components/ModalTrigger/index.jsx
+++ b/superset-frontend/src/components/ModalTrigger/index.jsx
@@ -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>
diff --git a/superset-frontend/src/components/ReportModal/index.tsx b/superset-frontend/src/components/ReportModal/index.tsx
index c8273bd..5cdc216 100644
--- a/superset-frontend/src/components/ReportModal/index.tsx
+++ b/superset-frontend/src/components/ReportModal/index.tsx
@@ -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);
diff --git a/superset-frontend/src/dashboard/components/Header/index.jsx b/superset-frontend/src/dashboard/components/Header/index.jsx
index ad668da..af68742 100644
--- a/superset-frontend/src/dashboard/components/Header/index.jsx
+++ b/superset-frontend/src/dashboard/components/Header/index.jsx
@@ -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"
/>
)}
diff --git a/superset-frontend/src/explore/components/EmbedCodeButton.jsx b/superset-frontend/src/explore/components/EmbedCodeButton.jsx
deleted file mode 100644
index 71f77a4..0000000
--- a/superset-frontend/src/explore/components/EmbedCodeButton.jsx
+++ /dev/null
@@ -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>
- );
- }
-}
diff --git a/superset-frontend/src/explore/components/EmbedCodeButton.test.jsx b/superset-frontend/src/explore/components/EmbedCodeButton.test.jsx
deleted file mode 100644
index 87af313..0000000
--- a/superset-frontend/src/explore/components/EmbedCodeButton.test.jsx
+++ /dev/null
@@ -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,
- );
- });
-});
diff --git a/superset-frontend/src/explore/components/EmbedCodeContent.jsx b/superset-frontend/src/explore/components/EmbedCodeContent.jsx
new file mode 100644
index 0000000..98fbd34
--- /dev/null
+++ b/superset-frontend/src/explore/components/EmbedCodeContent.jsx
@@ -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;
diff --git a/superset-frontend/src/explore/components/EmbedCodeContent.test.jsx b/superset-frontend/src/explore/components/EmbedCodeContent.test.jsx
new file mode 100644
index 0000000..3c77037
--- /dev/null
+++ b/superset-frontend/src/explore/components/EmbedCodeContent.test.jsx
@@ -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();
+ });
+});
diff --git a/superset-frontend/src/explore/components/ExploreActionButtons.test.jsx b/superset-frontend/src/explore/components/ExploreActionButtons.test.jsx
deleted file mode 100644
index 8e7db84..0000000
--- a/superset-frontend/src/explore/components/ExploreActionButtons.test.jsx
+++ /dev/null
@@ -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();
- });
- });
-});
diff --git a/superset-frontend/src/explore/components/ExploreActionButtons.tsx b/superset-frontend/src/explore/components/ExploreActionButtons.tsx
deleted file mode 100644
index adb85e1..0000000
--- a/superset-frontend/src/explore/components/ExploreActionButtons.tsx
+++ /dev/null
@@ -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);
diff --git a/superset-frontend/src/explore/components/ExploreAdditionalActionsMenu/ExploreAdditionalActionsMenu.test.jsx b/superset-frontend/src/explore/components/ExploreAdditionalActionsMenu/ExploreAdditionalActionsMenu.test.jsx
deleted file mode 100644
index f0fd4a1..0000000
--- a/superset-frontend/src/explore/components/ExploreAdditionalActionsMenu/ExploreAdditionalActionsMenu.test.jsx
+++ /dev/null
@@ -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);
- });
-});
diff --git a/superset-frontend/src/explore/components/ExploreAdditionalActionsMenu/ExploreAdditionalActionsMenu.test.tsx b/superset-frontend/src/explore/components/ExploreAdditionalActionsMenu/ExploreAdditionalActionsMenu.test.tsx
index 1fbb162..93c6b9d 100644
--- a/superset-frontend/src/explore/components/ExploreAdditionalActionsMenu/ExploreAdditionalActionsMenu.test.tsx
+++ b/superset-frontend/src/explore/components/ExploreAdditionalActionsMenu/ExploreAdditionalActionsMenu.test.tsx
@@ -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.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('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.getByRole('menuitem', { name: 'Edit properties' }),
- ).toBeInTheDocument();
- expect(
- screen.getByRole('menuitem', { name: 'View query' }),
- ).toBeInTheDocument();
- expect(
- screen.getByRole('menuitem', { name: 'Run in SQL Lab' }),
- ).toBeInTheDocument();
+ 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(
- screen.getByRole('menuitem', { name: 'Download as image' }),
+ 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 call onOpenPropertiesModal when click on "Edit properties"', () => {
+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);
+ 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);
+ });
+
+ 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();
+ });
+
+ test('Should export to CSV if canDownloadCSV=true', async () => {
+ const props = createProps();
+ props.canDownloadCSV = true;
+ 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(1);
+ spyExportChart.restore();
+ });
- userEvent.click(screen.getByRole('menuitem', { name: 'Download as image' }));
+ test('Should export to JSON', async () => {
+ const props = createProps();
+ render(<ExploreAdditionalActionsMenu {...props} />, {
+ useRedux: true,
+ });
- expect(spy).toBeCalledTimes(1);
+ 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);
+ });
});
diff --git a/superset-frontend/src/explore/components/ExploreAdditionalActionsMenu/ExploreReport.tsx b/superset-frontend/src/explore/components/ExploreAdditionalActionsMenu/ExploreReport.tsx
new file mode 100644
index 0000000..967903d
--- /dev/null
+++ b/superset-frontend/src/explore/components/ExploreAdditionalActionsMenu/ExploreReport.tsx
@@ -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?')}
+ />
+ )}
+ </>
+ );
+};
diff --git a/superset-frontend/src/explore/components/ExploreAdditionalActionsMenu/index.jsx b/superset-frontend/src/explore/components/ExploreAdditionalActionsMenu/index.jsx
index b833cab..640912d 100644
--- a/superset-frontend/src/explore/components/ExploreAdditionalActionsMenu/index.jsx
+++ b/superset-frontend/src/explore/components/ExploreAdditionalActionsMenu/index.jsx
@@ -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 VIZ_TYPES_PIVOTABLE = ['pivot_table', 'pivot_table_v2'];
+
+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 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 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();
+ }
+ 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,
+ ],
+ );
- 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}
+ <>
+ <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
/>
- }
- draggable
- resizable
- responsive
- />
- </Menu.Item>
- {sqlSupported && (
- <Menu.Item key={MENU_KEYS.RUN_IN_SQL_LAB}>
- {t('Run in SQL Lab')}
+ </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>
- )}
- <Menu.Item key={MENU_KEYS.DOWNLOAD_AS_IMAGE}>
- {t('Download as image')}
- </Menu.Item>
- </Menu>
- }
- >
- <div
- role="button"
- id="query"
- tabIndex={0}
- className="btn btn-default btn-sm"
+ {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);
diff --git a/superset-frontend/src/explore/components/ExploreChartHeader/ExploreChartHeader.test.tsx b/superset-frontend/src/explore/components/ExploreChartHeader/ExploreChartHeader.test.tsx
index 3cfaf77..35dc9eb 100644
--- a/superset-frontend/src/explore/components/ExploreChartHeader/ExploreChartHeader.test.tsx
+++ b/superset-frontend/src/explore/components/ExploreChartHeader/ExploreChartHeader.test.tsx
@@ -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();
});
diff --git a/superset-frontend/src/explore/components/ExploreChartHeader/index.jsx b/superset-frontend/src/explore/components/ExploreChartHeader/index.jsx
index 21605c5..665a251 100644
--- a/superset-frontend/src/explore/components/ExploreChartHeader/index.jsx
+++ b/superset-frontend/src/explore/components/ExploreChartHeader/index.jsx
@@ -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));