superset-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From grace...@apache.org
Subject [incubator-superset] branch master updated: Add an "Edit Mode" to Dashboard view (#3940)
Date Tue, 28 Nov 2017 17:10:24 GMT
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 <maximebeauchemin@gmail.com>
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 (
-      <span className="editable-title">
+    let input = (
+      <input
+        required
+        type={this.state.isEditing ? 'text' : 'button'}
+        value={this.state.title}
+        onChange={this.handleChange}
+        onBlur={this.handleBlur}
+        onClick={this.handleClick}
+        onKeyPress={this.handleKeyPress}
+      />
+    );
+    if (this.props.showTooltip) {
+      input = (
         <TooltipWrapper
           label="title"
           tooltip={this.props.canEdit ? t('click to edit title') :
               this.props.noPermitTooltip || t('You don\'t have the rights to alter this title.')}
         >
-          <input
-            required
-            type={this.state.isEditing ? 'text' : 'button'}
-            value={this.state.title}
-            onChange={this.handleChange}
-            onBlur={this.handleBlur}
-            onClick={this.handleClick}
-            onKeyPress={this.handleKeyPress}
-          />
+          {input}
         </TooltipWrapper>
-      </span>
+      );
+    }
+    return (
+      <span className="editable-title">{input}</span>
     );
   }
 }
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()}
         </Button>
       );
