You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by cc...@apache.org on 2018/05/23 05:36:42 UTC

[incubator-superset] branch dashboard-builder updated: Dashboard save button (#4979)

This is an automated email from the ASF dual-hosted git repository.

ccwilliams pushed a commit to branch dashboard-builder
in repository https://gitbox.apache.org/repos/asf/incubator-superset.git


The following commit(s) were added to refs/heads/dashboard-builder by this push:
     new 82a0e3e  Dashboard save button (#4979)
82a0e3e is described below

commit 82a0e3ee64e825e30bb71ae758f4e2dd3571b989
Author: Grace Guo <gr...@airbnb.com>
AuthorDate: Tue May 22 22:36:33 2018 -0700

    Dashboard save button (#4979)
    
    * save button
    
    * fix slices list height
    
    * save custom css
    
    * merge save-dash changes from dashboard v1
    https://github.com/apache/incubator-superset/pull/4900
    https://github.com/apache/incubator-superset/pull/5051
---
 .../assets/src/dashboard/actions/dashboardState.js | 43 +++++++++-
 .../assets/src/dashboard/components/Controls.jsx   | 38 ++++-----
 .../assets/src/dashboard/components/Header.jsx     | 98 ++++++++++++++++++----
 .../assets/src/dashboard/components/SaveModal.jsx  | 78 ++++++-----------
 .../assets/src/dashboard/components/SliceAdder.jsx | 11 ++-
 .../dashboard/components/SliceHeaderControls.jsx   | 18 +++-
 .../dashboard/components/gridComponents/Chart.jsx  |  6 ++
 superset/assets/src/dashboard/containers/Chart.jsx |  2 +
 .../src/dashboard/containers/DashboardHeader.jsx   |  7 +-
 .../src/dashboard/reducers/dashboardState.js       |  6 +-
 .../src/dashboard/reducers/getInitialState.js      |  8 +-
 .../assets/src/dashboard/stylesheets/builder.less  |  5 ++
 .../src/dashboard/stylesheets/dashboard.less       | 32 ++++++-
 superset/assets/src/dashboard/util/constants.js    |  4 +
 superset/assets/src/modules/utils.js               |  2 +-
 15 files changed, 246 insertions(+), 112 deletions(-)

diff --git a/superset/assets/src/dashboard/actions/dashboardState.js b/superset/assets/src/dashboard/actions/dashboardState.js
index 10c0a26..42f68ad 100644
--- a/superset/assets/src/dashboard/actions/dashboardState.js
+++ b/superset/assets/src/dashboard/actions/dashboardState.js
@@ -6,7 +6,15 @@ import { addChart, removeChart, refreshChart } from '../../chart/chartAction';
 import { chart as initChart } from '../../chart/chartReducer';
 import { fetchDatasourceMetadata } from '../../dashboard/actions/datasources';
 import { applyDefaultFormData } from '../../explore/stores/store';
-import { addWarningToast } from './messageToasts';
+import { getAjaxErrorMsg } from '../../modules/utils';
+import { SAVE_TYPE_OVERWRITE } from '../util/constants';
+import { t } from '../../locales';
+
+import {
+  addSuccessToast,
+  addWarningToast,
+  addDangerToast,
+} from './messageToasts';
 
 export const SET_UNSAVED_CHANGES = 'SET_UNSAVED_CHANGES';
 export function setUnsavedChanges(hasUnsavedChanges) {
@@ -66,6 +74,11 @@ export function toggleExpandSlice(sliceId) {
   return { type: TOGGLE_EXPAND_SLICE, sliceId };
 }
 
+export const UPDATE_CSS = 'UPDATE_CSS';
+export function updateCss(css) {
+  return { type: UPDATE_CSS, css };
+}
+
 export const SET_EDIT_MODE = 'SET_EDIT_MODE';
 export function setEditMode(editMode) {
   return { type: SET_EDIT_MODE, editMode };
@@ -81,7 +94,7 @@ export function onSave() {
   return { type: ON_SAVE };
 }
 
-export function saveDashboard() {
+export function saveDashboardRequestSuccess() {
   return dispatch => {
     dispatch(onSave());
     // clear layout undo history
@@ -89,6 +102,32 @@ export function saveDashboard() {
   };
 }
 
+export function saveDashboardRequest(data, id, saveType) {
+  const path = saveType === SAVE_TYPE_OVERWRITE ? 'save_dash' : 'copy_dash';
+  const url = `/superset/${path}/${id}/`;
+  return dispatch =>
+    $.ajax({
+      type: 'POST',
+      url,
+      data: {
+        data: JSON.stringify(data),
+      },
+      success: () => {
+        dispatch(saveDashboardRequestSuccess());
+        dispatch(addSuccessToast(t('This dashboard was saved successfully.')));
+      },
+      error: error => {
+        const errorMsg = getAjaxErrorMsg(error);
+        dispatch(
+          addDangerToast(
+            `${t('Sorry, there was an error saving this dashboard: ')}
+          ${errorMsg}`,
+          ),
+        );
+      },
+    });
+}
+
 export function fetchCharts(chartList = [], force = false, interval = 0) {
   return (dispatch, getState) => {
     const timeout = getState().dashboardInfo.common.conf
diff --git a/superset/assets/src/dashboard/components/Controls.jsx b/superset/assets/src/dashboard/components/Controls.jsx
index 07b6c33..9d54b09 100644
--- a/superset/assets/src/dashboard/components/Controls.jsx
+++ b/superset/assets/src/dashboard/components/Controls.jsx
@@ -4,8 +4,8 @@ import PropTypes from 'prop-types';
 import $ from 'jquery';
 import { DropdownButton, MenuItem } from 'react-bootstrap';
 
+import CssEditor from './CssEditor';
 import RefreshIntervalModal from './RefreshIntervalModal';
-import SaveModal from './SaveModal';
 import { t } from '../../locales';
 
 function updateDom(css) {
@@ -31,12 +31,10 @@ const propTypes = {
   addDangerToast: PropTypes.func.isRequired,
   dashboardInfo: PropTypes.object.isRequired,
   dashboardTitle: PropTypes.string.isRequired,
-  layout: PropTypes.object.isRequired,
-  filters: PropTypes.object.isRequired,
-  expandedSlices: PropTypes.object.isRequired,
+  css: PropTypes.string.isRequired,
   slices: PropTypes.array,
-  onSave: PropTypes.func.isRequired,
   onChange: PropTypes.func.isRequired,
+  updateCss: PropTypes.func.isRequired,
   forceRefreshAllCharts: PropTypes.func.isRequired,
   startPeriodicRender: PropTypes.func.isRequired,
   editMode: PropTypes.bool,
@@ -51,9 +49,11 @@ class Controls extends React.PureComponent {
   constructor(props) {
     super(props);
     this.state = {
-      css: '',
+      css: props.css,
       cssTemplates: [],
     };
+
+    this.changeCss = this.changeCss.bind(this);
   }
 
   componentWillMount() {
@@ -74,17 +74,14 @@ class Controls extends React.PureComponent {
       updateDom(css);
     });
     this.props.onChange();
+    this.props.updateCss(css);
   }
 
   render() {
     const {
       dashboardTitle,
-      layout,
-      filters,
-      expandedSlices,
       startPeriodicRender,
       forceRefreshAllCharts,
-      onSave,
       editMode,
     } = this.props;
 
@@ -110,19 +107,6 @@ class Controls extends React.PureComponent {
             }
             triggerNode={<span>{t('Set auto-refresh interval')}</span>}
           />
-          <SaveModal
-            addSuccessToast={this.props.addSuccessToast}
-            addDangerToast={this.props.addDangerToast}
-            dashboardId={this.props.dashboardInfo.id}
-            dashboardTitle={dashboardTitle}
-            layout={layout}
-            filters={filters}
-            expandedSlices={expandedSlices}
-            onSave={onSave}
-            css={this.state.css}
-            triggerNode={<span>{editMode ? t('Save') : t('Save as')}</span>}
-            isMenuItem
-          />
           {editMode && (
             <MenuItem
               target="_blank"
@@ -134,6 +118,14 @@ class Controls extends React.PureComponent {
           {editMode && (
             <MenuItem href={emailLink}>{t('Email dashboard link')}</MenuItem>
           )}
+          {editMode && (
+            <CssEditor
+              triggerNode={<span>{t('Edit CSS')}</span>}
+              initialCss={this.state.css}
+              templates={this.state.cssTemplates}
+              onChange={this.changeCss}
+            />
+          )}
         </DropdownButton>
       </span>
     );
diff --git a/superset/assets/src/dashboard/components/Header.jsx b/superset/assets/src/dashboard/components/Header.jsx
index 21b01db..31bd08c 100644
--- a/superset/assets/src/dashboard/components/Header.jsx
+++ b/superset/assets/src/dashboard/components/Header.jsx
@@ -1,6 +1,12 @@
+/* eslint-env browser */
 import React from 'react';
 import PropTypes from 'prop-types';
-import { ButtonGroup, ButtonToolbar } from 'react-bootstrap';
+import {
+  DropdownButton,
+  MenuItem,
+  ButtonGroup,
+  ButtonToolbar,
+} from 'react-bootstrap';
 
 import Controls from './Controls';
 import EditableTitle from '../../components/EditableTitle';
@@ -9,7 +15,11 @@ import FaveStar from '../../components/FaveStar';
 import SaveModal from './SaveModal';
 import { chartPropShape } from '../util/propShapes';
 import { t } from '../../locales';
-import { UNDO_LIMIT } from '../util/constants';
+import {
+  UNDO_LIMIT,
+  SAVE_TYPE_NEWDASHBOARD,
+  SAVE_TYPE_OVERWRITE,
+} from '../util/constants';
 
 const propTypes = {
   addSuccessToast: PropTypes.func.isRequired,
@@ -20,6 +30,7 @@ const propTypes = {
   layout: PropTypes.object.isRequired,
   filters: PropTypes.object.isRequired,
   expandedSlices: PropTypes.object.isRequired,
+  css: PropTypes.string.isRequired,
   isStarred: PropTypes.bool.isRequired,
   onSave: PropTypes.func.isRequired,
   onChange: PropTypes.func.isRequired,
@@ -32,6 +43,7 @@ const propTypes = {
   setEditMode: PropTypes.func.isRequired,
   showBuilderPane: PropTypes.bool.isRequired,
   toggleBuilderPane: PropTypes.func.isRequired,
+  updateCss: PropTypes.func.isRequired,
   hasUnsavedChanges: PropTypes.bool.isRequired,
   maxUndoHistoryExceeded: PropTypes.bool.isRequired,
 
@@ -45,6 +57,10 @@ const propTypes = {
 };
 
 class Header extends React.PureComponent {
+  static discardChanges() {
+    window.location.reload();
+  }
+
   constructor(props) {
     super(props);
     this.state = {
@@ -54,6 +70,7 @@ class Header extends React.PureComponent {
     this.handleChangeText = this.handleChangeText.bind(this);
     this.toggleEditMode = this.toggleEditMode.bind(this);
     this.forceRefresh = this.forceRefresh.bind(this);
+    this.overwriteDashboard = this.overwriteDashboard.bind(this);
   }
 
   componentWillReceiveProps(nextProps) {
@@ -88,38 +105,62 @@ class Header extends React.PureComponent {
     this.props.setEditMode(!this.props.editMode);
   }
 
+  overwriteDashboard() {
+    const {
+      dashboardTitle,
+      layout: positions,
+      expandedSlices,
+      css,
+      filters,
+      dashboardInfo,
+    } = this.props;
+
+    const data = {
+      positions,
+      expanded_slices: expandedSlices,
+      css,
+      dashboard_title: dashboardTitle,
+      default_filters: JSON.stringify(filters),
+    };
+
+    this.props.onSave(data, dashboardInfo.id, SAVE_TYPE_OVERWRITE);
+  }
+
   render() {
     const {
       dashboardTitle,
       layout,
       filters,
       expandedSlices,
+      css,
       onUndo,
       onRedo,
       undoLength,
       redoLength,
       onChange,
       onSave,
+      updateCss,
       editMode,
       showBuilderPane,
       dashboardInfo,
       hasUnsavedChanges,
     } = this.props;
 
-    const userCanEdit = dashboardInfo.dash_save_perm;
+    const userCanEdit = dashboardInfo.dash_edit_perm;
+    const userCanSaveAs = dashboardInfo.dash_save_perm;
 
     return (
       <div className="dashboard-header">
         <div className="dashboard-component-header header-large">
           <EditableTitle
             title={dashboardTitle}
-            canEdit={this.props.dashboardInfo.dash_save_perm && editMode}
+            canEdit={userCanEdit && editMode}
             onSaveTitle={this.handleChangeText}
             showTooltip={false}
           />
           <span className="favstar m-l-5">
             <FaveStar
-              itemId={this.props.dashboardInfo.id}
+              itemId={dashboardInfo.id}
               fetchFaveStar={this.props.fetchFaveStar}
               saveFaveStar={this.props.saveFaveStar}
               isStarred={this.props.isStarred}
@@ -127,7 +168,7 @@ class Header extends React.PureComponent {
           </span>
         </div>
         <ButtonToolbar>
-          {userCanEdit && (
+          {userCanSaveAs && (
             <ButtonGroup>
               {editMode && (
                 <Button
@@ -161,44 +202,65 @@ class Header extends React.PureComponent {
                 <Button
                   bsSize="small"
                   onClick={this.toggleEditMode}
-                  bsStyle={editMode ? undefined : 'primary'}
+                  bsStyle={hasUnsavedChanges ? 'primary' : undefined}
+                  disabled={!userCanEdit}
                 >
-                  {editMode ? t('Switch to View Mode') : t('Edit Dashboard')}
+                  {editMode ? t('Switch to view mode') : t('Edit dashboard')}
                 </Button>
               ) : (
+                <Button
+                  bsSize="small"
+                  bsStyle={hasUnsavedChanges ? 'primary' : undefined}
+                  onClick={this.overwriteDashboard}
+                >
+                  {t('Save changes')}
+                </Button>
+              )}
+              <DropdownButton
+                title=""
+                id="save-dash-split-button"
+                bsStyle={hasUnsavedChanges ? 'primary' : undefined}
+                bsSize="small"
+                pullRight
+              >
                 <SaveModal
                   addSuccessToast={this.props.addSuccessToast}
                   addDangerToast={this.props.addDangerToast}
-                  dashboardId={this.props.dashboardInfo.id}
+                  dashboardId={dashboardInfo.id}
                   dashboardTitle={dashboardTitle}
+                  saveType={SAVE_TYPE_NEWDASHBOARD}
                   layout={layout}
                   filters={filters}
                   expandedSlices={expandedSlices}
+                  css={css}
                   onSave={onSave}
-                  // @TODO need to figure out css
-                  css=""
-                  triggerNode={
-                    <Button bsStyle="primary" bsSize="small">
-                      {t('Save changes')}
-                    </Button>
-                  }
+                  isMenuItem
+                  triggerNode={<span>{t('Save as')}</span>}
+                  canOverwrite={userCanEdit}
                 />
-              )}
+                {hasUnsavedChanges && (
+                  <MenuItem eventKey="discard" onSelect={Header.discardChanges}>
+                    {t('Discard changes')}
+                  </MenuItem>
+                )}
+              </DropdownButton>
             </ButtonGroup>
           )}
 
           <Controls
             addSuccessToast={this.props.addSuccessToast}
             addDangerToast={this.props.addDangerToast}
-            dashboardInfo={this.props.dashboardInfo}
+            dashboardInfo={dashboardInfo}
             dashboardTitle={dashboardTitle}
             layout={layout}
             filters={filters}
             expandedSlices={expandedSlices}
+            css={css}
             onSave={onSave}
             onChange={onChange}
             forceRefreshAllCharts={this.forceRefresh}
             startPeriodicRender={this.props.startPeriodicRender}
+            updateCss={updateCss}
             editMode={editMode}
           />
         </ButtonToolbar>
diff --git a/superset/assets/src/dashboard/components/SaveModal.jsx b/superset/assets/src/dashboard/components/SaveModal.jsx
index 4f05d2c..804674c 100644
--- a/superset/assets/src/dashboard/components/SaveModal.jsx
+++ b/superset/assets/src/dashboard/components/SaveModal.jsx
@@ -1,13 +1,12 @@
 /* eslint-env browser */
 import React from 'react';
 import PropTypes from 'prop-types';
-import $ from 'jquery';
 
 import { Button, FormControl, FormGroup, Radio } from 'react-bootstrap';
-import { getAjaxErrorMsg } from '../../modules/utils';
 import ModalTrigger from '../../components/ModalTrigger';
 import { t } from '../../locales';
 import Checkbox from '../../components/Checkbox';
+import { SAVE_TYPE_OVERWRITE, SAVE_TYPE_NEWDASHBOARD } from '../util/constants';
 
 const propTypes = {
   addSuccessToast: PropTypes.func.isRequired,
@@ -16,21 +15,25 @@ const propTypes = {
   dashboardTitle: PropTypes.string.isRequired,
   expandedSlices: PropTypes.object.isRequired,
   layout: PropTypes.object.isRequired,
+  saveType: PropTypes.oneOf([SAVE_TYPE_OVERWRITE, SAVE_TYPE_NEWDASHBOARD]),
   triggerNode: PropTypes.node.isRequired,
   filters: PropTypes.object.isRequired,
+  css: PropTypes.string.isRequired,
   onSave: PropTypes.func.isRequired,
   isMenuItem: PropTypes.bool,
+  canOverwrite: PropTypes.bool.isRequired,
 };
 
 const defaultProps = {
   isMenuItem: false,
+  saveType: SAVE_TYPE_OVERWRITE,
 };
 
 class SaveModal extends React.PureComponent {
   constructor(props) {
     super(props);
     this.state = {
-      saveType: 'overwrite',
+      saveType: props.saveType,
       newDashName: `${props.dashboardTitle} [copy]`,
       duplicateSlices: false,
     };
@@ -40,6 +43,7 @@ class SaveModal extends React.PureComponent {
     this.saveDashboard = this.saveDashboard.bind(this);
     this.setModalRef = this.setModalRef.bind(this);
     this.toggleDuplicateSlices = this.toggleDuplicateSlices.bind(this);
+    this.onSave = this.props.onSave.bind(this);
   }
 
   setModalRef(ref) {
@@ -59,37 +63,7 @@ class SaveModal extends React.PureComponent {
   handleNameChange(event) {
     this.setState({
       newDashName: event.target.value,
-      saveType: 'newDashboard',
-    });
-  }
-
-  // @TODO this should all be moved to actions
-  saveDashboardRequest(data, url, saveType) {
-    $.ajax({
-      type: 'POST',
-      url,
-      data: {
-        data: JSON.stringify(data),
-      },
-      success: resp => {
-        this.modal.close();
-        this.props.onSave();
-        if (saveType === 'newDashboard') {
-          window.location = `/superset/dashboard/${resp.id}/`;
-        } else {
-          this.props.addSuccessToast(
-            t('This dashboard was saved successfully.'),
-          );
-        }
-      },
-      error: error => {
-        this.modal.close();
-        const errorMsg = getAjaxErrorMsg(error);
-        this.props.addDangerToast(
-          `${t('Sorry, there was an error saving this dashboard: ')}
-          ${errorMsg}`,
-        );
-      },
+      saveType: SAVE_TYPE_NEWDASHBOARD,
     });
   }
 
@@ -98,6 +72,7 @@ class SaveModal extends React.PureComponent {
     const {
       dashboardTitle,
       layout: positions,
+      css,
       expandedSlices,
       filters,
       dashboardId,
@@ -105,26 +80,24 @@ class SaveModal extends React.PureComponent {
 
     const data = {
       positions,
+      css,
       expanded_slices: expandedSlices,
       dashboard_title: dashboardTitle,
       default_filters: JSON.stringify(filters),
       duplicate_slices: this.state.duplicateSlices,
     };
 
-    let url = null;
-    if (saveType === 'overwrite') {
-      url = `/superset/save_dash/${dashboardId}/`;
-      this.saveDashboardRequest(data, url, saveType);
-    } else if (saveType === 'newDashboard') {
-      if (!newDashName) {
-        this.props.addDangerToast(
-          t('You must pick a name for the new dashboard'),
-        );
-      } else {
-        data.dashboard_title = newDashName;
-        url = `/superset/copy_dash/${dashboardId}/`;
-        this.saveDashboardRequest(data, url, saveType);
-      }
+    if (saveType === SAVE_TYPE_NEWDASHBOARD && !newDashName) {
+      this.props.addDangerToast(
+        t('You must pick a name for the new dashboard'),
+      );
+    } else {
+      this.onSave(data, dashboardId, saveType).done(resp => {
+        if (saveType === SAVE_TYPE_NEWDASHBOARD) {
+          window.location = `/superset/dashboard/${resp.id}/`;
+        }
+      });
+      this.modal.close();
     }
   }
 
@@ -138,17 +111,18 @@ class SaveModal extends React.PureComponent {
         modalBody={
           <FormGroup>
             <Radio
-              value="overwrite"
+              value={SAVE_TYPE_OVERWRITE}
               onChange={this.handleSaveTypeChange}
-              checked={this.state.saveType === 'overwrite'}
+              checked={this.state.saveType === SAVE_TYPE_OVERWRITE}
+              disabled={!this.props.canOverwrite}
             >
               {t('Overwrite Dashboard [%s]', this.props.dashboardTitle)}
             </Radio>
             <hr />
             <Radio
-              value="newDashboard"
+              value={SAVE_TYPE_NEWDASHBOARD}
               onChange={this.handleSaveTypeChange}
-              checked={this.state.saveType === 'newDashboard'}
+              checked={this.state.saveType === SAVE_TYPE_NEWDASHBOARD}
             >
               {t('Save as:')}
             </Radio>
diff --git a/superset/assets/src/dashboard/components/SliceAdder.jsx b/superset/assets/src/dashboard/components/SliceAdder.jsx
index 05c4270..47451c4 100644
--- a/superset/assets/src/dashboard/components/SliceAdder.jsx
+++ b/superset/assets/src/dashboard/components/SliceAdder.jsx
@@ -39,6 +39,10 @@ const KEYS_TO_SORT = [
   { key: 'changed_on', label: 'Recent' },
 ];
 
+const MARGIN_BOTTOM = 16;
+const SIDEPANE_HEADER_HEIGHT = 55;
+const SLICE_ADDER_CONTROL_HEIGHT = 64;
+
 class SliceAdder extends React.Component {
   static sortByComparator(attr) {
     const desc = attr === 'changed_on' ? -1 : 1;
@@ -166,6 +170,11 @@ class SliceAdder extends React.Component {
   }
 
   render() {
+    const slicesListHeight =
+      this.props.height -
+      SIDEPANE_HEADER_HEIGHT -
+      SLICE_ADDER_CONTROL_HEIGHT -
+      MARGIN_BOTTOM;
     return (
       <div className="slice-adder-container">
         <div className="controls">
@@ -202,7 +211,7 @@ class SliceAdder extends React.Component {
           this.state.filteredSlices.length > 0 && (
             <List
               width={376}
-              height={this.props.height}
+              height={slicesListHeight}
               rowCount={this.state.filteredSlices.length}
               rowHeight={136}
               rowRenderer={this.rowRenderer}
diff --git a/superset/assets/src/dashboard/components/SliceHeaderControls.jsx b/superset/assets/src/dashboard/components/SliceHeaderControls.jsx
index 5326e0f..de8e653 100644
--- a/superset/assets/src/dashboard/components/SliceHeaderControls.jsx
+++ b/superset/assets/src/dashboard/components/SliceHeaderControls.jsx
@@ -11,6 +11,8 @@ const propTypes = {
   isCached: PropTypes.bool,
   isExpanded: PropTypes.bool,
   cachedDttm: PropTypes.string,
+  supersetCanExplore: PropTypes.bool,
+  sliceCanEdit: PropTypes.bool,
   toggleExpandSlice: PropTypes.func,
   forceRefresh: PropTypes.func,
   exploreChart: PropTypes.func,
@@ -25,6 +27,8 @@ const defaultProps = {
   cachedDttm: null,
   isCached: false,
   isExpanded: false,
+  supersetCanExplore: false,
+  sliceCanEdit: false,
 };
 
 const VerticalDotsTrigger = () => (
@@ -93,13 +97,19 @@ class SliceHeaderControls extends React.PureComponent {
             </MenuItem>
           )}
 
-          <MenuItem href={slice.edit_url} target="_blank">
-            {t('Edit chart metadata')}
-          </MenuItem>
+          {this.props.sliceCanEdit && (
+            <MenuItem href={slice.edit_url} target="_blank">
+              {t('Edit chart metadata')}
+            </MenuItem>
+          )}
 
           <MenuItem onClick={this.exportCSV}>{t('Export CSV')}</MenuItem>
 
-          <MenuItem onClick={this.exploreChart}>{t('Explore chart')}</MenuItem>
+          {this.props.supersetCanExplore && (
+            <MenuItem onClick={this.exploreChart}>
+              {t('Explore chart')}
+            </MenuItem>
+          )}
         </Dropdown.Menu>
       </Dropdown>
     );
diff --git a/superset/assets/src/dashboard/components/gridComponents/Chart.jsx b/superset/assets/src/dashboard/components/gridComponents/Chart.jsx
index 4742d71..9f8d723 100644
--- a/superset/assets/src/dashboard/components/gridComponents/Chart.jsx
+++ b/superset/assets/src/dashboard/components/gridComponents/Chart.jsx
@@ -29,6 +29,8 @@ const propTypes = {
   removeFilter: PropTypes.func.isRequired,
   editMode: PropTypes.bool.isRequired,
   isExpanded: PropTypes.bool.isRequired,
+  supersetCanExplore: PropTypes.bool.isRequired,
+  sliceCanEdit: PropTypes.bool.isRequired,
 };
 
 // we use state + shouldComponentUpdate() logic to prevent perf-wrecking
@@ -155,6 +157,8 @@ class Chart extends React.Component {
       sliceName,
       toggleExpandSlice,
       timeout,
+      supersetCanExplore,
+      sliceCanEdit,
     } = this.props;
 
     const { width } = this.state;
@@ -179,6 +183,8 @@ class Chart extends React.Component {
           exportCSV={this.exportCSV}
           updateSliceName={updateSliceName}
           sliceName={sliceName}
+          supersetCanExplore={supersetCanExplore}
+          sliceCanEdit={sliceCanEdit}
         />
 
         {/*
diff --git a/superset/assets/src/dashboard/containers/Chart.jsx b/superset/assets/src/dashboard/containers/Chart.jsx
index 61627d2..107e6c7 100644
--- a/superset/assets/src/dashboard/containers/Chart.jsx
+++ b/superset/assets/src/dashboard/containers/Chart.jsx
@@ -40,6 +40,8 @@ function mapStateToProps(
     }),
     editMode: dashboardState.editMode,
     isExpanded: !!dashboardState.expandedSlices[id],
+    supersetCanExplore: !!dashboardInfo.superset_can_explore,
+    sliceCanEdit: !!dashboardInfo.slice_can_edit,
   };
 }
 
diff --git a/superset/assets/src/dashboard/containers/DashboardHeader.jsx b/superset/assets/src/dashboard/containers/DashboardHeader.jsx
index fe7e7bb..19be06c 100644
--- a/superset/assets/src/dashboard/containers/DashboardHeader.jsx
+++ b/superset/assets/src/dashboard/containers/DashboardHeader.jsx
@@ -10,8 +10,9 @@ import {
   saveFaveStar,
   fetchCharts,
   startPeriodicRender,
+  updateCss,
   onChange,
-  saveDashboard,
+  saveDashboardRequest,
   setMaxUndoHistoryExceeded,
   maxUndoHistoryToast,
 } from '../actions/dashboardState';
@@ -42,6 +43,7 @@ function mapStateToProps({
       (undoableLayout.present[DASHBOARD_HEADER_ID] || {}).meta || {}
     ).text,
     expandedSlices: dashboard.expandedSlices,
+    css: dashboard.css,
     charts,
     userId: dashboardInfo.userId,
     isStarred: !!dashboard.isStarred,
@@ -66,8 +68,9 @@ function mapDispatchToProps(dispatch) {
       fetchCharts,
       startPeriodicRender,
       updateDashboardTitle,
+      updateCss,
       onChange,
-      onSave: saveDashboard,
+      onSave: saveDashboardRequest,
       setMaxUndoHistoryExceeded,
       maxUndoHistoryToast,
     },
diff --git a/superset/assets/src/dashboard/reducers/dashboardState.js b/superset/assets/src/dashboard/reducers/dashboardState.js
index 2d44399..2523494 100644
--- a/superset/assets/src/dashboard/reducers/dashboardState.js
+++ b/superset/assets/src/dashboard/reducers/dashboardState.js
@@ -14,13 +14,13 @@ import {
   TOGGLE_BUILDER_PANE,
   TOGGLE_EXPAND_SLICE,
   TOGGLE_FAVE_STAR,
-  UPDATE_DASHBOARD_TITLE,
+  UPDATE_CSS,
 } from '../actions/dashboardState';
 
 export default function dashboardStateReducer(state = {}, action) {
   const actionHandlers = {
-    [UPDATE_DASHBOARD_TITLE]() {
-      return { ...state, title: action.title };
+    [UPDATE_CSS]() {
+      return { ...state, css: action.css };
     },
     [ADD_SLICE]() {
       const updatedSliceIds = new Set(state.sliceIds);
diff --git a/superset/assets/src/dashboard/reducers/getInitialState.js b/superset/assets/src/dashboard/reducers/getInitialState.js
index b209043..f129bf7 100644
--- a/superset/assets/src/dashboard/reducers/getInitialState.js
+++ b/superset/assets/src/dashboard/reducers/getInitialState.js
@@ -58,10 +58,7 @@ export default function(bootstrapData) {
     future: [],
   };
 
-  delete dashboard.position_json;
-  delete dashboard.css;
-
-  // creat a lookup to sync layout names with slice names
+  // create a lookup to sync layout names with slice names
   const chartIdToLayoutId = {};
   Object.values(layout).forEach(layoutComponent => {
     if (layoutComponent.type === CHART_TYPE) {
@@ -124,6 +121,8 @@ export default function(bootstrapData) {
       userId: user_id,
       dash_edit_perm: dashboard.dash_edit_perm,
       dash_save_perm: dashboard.dash_save_perm,
+      superset_can_explore: dashboard.superset_can_explore,
+      slice_can_edit: dashboard.slice_can_edit,
       common,
     },
     dashboardState: {
@@ -131,6 +130,7 @@ export default function(bootstrapData) {
       refresh: false,
       filters,
       expandedSlices: dashboard.metadata.expanded_slices || {},
+      css: dashboard.css || '',
       editMode: false,
       showBuilderPane: false,
       hasUnsavedChanges: false,
diff --git a/superset/assets/src/dashboard/stylesheets/builder.less b/superset/assets/src/dashboard/stylesheets/builder.less
index 7c14056..ecf192e 100644
--- a/superset/assets/src/dashboard/stylesheets/builder.less
+++ b/superset/assets/src/dashboard/stylesheets/builder.less
@@ -46,9 +46,14 @@
 /* @TODO remove upon new theme */
 .btn.btn-primary {
   background: @almost-black !important;
+  border-color: @almost-black;
   color: white !important;
 }
 
+.dropdown-toggle.btn.btn-primary .caret {
+  color: white;
+}
+
 .background--transparent {
   background-color: transparent;
 }
diff --git a/superset/assets/src/dashboard/stylesheets/dashboard.less b/superset/assets/src/dashboard/stylesheets/dashboard.less
index 8d8c8be..5756786 100644
--- a/superset/assets/src/dashboard/stylesheets/dashboard.less
+++ b/superset/assets/src/dashboard/stylesheets/dashboard.less
@@ -38,6 +38,29 @@
   }
 }
 
+.dashboard .dashboard-header {
+  #save-dash-split-button {
+    border-radius: 0;
+    margin-left: -8px;
+    height: 30px;
+    width: 30px;
+
+    &.btn.btn-primary {
+      border-left-color: white;
+    }
+
+    .caret {
+      position: absolute;
+      top: 24px;
+      left: 3px;
+    }
+
+    & + .dropdown-menu.dropdown-menu-right {
+      min-width: unset;
+    }
+  }
+}
+
 .dashboard .chart-header,
 .dashboard .dashboard-header {
   .dropdown-menu {
@@ -63,7 +86,7 @@
   padding: 0 16px;
   position: absolute;
   top: 0;
-  right: -22px;
+  right: -16px; //increase the click-able area for the button
 
   &:hover {
     cursor: pointer;
@@ -80,12 +103,17 @@
 
   .is-cached & {
     background-color: @pink;
-    margin-right: 6px;
   }
 
   .vertical-dots-container & {
     display: block;
   }
+
+  a[role="menuitem"] & {
+    width: 8px;
+    height: 8px;
+    margin-right: 8px;
+  }
 }
 
 
diff --git a/superset/assets/src/dashboard/util/constants.js b/superset/assets/src/dashboard/util/constants.js
index d682687..ef2c8bb 100644
--- a/superset/assets/src/dashboard/util/constants.js
+++ b/superset/assets/src/dashboard/util/constants.js
@@ -41,3 +41,7 @@ export const DANGER_TOAST = 'DANGER_TOAST';
 
 // undo-redo
 export const UNDO_LIMIT = 50;
+
+// save dash options
+export const SAVE_TYPE_OVERWRITE = 'overwrite';
+export const SAVE_TYPE_NEWDASHBOARD = 'newDashboard';
diff --git a/superset/assets/src/modules/utils.js b/superset/assets/src/modules/utils.js
index eb937bb..c5d4e75 100644
--- a/superset/assets/src/modules/utils.js
+++ b/superset/assets/src/modules/utils.js
@@ -198,7 +198,7 @@ export function slugify(string) {
 
 export function getAjaxErrorMsg(error) {
   const respJSON = error.responseJSON;
-  return (respJSON && respJSON.message) ? respJSON.message :
+  return (respJSON && respJSON.error) ? respJSON.error :
           error.responseText;
 }
 

-- 
To stop receiving notification emails like this one, please contact
ccwilliams@apache.org.