superset-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From maximebeauche...@apache.org
Subject [incubator-superset] branch master updated: Re-add dashboard short links (#5398)
Date Fri, 03 Aug 2018 16:22:42 GMT
This is an automated email from the ASF dual-hosted git repository.

maximebeauchemin pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/incubator-superset.git


The following commit(s) were added to refs/heads/master by this push:
     new aa9b30c  Re-add dashboard short links (#5398)
aa9b30c is described below

commit aa9b30cf55bd0ee393f688f4c3951a5cc1ff77b7
Author: Jay Lindquist <jay@bitwiseor.com>
AuthorDate: Fri Aug 3 11:22:39 2018 -0500

    Re-add dashboard short links (#5398)
    
    * Re-add dashboard short links
    
    * Make the short link available to all users
    * Include filters in the short link for dashboards
    
    * Remove duplicate key causing linter error
    
    * Change URL Short link button into a menu item with Modal
    
    * Split out tests, bind URL short link in constructor
---
 .../components/URLShortLinkModal_spec.jsx          |  27 ++++
 .../components/HeaderActionsDropdown_spec.jsx      | 141 ++++++++++++++++++
 .../dashboard/components/Header_spec.jsx           | 147 +++++++++++++++++++
 .../dashboard/util/getDashboardUrl_spec.js         |  14 ++
 .../assets/src/components/URLShortLinkButton.jsx   |   3 +-
 .../assets/src/components/URLShortLinkModal.jsx    |  78 ++++++++++
 .../assets/src/dashboard/components/Header.jsx     | 157 +++++++++++----------
 .../dashboard/components/HeaderActionsDropdown.jsx |  74 ++++++----
 .../assets/src/dashboard/util/getDashboardUrl.js   |   6 +
 9 files changed, 541 insertions(+), 106 deletions(-)

diff --git a/superset/assets/spec/javascripts/components/URLShortLinkModal_spec.jsx b/superset/assets/spec/javascripts/components/URLShortLinkModal_spec.jsx
new file mode 100644
index 0000000..494d0d3
--- /dev/null
+++ b/superset/assets/spec/javascripts/components/URLShortLinkModal_spec.jsx
@@ -0,0 +1,27 @@
+import React from 'react';
+import configureStore from 'redux-mock-store';
+import { expect } from 'chai';
+import { describe, it } from 'mocha';
+import { shallow } from 'enzyme';
+
+import URLShortLinkModal from '../../../src/components/URLShortLinkModal';
+import ModalTrigger from '../../../src/components/ModalTrigger';
+
+describe('URLShortLinkModal', () => {
+  const defaultProps = {
+    url: 'mockURL',
+    emailSubject: 'Mock Subject',
+    emailContent: 'mock content',
+  };
+
+  function setup() {
+    const mockStore = configureStore([]);
+    const store = mockStore({});
+    return shallow(<URLShortLinkModal {...defaultProps} />, { context: { store } }).dive();
+  }
+
+  it('renders ModalTrigger', () => {
+    const wrapper = setup();
+    expect(wrapper.find(ModalTrigger)).have.length(1);
+  });
+});
diff --git a/superset/assets/spec/javascripts/dashboard/components/HeaderActionsDropdown_spec.jsx
b/superset/assets/spec/javascripts/dashboard/components/HeaderActionsDropdown_spec.jsx
new file mode 100644
index 0000000..673118b
--- /dev/null
+++ b/superset/assets/spec/javascripts/dashboard/components/HeaderActionsDropdown_spec.jsx
@@ -0,0 +1,141 @@
+import React from 'react';
+import { describe, it } from 'mocha';
+import { expect } from 'chai';
+import { shallow } from 'enzyme';
+import { DropdownButton, MenuItem } from 'react-bootstrap';
+import RefreshIntervalModal from '../../../../src/dashboard/components/RefreshIntervalModal';
+import URLShortLinkModal from '../../../../src/components/URLShortLinkModal';
+import HeaderActionsDropdown from '../../../../src/dashboard/components/HeaderActionsDropdown';
+import SaveModal from '../../../../src/dashboard/components/SaveModal';
+import CssEditor from '../../../../src/dashboard/components/CssEditor';
+
+describe('HeaderActionsDropdown', () => {
+  const props = {
+    addSuccessToast: () => {},
+    addDangerToast: () => {},
+    dashboardId: 1,
+    dashboardTitle: 'Title',
+    hasUnsavedChanges: false,
+    css: '',
+    onChange: () => {},
+    updateCss: () => {},
+    forceRefreshAllCharts: () => {},
+    startPeriodicRender: () => {},
+    editMode: false,
+    userCanEdit: false,
+    userCanSave: false,
+    layout: {},
+    filters: {},
+    expandedSlices: {},
+    onSave: () => {},
+  };
+
+  function setup(overrideProps) {
+    const wrapper = shallow(
+      <HeaderActionsDropdown {...props} {...overrideProps} />,
+    );
+    return wrapper;
+  }
+
+  describe('readonly-user', () => {
+    const overrideProps = { userCanSave: false };
+
+    it('should render the DropdownButton', () => {
+      const wrapper = setup(overrideProps);
+      expect(wrapper.find(DropdownButton)).to.have.lengthOf(1);
+    });
+
+    it('should not render the SaveModal', () => {
+      const wrapper = setup(overrideProps);
+      expect(wrapper.find(SaveModal)).to.have.lengthOf(0);
+    });
+
+    it('should render one MenuItem', () => {
+      const wrapper = setup(overrideProps);
+      expect(wrapper.find(MenuItem)).to.have.lengthOf(1);
+    });
+
+    it('should render the RefreshIntervalModal', () => {
+      const wrapper = setup(overrideProps);
+      expect(wrapper.find(RefreshIntervalModal)).to.have.lengthOf(1);
+    });
+
+    it('should render the URLShortLinkModal', () => {
+      const wrapper = setup(overrideProps);
+      expect(wrapper.find(URLShortLinkModal)).to.have.lengthOf(1);
+    });
+
+    it('should not render the CssEditor', () => {
+      const wrapper = setup(overrideProps);
+      expect(wrapper.find(CssEditor)).to.have.lengthOf(0);
+    });
+  });
+
+  describe('write-user', () => {
+    const overrideProps = { userCanSave: true };
+
+    it('should render the DropdownButton', () => {
+      const wrapper = setup(overrideProps);
+      expect(wrapper.find(DropdownButton)).to.have.lengthOf(1);
+    });
+
+    it('should render the SaveModal', () => {
+      const wrapper = setup(overrideProps);
+      expect(wrapper.find(SaveModal)).to.have.lengthOf(1);
+    });
+
+    it('should render two MenuItems', () => {
+      const wrapper = setup(overrideProps);
+      expect(wrapper.find(MenuItem)).to.have.lengthOf(2);
+    });
+
+    it('should render the RefreshIntervalModal', () => {
+      const wrapper = setup(overrideProps);
+      expect(wrapper.find(RefreshIntervalModal)).to.have.lengthOf(1);
+    });
+
+    it('should render the URLShortLinkModal', () => {
+      const wrapper = setup(overrideProps);
+      expect(wrapper.find(URLShortLinkModal)).to.have.lengthOf(1);
+    });
+
+    it('should not render the CssEditor', () => {
+      const wrapper = setup(overrideProps);
+      expect(wrapper.find(CssEditor)).to.have.lengthOf(0);
+    });
+  });
+
+  describe('write-user-with-edit-mode', () => {
+    const overrideProps = { userCanSave: true, editMode: true };
+
+    it('should render the DropdownButton', () => {
+      const wrapper = setup(overrideProps);
+      expect(wrapper.find(DropdownButton)).to.have.lengthOf(1);
+    });
+
+    it('should render the SaveModal', () => {
+      const wrapper = setup(overrideProps);
+      expect(wrapper.find(SaveModal)).to.have.lengthOf(1);
+    });
+
+    it('should render three MenuItems', () => {
+      const wrapper = setup(overrideProps);
+      expect(wrapper.find(MenuItem)).to.have.lengthOf(3);
+    });
+
+    it('should render the RefreshIntervalModal', () => {
+      const wrapper = setup(overrideProps);
+      expect(wrapper.find(RefreshIntervalModal)).to.have.lengthOf(1);
+    });
+
+    it('should render the URLShortLinkModal', () => {
+      const wrapper = setup(overrideProps);
+      expect(wrapper.find(URLShortLinkModal)).to.have.lengthOf(1);
+    });
+
+    it('should render the CssEditor', () => {
+      const wrapper = setup(overrideProps);
+      expect(wrapper.find(CssEditor)).to.have.lengthOf(1);
+    });
+  });
+});
diff --git a/superset/assets/spec/javascripts/dashboard/components/Header_spec.jsx b/superset/assets/spec/javascripts/dashboard/components/Header_spec.jsx
new file mode 100644
index 0000000..e7ecfc1
--- /dev/null
+++ b/superset/assets/spec/javascripts/dashboard/components/Header_spec.jsx
@@ -0,0 +1,147 @@
+import React from 'react';
+import { describe, it } from 'mocha';
+import { expect } from 'chai';
+import { shallow } from 'enzyme';
+import Header from '../../../../src/dashboard/components/Header';
+import EditableTitle from '../../../../src/components/EditableTitle';
+import FaveStar from '../../../../src/components/FaveStar';
+import HeaderActionsDropdown from '../../../../src/dashboard/components/HeaderActionsDropdown';
+import Button from '../../../../src/components/Button';
+import UndoRedoKeylisteners from '../../../../src/dashboard/components/UndoRedoKeylisteners';
+
+describe('Header', () => {
+  const props = {
+    addSuccessToast: () => {},
+    addDangerToast: () => {},
+    dashboardInfo: { id: 1, dash_edit_perm: true, dash_save_perm: true },
+    dashboardTitle: 'title',
+    charts: {},
+    layout: {},
+    filters: {},
+    expandedSlices: {},
+    css: '',
+    isStarred: false,
+    onSave: () => {},
+    onChange: () => {},
+    fetchFaveStar: () => {},
+    fetchCharts: () => {},
+    saveFaveStar: () => {},
+    startPeriodicRender: () => {},
+    updateDashboardTitle: () => {},
+    editMode: false,
+    setEditMode: () => {},
+    showBuilderPane: false,
+    toggleBuilderPane: () => {},
+    updateCss: () => {},
+    hasUnsavedChanges: false,
+    maxUndoHistoryExceeded: false,
+
+    // redux
+    onUndo: () => {},
+    onRedo: () => {},
+    undoLength: 0,
+    redoLength: 0,
+    setMaxUndoHistoryExceeded: () => {},
+    maxUndoHistoryToast: () => {},
+  };
+
+  function setup(overrideProps) {
+    const wrapper = shallow(<Header {...props} {...overrideProps} />);
+    return wrapper;
+  }
+
+  describe('read-only-user', () => {
+    const overrideProps = {
+      dashboardInfo: { id: 1, dash_edit_perm: false, dash_save_perm: false },
+    };
+
+    it('should render the EditableTitle', () => {
+      const wrapper = setup(overrideProps);
+      expect(wrapper.find(EditableTitle)).to.have.lengthOf(1);
+    });
+
+    it('should render the FaveStar', () => {
+      const wrapper = setup(overrideProps);
+      expect(wrapper.find(FaveStar)).to.have.lengthOf(1);
+    });
+
+    it('should render the HeaderActionsDropdown', () => {
+      const wrapper = setup(overrideProps);
+      expect(wrapper.find(HeaderActionsDropdown)).to.have.lengthOf(1);
+    });
+
+    it('should render one Button', () => {
+      const wrapper = setup(overrideProps);
+      expect(wrapper.find(Button)).to.have.lengthOf(1);
+    });
+
+    it('should not set up undo/redo', () => {
+      const wrapper = setup(overrideProps);
+      expect(wrapper.find(UndoRedoKeylisteners)).to.have.lengthOf(0);
+    });
+  });
+
+  describe('write-user', () => {
+    const overrideProps = {
+      editMode: false,
+      dashboardInfo: { id: 1, dash_edit_perm: true, dash_save_perm: true },
+    };
+
+    it('should render the EditableTitle', () => {
+      const wrapper = setup(overrideProps);
+      expect(wrapper.find(EditableTitle)).to.have.lengthOf(1);
+    });
+
+    it('should render the FaveStar', () => {
+      const wrapper = setup(overrideProps);
+      expect(wrapper.find(FaveStar)).to.have.lengthOf(1);
+    });
+
+    it('should render the HeaderActionsDropdown', () => {
+      const wrapper = setup(overrideProps);
+      expect(wrapper.find(HeaderActionsDropdown)).to.have.lengthOf(1);
+    });
+
+    it('should render one Button', () => {
+      const wrapper = setup(overrideProps);
+      expect(wrapper.find(Button)).to.have.lengthOf(1);
+    });
+
+    it('should not set up undo/redo', () => {
+      const wrapper = setup(overrideProps);
+      expect(wrapper.find(UndoRedoKeylisteners)).to.have.lengthOf(0);
+    });
+  });
+
+  describe('write-user-with-edit-mode', () => {
+    const overrideProps = {
+      editMode: true,
+      dashboardInfo: { id: 1, dash_edit_perm: true, dash_save_perm: true },
+    };
+
+    it('should render the EditableTitle', () => {
+      const wrapper = setup(overrideProps);
+      expect(wrapper.find(EditableTitle)).to.have.lengthOf(1);
+    });
+
+    it('should render the FaveStar', () => {
+      const wrapper = setup(overrideProps);
+      expect(wrapper.find(FaveStar)).to.have.lengthOf(1);
+    });
+
+    it('should render the HeaderActionsDropdown', () => {
+      const wrapper = setup(overrideProps);
+      expect(wrapper.find(HeaderActionsDropdown)).to.have.lengthOf(1);
+    });
+
+    it('should render four Buttons', () => {
+      const wrapper = setup(overrideProps);
+      expect(wrapper.find(Button)).to.have.lengthOf(4);
+    });
+
+    it('should set up undo/redo', () => {
+      const wrapper = setup(overrideProps);
+      expect(wrapper.find(UndoRedoKeylisteners)).to.have.lengthOf(1);
+    });
+  });
+});
diff --git a/superset/assets/spec/javascripts/dashboard/util/getDashboardUrl_spec.js b/superset/assets/spec/javascripts/dashboard/util/getDashboardUrl_spec.js
new file mode 100644
index 0000000..c45e65a
--- /dev/null
+++ b/superset/assets/spec/javascripts/dashboard/util/getDashboardUrl_spec.js
@@ -0,0 +1,14 @@
+import { describe, it } from 'mocha';
+import { expect } from 'chai';
+
+import getDashboardUrl from '../../../../src/dashboard/util/getDashboardUrl';
+
+describe('getChartIdsFromLayout', () => {
+  it('should encode filters', () => {
+    const filters = { 35: { key: ['value'] } };
+    const url = getDashboardUrl('path', filters);
+    expect(url).to.equal(
+      'path?preselect_filters=%7B%2235%22%3A%7B%22key%22%3A%5B%22value%22%5D%7D%7D',
+    );
+  });
+});
diff --git a/superset/assets/src/components/URLShortLinkButton.jsx b/superset/assets/src/components/URLShortLinkButton.jsx
index 1efd4f7..19c77ab 100644
--- a/superset/assets/src/components/URLShortLinkButton.jsx
+++ b/superset/assets/src/components/URLShortLinkButton.jsx
@@ -20,6 +20,7 @@ class URLShortLinkButton extends React.Component {
       shortUrl: '',
     };
     this.onShortUrlSuccess = this.onShortUrlSuccess.bind(this);
+    this.getCopyUrl = this.getCopyUrl.bind(this);
   }
 
   onShortUrlSuccess(data) {
@@ -54,7 +55,7 @@ class URLShortLinkButton extends React.Component {
         trigger="click"
         rootClose
         placement="left"
-        onEnter={this.getCopyUrl.bind(this)}
+        onEnter={this.getCopyUrl}
         overlay={this.renderPopover()}
       >
         <span className="btn btn-default btn-sm">
diff --git a/superset/assets/src/components/URLShortLinkModal.jsx b/superset/assets/src/components/URLShortLinkModal.jsx
new file mode 100644
index 0000000..9f7a36b
--- /dev/null
+++ b/superset/assets/src/components/URLShortLinkModal.jsx
@@ -0,0 +1,78 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import CopyToClipboard from './CopyToClipboard';
+import { getShortUrl } from '../utils/common';
+import { t } from '../locales';
+import withToasts from '../messageToasts/enhancers/withToasts';
+import ModalTrigger from './ModalTrigger';
+
+const propTypes = {
+  url: PropTypes.string,
+  emailSubject: PropTypes.string,
+  emailContent: PropTypes.string,
+  addDangerToast: PropTypes.func.isRequired,
+  isMenuItem: PropTypes.bool,
+  triggerNode: PropTypes.node.isRequired,
+};
+
+class URLShortLinkModal extends React.Component {
+  constructor(props) {
+    super(props);
+    this.state = {
+      shortUrl: '',
+    };
+    this.modal = null;
+    this.setModalRef = this.setModalRef.bind(this);
+    this.onShortUrlSuccess = this.onShortUrlSuccess.bind(this);
+    this.getCopyUrl = this.getCopyUrl.bind(this);
+  }
+
+  onShortUrlSuccess(data) {
+    this.setState({
+      shortUrl: data,
+    });
+  }
+
+  setModalRef(ref) {
+    this.modal = ref;
+  }
+
+  getCopyUrl() {
+    getShortUrl(this.props.url, this.onShortUrlSuccess, this.props.addDangerToast);
+  }
+
+  render() {
+    const emailBody = t('%s%s', this.props.emailContent, this.state.shortUrl);
+    return (
+      <ModalTrigger
+        ref={this.setModalRef}
+        isMenuItem={this.props.isMenuItem}
+        triggerNode={this.props.triggerNode}
+        beforeOpen={this.getCopyUrl}
+        modalTitle={t('Share Dashboard')}
+        modalBody={
+          <div>
+            <CopyToClipboard
+              text={this.state.shortUrl}
+              copyNode={<i className="fa fa-clipboard" title={t('Copy to clipboard')}
/>}
+            />
+            &nbsp;&nbsp;
+            <a href={`mailto:?Subject=${this.props.emailSubject}%20&Body=${emailBody}`}>
+              <i className="fa fa-envelope" />
+            </a>
+          </div>
+        }
+      />
+    );
+  }
+}
+
+URLShortLinkModal.defaultProps = {
+  url: window.location.href.substring(window.location.origin.length),
+  emailSubject: '',
+  emailContent: '',
+};
+
+URLShortLinkModal.propTypes = propTypes;
+
+export default withToasts(URLShortLinkModal);
diff --git a/superset/assets/src/dashboard/components/Header.jsx b/superset/assets/src/dashboard/components/Header.jsx
index 0c1951b..9f976cb 100644
--- a/superset/assets/src/dashboard/components/Header.jsx
+++ b/superset/assets/src/dashboard/components/Header.jsx
@@ -189,100 +189,103 @@ class Header extends React.PureComponent {
           </span>
         </div>
 
-        {userCanSaveAs && (
-          <div className="button-container">
-            {editMode && (
-              <Button
-                bsSize="small"
-                onClick={onUndo}
-                disabled={undoLength < 1}
-                bsStyle={this.state.emphasizeUndo ? 'primary' : undefined}
-              >
-                <div title="Undo" className="undo-action fa fa-reply" />
-              </Button>
-            )}
-
-            {editMode && (
-              <Button
-                bsSize="small"
-                onClick={onRedo}
-                disabled={redoLength < 1}
-                bsStyle={this.state.emphasizeRedo ? 'primary' : undefined}
-              >
-                <div title="Redo" className="redo-action fa fa-share" />
-              </Button>
-            )}
-
-            {editMode && (
-              <Button bsSize="small" onClick={this.props.toggleBuilderPane}>
-                {showBuilderPane
-                  ? t('Hide components')
-                  : t('Insert components')}
-              </Button>
-            )}
-
-            {editMode &&
-              hasUnsavedChanges && (
+        <div className="button-container">
+          {userCanSaveAs && (
+            <div className="button-container">
+              {editMode && (
                 <Button
                   bsSize="small"
-                  bsStyle={popButton ? 'primary' : undefined}
-                  onClick={this.overwriteDashboard}
+                  onClick={onUndo}
+                  disabled={undoLength < 1}
+                  bsStyle={this.state.emphasizeUndo ? 'primary' : undefined}
                 >
-                  {t('Save changes')}
+                  <div title="Undo" className="undo-action fa fa-reply" />
                 </Button>
               )}
 
-            {!editMode &&
-              !hasUnsavedChanges && (
+              {editMode && (
                 <Button
                   bsSize="small"
-                  onClick={this.toggleEditMode}
-                  bsStyle={popButton ? 'primary' : undefined}
-                  disabled={!userCanEdit}
+                  onClick={onRedo}
+                  disabled={redoLength < 1}
+                  bsStyle={this.state.emphasizeRedo ? 'primary' : undefined}
                 >
-                  {t('Edit dashboard')}
+                  <div title="Redo" className="redo-action fa fa-share" />
                 </Button>
               )}
 
-            {editMode &&
-              !hasUnsavedChanges && (
-                <Button
-                  bsSize="small"
-                  onClick={this.toggleEditMode}
-                  bsStyle={undefined}
-                  disabled={!userCanEdit}
-                >
-                  {t('Switch to view mode')}
+              {editMode && (
+                <Button bsSize="small" onClick={this.props.toggleBuilderPane}>
+                  {showBuilderPane
+                    ? t('Hide components')
+                    : t('Insert components')}
                 </Button>
               )}
 
-            <HeaderActionsDropdown
-              addSuccessToast={this.props.addSuccessToast}
-              addDangerToast={this.props.addDangerToast}
-              dashboardId={dashboardInfo.id}
-              dashboardTitle={dashboardTitle}
-              layout={layout}
-              filters={filters}
-              expandedSlices={expandedSlices}
-              css={css}
-              onSave={onSave}
-              onChange={onChange}
-              forceRefreshAllCharts={this.forceRefresh}
-              startPeriodicRender={this.props.startPeriodicRender}
-              updateCss={updateCss}
-              editMode={editMode}
-              hasUnsavedChanges={hasUnsavedChanges}
-              userCanEdit={userCanEdit}
-            />
+              {editMode &&
+                hasUnsavedChanges && (
+                  <Button
+                    bsSize="small"
+                    bsStyle={popButton ? 'primary' : undefined}
+                    onClick={this.overwriteDashboard}
+                  >
+                    {t('Save changes')}
+                  </Button>
+                )}
+
+              {editMode &&
+                !hasUnsavedChanges && (
+                  <Button
+                    bsSize="small"
+                    onClick={this.toggleEditMode}
+                    bsStyle={undefined}
+                    disabled={!userCanEdit}
+                  >
+                    {t('Switch to view mode')}
+                  </Button>
+                )}
+
+              {editMode && (
+                <UndoRedoKeylisteners
+                  onUndo={this.handleCtrlZ}
+                  onRedo={this.handleCtrlY}
+                />
+              )}
+            </div>
+          )}
 
-            {editMode && (
-              <UndoRedoKeylisteners
-                onUndo={this.handleCtrlZ}
-                onRedo={this.handleCtrlY}
-              />
+          {!editMode &&
+            !hasUnsavedChanges && (
+              <Button
+                bsSize="small"
+                onClick={this.toggleEditMode}
+                bsStyle={popButton ? 'primary' : undefined}
+                disabled={!userCanEdit}
+              >
+                {t('Edit dashboard')}
+              </Button>
             )}
-          </div>
-        )}
+
+          <HeaderActionsDropdown
+            addSuccessToast={this.props.addSuccessToast}
+            addDangerToast={this.props.addDangerToast}
+            dashboardId={dashboardInfo.id}
+            dashboardTitle={dashboardTitle}
+            layout={layout}
+            filters={filters}
+            expandedSlices={expandedSlices}
+            css={css}
+            onSave={onSave}
+            onChange={onChange}
+            forceRefreshAllCharts={this.forceRefresh}
+            startPeriodicRender={this.props.startPeriodicRender}
+            updateCss={updateCss}
+            editMode={editMode}
+            hasUnsavedChanges={hasUnsavedChanges}
+            userCanEdit={userCanEdit}
+            userCanSave={userCanSaveAs}
+          />
+        </div>
       </div>
     );
   }
diff --git a/superset/assets/src/dashboard/components/HeaderActionsDropdown.jsx b/superset/assets/src/dashboard/components/HeaderActionsDropdown.jsx
index dab11c3..b5e5d02 100644
--- a/superset/assets/src/dashboard/components/HeaderActionsDropdown.jsx
+++ b/superset/assets/src/dashboard/components/HeaderActionsDropdown.jsx
@@ -10,6 +10,8 @@ import SaveModal from './SaveModal';
 import injectCustomCss from '../util/injectCustomCss';
 import { SAVE_TYPE_NEWDASHBOARD } from '../util/constants';
 import { t } from '../../locales';
+import URLShortLinkModal from '../../components/URLShortLinkModal';
+import getDashboardUrl from '../util/getDashboardUrl';
 
 const propTypes = {
   addSuccessToast: PropTypes.func.isRequired,
@@ -24,6 +26,7 @@ const propTypes = {
   startPeriodicRender: PropTypes.func.isRequired,
   editMode: PropTypes.bool.isRequired,
   userCanEdit: PropTypes.bool.isRequired,
+  userCanSave: PropTypes.bool.isRequired,
   layout: PropTypes.object.isRequired,
   filters: PropTypes.object.isRequired,
   expandedSlices: PropTypes.object.isRequired,
@@ -82,10 +85,12 @@ class HeaderActionsDropdown extends React.PureComponent {
       expandedSlices,
       onSave,
       userCanEdit,
+      userCanSave,
     } = this.props;
 
-    const emailBody = t('Check out this dashboard: %s', window.location.href);
-    const emailLink = `mailto:?Subject=Superset%20Dashboard%20${dashboardTitle}&Body=${emailBody}`;
+    const emailTitle = t('Superset Dashboard');
+    const emailSubject = `${emailTitle} ${dashboardTitle}`;
+    const emailBody = t('Check out this dashboard: ');
 
     return (
       <DropdownButton
@@ -95,31 +100,37 @@ class HeaderActionsDropdown extends React.PureComponent {
         bsSize="small"
         pullRight
       >
-        <SaveModal
-          addSuccessToast={this.props.addSuccessToast}
-          addDangerToast={this.props.addDangerToast}
-          dashboardId={dashboardId}
-          dashboardTitle={dashboardTitle}
-          saveType={SAVE_TYPE_NEWDASHBOARD}
-          layout={layout}
-          filters={filters}
-          expandedSlices={expandedSlices}
-          css={css}
-          onSave={onSave}
-          isMenuItem
-          triggerNode={<span>{t('Save as')}</span>}
-          canOverwrite={userCanEdit}
-        />
-        {hasUnsavedChanges && (
-          <MenuItem
-            eventKey="discard"
-            onSelect={HeaderActionsDropdown.discardChanges}
-          >
-            {t('Discard changes')}
-          </MenuItem>
+        {userCanSave && (
+          <SaveModal
+            addSuccessToast={this.props.addSuccessToast}
+            addDangerToast={this.props.addDangerToast}
+            dashboardId={dashboardId}
+            dashboardTitle={dashboardTitle}
+            saveType={SAVE_TYPE_NEWDASHBOARD}
+            layout={layout}
+            filters={filters}
+            expandedSlices={expandedSlices}
+            css={css}
+            onSave={onSave}
+            isMenuItem
+            triggerNode={<span>{t('Save as')}</span>}
+            canOverwrite={userCanEdit}
+          />
         )}
 
-        <MenuItem divider />
+        {hasUnsavedChanges &&
+          userCanSave && (
+            <div>
+              <MenuItem
+                eventKey="discard"
+                onSelect={HeaderActionsDropdown.discardChanges}
+              >
+                {t('Discard changes')}
+              </MenuItem>
+            </div>
+          )}
+
+        {userCanSave && <MenuItem divider />}
 
         <MenuItem onClick={forceRefreshAllCharts}>
           {t('Force refresh dashboard')}
@@ -138,9 +149,16 @@ class HeaderActionsDropdown extends React.PureComponent {
             {t('Edit dashboard metadata')}
           </MenuItem>
         )}
-        {editMode && (
-          <MenuItem href={emailLink}>{t('Email dashboard link')}</MenuItem>
-        )}
+
+        <URLShortLinkModal
+          url={getDashboardUrl(window.location.pathname, this.props.filters)}
+          emailSubject={emailSubject}
+          emailContent={emailBody}
+          addDangerToast={this.props.addDangerToast}
+          isMenuItem
+          triggerNode={<span>{t('Share dashboard')}</span>}
+        />
+
         {editMode && (
           <CssEditor
             triggerNode={<span>{t('Edit CSS')}</span>}
diff --git a/superset/assets/src/dashboard/util/getDashboardUrl.js b/superset/assets/src/dashboard/util/getDashboardUrl.js
new file mode 100644
index 0000000..d26ca90
--- /dev/null
+++ b/superset/assets/src/dashboard/util/getDashboardUrl.js
@@ -0,0 +1,6 @@
+/* eslint camelcase: 0 */
+
+export default function getDashboardUrl(pathname, filters = {}) {
+  const preselect_filters = encodeURIComponent(JSON.stringify(filters));
+  return `${pathname}?preselect_filters=${preselect_filters}`;
+}


Mime
View raw message