Return-Path: X-Original-To: archive-asf-public-internal@cust-asf2.ponee.io Delivered-To: archive-asf-public-internal@cust-asf2.ponee.io Received: from cust-asf.ponee.io (cust-asf.ponee.io [163.172.22.183]) by cust-asf2.ponee.io (Postfix) with ESMTP id 1D4E3200D4A for ; Tue, 28 Nov 2017 18:10:28 +0100 (CET) Received: by cust-asf.ponee.io (Postfix) id 1BABD160C07; Tue, 28 Nov 2017 17:10:28 +0000 (UTC) Delivered-To: archive-asf-public@cust-asf.ponee.io Received: from mail.apache.org (hermes.apache.org [140.211.11.3]) by cust-asf.ponee.io (Postfix) with SMTP id 8EFFC160BE7 for ; Tue, 28 Nov 2017 18:10:26 +0100 (CET) Received: (qmail 86587 invoked by uid 500); 28 Nov 2017 17:10:25 -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 86578 invoked by uid 99); 28 Nov 2017 17:10:25 -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; Tue, 28 Nov 2017 17:10:25 +0000 Received: by gitbox.apache.org (ASF Mail Server at gitbox.apache.org, from userid 33) id 973ED8556E; Tue, 28 Nov 2017 17:10:24 +0000 (UTC) Date: Tue, 28 Nov 2017 17:10:24 +0000 To: "commits@superset.apache.org" Subject: [incubator-superset] branch master updated: Add an "Edit Mode" to Dashboard view (#3940) MIME-Version: 1.0 Content-Type: text/plain; charset=utf-8 Content-Transfer-Encoding: 8bit Message-ID: <151188902418.31533.6120022759564334065@gitbox.apache.org> From: graceguo@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: 6cbe0e60966d77037e691a8e4de8a05e3a77edad X-Git-Newrev: d9fda346cbddc7868e99d03d01e8de43b5c02046 X-Git-Rev: d9fda346cbddc7868e99d03d01e8de43b5c02046 X-Git-NotificationType: ref_changed_plus_diff X-Git-Multimail-Version: 1.5.dev Auto-Submitted: auto-generated archived-at: Tue, 28 Nov 2017 17:10:28 -0000 This is an automated email from the ASF dual-hosted git repository. graceguo 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 d9fda34 Add an "Edit Mode" to Dashboard view (#3940) d9fda34 is described below commit d9fda346cbddc7868e99d03d01e8de43b5c02046 Author: Maxime Beauchemin AuthorDate: Tue Nov 28 09:10:21 2017 -0800 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 ++-- .../assets/javascripts/components/ModalTrigger.jsx | 12 +- superset/assets/javascripts/dashboard/actions.js | 5 + .../javascripts/dashboard/components/Controls.jsx | 184 ++++++++++++++------- .../javascripts/dashboard/components/CssEditor.jsx | 2 +- .../javascripts/dashboard/components/Dashboard.jsx | 38 +---- .../dashboard/components/DashboardAlert.jsx | 21 --- .../dashboard/components/DashboardContainer.jsx | 1 + .../javascripts/dashboard/components/GridCell.jsx | 3 + .../dashboard/components/GridLayout.jsx | 2 + .../javascripts/dashboard/components/Header.jsx | 53 +++++- .../dashboard/components/RefreshIntervalModal.jsx | 2 +- .../javascripts/dashboard/components/SaveModal.jsx | 1 + .../dashboard/components/SliceAdder.jsx | 7 +- .../dashboard/components/SliceHeader.jsx | 44 ++--- superset/assets/javascripts/dashboard/reducers.js | 5 +- superset/assets/package.json | 4 +- 17 files changed, 259 insertions(+), 157 deletions(-) diff --git a/superset/assets/javascripts/components/EditableTitle.jsx b/superset/assets/javascripts/components/EditableTitle.jsx index 31c4c53..b773340 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 315a753..67a83e6 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 6e88ca6..25fa117 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 ecbc907..ead2c9a 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 bbcc19f..a9434a8 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 553daf6..064ed5f 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 4579ce8..0000000 --- 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 24127aa..f575ab7 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 b0c86ad..854aea0 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 dfba7e8..b7eece0 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 e927e63..4cba010 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 cc91dae..a55fbb2 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 6c2ea0e..d5be8ca 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 4e8a335..36107fe 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 487f56f..4919dc4 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 16a2757..c3c2174 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": { -- To stop receiving notification emails like this one, please contact ['"commits@superset.apache.org" '].