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/06/22 00:54:26 UTC
[incubator-superset] 11/26: 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
commit ae37277ddf56ee4b1fc6d6309a9c0499d0bf94e7
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 1d287d6..9d63331 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 e793bc2..6729e57 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 = () => (
@@ -96,13 +100,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;
}