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 0000000000..494d0d390a --- /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 0000000000..673118bd3b --- /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 0000000000..e7ecfc142c --- /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 0000000000..c45e65a02a --- /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 1efd4f7122..19c77ab332 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 0000000000..9f7a36bce4 --- /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 0c1951b8d7..9f976cbab9 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 && ( +
+ {userCanSaveAs && ( +
+ {editMode && ( + + )} + + {editMode && ( + + )} + + {editMode && ( + + )} + + {editMode && + hasUnsavedChanges && ( + + )} + + {editMode && + !hasUnsavedChanges && ( + + )} + + {editMode && ( + + )} +
+ )} + + {!editMode && + !hasUnsavedChanges && ( )} - {editMode && ( - - )} - - {editMode && ( - - )} - - {editMode && - hasUnsavedChanges && ( - - )} - - {!editMode && - !hasUnsavedChanges && ( - - )} - - {editMode && - !hasUnsavedChanges && ( - - )} - - - - {editMode && ( - - )} -
- )} + +
); } diff --git a/superset/assets/src/dashboard/components/HeaderActionsDropdown.jsx b/superset/assets/src/dashboard/components/HeaderActionsDropdown.jsx index dab11c382f..b5e5d022b9 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 0000000000..d26ca90330 --- /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}`; +}