+    } else if (this.props.isMenuItem) {
+      return (
+        <MenuItem onClick={this.open}>
+          {this.props.triggerNode}
+          {this.renderModal()}
+        </MenuItem>
+      );
     }
     /* 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 (
+    <span>
+      <i className={`fa fa-${faIcon}`} /> {text} {''}
+      <InfoTooltipWithTrigger
+        tooltip={tooltip}
+        label={`dash-${faIcon}`}
+        placement="top"
+      />
+      {children}
+    </span>
+  );
+}
+MenuItemContent.propTypes = {
+  faIcon: PropTypes.string.isRequired,
+  text: PropTypes.string,
+  tooltip: PropTypes.string,
+  children: PropTypes.node,
+};
+
+function ActionMenuItem(props) {
+  return (
+    <MenuItem onClick={props.onClick}>
+      <MenuItemContent {...props} />
+    </MenuItem>
+  );
+}
+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 (
-      <ButtonGroup>
-        <Button
-          tooltip={t('Force refresh the whole dashboard')}
-          onClick={this.refresh.bind(this)}
-        >
-          <i className="fa fa-refresh" />
-        </Button>
-        <SliceAdder
-          dashboard={dashboard}
-          addSlicesToDashboard={addSlicesToDashboard}
-          userId={userId}
-          triggerNode={
-            <i className="fa fa-plus" />
+      <span>
+        <DropdownButton title="Actions" bsSize="small" id="bg-nested-dropdown" pullRight>
+          <ActionMenuItem
+            text={t('Force Refresh')}
+            tooltip={t('Force refresh the whole dashboard')}
+            faIcon="refresh"
+            onClick={this.refresh}
+          />
+          <RefreshIntervalModal
+            onChange={refreshInterval => startPeriodicRender(refreshInterval * 1000)}
+            triggerNode={
+              <MenuItemContent
+                text={t('Set autorefresh')}
+                tooltip={t('Set the auto-refresh interval for this session')}
+                faIcon="clock-o"
+              />
+            }
+          />
+          <SaveModal
+            dashboard={dashboard}
+            readFilters={readFilters}
+            serialize={serialize}
+            onSave={onSave}
+            css={this.state.css}
+            triggerNode={
+              <MenuItemContent
+                text={saveText}
+                tooltip={t('Save the dashboard')}
+                faIcon="save"
+              />
+            }
+          />
+          {editMode &&
+            <ActionMenuItem
+              text={t('Edit properties')}
+              tooltip={t("Edit the dashboards's properties")}
+              faIcon="edit"
+              onClick={() => { window.location = `/dashboardmodelview/edit/${dashboard.id}`;
}}
+            />
           }
-        />
-        <RefreshIntervalModal
-          onChange={refreshInterval => startPeriodicRender(refreshInterval * 1000)}
-          triggerNode={
-            <i className="fa fa-clock-o" />
+          {editMode &&
+            <ActionMenuItem
+              text={t('Email')}
+              tooltip={t('Email a link to this dashbaord')}
+              onClick={() => { window.location = emailLink; }}
+              faIcon="envelope"
+            />
           }
-        />
-        <CodeModal
-          codeCallback={readFilters}
-          triggerNode={<i className="fa fa-filter" />}
-        />
-        <CssEditor
-          dashboard={dashboard}
-          triggerNode={
-            <i className="fa fa-css3" />
+          {editMode &&
+            <SliceAdder
+              dashboard={dashboard}
+              addSlicesToDashboard={addSlicesToDashboard}
+              userId={userId}
+              triggerNode={
+                <MenuItemContent
+                  text={t('Add Slices')}
+                  tooltip={t('Add some slices to this dashbaord')}
+                  faIcon="plus"
+                />
+              }
+            />
           }
-          initialCss={dashboard.css}
-          templates={this.state.cssTemplates}
-          onChange={this.changeCss.bind(this)}
-        />
-        <Button
-          onClick={() => { window.location = emailLink; }}
-        >
-          <i className="fa fa-envelope" />
-        </Button>
-        <Button
-          disabled={!dashboard.dash_edit_perm}
-          onClick={() => {
-            window.location = `/dashboardmodelview/edit/${dashboard.id}`;
-          }}
-          tooltip={t('Edit this dashboard\'s properties')}
-        >
-          <i className="fa fa-edit" />
-        </Button>
-        <SaveModal
-          dashboard={dashboard}
-          readFilters={readFilters}
-          serialize={serialize}
-          onSave={onSave}
-          css={this.state.css}
-          triggerNode={
-            <Button disabled={!dashboard.dash_save_perm}>
-              <i className="fa fa-save" />
-            </Button>
+          {editMode &&
+            <CssEditor
+              dashboard={dashboard}
+              triggerNode={
+                <MenuItemContent
+                  text={t('Edit CSS')}
+                  tooltip={t('Change the style of the dashboard using CSS code')}
+                  faIcon="css3"
+                />
+              }
+              initialCss={dashboard.css}
+              templates={this.state.cssTemplates}
+              onChange={this.changeCss.bind(this)}
+            />
           }
-        />
-      </ButtonGroup>
+        </DropdownButton>
+      </span>
     );
   }
 }
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 {
       <ModalTrigger
         triggerNode={this.props.triggerNode}
         modalTitle={t('CSS')}
-        isButton
+        isMenuItem
         modalBody={
           <div>
             {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: (
-        <span>
-          <strong>{t('You have unsaved changes.')}</strong> {t('Click the')}
&nbsp;
-          <i className="fa fa-save" />&nbsp;
-          {t('button on the top right to save your changes.')}
-        </span>
-      ),
-    });
-  }
-
   render() {
     return (
       <div id="dashboard-container">
-        {this.state.alert && <DashboardAlert alertContent={this.state.alert} />}
         <div id="dashboard-header">
           <AlertsWrapper initMessages={this.props.initMessages} />
           <Header
             dashboard={this.props.dashboard}
+            unsavedChanges={this.state.unsavedChanges}
             userId={this.props.userId}
             isStarred={this.props.isStarred}
             updateDashboardTitle={this.updateDashboardTitle}
@@ -315,6 +292,8 @@ class Dashboard extends React.PureComponent {
             renderSlices={this.fetchAllSlices}
             startPeriodicRender={this.startPeriodicRender}
             addSlicesToDashboard={this.addSlicesToDashboard}
+            editMode={this.props.editMode}
+            setEditMode={this.props.actions.setEditMode}
           />
         </div>
         <div id="grid-container" className="slice-grid gridster">
@@ -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}
           />
         </div>
       </div>
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 }) => (
-  <div id="alert-container">
-    <div className="container-fluid">
-      <Alert bsStyle="warning">
-        {alertContent}
-      </Alert>
-    </div>
-  </div>
-);
-
-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}
           />
         </div>
         <div
diff --git a/superset/assets/javascripts/dashboard/components/GridLayout.jsx b/superset/assets/javascripts/dashboard/components/GridLayout.jsx
index 210c295..dad66e1 100644
--- a/superset/assets/javascripts/dashboard/components/GridLayout.jsx
+++ b/superset/assets/javascripts/dashboard/components/GridLayout.jsx
@@ -28,6 +28,7 @@ const propTypes = {
   getFilters: PropTypes.func,
   clearFilter: PropTypes.func,
   removeFilter: PropTypes.func,
+  editMode: PropTypes.bool.isRequired,
 };
 
 const defaultProps = {
@@ -162,6 +163,7 @@ class GridLayout extends React.Component {
             getFilters={this.props.getFilters}
             clearFilter={this.props.clearFilter}
             removeFilter={this.props.removeFilter}
+            editMode={this.props.editMode}
           />
         </div>);
     });
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 (
+      <InfoTooltipWithTrigger
+        label="unsaved"
+        tooltip={t('Unsaved changes')}
+        icon="exclamation-triangle"
+        className="text-danger m-r-5"
+        placement="top"
+      />
+    );
+  }
+  renderEditButton() {
+    if (!this.props.dashboard.dash_save_perm) {
+      return null;
+    }
+    const btnText = this.props.editMode ? 'Switch to View Mode' : 'Edit Dashboard';
+    return (
+      <Button
+        bsStyle="default"
+        className="m-r-5"
+        style={{ width: '150px' }}
+        onClick={this.toggleEditMode}
+      >
+        {btnText}
+      </Button>);
+  }
   render() {
     const dashboard = this.props.dashboard;
     return (
       <div className="title">
         <div className="pull-left">
-          <h1 className="outer-container">
+          <h1 className="outer-container pull-left">
             <EditableTitle
               title={dashboard.dashboard_title}
-              canEdit={dashboard.dash_save_perm}
+              canEdit={dashboard.dash_save_perm && this.props.editMode}
               onSaveTitle={this.handleSaveTitle}
-              noPermitTooltip={'You don\'t have the rights to alter this dashboard.'}
+              showTooltip={this.props.editMode}
             />
-            <span className="favstar">
+            <span className="favstar m-r-5">
               <FaveStar
                 itemId={dashboard.id}
                 fetchFaveStar={this.props.fetchFaveStar}
@@ -50,10 +88,11 @@ class Header extends React.PureComponent {
                 isStarred={this.props.isStarred}
               />
             </span>
+            {this.renderUnsaved()}
           </h1>
         </div>
         <div className="pull-right" style={{ marginTop: '35px' }}>
-          {!this.props.dashboard.standalone_mode &&
+          {this.renderEditButton()}
           <Controls
             dashboard={dashboard}
             userId={this.props.userId}
@@ -64,8 +103,8 @@ class Header extends React.PureComponent {
             renderSlices={this.props.renderSlices}
             serialize={this.props.serialize}
             startPeriodicRender={this.props.startPeriodicRender}
+            editMode={this.props.editMode}
           />
-        }
         </div>
         <div className="clearfix" />
       </div>
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 (
       <ModalTrigger
         triggerNode={this.props.triggerNode}
-        isButton
+        isMenuItem
         modalTitle={t('Refresh Interval')}
         modalBody={
           <div>
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 (
       <ModalTrigger
         ref={(modal) => { 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 {
           <div className="header">
             <EditableTitle
               title={slice.slice_name}
-              canEdit={!!this.props.updateSliceName}
+              canEdit={!!this.props.updateSliceName && this.props.editMode}
               onSaveTitle={this.onSaveTitle}
               noPermitTooltip={'You don\'t have the rights to alter this dashboard.'}
             />
           </div>
           <div className="chart-controls">
             <div id={'controls_' + slice.slice_id} className="pull-right">
-              <a>
-                <TooltipWrapper
-                  placement="top"
-                  label="move"
-                  tooltip={t('Move chart')}
-                >
-                  <i className="fa fa-arrows drag" />
-                </TooltipWrapper>
-              </a>
+              {this.props.editMode &&
+                <a>
+                  <TooltipWrapper
+                    placement="top"
+                    label="move"
+                    tooltip={t('Move chart')}
+                  >
+                    <i className="fa fa-arrows drag" />
+                  </TooltipWrapper>
+                </a>
+              }
               <a
                 className={`refresh ${isCached ? 'danger' : ''}`}
                 onClick={() => (this.props.forceRefresh(slice.slice_id))}
@@ -121,15 +125,17 @@ class SliceHeader extends React.PureComponent {
                   <i className="fa fa-share" />
                 </TooltipWrapper>
               </a>
-              <a className="remove-chart" onClick={() => (this.props.removeSlice(slice))}>
-                <TooltipWrapper
-                  placement="top"
-                  label="close"
-                  tooltip={t('Remove chart from dashboard')}
-                >
-                  <i className="fa fa-close" />
-                </TooltipWrapper>
-              </a>
+              {this.props.editMode &&
+                <a className="remove-chart" onClick={() => (this.props.removeSlice(slice))}>
+                  <TooltipWrapper
+                    placement="top"
+                    label="close"
+                    tooltip={t('Remove chart from dashboard')}
+                  >
+                    <i className="fa fa-close" />
+                  </TooltipWrapper>
+                </a>
+              }
             </div>
           </div>
         </div>
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" <commits@superset.apache.org>'].

Mime
View raw message