From 5ee2588111b3309d42fabb6bed8e8b93ef286aa7 Mon Sep 17 00:00:00 2001 From: Maxime Beauchemin Date: Tue, 28 Nov 2017 09:10:21 -0800 Subject: [PATCH] Add an "Edit Mode" to Dashboard view (#3940) * Add a togglable edit mode to dashboard * Submenu for controls * Allowing 'Save as' outside of editMode * Set editMode to false as default --- .../javascripts/components/EditableTitle.jsx | 32 +-- .../javascripts/components/ModalTrigger.jsx | 12 +- .../assets/javascripts/dashboard/actions.js | 5 + .../dashboard/components/Controls.jsx | 184 ++++++++++++------ .../dashboard/components/CssEditor.jsx | 2 +- .../dashboard/components/Dashboard.jsx | 38 +--- .../dashboard/components/DashboardAlert.jsx | 21 -- .../components/DashboardContainer.jsx | 1 + .../dashboard/components/GridCell.jsx | 3 + .../dashboard/components/GridLayout.jsx | 2 + .../dashboard/components/Header.jsx | 53 ++++- .../components/RefreshIntervalModal.jsx | 2 +- .../dashboard/components/SaveModal.jsx | 1 + .../dashboard/components/SliceAdder.jsx | 7 +- .../dashboard/components/SliceHeader.jsx | 44 +++-- .../assets/javascripts/dashboard/reducers.js | 5 +- superset/assets/package.json | 4 +- 17 files changed, 259 insertions(+), 157 deletions(-) delete mode 100644 superset/assets/javascripts/dashboard/components/DashboardAlert.jsx diff --git a/superset/assets/javascripts/components/EditableTitle.jsx b/superset/assets/javascripts/components/EditableTitle.jsx index 31c4c53c96e03..b773340846622 100644 --- a/superset/assets/javascripts/components/EditableTitle.jsx +++ b/superset/assets/javascripts/components/EditableTitle.jsx @@ -8,10 +8,12 @@ const propTypes = { canEdit: PropTypes.bool, onSaveTitle: PropTypes.func, noPermitTooltip: PropTypes.string, + showTooltip: PropTypes.bool, }; const defaultProps = { title: t('Title'), canEdit: false, + showTooltip: true, }; class EditableTitle extends React.PureComponent { @@ -85,24 +87,30 @@ class EditableTitle extends React.PureComponent { } } render() { - return ( - + let input = ( + + ); + if (this.props.showTooltip) { + input = ( - + {input} - + ); + } + return ( + {input} ); } } diff --git a/superset/assets/javascripts/components/ModalTrigger.jsx b/superset/assets/javascripts/components/ModalTrigger.jsx index 315a75354d97e..67a83e6c21627 100644 --- a/superset/assets/javascripts/components/ModalTrigger.jsx +++ b/superset/assets/javascripts/components/ModalTrigger.jsx @@ -1,7 +1,8 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { Modal } from 'react-bootstrap'; +import { Modal, MenuItem } from 'react-bootstrap'; import cx from 'classnames'; + import Button from './Button'; const propTypes = { @@ -13,6 +14,7 @@ const propTypes = { beforeOpen: PropTypes.func, onExit: PropTypes.func, isButton: PropTypes.bool, + isMenuItem: PropTypes.bool, bsSize: PropTypes.string, className: PropTypes.string, tooltip: PropTypes.string, @@ -23,6 +25,7 @@ const defaultProps = { beforeOpen: () => {}, onExit: () => {}, isButton: false, + isMenuItem: false, bsSize: null, className: '', }; @@ -86,6 +89,13 @@ export default class ModalTrigger extends React.Component { {this.renderModal()} ); + } else if (this.props.isMenuItem) { + return ( + + {this.props.triggerNode} + {this.renderModal()} + + ); } /* eslint-disable jsx-a11y/interactive-supports-focus */ return ( diff --git a/superset/assets/javascripts/dashboard/actions.js b/superset/assets/javascripts/dashboard/actions.js index 6e88ca64041a2..25fa117c47dfc 100644 --- a/superset/assets/javascripts/dashboard/actions.js +++ b/superset/assets/javascripts/dashboard/actions.js @@ -110,3 +110,8 @@ export const TOGGLE_EXPAND_SLICE = 'TOGGLE_EXPAND_SLICE'; export function toggleExpandSlice(slice, isExpanded) { return { type: TOGGLE_EXPAND_SLICE, slice, isExpanded }; } + +export const SET_EDIT_MODE = 'SET_EDIT_MODE'; +export function setEditMode(editMode) { + return { type: SET_EDIT_MODE, editMode }; +} diff --git a/superset/assets/javascripts/dashboard/components/Controls.jsx b/superset/assets/javascripts/dashboard/components/Controls.jsx index ecbc907ef0e5d..ead2c9af6d1e5 100644 --- a/superset/assets/javascripts/dashboard/components/Controls.jsx +++ b/superset/assets/javascripts/dashboard/components/Controls.jsx @@ -1,14 +1,13 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { ButtonGroup } from 'react-bootstrap'; +import { DropdownButton, MenuItem } from 'react-bootstrap'; -import Button from '../../components/Button'; import CssEditor from './CssEditor'; import RefreshIntervalModal from './RefreshIntervalModal'; import SaveModal from './SaveModal'; -import CodeModal from './CodeModal'; import SliceAdder from './SliceAdder'; import { t } from '../../locales'; +import InfoTooltipWithTrigger from '../../components/InfoTooltipWithTrigger'; const $ = window.$ = require('jquery'); @@ -23,6 +22,38 @@ const propTypes = { renderSlices: PropTypes.func, serialize: PropTypes.func, startPeriodicRender: PropTypes.func, + editMode: PropTypes.bool, +}; + +function MenuItemContent({ faIcon, text, tooltip, children }) { + return ( + + {text} {''} + + {children} + + ); +} +MenuItemContent.propTypes = { + faIcon: PropTypes.string.isRequired, + text: PropTypes.string, + tooltip: PropTypes.string, + children: PropTypes.node, +}; + +function ActionMenuItem(props) { + return ( + + + + ); +} +ActionMenuItem.propTypes = { + onClick: PropTypes.func, }; class Controls extends React.PureComponent { @@ -32,6 +63,8 @@ class Controls extends React.PureComponent { css: props.dashboard.css || '', cssTemplates: [], }; + this.refresh = this.refresh.bind(this); + this.toggleModal = this.toggleModal.bind(this); } componentWillMount() { $.get('/csstemplateasyncmodelview/api/read', (data) => { @@ -47,6 +80,13 @@ class Controls extends React.PureComponent { // Force refresh all slices this.props.renderSlices(true); } + toggleModal(modal) { + let currentModal; + if (modal !== this.state.currentModal) { + currentModal = modal; + } + this.setState({ currentModal }); + } changeCss(css) { this.setState({ css }); this.props.onChange(); @@ -54,72 +94,94 @@ class Controls extends React.PureComponent { render() { const { dashboard, userId, addSlicesToDashboard, startPeriodicRender, readFilters, - serialize, onSave } = this.props; + serialize, onSave, editMode } = this.props; const emailBody = t('Checkout this dashboard: %s', window.location.href); const emailLink = 'mailto:?Subject=Superset%20Dashboard%20' + `${dashboard.dashboard_title}&Body=${emailBody}`; + let saveText = t('Save as'); + if (editMode) { + saveText = t('Save'); + } return ( - - - + + + + startPeriodicRender(refreshInterval * 1000)} + triggerNode={ + + } + /> + + } + /> + {editMode && + { window.location = `/dashboardmodelview/edit/${dashboard.id}`; }} + /> } - /> - startPeriodicRender(refreshInterval * 1000)} - triggerNode={ - + {editMode && + { window.location = emailLink; }} + faIcon="envelope" + /> } - /> - } - /> - + {editMode && + + } + /> } - initialCss={dashboard.css} - templates={this.state.cssTemplates} - onChange={this.changeCss.bind(this)} - /> - - - - - + {editMode && + + } + initialCss={dashboard.css} + templates={this.state.cssTemplates} + onChange={this.changeCss.bind(this)} + /> } - /> - + + ); } } diff --git a/superset/assets/javascripts/dashboard/components/CssEditor.jsx b/superset/assets/javascripts/dashboard/components/CssEditor.jsx index bbcc19f078604..a9434a8e10a7f 100644 --- a/superset/assets/javascripts/dashboard/components/CssEditor.jsx +++ b/superset/assets/javascripts/dashboard/components/CssEditor.jsx @@ -78,7 +78,7 @@ class CssEditor extends React.PureComponent { {this.renderTemplateSelector()} diff --git a/superset/assets/javascripts/dashboard/components/Dashboard.jsx b/superset/assets/javascripts/dashboard/components/Dashboard.jsx index 553daf6e8d891..064ed5fe804a9 100644 --- a/superset/assets/javascripts/dashboard/components/Dashboard.jsx +++ b/superset/assets/javascripts/dashboard/components/Dashboard.jsx @@ -4,7 +4,6 @@ import PropTypes from 'prop-types'; import AlertsWrapper from '../../components/AlertsWrapper'; import GridLayout from './GridLayout'; import Header from './Header'; -import DashboardAlert from './DashboardAlert'; import { getExploreUrl } from '../../explore/exploreUtils'; import { areObjectsEqual } from '../../reduxUtils'; import { t } from '../../locales'; @@ -22,6 +21,7 @@ const propTypes = { timeout: PropTypes.number, userId: PropTypes.string, isStarred: PropTypes.bool, + editMode: PropTypes.bool, }; const defaultProps = { @@ -33,6 +33,7 @@ const defaultProps = { timeout: 60, userId: '', isStarred: false, + editMode: false, }; class Dashboard extends React.PureComponent { @@ -42,10 +43,7 @@ class Dashboard extends React.PureComponent { this.firstLoad = true; // alert for unsaved changes - this.state = { - alert: null, - trigger: false, - }; + this.state = { unsavedChanges: false }; this.rerenderCharts = this.rerenderCharts.bind(this); this.updateDashboardTitle = this.updateDashboardTitle.bind(this); @@ -76,13 +74,6 @@ class Dashboard extends React.PureComponent { window.addEventListener('resize', this.rerenderCharts); } - componentWillReceiveProps(nextProps) { - // check filters is changed - if (!areObjectsEqual(nextProps.filters, this.props.filters)) { - this.renderUnsavedChangeAlert(); - } - } - componentDidUpdate(prevProps) { if (!areObjectsEqual(prevProps.filters, this.props.filters) && this.props.refresh) { Object.keys(this.props.filters).forEach(sliceId => (this.refreshExcept(sliceId))); @@ -103,14 +94,12 @@ class Dashboard extends React.PureComponent { onChange() { this.onBeforeUnload(true); - this.renderUnsavedChangeAlert(); + this.setState({ unsavedChanges: true }); } onSave() { this.onBeforeUnload(false); - this.setState({ - alert: '', - }); + this.setState({ unsavedChanges: false }); } // return charts in array @@ -283,26 +272,14 @@ class Dashboard extends React.PureComponent { }); } - renderUnsavedChangeAlert() { - this.setState({ - alert: ( - - {t('You have unsaved changes.')} {t('Click the')}   -   - {t('button on the top right to save your changes.')} - - ), - }); - } - render() { return (
- {this.state.alert && }
@@ -336,6 +315,7 @@ class Dashboard extends React.PureComponent { getFilters={this.getFilters} clearFilter={this.props.actions.clearFilter} removeFilter={this.props.actions.removeFilter} + editMode={this.props.editMode} />
diff --git a/superset/assets/javascripts/dashboard/components/DashboardAlert.jsx b/superset/assets/javascripts/dashboard/components/DashboardAlert.jsx deleted file mode 100644 index 4579ce880dacb..0000000000000 --- a/superset/assets/javascripts/dashboard/components/DashboardAlert.jsx +++ /dev/null @@ -1,21 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { Alert } from 'react-bootstrap'; - -const propTypes = { - alertContent: PropTypes.node.isRequired, -}; - -const DashboardAlert = ({ alertContent }) => ( -
-
- - {alertContent} - -
-
-); - -DashboardAlert.propTypes = propTypes; - -export default DashboardAlert; diff --git a/superset/assets/javascripts/dashboard/components/DashboardContainer.jsx b/superset/assets/javascripts/dashboard/components/DashboardContainer.jsx index 24127aaa672d9..f575ab71240ac 100644 --- a/superset/assets/javascripts/dashboard/components/DashboardContainer.jsx +++ b/superset/assets/javascripts/dashboard/components/DashboardContainer.jsx @@ -16,6 +16,7 @@ function mapStateToProps({ charts, dashboard }) { refresh: dashboard.refresh, userId: dashboard.userId, isStarred: !!dashboard.isStarred, + editMode: dashboard.editMode, }; } diff --git a/superset/assets/javascripts/dashboard/components/GridCell.jsx b/superset/assets/javascripts/dashboard/components/GridCell.jsx index b0c86ad0c4990..854aea01fe74a 100644 --- a/superset/assets/javascripts/dashboard/components/GridCell.jsx +++ b/superset/assets/javascripts/dashboard/components/GridCell.jsx @@ -30,6 +30,7 @@ const propTypes = { getFilters: PropTypes.func, clearFilter: PropTypes.func, removeFilter: PropTypes.func, + editMode: PropTypes.bool, }; const defaultProps = { @@ -41,6 +42,7 @@ const defaultProps = { getFilters: () => ({}), clearFilter: () => ({}), removeFilter: () => ({}), + editMode: false, }; class GridCell extends React.PureComponent { @@ -101,6 +103,7 @@ class GridCell extends React.PureComponent { updateSliceName={updateSliceName} toggleExpandSlice={toggleExpandSlice} forceRefresh={forceRefresh} + editMode={this.props.editMode} />
); }); diff --git a/superset/assets/javascripts/dashboard/components/Header.jsx b/superset/assets/javascripts/dashboard/components/Header.jsx index dfba7e86f16f4..b7eece0b1de05 100644 --- a/superset/assets/javascripts/dashboard/components/Header.jsx +++ b/superset/assets/javascripts/dashboard/components/Header.jsx @@ -3,7 +3,10 @@ import PropTypes from 'prop-types'; import Controls from './Controls'; import EditableTitle from '../../components/EditableTitle'; +import Button from '../../components/Button'; import FaveStar from '../../components/FaveStar'; +import InfoTooltipWithTrigger from '../../components/InfoTooltipWithTrigger'; +import { t } from '../../locales'; const propTypes = { dashboard: PropTypes.object.isRequired, @@ -19,30 +22,65 @@ const propTypes = { serialize: PropTypes.func, startPeriodicRender: PropTypes.func, updateDashboardTitle: PropTypes.func, + editMode: PropTypes.bool.isRequired, + setEditMode: PropTypes.func.isRequired, + unsavedChanges: PropTypes.bool.isRequired, }; class Header extends React.PureComponent { constructor(props) { super(props); - this.handleSaveTitle = this.handleSaveTitle.bind(this); + this.toggleEditMode = this.toggleEditMode.bind(this); } handleSaveTitle(title) { this.props.updateDashboardTitle(title); } + toggleEditMode() { + this.props.setEditMode(!this.props.editMode); + } + renderUnsaved() { + if (!this.props.unsavedChanges) { + return null; + } + return ( + + ); + } + renderEditButton() { + if (!this.props.dashboard.dash_save_perm) { + return null; + } + const btnText = this.props.editMode ? 'Switch to View Mode' : 'Edit Dashboard'; + return ( + ); + } render() { const dashboard = this.props.dashboard; return (
-

+

- + + {this.renderUnsaved()}

- {!this.props.dashboard.standalone_mode && + {this.renderEditButton()} - }
diff --git a/superset/assets/javascripts/dashboard/components/RefreshIntervalModal.jsx b/superset/assets/javascripts/dashboard/components/RefreshIntervalModal.jsx index e927e63755697..4cba010d9540f 100644 --- a/superset/assets/javascripts/dashboard/components/RefreshIntervalModal.jsx +++ b/superset/assets/javascripts/dashboard/components/RefreshIntervalModal.jsx @@ -34,7 +34,7 @@ class RefreshIntervalModal extends React.PureComponent { return ( diff --git a/superset/assets/javascripts/dashboard/components/SaveModal.jsx b/superset/assets/javascripts/dashboard/components/SaveModal.jsx index cc91daee4cb19..a55fbb2180067 100644 --- a/superset/assets/javascripts/dashboard/components/SaveModal.jsx +++ b/superset/assets/javascripts/dashboard/components/SaveModal.jsx @@ -106,6 +106,7 @@ class SaveModal extends React.PureComponent { return ( { this.modal = modal; }} + isMenuItem triggerNode={this.props.triggerNode} modalTitle={t('Save Dashboard')} modalBody={ diff --git a/superset/assets/javascripts/dashboard/components/SliceAdder.jsx b/superset/assets/javascripts/dashboard/components/SliceAdder.jsx index 6c2ea0e4b73df..d5be8caff6cfa 100644 --- a/superset/assets/javascripts/dashboard/components/SliceAdder.jsx +++ b/superset/assets/javascripts/dashboard/components/SliceAdder.jsx @@ -41,7 +41,9 @@ class SliceAdder extends React.Component { } componentWillUnmount() { - this.slicesRequest.abort(); + if (this.slicesRequest) { + this.slicesRequest.abort(); + } } onEnterModal() { @@ -202,9 +204,10 @@ class SliceAdder extends React.Component { triggerNode={this.props.triggerNode} tooltip={t('Add a new slice to the dashboard')} beforeOpen={this.onEnterModal.bind(this)} - isButton + isMenuItem modalBody={modalContent} bsSize="large" + setModalAsTriggerChildren modalTitle={t('Add Slices to Dashboard')} /> ); diff --git a/superset/assets/javascripts/dashboard/components/SliceHeader.jsx b/superset/assets/javascripts/dashboard/components/SliceHeader.jsx index 4e8a33537c9a2..36107fedf1e0d 100644 --- a/superset/assets/javascripts/dashboard/components/SliceHeader.jsx +++ b/superset/assets/javascripts/dashboard/components/SliceHeader.jsx @@ -18,6 +18,7 @@ const propTypes = { updateSliceName: PropTypes.func, toggleExpandSlice: PropTypes.func, forceRefresh: PropTypes.func, + editMode: PropTypes.bool, }; const defaultProps = { @@ -25,6 +26,7 @@ const defaultProps = { removeSlice: () => ({}), updateSliceName: () => ({}), toggleExpandSlice: () => ({}), + editMode: false, }; class SliceHeader extends React.PureComponent { @@ -55,22 +57,24 @@ class SliceHeader extends React.PureComponent {
diff --git a/superset/assets/javascripts/dashboard/reducers.js b/superset/assets/javascripts/dashboard/reducers.js index 487f56feafb05..4919dc43f35dc 100644 --- a/superset/assets/javascripts/dashboard/reducers.js +++ b/superset/assets/javascripts/dashboard/reducers.js @@ -83,7 +83,7 @@ export function getInitialState(bootstrapData) { return { charts: initCharts, - dashboard: { filters, dashboard, userId: user_id, datasources, common }, + dashboard: { filters, dashboard, userId: user_id, datasources, common, editMode: false }, }; } @@ -107,6 +107,9 @@ const dashboard = function (state = {}, action) { [actions.TOGGLE_FAVE_STAR]() { return { ...state, isStarred: action.isStarred }; }, + [actions.SET_EDIT_MODE]() { + return { ...state, editMode: action.editMode }; + }, [actions.TOGGLE_EXPAND_SLICE]() { const updatedExpandedSlices = { ...state.dashboard.metadata.expanded_slices }; const sliceId = action.slice.slice_id; diff --git a/superset/assets/package.json b/superset/assets/package.json index 16a2757d2879c..c3c217437a589 100644 --- a/superset/assets/package.json +++ b/superset/assets/package.json @@ -79,7 +79,7 @@ "react-datetime": "2.9.0", "react-dom": "^15.6.2", "react-gravatar": "^2.6.1", - "react-grid-layout": "^0.14.4", + "react-grid-layout": "^0.16.0", "react-map-gl": "^3.0.4", "react-redux": "^5.0.2", "react-resizable": "^1.3.3", @@ -98,8 +98,8 @@ "sprintf-js": "^1.1.1", "srcdoc-polyfill": "^1.0.0", "supercluster": "https://github.com/georgeke/supercluster/tarball/ac3492737e7ce98e07af679623aad452373bbc40", - "urijs": "^1.18.10", "underscore": "^1.8.3", + "urijs": "^1.18.10", "viewport-mercator-project": "^2.1.0" }, "devDependencies": {