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));