From commits-return-1277-archive-asf-public=cust-asf.ponee.io@superset.incubator.apache.org Fri Aug 3 18:22:45 2018 Return-Path: X-Original-To: archive-asf-public@cust-asf.ponee.io Delivered-To: archive-asf-public@cust-asf.ponee.io Received: from mail.apache.org (hermes.apache.org [140.211.11.3]) by mx-eu-01.ponee.io (Postfix) with SMTP id 5C64D180647 for ; Fri, 3 Aug 2018 18:22:44 +0200 (CEST) Received: (qmail 34898 invoked by uid 500); 3 Aug 2018 16:22:43 -0000 Mailing-List: contact commits-help@superset.incubator.apache.org; run by ezmlm Precedence: bulk List-Help: List-Unsubscribe: List-Post: List-Id: Reply-To: dev@superset.incubator.apache.org Delivered-To: mailing list commits@superset.incubator.apache.org Received: (qmail 34889 invoked by uid 99); 3 Aug 2018 16:22:43 -0000 Received: from ec2-52-202-80-70.compute-1.amazonaws.com (HELO gitbox.apache.org) (52.202.80.70) by apache.org (qpsmtpd/0.29) with ESMTP; Fri, 03 Aug 2018 16:22:43 +0000 Received: by gitbox.apache.org (ASF Mail Server at gitbox.apache.org, from userid 33) id E3CA382108; Fri, 3 Aug 2018 16:22:42 +0000 (UTC) Date: Fri, 03 Aug 2018 16:22:42 +0000 To: "commits@superset.apache.org" Subject: [incubator-superset] branch master updated: Re-add dashboard short links (#5398) MIME-Version: 1.0 Content-Type: text/plain; charset=utf-8 Content-Transfer-Encoding: 8bit Message-ID: <153331336272.7304.3121168675744028245@gitbox.apache.org> From: maximebeauchemin@apache.org X-Git-Host: gitbox.apache.org X-Git-Repo: incubator-superset X-Git-Refname: refs/heads/master X-Git-Reftype: branch X-Git-Oldrev: 0aff8659d81345374bd08d513adb10a0884867e5 X-Git-Newrev: aa9b30cf55bd0ee393f688f4c3951a5cc1ff77b7 X-Git-Rev: aa9b30cf55bd0ee393f688f4c3951a5cc1ff77b7 X-Git-NotificationType: ref_changed_plus_diff X-Git-Multimail-Version: 1.5.dev Auto-Submitted: auto-generated 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 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(, { 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( + , + ); + 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(
); + 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()} > 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 ( + + } + /> +    + + + + + } + /> + ); + } +} + +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 { - {userCanSaveAs && ( -
- {editMode && ( - - )} - - {editMode && ( - - )} - - {editMode && ( - - )} - - {editMode && - hasUnsavedChanges && ( +
+ {userCanSaveAs && ( +
+ {editMode && ( )} - {!editMode && - !hasUnsavedChanges && ( + {editMode && ( )} - {editMode && - !hasUnsavedChanges && ( - )} - + {editMode && + hasUnsavedChanges && ( + + )} + + {editMode && + !hasUnsavedChanges && ( + + )} + + {editMode && ( + + )} +
+ )} - {editMode && ( - + {!editMode && + !hasUnsavedChanges && ( + )} -
- )} + + +
); } 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 ( - {t('Save as')}} - canOverwrite={userCanEdit} - /> - {hasUnsavedChanges && ( - - {t('Discard changes')} - + {userCanSave && ( + {t('Save as')}} + canOverwrite={userCanEdit} + /> )} - + {hasUnsavedChanges && + userCanSave && ( +
+ + {t('Discard changes')} + +
+ )} + + {userCanSave && } {t('Force refresh dashboard')} @@ -138,9 +149,16 @@ class HeaderActionsDropdown extends React.PureComponent { {t('Edit dashboard metadata')} )} - {editMode && ( - {t('Email dashboard link')} - )} + + {t('Share dashboard')}} + /> + {editMode && ( {t('Edit CSS')}} 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}`; +}