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/08 18:33:17 UTC
[incubator-superset] branch dashboard-builder updated: add sticky
tabs + sidepane, better tabs perf, better container hierarchy,
better chart header (#4893)
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 b5644be add sticky tabs + sidepane, better tabs perf, better container hierarchy, better chart header (#4893)
b5644be is described below
commit b5644be2f5c60e43b775ae9a497d67e2d565eec7
Author: Grace Guo <gr...@airbnb.com>
AuthorDate: Tue May 8 11:33:14 2018 -0700
add sticky tabs + sidepane, better tabs perf, better container hierarchy, better chart header (#4893)
* dashboard header, slice header UI improvement
* add slider and sticky
* dashboard header, slice header UI improvement
* make builder pane floating
* [dashboard builder] add sticky top-level tabs, refactor for performant tabs
* [dashboard builder] visually distinct containers, icons for undo-redo, fix some isValidChild bugs
* [dashboard builder] better undo redo <> save changes state, notify upon reaching undo limit
* [dashboard builder] hook up edit + create component actions to saved-state pop.
* [dashboard builder] visual refinement, refactor Dashboard header content and updates into layout for undo-redo, refactor save dashboard modal to use toasts instead of notify.
* [dashboard builder] refactor chart name update logic to use layout for undo redo, save slice name changes on dashboard save
* add slider and sticky
* [dashboard builder] fix layout converter slice_id + chartId type casting, don't change grid size upon edit (perf)
* [dashboard builder] don't set version key in getInitialState
* [dashboard builder] make top level tabs addition/removal undoable, fix double sticky tabs + side panel.
* [dashboard builder] fix sticky tabs offset bug
* [dashboard builder] fix drag preview width, css polish, fix rebase issue
* [dashboard builder] fix side pane labels and hove z-index
---
superset/assets/package.json | 1 +
.../{dashboard => }/components/ActionMenuItem.jsx | 2 +-
.../src/dashboard/actions/dashboardLayout.js | 118 +++++++++++-
.../assets/src/dashboard/actions/dashboardState.js | 43 ++++-
.../assets/src/dashboard/actions/sliceEntities.js | 35 ----
.../dashboard/components/BuilderComponentPane.jsx | 114 ++++++++----
.../assets/src/dashboard/components/Controls.jsx | 52 ++----
.../assets/src/dashboard/components/Dashboard.jsx | 7 +-
.../src/dashboard/components/DashboardBuilder.jsx | 134 ++++++++++----
.../src/dashboard/components/DashboardGrid.jsx | 204 +++++++++++----------
.../assets/src/dashboard/components/Header.jsx | 58 ++++--
.../assets/src/dashboard/components/SaveModal.jsx | 32 ++--
.../assets/src/dashboard/components/SliceAdder.jsx | 6 +-
.../src/dashboard/components/SliceHeader.jsx | 26 +--
.../dashboard/components/SliceHeaderControls.jsx | 86 +++++----
.../components/dnd/AddSliceDragPreview.jsx | 17 +-
.../dashboard/components/gridComponents/Chart.jsx | 70 +++----
.../components/gridComponents/ChartHolder.jsx | 19 +-
.../dashboard/components/gridComponents/Column.jsx | 25 ++-
.../dashboard/components/gridComponents/Row.jsx | 23 +--
.../dashboard/components/gridComponents/Tab.jsx | 2 +-
.../dashboard/components/gridComponents/Tabs.jsx | 42 ++---
.../dashboard/components/menu/WithPopoverMenu.jsx | 5 +-
superset/assets/src/dashboard/containers/Chart.jsx | 4 +-
.../assets/src/dashboard/containers/Dashboard.jsx | 2 -
.../dashboard/containers/DashboardComponent.jsx | 8 +-
.../src/dashboard/containers/DashboardHeader.jsx | 38 ++--
.../assets/src/dashboard/containers/SliceAdder.js | 28 +++
.../src/dashboard/reducers/dashboardState.js | 11 +-
.../src/dashboard/reducers/getInitialState.js | 44 ++++-
.../dashboard/reducers/undoableDashboardLayout.js | 5 +-
.../dashboard/stylesheets/builder-sidepane.less | 78 +++++---
.../dashboard/stylesheets/components/chart.less | 5 -
.../dashboard/stylesheets/components/column.less | 6 +-
.../dashboard/stylesheets/components/header.less | 10 +-
.../src/dashboard/stylesheets/components/row.less | 6 +-
.../src/dashboard/stylesheets/components/tabs.less | 13 +-
.../src/dashboard/stylesheets/dashboard.less | 98 +++++++---
superset/assets/src/dashboard/stylesheets/dnd.less | 4 +-
.../assets/src/dashboard/stylesheets/grid.less | 9 +-
.../src/dashboard/stylesheets/hover-menu.less | 53 +++++-
.../src/dashboard/stylesheets/popover-menu.less | 28 +--
.../src/dashboard/stylesheets/variables.less | 4 +
superset/assets/src/dashboard/util/constants.js | 4 +
.../src/dashboard/util/dashboardLayoutConverter.js | 63 ++-----
superset/assets/src/dashboard/util/isValidChild.js | 9 +-
.../src/dashboard/util/newComponentFactory.js | 1 -
superset/assets/src/dashboard/util/propShapes.jsx | 1 -
superset/assets/src/theme.js | 1 -
superset/assets/src/visualizations/nvd3_vis.js | 2 +
superset/assets/stylesheets/superset.less | 6 +-
superset/views/core.py | 34 +++-
52 files changed, 1058 insertions(+), 638 deletions(-)
diff --git a/superset/assets/package.json b/superset/assets/package.json
index de40936..576920a 100644
--- a/superset/assets/package.json
+++ b/superset/assets/package.json
@@ -105,6 +105,7 @@
"react-select-fast-filter-options": "^0.2.1",
"react-sortable-hoc": "^0.6.7",
"react-split-pane": "^0.1.66",
+ "react-sticky": "^6.0.2",
"react-syntax-highlighter": "^5.7.0",
"react-virtualized": "9.3.0",
"react-virtualized-select": "2.4.0",
diff --git a/superset/assets/src/dashboard/components/ActionMenuItem.jsx b/superset/assets/src/components/ActionMenuItem.jsx
similarity index 94%
rename from superset/assets/src/dashboard/components/ActionMenuItem.jsx
rename to superset/assets/src/components/ActionMenuItem.jsx
index a0ecb78..e6c4447 100644
--- a/superset/assets/src/dashboard/components/ActionMenuItem.jsx
+++ b/superset/assets/src/components/ActionMenuItem.jsx
@@ -2,7 +2,7 @@ import React from 'react';
import PropTypes from 'prop-types';
import { MenuItem } from 'react-bootstrap';
-import InfoTooltipWithTrigger from '../../components/InfoTooltipWithTrigger';
+import InfoTooltipWithTrigger from './InfoTooltipWithTrigger';
export function MenuItemContent({ faIcon, text, tooltip, children }) {
return (
diff --git a/superset/assets/src/dashboard/actions/dashboardLayout.js b/superset/assets/src/dashboard/actions/dashboardLayout.js
index 5a04de5..c64ea0d 100644
--- a/superset/assets/src/dashboard/actions/dashboardLayout.js
+++ b/superset/assets/src/dashboard/actions/dashboardLayout.js
@@ -1,3 +1,5 @@
+import { ActionCreators as UndoActionCreators } from 'redux-undo';
+
import { addInfoToast } from './messageToasts';
import { setUnsavedChanges } from './dashboardState';
import { CHART_TYPE, MARKDOWN_TYPE, TABS_TYPE } from '../util/componentTypes';
@@ -5,13 +7,14 @@ import {
DASHBOARD_ROOT_ID,
NEW_COMPONENTS_SOURCE_ID,
GRID_MIN_COLUMN_COUNT,
+ DASHBOARD_HEADER_ID,
} from '../util/constants';
import dropOverflowsParent from '../util/dropOverflowsParent';
import findParentId from '../util/findParentId';
// Component CRUD -------------------------------------------------------------
export const UPDATE_COMPONENTS = 'UPDATE_COMPONENTS';
-export function updateComponents(nextComponents) {
+function updateLayoutComponents(nextComponents) {
return {
type: UPDATE_COMPONENTS,
payload: {
@@ -20,8 +23,34 @@ export function updateComponents(nextComponents) {
};
}
+export function updateComponents(nextComponents) {
+ return (dispatch, getState) => {
+ dispatch(updateLayoutComponents(nextComponents));
+
+ if (!getState().dashboardState.hasUnsavedChanges) {
+ dispatch(setUnsavedChanges(true));
+ }
+ };
+}
+
+export function updateDashboardTitle(text) {
+ return (dispatch, getState) => {
+ const { dashboardLayout } = getState();
+ dispatch(
+ updateComponents({
+ [DASHBOARD_HEADER_ID]: {
+ ...dashboardLayout.present[DASHBOARD_HEADER_ID],
+ meta: {
+ text,
+ },
+ },
+ }),
+ );
+ };
+}
+
export const DELETE_COMPONENT = 'DELETE_COMPONENT';
-export function deleteComponent(id, parentId) {
+function deleteLayoutComponent(id, parentId) {
return {
type: DELETE_COMPONENT,
payload: {
@@ -31,8 +60,18 @@ export function deleteComponent(id, parentId) {
};
}
+export function deleteComponent(id, parentId) {
+ return (dispatch, getState) => {
+ dispatch(deleteLayoutComponent(id, parentId));
+
+ if (!getState().dashboardState.hasUnsavedChanges) {
+ dispatch(setUnsavedChanges(true));
+ }
+ };
+}
+
export const CREATE_COMPONENT = 'CREATE_COMPONENT';
-export function createComponent(dropResult) {
+function createLayoutComponent(dropResult) {
return {
type: CREATE_COMPONENT,
payload: {
@@ -41,9 +80,19 @@ export function createComponent(dropResult) {
};
}
+export function createComponent(dropResult) {
+ return (dispatch, getState) => {
+ dispatch(createLayoutComponent(dropResult));
+
+ if (!getState().dashboardState.hasUnsavedChanges) {
+ dispatch(setUnsavedChanges(true));
+ }
+ };
+}
+
// Tabs -----------------------------------------------------------------------
export const CREATE_TOP_LEVEL_TABS = 'CREATE_TOP_LEVEL_TABS';
-export function createTopLevelTabs(dropResult) {
+function createTopLevelTabsAction(dropResult) {
return {
type: CREATE_TOP_LEVEL_TABS,
payload: {
@@ -52,19 +101,39 @@ export function createTopLevelTabs(dropResult) {
};
}
+export function createTopLevelTabs(dropResult) {
+ return (dispatch, getState) => {
+ dispatch(createTopLevelTabsAction(dropResult));
+
+ if (!getState().dashboardState.hasUnsavedChanges) {
+ dispatch(setUnsavedChanges(true));
+ }
+ };
+}
+
export const DELETE_TOP_LEVEL_TABS = 'DELETE_TOP_LEVEL_TABS';
-export function deleteTopLevelTabs() {
+function deleteTopLevelTabsAction() {
return {
type: DELETE_TOP_LEVEL_TABS,
payload: {},
};
}
+export function deleteTopLevelTabs(dropResult) {
+ return (dispatch, getState) => {
+ dispatch(deleteTopLevelTabsAction(dropResult));
+
+ if (!getState().dashboardState.hasUnsavedChanges) {
+ dispatch(setUnsavedChanges(true));
+ }
+ };
+}
+
// Resize ---------------------------------------------------------------------
export const RESIZE_COMPONENT = 'RESIZE_COMPONENT';
export function resizeComponent({ id, width, height }) {
return (dispatch, getState) => {
- const { dashboardLayout: undoableLayout } = getState();
+ const { dashboardLayout: undoableLayout, dashboardState } = getState();
const { present: dashboard } = undoableLayout;
const component = dashboard[id];
const widthChanged = width && component.meta.width !== width;
@@ -99,7 +168,9 @@ export function resizeComponent({ id, width, height }) {
});
dispatch(updateComponents(updatedComponents));
- dispatch(setUnsavedChanges(true));
+ if (!dashboardState.hasUnsavedChanges) {
+ dispatch(setUnsavedChanges(true));
+ }
}
};
}
@@ -149,9 +220,10 @@ export function handleComponentDrop(dropResult) {
dispatch(moveComponent(dropResult));
}
+ const { dashboardLayout: undoableLayout, dashboardState } = getState();
+
// if we moved a Tab and the parent Tabs no longer has children, delete it.
if (!isNewComponent) {
- const { dashboardLayout: undoableLayout } = getState();
const { present: layout } = undoableLayout;
const sourceComponent = layout[source.id];
@@ -167,8 +239,36 @@ export function handleComponentDrop(dropResult) {
}
}
- dispatch(setUnsavedChanges(true));
+ if (!dashboardState.hasUnsavedChanges) {
+ dispatch(setUnsavedChanges(true));
+ }
return null;
};
}
+
+// Undo redo ------------------------------------------------------------------
+export function undoLayoutAction() {
+ return (dispatch, getState) => {
+ dispatch(UndoActionCreators.undo());
+
+ const { dashboardLayout, dashboardState } = getState();
+
+ if (
+ dashboardLayout.past.length === 0 &&
+ !dashboardState.maxUndoHistoryExceeded
+ ) {
+ dispatch(setUnsavedChanges(false));
+ }
+ };
+}
+
+export function redoLayoutAction() {
+ return (dispatch, getState) => {
+ dispatch(UndoActionCreators.redo());
+
+ if (!getState().dashboardState.hasUnsavedChanges) {
+ dispatch(setUnsavedChanges(true));
+ }
+ };
+}
diff --git a/superset/assets/src/dashboard/actions/dashboardState.js b/superset/assets/src/dashboard/actions/dashboardState.js
index d80ec83..10c0a26 100644
--- a/superset/assets/src/dashboard/actions/dashboardState.js
+++ b/superset/assets/src/dashboard/actions/dashboardState.js
@@ -1,10 +1,12 @@
/* eslint camelcase: 0 */
import $ from 'jquery';
+import { ActionCreators as UndoActionCreators } from 'redux-undo';
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';
export const SET_UNSAVED_CHANGES = 'SET_UNSAVED_CHANGES';
export function setUnsavedChanges(hasUnsavedChanges) {
@@ -21,11 +23,6 @@ export function removeFilter(sliceId, col, vals, refresh = true) {
return { type: REMOVE_FILTER, sliceId, col, vals, refresh };
}
-export const UPDATE_DASHBOARD_TITLE = 'UPDATE_DASHBOARD_TITLE';
-export function updateDashboardTitle(title) {
- return { type: UPDATE_DASHBOARD_TITLE, title };
-}
-
export const ADD_SLICE = 'ADD_SLICE';
export function addSlice(slice) {
return { type: ADD_SLICE, slice };
@@ -84,6 +81,14 @@ export function onSave() {
return { type: ON_SAVE };
}
+export function saveDashboard() {
+ return dispatch => {
+ dispatch(onSave());
+ // clear layout undo history
+ dispatch(UndoActionCreators.clearHistory());
+ };
+}
+
export function fetchCharts(chartList = [], force = false, interval = 0) {
return (dispatch, getState) => {
const timeout = getState().dashboardInfo.common.conf
@@ -168,9 +173,31 @@ export function addSliceToDashboard(id) {
};
}
-export function removeSliceFromDashboard(chart) {
+export function removeSliceFromDashboard(id) {
return dispatch => {
- dispatch(removeSlice(chart.id));
- dispatch(removeChart(chart.id));
+ dispatch(removeSlice(id));
+ dispatch(removeChart(id));
+ };
+}
+
+// Undo history ---------------------------------------------------------------
+export const SET_MAX_UNDO_HISTORY_EXCEEDED = 'SET_MAX_UNDO_HISTORY_EXCEEDED';
+export function setMaxUndoHistoryExceeded(maxUndoHistoryExceeded = true) {
+ return {
+ type: SET_MAX_UNDO_HISTORY_EXCEEDED,
+ payload: { maxUndoHistoryExceeded },
+ };
+}
+
+export function maxUndoHistoryToast() {
+ return (dispatch, getState) => {
+ const { dashboardLayout } = getState();
+ const historyLength = dashboardLayout.past.length;
+
+ return dispatch(
+ addWarningToast(
+ `You have used all ${historyLength} undo slots and will not be able to fully undo subsequent actions. You may save your current state to reset the history.`,
+ ),
+ );
};
}
diff --git a/superset/assets/src/dashboard/actions/sliceEntities.js b/superset/assets/src/dashboard/actions/sliceEntities.js
index 6922753..37781f9 100644
--- a/superset/assets/src/dashboard/actions/sliceEntities.js
+++ b/superset/assets/src/dashboard/actions/sliceEntities.js
@@ -1,41 +1,6 @@
/* eslint camelcase: 0 */
-/* global notify */
import $ from 'jquery';
-export const UPDATE_SLICE_NAME = 'UPDATE_SLICE_NAME';
-export function updateSliceName(key, sliceName) {
- return { type: UPDATE_SLICE_NAME, key, sliceName };
-}
-
-export function saveSliceName(slice, sliceName) {
- const oldName = slice.slice_name;
- return dispatch => {
- const sliceParams = {};
- sliceParams.slice_id = slice.slice_id;
- sliceParams.action = 'overwrite';
- sliceParams.slice_name = sliceName;
-
- const url = `${slice.slice_url}&${Object.keys(sliceParams)
- .map(key => `${key}=${sliceParams[key]}`)
- .join('&')}`;
- const key = slice.slice_id;
- return $.ajax({
- url,
- type: 'POST',
- success: () => {
- dispatch(updateSliceName(key, sliceName));
- notify.success('This slice name was saved successfully.');
- },
- error: () => {
- // if server-side reject the overwrite action,
- // revert to old state
- dispatch(updateSliceName(key, oldName));
- notify.error("You don't have the rights to alter this slice");
- },
- });
- };
-}
-
export const SET_ALL_SLICES = 'SET_ALL_SLICES';
export function setAllSlices(slices) {
return { type: SET_ALL_SLICES, slices };
diff --git a/superset/assets/src/dashboard/components/BuilderComponentPane.jsx b/superset/assets/src/dashboard/components/BuilderComponentPane.jsx
index e5bc74c..b42650e 100644
--- a/superset/assets/src/dashboard/components/BuilderComponentPane.jsx
+++ b/superset/assets/src/dashboard/components/BuilderComponentPane.jsx
@@ -1,70 +1,106 @@
+/* eslint-env browser */
+import PropTypes from 'prop-types';
import React from 'react';
import cx from 'classnames';
+import { StickyContainer, Sticky } from 'react-sticky';
import NewColumn from './gridComponents/new/NewColumn';
import NewDivider from './gridComponents/new/NewDivider';
import NewHeader from './gridComponents/new/NewHeader';
import NewRow from './gridComponents/new/NewRow';
import NewTabs from './gridComponents/new/NewTabs';
-import SliceAdderContainer from '../containers/SliceAdder';
+import SliceAdder from '../containers/SliceAdder';
+import { t } from '../../locales';
+
+const propTypes = {
+ topOffset: PropTypes.number,
+};
+
+const defaultProps = {
+ topOffset: 0,
+};
class BuilderComponentPane extends React.PureComponent {
constructor(props) {
super(props);
this.state = {
- showSlices: false,
+ slideDirection: 'slide-out',
};
- this.openSlicesPane = this.showSlices.bind(this, true);
- this.closeSlicesPane = this.showSlices.bind(this, false);
+ this.openSlicesPane = this.slide.bind(this, 'slide-in');
+ this.closeSlicesPane = this.slide.bind(this, 'slide-out');
}
- showSlices(show) {
+ slide(direction) {
this.setState({
- showSlices: show,
+ slideDirection: direction,
});
}
render() {
+ const { topOffset } = this.props;
return (
- <div className="dashboard-builder-sidepane">
- <div className="dashboard-builder-sidepane-header">
- Insert components
- {this.state.showSlices && (
- <i
- className="fa fa-times close trigger"
- onClick={this.closeSlicesPane}
- role="none"
- />
- )}
- </div>
+ <StickyContainer className="dashboard-builder-sidepane">
+ <Sticky topOffset={-topOffset}>
+ {({ style, calculatedHeight, isSticky }) => (
+ <div
+ className="viewport"
+ style={isSticky ? { ...style, top: topOffset } : null}
+ >
+ <div
+ className={cx('slider-container', this.state.slideDirection)}
+ >
+ <div className="component-layer slide-content">
+ <div className="dashboard-builder-sidepane-header">
+ {t('Saved components')}
+ </div>
+ <div
+ className="new-component static"
+ role="none"
+ onClick={this.openSlicesPane}
+ >
+ <div className="new-component-placeholder fa fa-area-chart" />
+ <div className="new-component-label">
+ {t('Charts & filters')}
+ </div>
- <div className="component-layer">
- <div
- className="dragdroppable dragdroppable-row"
- onClick={this.openSlicesPane}
- role="none"
- >
- <div className="new-component static">
- <div className="new-component-placeholder fa fa-area-chart" />
- Chart
- <i className="fa fa-arrow-right open trigger" />
- </div>
- </div>
+ <i className="fa fa-arrow-right trigger" />
+ </div>
- <NewHeader />
- <NewDivider />
- <NewTabs />
- <NewRow />
- <NewColumn />
- </div>
+ <div className="dashboard-builder-sidepane-header">
+ {t('Containers')}
+ </div>
+ <NewTabs />
+ <NewRow />
+ <NewColumn />
- <div className={cx('slices-layer', this.state.showSlices && 'show')}>
- <SliceAdderContainer />
- </div>
- </div>
+ <div className="dashboard-builder-sidepane-header">
+ {t('More components')}
+ </div>
+ <NewHeader />
+ <NewDivider />
+ </div>
+ <div className="slices-layer slide-content">
+ <div
+ className="dashboard-builder-sidepane-header"
+ onClick={this.closeSlicesPane}
+ role="none"
+ >
+ <i className="fa fa-arrow-left trigger" />
+ {t('All components')}
+ </div>
+ <SliceAdder height={calculatedHeight} />
+ </div>
+ </div>
+ </div>
+ )}
+ </Sticky>
+ </StickyContainer>
);
}
}
+BuilderComponentPane.propTypes = propTypes;
+BuilderComponentPane.defaultProps = defaultProps;
+
export default BuilderComponentPane;
diff --git a/superset/assets/src/dashboard/components/Controls.jsx b/superset/assets/src/dashboard/components/Controls.jsx
index 06b4f7f..07b6c33 100644
--- a/superset/assets/src/dashboard/components/Controls.jsx
+++ b/superset/assets/src/dashboard/components/Controls.jsx
@@ -2,11 +2,10 @@
import React from 'react';
import PropTypes from 'prop-types';
import $ from 'jquery';
-import { DropdownButton } from 'react-bootstrap';
+import { DropdownButton, MenuItem } from 'react-bootstrap';
import RefreshIntervalModal from './RefreshIntervalModal';
import SaveModal from './SaveModal';
-import { ActionMenuItem, MenuItemContent } from './ActionMenuItem';
import { t } from '../../locales';
function updateDom(css) {
@@ -28,6 +27,8 @@ function updateDom(css) {
}
const propTypes = {
+ addSuccessToast: PropTypes.func.isRequired,
+ addDangerToast: PropTypes.func.isRequired,
dashboardInfo: PropTypes.object.isRequired,
dashboardTitle: PropTypes.string.isRequired,
layout: PropTypes.object.isRequired,
@@ -100,23 +101,18 @@ class Controls extends React.PureComponent {
id="bg-nested-dropdown"
pullRight
>
- <ActionMenuItem
- text={t('Force Refresh')}
- tooltip={t('Force refresh the whole dashboard')}
- onClick={forceRefreshAllCharts}
- />
+ <MenuItem onClick={forceRefreshAllCharts}>
+ {t('Force refresh dashboard')}
+ </MenuItem>
<RefreshIntervalModal
onChange={refreshInterval =>
startPeriodicRender(refreshInterval * 1000)
}
- triggerNode={
- <MenuItemContent
- text={t('Set autorefresh')}
- tooltip={t('Set the auto-refresh interval for this session')}
- />
- }
+ 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}
@@ -124,33 +120,19 @@ class Controls extends React.PureComponent {
expandedSlices={expandedSlices}
onSave={onSave}
css={this.state.css}
- triggerNode={
- <MenuItemContent
- text={editMode ? t('Save') : t('Save as')}
- tooltip={t('Save the dashboard')}
- />
- }
+ triggerNode={<span>{editMode ? t('Save') : t('Save as')}</span>}
isMenuItem
/>
{editMode && (
- <ActionMenuItem
- text={t('Edit properties')}
- tooltip={t("Edit the dashboards's properties")}
- onClick={() => {
- window.location = `/dashboardmodelview/edit/${
- this.props.dashboardInfo.id
- }`;
- }}
- />
+ <MenuItem
+ target="_blank"
+ href={`/dashboardmodelview/edit/${this.props.dashboardInfo.id}`}
+ >
+ {t('Edit dashboard metadata')}
+ </MenuItem>
)}
{editMode && (
- <ActionMenuItem
- text={t('Email')}
- tooltip={t('Email a link to this dashboard')}
- onClick={() => {
- window.location = emailLink;
- }}
- />
+ <MenuItem href={emailLink}>{t('Email dashboard link')}</MenuItem>
)}
</DropdownButton>
</span>
diff --git a/superset/assets/src/dashboard/components/Dashboard.jsx b/superset/assets/src/dashboard/components/Dashboard.jsx
index 2d85ebf..369ed46 100644
--- a/superset/assets/src/dashboard/components/Dashboard.jsx
+++ b/superset/assets/src/dashboard/components/Dashboard.jsx
@@ -27,7 +27,6 @@ import '../stylesheets/index.less';
const propTypes = {
actions: PropTypes.shape({
addSliceToDashboard: PropTypes.func.isRequired,
- onChange: PropTypes.func.isRequired,
removeSliceFromDashboard: PropTypes.func.isRequired,
runQuery: PropTypes.func.isRequired,
}).isRequired,
@@ -98,16 +97,12 @@ class Dashboard extends React.PureComponent {
key => currentChartIds.indexOf(key) === -1,
);
this.props.actions.addSliceToDashboard(newChartId);
- this.props.actions.onChange();
} else if (currentChartIds.length > nextChartIds.length) {
// remove chart
const removedChartId = currentChartIds.find(
key => nextChartIds.indexOf(key) === -1,
);
- this.props.actions.removeSliceFromDashboard(
- this.props.charts[removedChartId],
- );
- this.props.actions.onChange();
+ this.props.actions.removeSliceFromDashboard(removedChartId);
}
}
diff --git a/superset/assets/src/dashboard/components/DashboardBuilder.jsx b/superset/assets/src/dashboard/components/DashboardBuilder.jsx
index 79eb35d..7f92948 100644
--- a/superset/assets/src/dashboard/components/DashboardBuilder.jsx
+++ b/superset/assets/src/dashboard/components/DashboardBuilder.jsx
@@ -1,8 +1,14 @@
+/* eslint-env browser */
import cx from 'classnames';
-import React from 'react';
-import PropTypes from 'prop-types';
-import HTML5Backend from 'react-dnd-html5-backend';
import { DragDropContext } from 'react-dnd';
+import HTML5Backend from 'react-dnd-html5-backend';
+// ParentSize uses resize observer so the dashboard will update size
+// when its container size changes, due to e.g., builder side panel opening
+import ParentSize from '@vx/responsive/build/components/ParentSize';
+import PropTypes from 'prop-types';
+import React from 'react';
+import { Sticky, StickyContainer } from 'react-sticky';
+import { TabContainer, TabContent, TabPane } from 'react-bootstrap';
import BuilderComponentPane from './BuilderComponentPane';
import DashboardHeader from '../containers/DashboardHeader';
@@ -19,6 +25,8 @@ import {
DASHBOARD_ROOT_DEPTH,
} from '../util/constants';
+const TABS_HEIGHT = 47;
+
const propTypes = {
// redux
dashboardLayout: PropTypes.object.isRequired,
@@ -52,31 +60,35 @@ class DashboardBuilder extends React.Component {
handleChangeTab({ tabIndex }) {
this.setState(() => ({ tabIndex }));
+ setTimeout(() => {
+ if (window)
+ window.scrollTo({
+ top: 0,
+ behavior: 'smooth',
+ });
+ }, 100);
}
render() {
- const { tabIndex } = this.state;
const {
handleComponentDrop,
dashboardLayout,
deleteTopLevelTabs,
editMode,
} = this.props;
+
+ const { tabIndex } = this.state;
const dashboardRoot = dashboardLayout[DASHBOARD_ROOT_ID];
const rootChildId = dashboardRoot.children[0];
const topLevelTabs =
rootChildId !== DASHBOARD_GRID_ID && dashboardLayout[rootChildId];
- const gridComponentId = topLevelTabs
- ? topLevelTabs.children[
- Math.min(topLevelTabs.children.length - 1, tabIndex)
- ]
- : DASHBOARD_GRID_ID;
-
- const gridComponent = dashboardLayout[gridComponentId];
+ const childIds = topLevelTabs ? topLevelTabs.children : [DASHBOARD_GRID_ID];
return (
- <div className={cx('dashboard', editMode && 'dashboard--editing')}>
+ <StickyContainer
+ className={cx('dashboard', editMode && 'dashboard--editing')}
+ >
{topLevelTabs || !editMode ? ( // you cannot drop on/displace tabs if they already exist
<DashboardHeader />
) : (
@@ -99,38 +111,84 @@ class DashboardBuilder extends React.Component {
)}
{topLevelTabs && (
- <WithPopoverMenu
- shouldFocus={DashboardBuilder.shouldFocusTabs}
- menuItems={[
- <IconButton
- className="fa fa-level-down"
- label="Collapse tab content"
- onClick={deleteTopLevelTabs}
- />,
- ]}
- editMode={editMode}
- >
- <DashboardComponent
- id={topLevelTabs.id}
- parentId={DASHBOARD_ROOT_ID}
- depth={DASHBOARD_ROOT_DEPTH + 1}
- index={0}
- renderTabContent={false}
- onChangeTab={this.handleChangeTab}
- />
- </WithPopoverMenu>
+ <Sticky topOffset={50}>
+ {({ style }) => (
+ <WithPopoverMenu
+ shouldFocus={DashboardBuilder.shouldFocusTabs}
+ menuItems={[
+ <IconButton
+ className="fa fa-level-down"
+ label="Collapse tab content"
+ onClick={deleteTopLevelTabs}
+ />,
+ ]}
+ editMode={editMode}
+ style={{ zIndex: 100, ...style }}
+ >
+ <DashboardComponent
+ id={topLevelTabs.id}
+ parentId={DASHBOARD_ROOT_ID}
+ depth={DASHBOARD_ROOT_DEPTH + 1}
+ index={0}
+ renderTabContent={false}
+ onChangeTab={this.handleChangeTab}
+ />
+ </WithPopoverMenu>
+ )}
+ </Sticky>
)}
<div className="dashboard-content">
- <DashboardGrid
- gridComponent={gridComponent}
- depth={DASHBOARD_ROOT_DEPTH + 1}
- />
+ <div className="grid-container">
+ <ParentSize>
+ {({ width }) => (
+ /*
+ We use a TabContainer irrespective of whether top-level tabs exist to maintain
+ a consistent React component tree. This avoids expensive mounts/unmounts of
+ the entire dashboard upon adding/removing top-level tabs, which would otherwise
+ happen because of React's diffing algorithm
+ */
+ <TabContainer
+ id={DASHBOARD_GRID_ID}
+ activeKey={tabIndex}
+ onSelect={this.handleChangeTab}
+ // these are important for performant loading of tabs. also, there is a
+ // react-bootstrap bug where mountOnEnter has no effect unless animation=true
+ animation
+ mountOnEnter
+ unmountOnExit={false}
+ >
+ <TabContent>
+ {childIds.map((id, index) => (
+ // Matching the key of the first TabPane irrespective of topLevelTabs
+ // lets us keep the same React component tree when !!topLevelTabs changes.
+ // This avoids expensive mounts/unmounts of the entire dashboard.
+ <TabPane
+ key={index === 0 ? DASHBOARD_GRID_ID : id}
+ eventKey={index}
+ >
+ <DashboardGrid
+ gridComponent={dashboardLayout[id]}
+ depth={DASHBOARD_ROOT_DEPTH + 1}
+ width={width}
+ />
+ </TabPane>
+ ))}
+ </TabContent>
+ </TabContainer>
+ )}
+ </ParentSize>
+ </div>
+
{this.props.editMode &&
- this.props.showBuilderPane && <BuilderComponentPane />}
+ this.props.showBuilderPane && (
+ <BuilderComponentPane
+ topOffset={topLevelTabs ? TABS_HEIGHT : 0}
+ />
+ )}
</div>
<ToastPresenter />
- </div>
+ </StickyContainer>
);
}
}
diff --git a/superset/assets/src/dashboard/components/DashboardGrid.jsx b/superset/assets/src/dashboard/components/DashboardGrid.jsx
index 3e6fc0c..77503bb 100644
--- a/superset/assets/src/dashboard/components/DashboardGrid.jsx
+++ b/superset/assets/src/dashboard/components/DashboardGrid.jsx
@@ -1,8 +1,5 @@
import React from 'react';
import PropTypes from 'prop-types';
-// ParentSize uses resize observer so the dashboard will update size
-// when its container size changes, due to e.g., builder side panel opening
-import ParentSize from '@vx/responsive/build/components/ParentSize';
import { componentShape } from '../util/propShapes';
import DashboardComponent from '../containers/DashboardComponent';
@@ -16,6 +13,7 @@ const propTypes = {
gridComponent: componentShape.isRequired,
handleComponentDrop: PropTypes.func.isRequired,
resizeComponent: PropTypes.func.isRequired,
+ width: PropTypes.number.isRequired,
};
const defaultProps = {};
@@ -28,6 +26,7 @@ class DashboardGrid extends React.PureComponent {
rowGuideTop: null,
};
+ this.handleTopDropTargetDrop = this.handleTopDropTargetDrop.bind(this);
this.handleResizeStart = this.handleResizeStart.bind(this);
this.handleResize = this.handleResize.bind(this);
this.handleResizeStop = this.handleResizeStop.bind(this);
@@ -77,100 +76,117 @@ class DashboardGrid extends React.PureComponent {
}));
}
+ handleTopDropTargetDrop(dropResult) {
+ if (dropResult) {
+ this.props.handleComponentDrop({
+ ...dropResult,
+ destination: {
+ ...dropResult.destination,
+ // force appending as the first child if top drop target
+ index: 0,
+ },
+ });
+ }
+ }
+
render() {
- const { gridComponent, handleComponentDrop, depth, editMode } = this.props;
+ const {
+ gridComponent,
+ handleComponentDrop,
+ depth,
+ editMode,
+ width,
+ } = this.props;
+
+ const columnPlusGutterWidth =
+ (width + GRID_GUTTER_SIZE) / GRID_COLUMN_COUNT;
+
+ const columnWidth = columnPlusGutterWidth - GRID_GUTTER_SIZE;
const { isResizing, rowGuideTop } = this.state;
- return (
- <div className="grid-container" ref={this.setGridRef}>
- <ParentSize>
- {({ width }) => {
- const columnPlusGutterWidth =
- (width + GRID_GUTTER_SIZE) / GRID_COLUMN_COUNT;
- const columnWidth = columnPlusGutterWidth - GRID_GUTTER_SIZE;
- return width < 50 ? null : (
- <div className="grid-content">
- {editMode && (
- <DragDroppable
- component={gridComponent}
- depth={depth}
- parentComponent={null}
- index={0}
- orientation="column"
- onDrop={handleComponentDrop}
- editMode
- >
- {({ dropIndicatorProps }) =>
- dropIndicatorProps && (
- <div className="drop-indicator drop-indicator--bottom" />
- )
- }
- </DragDroppable>
- )}
-
- {gridComponent.children.map((id, index) => (
- <DashboardComponent
- key={id}
- id={id}
- parentId={gridComponent.id}
- depth={depth + 1}
- index={index}
- availableColumnCount={GRID_COLUMN_COUNT}
- columnWidth={columnWidth}
- onResizeStart={this.handleResizeStart}
- onResize={this.handleResize}
- onResizeStop={this.handleResizeStop}
- />
- ))}
-
- {/* render an empty drop target */}
- {editMode && (
- <DragDroppable
- component={gridComponent}
- depth={depth}
- parentComponent={null}
- index={gridComponent.children.length}
- orientation="column"
- onDrop={handleComponentDrop}
- className="empty-grid-droptarget"
- editMode
- >
- {({ dropIndicatorProps }) =>
- dropIndicatorProps && (
- <div className="drop-indicator drop-indicator--top" />
- )
- }
- </DragDroppable>
- )}
-
- {isResizing &&
- Array(GRID_COLUMN_COUNT)
- .fill(null)
- .map((_, i) => (
- <div
- key={`grid-column-${i}`}
- className="grid-column-guide"
- style={{
- left: i * GRID_GUTTER_SIZE + i * columnWidth,
- width: columnWidth,
- }}
- />
- ))}
-
- {isResizing &&
- rowGuideTop && (
- <div
- className="grid-row-guide"
- style={{
- top: rowGuideTop,
- width,
- }}
- />
- )}
- </div>
- );
- }}
- </ParentSize>
+ return width < 100 ? null : (
+ <div className="dashboard-grid" ref={this.setGridRef}>
+ <div className="grid-content">
+ {/* empty drop target makes top droppable */}
+ {editMode && (
+ <DragDroppable
+ component={gridComponent}
+ depth={depth}
+ parentComponent={null}
+ index={0}
+ orientation="column"
+ onDrop={this.handleTopDropTargetDrop}
+ className="empty-grid-droptarget--top"
+ editMode
+ >
+ {({ dropIndicatorProps }) =>
+ dropIndicatorProps && (
+ <div className="drop-indicator drop-indicator--bottom" />
+ )
+ }
+ </DragDroppable>
+ )}
+
+ {gridComponent.children.map((id, index) => (
+ <DashboardComponent
+ key={id}
+ id={id}
+ parentId={gridComponent.id}
+ depth={depth + 1}
+ index={index}
+ availableColumnCount={GRID_COLUMN_COUNT}
+ columnWidth={columnWidth}
+ onResizeStart={this.handleResizeStart}
+ onResize={this.handleResize}
+ onResizeStop={this.handleResizeStop}
+ />
+ ))}
+
+ {/* empty drop target makes bottom droppable */}
+ {editMode && (
+ <DragDroppable
+ component={gridComponent}
+ depth={depth}
+ parentComponent={null}
+ index={gridComponent.children.length}
+ orientation="column"
+ onDrop={handleComponentDrop}
+ className="empty-grid-droptarget--bottom"
+ editMode
+ >
+ {({ dropIndicatorProps }) =>
+ dropIndicatorProps && (
+ <div className="drop-indicator drop-indicator--top" />
+ )
+ }
+ </DragDroppable>
+ )}
+
+ {isResizing &&
+ Array(GRID_COLUMN_COUNT)
+ .fill(null)
+ .map((_, i) => (
+ <div
+ key={`grid-column-${i}`}
+ className="grid-column-guide"
+ style={{
+ left: i * GRID_GUTTER_SIZE + i * columnWidth,
+ width: columnWidth,
+ }}
+ />
+ ))}
+
+ {isResizing &&
+ rowGuideTop && (
+ <div
+ className="grid-row-guide"
+ style={{
+ top: rowGuideTop,
+ width,
+ }}
+ />
+ )}
+ </div>
</div>
);
}
diff --git a/superset/assets/src/dashboard/components/Header.jsx b/superset/assets/src/dashboard/components/Header.jsx
index 242102e..21b01db 100644
--- a/superset/assets/src/dashboard/components/Header.jsx
+++ b/superset/assets/src/dashboard/components/Header.jsx
@@ -6,12 +6,14 @@ 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 SaveModal from './SaveModal';
import { chartPropShape } from '../util/propShapes';
import { t } from '../../locales';
+import { UNDO_LIMIT } from '../util/constants';
const propTypes = {
+ addSuccessToast: PropTypes.func.isRequired,
+ addDangerToast: PropTypes.func.isRequired,
dashboardInfo: PropTypes.object.isRequired,
dashboardTitle: PropTypes.string.isRequired,
charts: PropTypes.objectOf(chartPropShape).isRequired,
@@ -31,23 +33,45 @@ const propTypes = {
showBuilderPane: PropTypes.bool.isRequired,
toggleBuilderPane: PropTypes.func.isRequired,
hasUnsavedChanges: PropTypes.bool.isRequired,
+ maxUndoHistoryExceeded: PropTypes.bool.isRequired,
// redux
onUndo: PropTypes.func.isRequired,
onRedo: PropTypes.func.isRequired,
- canUndo: PropTypes.bool.isRequired,
- canRedo: PropTypes.bool.isRequired,
+ undoLength: PropTypes.number.isRequired,
+ redoLength: PropTypes.number.isRequired,
+ setMaxUndoHistoryExceeded: PropTypes.func.isRequired,
+ maxUndoHistoryToast: PropTypes.func.isRequired,
};
class Header extends React.PureComponent {
constructor(props) {
super(props);
+ this.state = {
+ didNotifyMaxUndoHistoryToast: false,
+ };
this.handleChangeText = this.handleChangeText.bind(this);
this.toggleEditMode = this.toggleEditMode.bind(this);
this.forceRefresh = this.forceRefresh.bind(this);
}
+ componentWillReceiveProps(nextProps) {
+ if (
+ UNDO_LIMIT - nextProps.undoLength <= 0 &&
+ !this.state.didNotifyMaxUndoHistoryToast
+ ) {
+ this.setState(() => ({ didNotifyMaxUndoHistoryToast: true }));
+ this.props.maxUndoHistoryToast();
+ }
+ if (
+ nextProps.undoLength > UNDO_LIMIT &&
+ !this.props.maxUndoHistoryExceeded
+ ) {
+ this.props.setMaxUndoHistoryExceeded();
+ }
+ }
+
forceRefresh() {
return this.props.fetchCharts(Object.values(this.props.charts), true);
}
@@ -72,8 +96,8 @@ class Header extends React.PureComponent {
expandedSlices,
onUndo,
onRedo,
- canUndo,
- canRedo,
+ undoLength,
+ redoLength,
onChange,
onSave,
editMode,
@@ -91,9 +115,9 @@ class Header extends React.PureComponent {
title={dashboardTitle}
canEdit={this.props.dashboardInfo.dash_save_perm && editMode}
onSaveTitle={this.handleChangeText}
- showTooltip={editMode}
+ showTooltip={false}
/>
- <span className="favstar m-r-5">
+ <span className="favstar m-l-5">
<FaveStar
itemId={this.props.dashboardInfo.id}
fetchFaveStar={this.props.fetchFaveStar}
@@ -106,14 +130,22 @@ class Header extends React.PureComponent {
{userCanEdit && (
<ButtonGroup>
{editMode && (
- <Button bsSize="small" onClick={onUndo} disabled={!canUndo}>
- Undo
+ <Button
+ bsSize="small"
+ onClick={onUndo}
+ disabled={undoLength < 1}
+ >
+ <div title="Undo" className="undo-action fa fa-reply" />
</Button>
)}
{editMode && (
- <Button bsSize="small" onClick={onRedo} disabled={!canRedo}>
- Redo
+ <Button
+ bsSize="small"
+ onClick={onRedo}
+ disabled={redoLength < 1}
+ >
+ <div title="Redo" className="redo-action fa fa-share" />
</Button>
)}
@@ -135,6 +167,8 @@ class Header extends React.PureComponent {
</Button>
) : (
<SaveModal
+ addSuccessToast={this.props.addSuccessToast}
+ addDangerToast={this.props.addDangerToast}
dashboardId={this.props.dashboardInfo.id}
dashboardTitle={dashboardTitle}
layout={layout}
@@ -154,6 +188,8 @@ class Header extends React.PureComponent {
)}
<Controls
+ addSuccessToast={this.props.addSuccessToast}
+ addDangerToast={this.props.addDangerToast}
dashboardInfo={this.props.dashboardInfo}
dashboardTitle={dashboardTitle}
layout={layout}
diff --git a/superset/assets/src/dashboard/components/SaveModal.jsx b/superset/assets/src/dashboard/components/SaveModal.jsx
index 07b904b..4f05d2c 100644
--- a/superset/assets/src/dashboard/components/SaveModal.jsx
+++ b/superset/assets/src/dashboard/components/SaveModal.jsx
@@ -1,4 +1,4 @@
-/* global notify, window */
+/* eslint-env browser */
import React from 'react';
import PropTypes from 'prop-types';
import $ from 'jquery';
@@ -10,6 +10,8 @@ import { t } from '../../locales';
import Checkbox from '../../components/Checkbox';
const propTypes = {
+ addSuccessToast: PropTypes.func.isRequired,
+ addDangerToast: PropTypes.func.isRequired,
dashboardId: PropTypes.number.isRequired,
dashboardTitle: PropTypes.string.isRequired,
expandedSlices: PropTypes.object.isRequired,
@@ -61,31 +63,31 @@ class SaveModal extends React.PureComponent {
});
}
+ // @TODO this should all be moved to actions
saveDashboardRequest(data, url, saveType) {
- const saveModal = this.modal;
- const onSaveDashboard = this.props.onSave;
$.ajax({
type: 'POST',
url,
data: {
data: JSON.stringify(data),
},
- success(resp) {
- saveModal.close();
- onSaveDashboard();
+ success: resp => {
+ this.modal.close();
+ this.props.onSave();
if (saveType === 'newDashboard') {
window.location = `/superset/dashboard/${resp.id}/`;
} else {
- notify.success(t('This dashboard was saved successfully.'));
+ this.props.addSuccessToast(
+ t('This dashboard was saved successfully.'),
+ );
}
},
- error(error) {
- saveModal.close();
+ error: error => {
+ this.modal.close();
const errorMsg = getAjaxErrorMsg(error);
- notify.error(
- `${t(
- 'Sorry, there was an error saving this dashboard: ',
- )}</ br>${errorMsg}`,
+ this.props.addDangerToast(
+ `${t('Sorry, there was an error saving this dashboard: ')}
+ ${errorMsg}`,
);
},
});
@@ -115,7 +117,9 @@ class SaveModal extends React.PureComponent {
this.saveDashboardRequest(data, url, saveType);
} else if (saveType === 'newDashboard') {
if (!newDashName) {
- notify.error('You must pick a name for the new dashboard');
+ this.props.addDangerToast(
+ t('You must pick a name for the new dashboard'),
+ );
} else {
data.dashboard_title = newDashName;
url = `/superset/copy_dash/${dashboardId}/`;
diff --git a/superset/assets/src/dashboard/components/SliceAdder.jsx b/superset/assets/src/dashboard/components/SliceAdder.jsx
index 37ce21f..05c4270 100644
--- a/superset/assets/src/dashboard/components/SliceAdder.jsx
+++ b/superset/assets/src/dashboard/components/SliceAdder.jsx
@@ -1,3 +1,4 @@
+/* eslint-env browser */
import React from 'react';
import PropTypes from 'prop-types';
import { DropdownButton, MenuItem } from 'react-bootstrap';
@@ -20,12 +21,14 @@ const propTypes = {
userId: PropTypes.string.isRequired,
selectedSliceIds: PropTypes.object,
editMode: PropTypes.bool,
+ height: PropTypes.number,
};
const defaultProps = {
selectedSliceIds: new Set(),
editMode: false,
errorMessage: '',
+ height: window.innerHeight,
};
const KEYS_TO_FILTERS = ['slice_name', 'viz_type', 'datasource_name'];
@@ -179,6 +182,7 @@ class SliceAdder extends React.Component {
</DropdownButton>
<SearchInput
+ className="search-input"
onChange={this.searchUpdated}
onKeyPress={this.handleKeyPress}
/>
@@ -198,7 +202,7 @@ class SliceAdder extends React.Component {
this.state.filteredSlices.length > 0 && (
<List
width={376}
- height={500}
+ height={this.props.height}
rowCount={this.state.filteredSlices.length}
rowHeight={136}
rowRenderer={this.rowRenderer}
diff --git a/superset/assets/src/dashboard/components/SliceHeader.jsx b/superset/assets/src/dashboard/components/SliceHeader.jsx
index bcdaedf..0c572d8 100644
--- a/superset/assets/src/dashboard/components/SliceHeader.jsx
+++ b/superset/assets/src/dashboard/components/SliceHeader.jsx
@@ -20,6 +20,7 @@ const propTypes = {
editMode: PropTypes.bool,
annotationQuery: PropTypes.object,
annotationError: PropTypes.object,
+ sliceName: PropTypes.string,
};
const defaultProps = {
@@ -36,21 +37,10 @@ const defaultProps = {
cachedDttm: null,
isCached: false,
isExpanded: false,
+ sliceName: '',
};
class SliceHeader extends React.PureComponent {
- constructor(props) {
- super(props);
-
- this.onSaveTitle = this.onSaveTitle.bind(this);
- }
-
- onSaveTitle(newTitle) {
- if (this.props.updateSliceName) {
- this.props.updateSliceName(this.props.slice.slice_id, newTitle);
- }
- }
-
render() {
const {
slice,
@@ -62,6 +52,7 @@ class SliceHeader extends React.PureComponent {
exploreChart,
exportCSV,
innerRef,
+ sliceName,
} = this.props;
const annoationsLoading = t('Annotation layers are still loading.');
@@ -71,13 +62,10 @@ class SliceHeader extends React.PureComponent {
<div className="chart-header" ref={innerRef}>
<div className="header">
<EditableTitle
- title={slice.slice_name}
- canEdit={!!this.props.updateSliceName && this.props.editMode}
- onSaveTitle={this.onSaveTitle}
- noPermitTooltip={
- "You don't have the rights to alter this dashboard."
- }
- showTooltip={!!this.props.updateSliceName && this.props.editMode}
+ title={sliceName}
+ canEdit={this.props.editMode}
+ onSaveTitle={this.props.updateSliceName}
+ showTooltip={this.props.editMode}
/>
{!!Object.values(this.props.annotationQuery).length && (
<TooltipWrapper
diff --git a/superset/assets/src/dashboard/components/SliceHeaderControls.jsx b/superset/assets/src/dashboard/components/SliceHeaderControls.jsx
index ee1f261..5326e0f 100644
--- a/superset/assets/src/dashboard/components/SliceHeaderControls.jsx
+++ b/superset/assets/src/dashboard/components/SliceHeaderControls.jsx
@@ -2,9 +2,8 @@ import React from 'react';
import PropTypes from 'prop-types';
import cx from 'classnames';
import moment from 'moment';
-import { DropdownButton } from 'react-bootstrap';
+import { Dropdown, MenuItem } from 'react-bootstrap';
-import { ActionMenuItem } from './ActionMenuItem';
import { t } from '../../locales';
const propTypes = {
@@ -28,6 +27,14 @@ const defaultProps = {
isExpanded: false,
};
+const VerticalDotsTrigger = () => (
+ <div className="vertical-dots-container">
+ <span className="dot" />
+ <span className="dot" />
+ <span className="dot" />
+ </div>
+);
+
class SliceHeaderControls extends React.PureComponent {
constructor(props) {
super(props);
@@ -57,53 +64,44 @@ class SliceHeaderControls extends React.PureComponent {
const slice = this.props.slice;
const isCached = this.props.isCached;
const cachedWhen = moment.utc(this.props.cachedDttm).fromNow();
- const refreshTooltip = isCached
- ? t('Served from data cached %s . Click to force refresh.', cachedWhen)
- : t('Force refresh data');
+ const refreshTooltip = isCached ? t('Cached %s', cachedWhen) : '';
return (
- <DropdownButton
- title=""
+ <Dropdown
id={`slice_${slice.slice_id}-controls`}
- className={cx('slice-header-controls-trigger', 'fa fa-ellipsis-v', {
- 'is-cached': isCached,
- })}
+ className={cx(isCached && 'is-cached')}
pullRight
- noCaret
>
- <ActionMenuItem
- text={t('Force refresh data')}
- tooltip={refreshTooltip}
- onClick={this.props.forceRefresh}
- />
-
- {slice.description && (
- <ActionMenuItem
- text={t('Toggle chart description')}
- tooltip={t('Toggle chart description')}
- onClick={this.toggleExpandSlice}
- />
- )}
-
- <ActionMenuItem
- text={t('Edit chart')}
- tooltip={t("Edit the chart's properties")}
- href={slice.edit_url}
- target="_blank"
- />
-
- <ActionMenuItem
- text={t('Export CSV')}
- tooltip={t('Export CSV')}
- onClick={this.exportCSV}
- />
-
- <ActionMenuItem
- text={t('Explore chart')}
- tooltip={t('Explore chart')}
- onClick={this.exploreChart}
- />
- </DropdownButton>
+ <Dropdown.Toggle className="slice-header-controls-trigger" noCaret>
+ <VerticalDotsTrigger />
+ </Dropdown.Toggle>
+
+ <Dropdown.Menu>
+ <MenuItem onClick={this.props.forceRefresh}>
+ {isCached && <span className="dot" />}
+ {t('Force refresh')}
+ {isCached && (
+ <div className="refresh-tooltip">{refreshTooltip}</div>
+ )}
+ </MenuItem>
+
+ <MenuItem divider />
+
+ {slice.description && (
+ <MenuItem onClick={this.toggleExpandSlice}>
+ {t('Toggle chart description')}
+ </MenuItem>
+ )}
+
+ <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>
+ </Dropdown.Menu>
+ </Dropdown>
);
}
}
diff --git a/superset/assets/src/dashboard/components/dnd/AddSliceDragPreview.jsx b/superset/assets/src/dashboard/components/dnd/AddSliceDragPreview.jsx
index 94cab42..91fc055 100644
--- a/superset/assets/src/dashboard/components/dnd/AddSliceDragPreview.jsx
+++ b/superset/assets/src/dashboard/components/dnd/AddSliceDragPreview.jsx
@@ -9,6 +9,16 @@ import {
CHART_TYPE,
} from '../../util/componentTypes';
+const staticCardStyles = {
+ position: 'fixed',
+ background: 'white',
+ pointerEvents: 'none',
+ top: 0,
+ left: 0,
+ zIndex: 100,
+ width: 376 - 2 * 16,
+};
+
const propTypes = {
dragItem: PropTypes.shape({
index: PropTypes.number.isRequired,
@@ -41,12 +51,7 @@ function AddSliceDragPreview({ dragItem, slices, isDragging, currentOffset }) {
return !shouldRender ? null : (
<AddSliceCard
style={{
- position: 'fixed',
- background: 'white',
- pointerEvents: 'none',
- top: 0,
- left: 0,
- zIndex: 100,
+ ...staticCardStyles,
transform: `translate(${currentOffset.x}px, ${currentOffset.y}px)`,
}}
sliceName={slice.slice_name}
diff --git a/superset/assets/src/dashboard/components/gridComponents/Chart.jsx b/superset/assets/src/dashboard/components/gridComponents/Chart.jsx
index 54e1536..4742d71 100644
--- a/superset/assets/src/dashboard/components/gridComponents/Chart.jsx
+++ b/superset/assets/src/dashboard/components/gridComponents/Chart.jsx
@@ -13,16 +13,17 @@ const propTypes = {
id: PropTypes.number.isRequired,
width: PropTypes.number.isRequired,
height: PropTypes.number.isRequired,
+ updateSliceName: PropTypes.func.isRequired,
// from redux
chart: PropTypes.shape(chartPropType).isRequired,
formData: PropTypes.object.isRequired,
datasource: PropTypes.object.isRequired,
slice: slicePropShape.isRequired,
+ sliceName: PropTypes.string.isRequired,
timeout: PropTypes.number.isRequired,
filters: PropTypes.object.isRequired,
refreshChart: PropTypes.func.isRequired,
- saveSliceName: PropTypes.func.isRequired,
toggleExpandSlice: PropTypes.func.isRequired,
addFilter: PropTypes.func.isRequired,
removeFilter: PropTypes.func.isRequired,
@@ -150,6 +151,8 @@ class Chart extends React.Component {
isExpanded,
editMode,
formData,
+ updateSliceName,
+ sliceName,
toggleExpandSlice,
timeout,
} = this.props;
@@ -161,25 +164,21 @@ class Chart extends React.Component {
const isOverflowable = OVERFLOWABLE_VIZ_TYPES.has(slice && slice.viz_type);
return (
- <div
- className={cx(
- 'dashboard-chart',
- isOverflowable && 'dashboard-chart--overflowable',
- )}
- >
+ <div>
<SliceHeader
innerRef={this.setHeaderRef}
slice={slice}
isExpanded={!!isExpanded}
isCached={isCached}
cachedDttm={cachedDttm}
- updateSliceName={this.updateSliceName}
toggleExpandSlice={toggleExpandSlice}
forceRefresh={this.forceRefresh}
editMode={editMode}
annotationQuery={chart.annotationQuery}
exploreChart={this.exploreChart}
exportCSV={this.exportCSV}
+ updateSliceName={updateSliceName}
+ sliceName={sliceName}
/>
{/*
@@ -199,30 +198,37 @@ class Chart extends React.Component {
/>
)}
- <ChartContainer
- containerId={`slice-container-${id}`}
- chartId={id}
- datasource={datasource}
- formData={formData}
- headerHeight={this.getHeaderHeight()}
- height={this.getChartHeight()}
- width={width}
- timeout={timeout}
- vizType={slice.viz_type}
- addFilter={this.addFilter}
- getFilters={this.getFilters}
- removeFilter={this.removeFilter}
- annotationData={chart.annotationData}
- chartAlert={chart.chartAlert}
- chartStatus={chart.chartStatus}
- chartUpdateEndTime={chart.chartUpdateEndTime}
- chartUpdateStartTime={chart.chartUpdateStartTime}
- latestQueryFormData={chart.latestQueryFormData}
- lastRendered={chart.lastRendered}
- queryResponse={chart.queryResponse}
- queryRequest={chart.queryRequest}
- triggerQuery={chart.triggerQuery}
- />
+ <div
+ className={cx(
+ 'dashboard-chart',
+ isOverflowable && 'dashboard-chart--overflowable',
+ )}
+ >
+ <ChartContainer
+ containerId={`slice-container-${id}`}
+ chartId={id}
+ datasource={datasource}
+ formData={formData}
+ headerHeight={this.getHeaderHeight()}
+ height={this.getChartHeight()}
+ width={width}
+ timeout={timeout}
+ vizType={slice.viz_type}
+ addFilter={this.addFilter}
+ getFilters={this.getFilters}
+ removeFilter={this.removeFilter}
+ annotationData={chart.annotationData}
+ chartAlert={chart.chartAlert}
+ chartStatus={chart.chartStatus}
+ chartUpdateEndTime={chart.chartUpdateEndTime}
+ chartUpdateStartTime={chart.chartUpdateStartTime}
+ latestQueryFormData={chart.latestQueryFormData}
+ lastRendered={chart.lastRendered}
+ queryResponse={chart.queryResponse}
+ queryRequest={chart.queryRequest}
+ triggerQuery={chart.triggerQuery}
+ />
+ </div>
</div>
);
}
diff --git a/superset/assets/src/dashboard/components/gridComponents/ChartHolder.jsx b/superset/assets/src/dashboard/components/gridComponents/ChartHolder.jsx
index a684230..bc9f430 100644
--- a/superset/assets/src/dashboard/components/gridComponents/ChartHolder.jsx
+++ b/superset/assets/src/dashboard/components/gridComponents/ChartHolder.jsx
@@ -4,7 +4,6 @@ import PropTypes from 'prop-types';
import Chart from '../../containers/Chart';
import DeleteComponentButton from '../DeleteComponentButton';
import DragDroppable from '../dnd/DragDroppable';
-import DragHandle from '../dnd/DragHandle';
import HoverMenu from '../menu/HoverMenu';
import ResizableContainer from '../resizable/ResizableContainer';
import { componentShape } from '../../util/propShapes';
@@ -35,6 +34,7 @@ const propTypes = {
// dnd
deleteComponent: PropTypes.func.isRequired,
+ updateComponents: PropTypes.func.isRequired,
handleComponentDrop: PropTypes.func.isRequired,
};
@@ -49,6 +49,7 @@ class ChartHolder extends React.Component {
this.handleChangeFocus = this.handleChangeFocus.bind(this);
this.handleDeleteComponent = this.handleDeleteComponent.bind(this);
+ this.handleUpdateSliceName = this.handleUpdateSliceName.bind(this);
}
handleChangeFocus(nextFocus) {
@@ -60,6 +61,19 @@ class ChartHolder extends React.Component {
deleteComponent(id, parentId);
}
+ handleUpdateSliceName(nextName) {
+ const { component, updateComponents } = this.props;
+ updateComponents({
+ [component.id]: {
+ ...component,
+ meta: {
+ ...component.meta,
+ chartName: nextName,
+ },
+ },
+ });
+ }
+
render() {
const { isFocused } = this.state;
@@ -119,10 +133,11 @@ class ChartHolder extends React.Component {
id={component.meta.chartId}
width={widthMultiple * columnWidth}
height={component.meta.height * GRID_BASE_UNIT - CHART_MARGIN}
+ sliceName={component.meta.chartName}
+ updateSliceName={this.handleUpdateSliceName}
/>
{editMode && (
<HoverMenu position="top">
- <DragHandle position="top" />
<DeleteComponentButton
onDelete={this.handleDeleteComponent}
/>
diff --git a/superset/assets/src/dashboard/components/gridComponents/Column.jsx b/superset/assets/src/dashboard/components/gridComponents/Column.jsx
index a71d732..7249034 100644
--- a/superset/assets/src/dashboard/components/gridComponents/Column.jsx
+++ b/superset/assets/src/dashboard/components/gridComponents/Column.jsx
@@ -142,6 +142,18 @@ class Column extends React.PureComponent {
]}
editMode={editMode}
>
+ {editMode && (
+ <HoverMenu innerRef={dragSourceRef} position="top">
+ <DragHandle position="top" />
+ <DeleteComponentButton
+ onDelete={this.handleDeleteComponent}
+ />
+ <IconButton
+ onClick={this.handleChangeFocus}
+ className="fa fa-cog"
+ />
+ </HoverMenu>
+ )}
<div
className={cx(
'grid-column',
@@ -149,19 +161,6 @@ class Column extends React.PureComponent {
backgroundStyle.className,
)}
>
- {editMode && (
- <HoverMenu innerRef={dragSourceRef} position="top">
- <DragHandle position="top" />
- <DeleteComponentButton
- onDelete={this.handleDeleteComponent}
- />
- <IconButton
- onClick={this.handleChangeFocus}
- className="fa fa-cog"
- />
- </HoverMenu>
- )}
-
{columnItems.map((componentId, itemIndex) => (
<DashboardComponent
key={componentId}
diff --git a/superset/assets/src/dashboard/components/gridComponents/Row.jsx b/superset/assets/src/dashboard/components/gridComponents/Row.jsx
index 91f200d..3119a08 100644
--- a/superset/assets/src/dashboard/components/gridComponents/Row.jsx
+++ b/superset/assets/src/dashboard/components/gridComponents/Row.jsx
@@ -128,6 +128,16 @@ class Row extends React.PureComponent {
]}
editMode={editMode}
>
+ {editMode && (
+ <HoverMenu innerRef={dragSourceRef} position="left">
+ <DragHandle position="left" />
+ <DeleteComponentButton onDelete={this.handleDeleteComponent} />
+ <IconButton
+ onClick={this.handleChangeFocus}
+ className="fa fa-cog"
+ />
+ </HoverMenu>
+ )}
<div
className={cx(
'grid-row',
@@ -135,19 +145,6 @@ class Row extends React.PureComponent {
backgroundStyle.className,
)}
>
- {editMode && (
- <HoverMenu innerRef={dragSourceRef} position="left">
- <DragHandle position="left" />
- <DeleteComponentButton
- onDelete={this.handleDeleteComponent}
- />
- <IconButton
- onClick={this.handleChangeFocus}
- className="fa fa-cog"
- />
- </HoverMenu>
- )}
-
{rowItems.map((componentId, itemIndex) => (
<DashboardComponent
key={componentId}
diff --git a/superset/assets/src/dashboard/components/gridComponents/Tab.jsx b/superset/assets/src/dashboard/components/gridComponents/Tab.jsx
index d73bc0c..63619c1 100644
--- a/superset/assets/src/dashboard/components/gridComponents/Tab.jsx
+++ b/superset/assets/src/dashboard/components/gridComponents/Tab.jsx
@@ -136,7 +136,7 @@ export default class Tab extends React.PureComponent {
// disable drag drop of top-level Tab's to prevent invalid nesting of a child in
// itself, e.g. if a top-level Tab has a Tabs child, dragging the Tab into the Tabs would
// reusult in circular children
- disableDragDrop={isFocused || depth === DASHBOARD_ROOT_DEPTH + 1}
+ disableDragDrop={depth === DASHBOARD_ROOT_DEPTH + 1}
editMode={editMode}
>
{({ dropIndicatorProps, dragSourceRef }) => (
diff --git a/superset/assets/src/dashboard/components/gridComponents/Tabs.jsx b/superset/assets/src/dashboard/components/gridComponents/Tabs.jsx
index 585041f..813961d 100644
--- a/superset/assets/src/dashboard/components/gridComponents/Tabs.jsx
+++ b/superset/assets/src/dashboard/components/gridComponents/Tabs.jsx
@@ -164,7 +164,11 @@ class Tabs extends React.PureComponent {
id={tabsComponent.id}
activeKey={selectedTabIndex}
onSelect={this.handleClickTab}
- animation={false}
+ // these are important for performant loading of tabs. also, there is a
+ // react-bootstrap bug where mountOnEnter has no effect unless animation=true
+ animation
+ mountOnEnter
+ unmountOnExit={false}
>
{tabIds.map((tabId, tabIndex) => (
// react-bootstrap doesn't render a Tab if we move this to its own Tab.jsx so we
@@ -187,27 +191,21 @@ class Tabs extends React.PureComponent {
/>
}
>
- {/*
- react-bootstrap renders all children with display:none, so we don't
- render potentially-expensive charts (this also enables lazy loading
- their content)
- */}
- {tabIndex === selectedTabIndex &&
- renderTabContent && (
- <DashboardComponent
- id={tabId}
- parentId={tabsComponent.id}
- depth={depth} // see isValidChild.js for why tabs don't increment child depth
- index={tabIndex}
- renderType={RENDER_TAB_CONTENT}
- availableColumnCount={availableColumnCount}
- columnWidth={columnWidth}
- onResizeStart={onResizeStart}
- onResize={onResize}
- onResizeStop={onResizeStop}
- onDropOnTab={this.handleDropOnTab}
- />
- )}
+ {renderTabContent && (
+ <DashboardComponent
+ id={tabId}
+ parentId={tabsComponent.id}
+ depth={depth} // see isValidChild.js for why tabs don't increment child depth
+ index={tabIndex}
+ renderType={RENDER_TAB_CONTENT}
+ availableColumnCount={availableColumnCount}
+ columnWidth={columnWidth}
+ onResizeStart={onResizeStart}
+ onResize={onResize}
+ onResizeStop={onResizeStop}
+ onDropOnTab={this.handleDropOnTab}
+ />
+ )}
</BootstrapTab>
))}
diff --git a/superset/assets/src/dashboard/components/menu/WithPopoverMenu.jsx b/superset/assets/src/dashboard/components/menu/WithPopoverMenu.jsx
index 8a87fca..2a047ac 100644
--- a/superset/assets/src/dashboard/components/menu/WithPopoverMenu.jsx
+++ b/superset/assets/src/dashboard/components/menu/WithPopoverMenu.jsx
@@ -10,6 +10,7 @@ const propTypes = {
isFocused: PropTypes.bool,
shouldFocus: PropTypes.func,
editMode: PropTypes.bool.isRequired,
+ style: PropTypes.object,
};
const defaultProps = {
@@ -20,6 +21,7 @@ const defaultProps = {
menuItems: [],
isFocused: false,
shouldFocus: (event, container) => container.contains(event.target),
+ style: null,
};
class WithPopoverMenu extends React.PureComponent {
@@ -84,7 +86,7 @@ class WithPopoverMenu extends React.PureComponent {
}
render() {
- const { children, menuItems, editMode } = this.props;
+ const { children, menuItems, editMode, style } = this.props;
const { isFocused } = this.state;
return (
@@ -96,6 +98,7 @@ class WithPopoverMenu extends React.PureComponent {
'with-popover-menu',
editMode && isFocused && 'with-popover-menu--focused',
)}
+ style={style}
>
{children}
{editMode &&
diff --git a/superset/assets/src/dashboard/containers/Chart.jsx b/superset/assets/src/dashboard/containers/Chart.jsx
index 470176b..61627d2 100644
--- a/superset/assets/src/dashboard/containers/Chart.jsx
+++ b/superset/assets/src/dashboard/containers/Chart.jsx
@@ -8,7 +8,7 @@ import {
} from '../actions/dashboardState';
import { refreshChart } from '../../chart/chartAction';
import getFormDataWithExtraFilters from '../util/charts/getFormDataWithExtraFilters';
-import { saveSliceName } from '../actions/sliceEntities';
+import { updateComponents } from '../actions/dashboardLayout';
import Chart from '../components/gridComponents/Chart';
function mapStateToProps(
@@ -46,7 +46,7 @@ function mapStateToProps(
function mapDispatchToProps(dispatch) {
return bindActionCreators(
{
- saveSliceName,
+ updateComponents,
toggleExpandSlice,
addFilter,
refreshChart,
diff --git a/superset/assets/src/dashboard/containers/Dashboard.jsx b/superset/assets/src/dashboard/containers/Dashboard.jsx
index 9af0e81..bcf2ace 100644
--- a/superset/assets/src/dashboard/containers/Dashboard.jsx
+++ b/superset/assets/src/dashboard/containers/Dashboard.jsx
@@ -4,7 +4,6 @@ import { connect } from 'react-redux';
import {
addSliceToDashboard,
removeSliceFromDashboard,
- onChange,
} from '../actions/dashboardState';
import { runQuery } from '../../chart/chartAction';
import Dashboard from '../components/Dashboard';
@@ -37,7 +36,6 @@ function mapDispatchToProps(dispatch) {
actions: bindActionCreators(
{
addSliceToDashboard,
- onChange,
removeSliceFromDashboard,
runQuery,
},
diff --git a/superset/assets/src/dashboard/containers/DashboardComponent.jsx b/superset/assets/src/dashboard/containers/DashboardComponent.jsx
index 650313e..29071cb 100644
--- a/superset/assets/src/dashboard/containers/DashboardComponent.jsx
+++ b/superset/assets/src/dashboard/containers/DashboardComponent.jsx
@@ -26,13 +26,7 @@ const propTypes = {
};
function mapStateToProps(
- {
- dashboardLayout: undoableLayout,
- dashboardState,
- sliceEntities,
- charts,
- datasources,
- },
+ { dashboardLayout: undoableLayout, dashboardState },
ownProps,
) {
const dashboardLayout = undoableLayout.present;
diff --git a/superset/assets/src/dashboard/containers/DashboardHeader.jsx b/superset/assets/src/dashboard/containers/DashboardHeader.jsx
index 2b3431a..fe7e7bb 100644
--- a/superset/assets/src/dashboard/containers/DashboardHeader.jsx
+++ b/superset/assets/src/dashboard/containers/DashboardHeader.jsx
@@ -1,8 +1,8 @@
-import { ActionCreators as UndoActionCreators } from 'redux-undo';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import DashboardHeader from '../components/Header';
+
import {
setEditMode,
toggleBuilderPane,
@@ -10,11 +10,21 @@ import {
saveFaveStar,
fetchCharts,
startPeriodicRender,
- updateDashboardTitle,
onChange,
- onSave,
+ saveDashboard,
+ setMaxUndoHistoryExceeded,
+ maxUndoHistoryToast,
} from '../actions/dashboardState';
-import { handleComponentDrop } from '../actions/dashboardLayout';
+
+import {
+ undoLayoutAction,
+ redoLayoutAction,
+ updateDashboardTitle,
+} from '../actions/dashboardLayout';
+
+import { addSuccessToast, addDangerToast } from '../actions/messageToasts';
+
+import { DASHBOARD_HEADER_ID } from '../util/constants';
function mapStateToProps({
dashboardLayout: undoableLayout,
@@ -24,16 +34,19 @@ function mapStateToProps({
}) {
return {
dashboardInfo,
- canUndo: undoableLayout.past.length > 0,
- canRedo: undoableLayout.future.length > 0,
+ undoLength: undoableLayout.past.length,
+ redoLength: undoableLayout.future.length,
layout: undoableLayout.present,
filters: dashboard.filters,
- dashboardTitle: dashboard.title,
+ dashboardTitle: (
+ (undoableLayout.present[DASHBOARD_HEADER_ID] || {}).meta || {}
+ ).text,
expandedSlices: dashboard.expandedSlices,
charts,
userId: dashboardInfo.userId,
isStarred: !!dashboard.isStarred,
hasUnsavedChanges: !!dashboard.hasUnsavedChanges,
+ maxUndoHistoryExceeded: !!dashboard.maxUndoHistoryExceeded,
editMode: !!dashboard.editMode,
showBuilderPane: !!dashboard.showBuilderPane,
};
@@ -42,9 +55,10 @@ function mapStateToProps({
function mapDispatchToProps(dispatch) {
return bindActionCreators(
{
- handleComponentDrop,
- onUndo: UndoActionCreators.undo,
- onRedo: UndoActionCreators.redo,
+ addSuccessToast,
+ addDangerToast,
+ onUndo: undoLayoutAction,
+ onRedo: redoLayoutAction,
setEditMode,
toggleBuilderPane,
fetchFaveStar,
@@ -53,7 +67,9 @@ function mapDispatchToProps(dispatch) {
startPeriodicRender,
updateDashboardTitle,
onChange,
- onSave,
+ onSave: saveDashboard,
+ setMaxUndoHistoryExceeded,
+ maxUndoHistoryToast,
},
dispatch,
);
diff --git a/superset/assets/src/dashboard/containers/SliceAdder.js b/superset/assets/src/dashboard/containers/SliceAdder.js
new file mode 100644
index 0000000..e3d931d
--- /dev/null
+++ b/superset/assets/src/dashboard/containers/SliceAdder.js
@@ -0,0 +1,28 @@
+import { bindActionCreators } from 'redux';
+import { connect } from 'react-redux';
+
+import { fetchAllSlices } from '../actions/sliceEntities';
+import SliceAdder from '../components/SliceAdder';
+
+function mapStateToProps({ sliceEntities, dashboardInfo, dashboardState }) {
+ return {
+ userId: dashboardInfo.userId,
+ selectedSliceIds: dashboardState.sliceIds,
+ slices: sliceEntities.slices,
+ isLoading: sliceEntities.isLoading,
+ errorMessage: sliceEntities.errorMessage,
+ lastUpdated: sliceEntities.lastUpdated,
+ editMode: dashboardState.editMode,
+ };
+}
+
+function mapDispatchToProps(dispatch) {
+ return bindActionCreators(
+ {
+ fetchAllSlices,
+ },
+ dispatch,
+ );
+}
+
+export default connect(mapStateToProps, mapDispatchToProps)(SliceAdder);
diff --git a/superset/assets/src/dashboard/reducers/dashboardState.js b/superset/assets/src/dashboard/reducers/dashboardState.js
index 7b5a17a..2d44399 100644
--- a/superset/assets/src/dashboard/reducers/dashboardState.js
+++ b/superset/assets/src/dashboard/reducers/dashboardState.js
@@ -9,6 +9,7 @@ import {
REMOVE_SLICE,
REMOVE_FILTER,
SET_EDIT_MODE,
+ SET_MAX_UNDO_HISTORY_EXCEEDED,
SET_UNSAVED_CHANGES,
TOGGLE_BUILDER_PANE,
TOGGLE_EXPAND_SLICE,
@@ -55,6 +56,10 @@ export default function dashboardStateReducer(state = {}, action) {
[SET_EDIT_MODE]() {
return { ...state, editMode: action.editMode };
},
+ [SET_MAX_UNDO_HISTORY_EXCEEDED]() {
+ const { maxUndoHistoryExceeded = true } = action.payload;
+ return { ...state, maxUndoHistoryExceeded };
+ },
[TOGGLE_BUILDER_PANE]() {
return { ...state, showBuilderPane: !state.showBuilderPane };
},
@@ -72,7 +77,11 @@ export default function dashboardStateReducer(state = {}, action) {
return { ...state, hasUnsavedChanges: true };
},
[ON_SAVE]() {
- return { ...state, hasUnsavedChanges: false };
+ return {
+ ...state,
+ hasUnsavedChanges: false,
+ maxUndoHistoryExceeded: false,
+ };
},
// filters
diff --git a/superset/assets/src/dashboard/reducers/getInitialState.js b/superset/assets/src/dashboard/reducers/getInitialState.js
index d0b4d7b..ba24b36 100644
--- a/superset/assets/src/dashboard/reducers/getInitialState.js
+++ b/superset/assets/src/dashboard/reducers/getInitialState.js
@@ -7,7 +7,8 @@ import { getParam } from '../../modules/utils';
import { applyDefaultFormData } from '../../explore/stores/store';
import { getColorFromScheme } from '../../modules/colors';
import layoutConverter from '../util/dashboardLayoutConverter';
-import { DASHBOARD_ROOT_ID } from '../util/constants';
+import { DASHBOARD_VERSION_KEY, DASHBOARD_HEADER_ID } from '../util/constants';
+import { DASHBOARD_HEADER_TYPE, CHART_TYPE } from '../util/componentTypes';
export default function(bootstrapData) {
const { user_id, datasources, common } = bootstrapData;
@@ -35,22 +36,39 @@ export default function(bootstrapData) {
}
// dashboard layout
- const positionJson = dashboard.position_json;
- let layout;
- if (!positionJson || !positionJson[DASHBOARD_ROOT_ID]) {
- layout = layoutConverter(dashboard);
- } else {
- layout = positionJson;
- }
+ const { position_json: positionJson } = dashboard;
+
+ const layout =
+ !positionJson || positionJson[DASHBOARD_VERSION_KEY] !== 'v2'
+ ? layoutConverter(dashboard)
+ : positionJson;
+
+ // store the header as a layout component so we can undo/redo changes
+ layout[DASHBOARD_HEADER_ID] = {
+ id: DASHBOARD_HEADER_ID,
+ type: DASHBOARD_HEADER_TYPE,
+ meta: {
+ text: dashboard.dashboard_title,
+ },
+ };
const dashboardLayout = {
past: [],
present: layout,
future: [],
};
+
delete dashboard.position_json;
delete dashboard.css;
+ // creat a lookup to sync layout names with slice names
+ const chartIdToLayoutId = {};
+ Object.values(layout).forEach(layoutComponent => {
+ if (layoutComponent.type === CHART_TYPE) {
+ chartIdToLayoutId[layoutComponent.meta.chartId] = layoutComponent.id;
+ }
+ });
+
const chartQueries = {};
const slices = {};
const sliceIds = new Set();
@@ -76,6 +94,14 @@ export default function(bootstrapData) {
};
sliceIds.add(key);
+
+ // sync layout names with current slice names in case a slice was edited
+ // in explore since the layout was updated. name updates go through layout for undo/redo
+ // functionality and python updates slice names based on layout upon dashboard save
+ const layoutId = chartIdToLayoutId[key];
+ if (layoutId && layout[layoutId]) {
+ layout[layoutId].meta.chartName = slice.slice_name;
+ }
});
return {
@@ -99,7 +125,6 @@ export default function(bootstrapData) {
common,
},
dashboardState: {
- title: dashboard.dashboard_title,
sliceIds,
refresh: false,
filters,
@@ -107,6 +132,7 @@ export default function(bootstrapData) {
editMode: false,
showBuilderPane: false,
hasUnsavedChanges: false,
+ maxUndoHistoryExceeded: false,
},
dashboardLayout,
messageToasts: [],
diff --git a/superset/assets/src/dashboard/reducers/undoableDashboardLayout.js b/superset/assets/src/dashboard/reducers/undoableDashboardLayout.js
index b78c273..45e36ee 100644
--- a/superset/assets/src/dashboard/reducers/undoableDashboardLayout.js
+++ b/superset/assets/src/dashboard/reducers/undoableDashboardLayout.js
@@ -1,4 +1,5 @@
import undoable, { includeAction } from 'redux-undo';
+import { UNDO_LIMIT } from '../util/constants';
import {
UPDATE_COMPONENTS,
DELETE_COMPONENT,
@@ -13,7 +14,9 @@ import {
import dashboardLayout from './dashboardLayout';
export default undoable(dashboardLayout, {
- limit: 15,
+ // +1 because length of history seems max out at limit - 1
+ // +1 again so we can detect if we've exceeded the limit
+ limit: UNDO_LIMIT + 2,
filter: includeAction([
UPDATE_COMPONENTS,
DELETE_COMPONENT,
diff --git a/superset/assets/src/dashboard/stylesheets/builder-sidepane.less b/superset/assets/src/dashboard/stylesheets/builder-sidepane.less
index bdf342b..d45da4f 100644
--- a/superset/assets/src/dashboard/stylesheets/builder-sidepane.less
+++ b/superset/assets/src/dashboard/stylesheets/builder-sidepane.less
@@ -1,53 +1,67 @@
.dashboard-builder-sidepane {
- background: white;
- flex: 0 0 376px;
- border: 1px solid @gray-light;
+ flex: 0 0 @builder-pane-width;
z-index: 10;
position: relative;
+ box-shadow: -4px 0 4px 0 rgba(0, 0, 0, 0.1);
.dashboard-builder-sidepane-header {
font-size: 15px;
font-weight: 700;
+ border-top: 1px solid @gray-light;
border-bottom: 1px solid @gray-light;
- padding: 14px;
+ padding: 16px;
}
.trigger {
- height: 25px;
+ height: 18px;
width: 25px;
- color: @gray;
- position: relative;
+ color: @almost-black;
+ opacity: 1;
+ }
+
+ .viewport {
+ position: absolute;
+ transform: none !important;
+ background: white;
+ overflow: hidden;
+ width: @builder-pane-width;
+ height: 100%;
+ }
+
+ .slider-container {
+ position: absolute;
+ background: white;
+ width: @builder-pane-width * 2;
+ height: 100%;
+ display: flex;
+ transition: all 0.5s ease;
+
+ &.slide-in {
+ left: -@builder-pane-width;
+ }
- &.close {
- top: 3px;
+ &.slide-out {
+ left: 0;
}
- &.open {
- position: absolute;
- right: 14px;
+ .slide-content {
+ width: @builder-pane-width;
}
}
+ .component-layer .new-component.static,
+ .slices-layer .dashboard-builder-sidepane-header {
+ cursor: pointer;
+ }
+
.component-layer {
.new-component.static {
cursor: pointer;
}
}
- .slices-layer {
- position: absolute;
- width: 2px;
- top: 51px;
- right: 0;
- background: white;
- transition-property: width;
- transition-duration: 1s;
- transition-timing-function: ease;
- overflow: hidden;
-
- &.show {
- width: 374px;
- }
+ .new-component-label {
+ flex-grow: 1;
}
.chart-card-container {
@@ -89,21 +103,27 @@
display: flex;
padding: 16px;
+ /* the input is wrapped in a div */
+ .search-input {
+ flex-grow: 1;
+ margin-left: 16px;
+ }
+
.dropdown.btn-group button,
input {
font-size: 14px;
line-height: 16px;
padding: 7px 12px;
height: 32px;
+ border: 1px solid @gray-light;
}
input {
- margin-left: 16px;
- width: 169px;
- border: 1px solid @gray;
+ width: 100%;
&:focus {
outline: none;
+ border-color: @gray;
}
}
}
diff --git a/superset/assets/src/dashboard/stylesheets/components/chart.less b/superset/assets/src/dashboard/stylesheets/components/chart.less
index dc366a1..73914fb 100644
--- a/superset/assets/src/dashboard/stylesheets/components/chart.less
+++ b/superset/assets/src/dashboard/stylesheets/components/chart.less
@@ -62,8 +62,3 @@
/* disable chart interactions in edit mode */
pointer-events: none;
}
-
-.dashboard-chart .chart-header {
- font-size: 16px;
- font-weight: bold;
-}
diff --git a/superset/assets/src/dashboard/stylesheets/components/column.less b/superset/assets/src/dashboard/stylesheets/components/column.less
index 5fcb442..2f26d95 100644
--- a/superset/assets/src/dashboard/stylesheets/components/column.less
+++ b/superset/assets/src/dashboard/stylesheets/components/column.less
@@ -23,15 +23,11 @@
.dashboard--editing
.resizable-container.resizable-container--resizing:hover
> .grid-column:after,
-.dashboard--editing .grid-column:hover:after {
+.dashboard--editing .hover-menu:hover + .grid-column:after {
border: 1px dashed @gray-light;
box-shadow: 0 4px 4px 0 rgba(0, 0, 0, 0.1);
}
-.grid-column > .hover-menu--top {
- top: -20px;
-}
-
.grid-column--empty {
min-height: 72px;
}
diff --git a/superset/assets/src/dashboard/stylesheets/components/header.less b/superset/assets/src/dashboard/stylesheets/components/header.less
index 8b93164..9403103 100644
--- a/superset/assets/src/dashboard/stylesheets/components/header.less
+++ b/superset/assets/src/dashboard/stylesheets/components/header.less
@@ -1,6 +1,6 @@
.dashboard-component-header {
width: 100%;
- line-height: 1em;
+ line-height: 1.1;
font-weight: 700;
padding: 16px 0;
color: @almost-black;
@@ -15,7 +15,13 @@
margin-right: 8px;
}
-.dragdroppable-row .dashboard-component-header {
+.dashboard-header .undo-action,
+.dashboard-header .redo-action {
+ line-height: 18px;
+ font-size: 12px;
+}
+
+.dashboard--editing .dragdroppable-row .dashboard-component-header {
cursor: move;
}
diff --git a/superset/assets/src/dashboard/stylesheets/components/row.less b/superset/assets/src/dashboard/stylesheets/components/row.less
index 7df5675..382417e 100644
--- a/superset/assets/src/dashboard/stylesheets/components/row.less
+++ b/superset/assets/src/dashboard/stylesheets/components/row.less
@@ -14,7 +14,8 @@
}
/* hover indicator */
-.dashboard--editing .grid-row:after {
+.dashboard--editing .grid-row:after,
+.dashboard--editing .dashboard-component-tabs > .hover-menu:hover + div:after {
border: 1px dashed transparent;
content: '';
position: absolute;
@@ -29,7 +30,8 @@
.dashboard--editing
.resizable-container.resizable-container--resizing:hover
> .grid-row:after,
-.dashboard--editing .grid-row:hover:after {
+.dashboard--editing .hover-menu:hover + .grid-row:after,
+.dashboard--editing .dashboard-component-tabs > .hover-menu:hover + div:after {
border: 1px dashed @gray-light;
box-shadow: 0 4px 4px 0 rgba(0, 0, 0, 0.1);
}
diff --git a/superset/assets/src/dashboard/stylesheets/components/tabs.less b/superset/assets/src/dashboard/stylesheets/components/tabs.less
index f67c151..02039b4 100644
--- a/superset/assets/src/dashboard/stylesheets/components/tabs.less
+++ b/superset/assets/src/dashboard/stylesheets/components/tabs.less
@@ -30,12 +30,12 @@
}
.dashboard-component-tabs .nav-tabs > li.active > a:after {
- content: "";
+ content: '';
position: absolute;
height: 3px;
width: 100%;
bottom: 0;
- background: linear-gradient(to right, #E32464, #2C2261);
+ background: linear-gradient(to right, #e32464, #2c2261);
}
.dashboard-component-tabs .nav-tabs > li > a:hover {
@@ -53,9 +53,10 @@
cursor: move;
}
+/* These expande the outline border + drop indicator for tabs */
.dashboard-component-tabs .nav-tabs > li .drop-indicator {
top: -12px !important;
- height: ~"calc(100% + 24px)" !important;
+ height: ~'calc(100% + 24px)' !important;
}
.dashboard-component-tabs .nav-tabs > li .drop-indicator--left {
@@ -69,7 +70,7 @@
.dashboard-component-tabs .nav-tabs > li .drop-indicator--top,
.dashboard-component-tabs .nav-tabs > li .drop-indicator--bottom {
left: -12px !important;
- width: ~"calc(100% + 24px)" !important; /* escape for .less */
+ width: ~'calc(100% + 24px)' !important; /* escape for .less */
opacity: 0.4;
}
@@ -78,3 +79,7 @@
font-size: 14px;
margin-top: 3px;
}
+
+.dashboard-component-tabs li .editable-title input[type='button'] {
+ cursor: pointer;
+}
diff --git a/superset/assets/src/dashboard/stylesheets/dashboard.less b/superset/assets/src/dashboard/stylesheets/dashboard.less
index 03c804b..8d8c8be 100644
--- a/superset/assets/src/dashboard/stylesheets/dashboard.less
+++ b/superset/assets/src/dashboard/stylesheets/dashboard.less
@@ -1,52 +1,94 @@
-// @import './less/cosmo/variables.less';
-
.dashboard .chart-header {
position: relative;
+ font-size: 16px;
+ font-weight: bold;
.dropdown.btn-group {
position: absolute;
right: 0;
}
+ .dropdown-toggle.btn.btn-default {
+ background: none;
+ border: none;
+ box-shadow: none;
+ }
+
.dropdown-menu.dropdown-menu-right {
- right: 7px;
- top: -3px;
+ top: 20px;
}
-}
-.slice-header-controls-trigger {
- border: 0;
- padding: 0 0 0 20px;
- background: none;
- outline: none;
- box-shadow: none;
- color: #263238;
-
- &.is-cached {
- color: red;
+ .divider {
+ margin: 5px 0;
}
- &:hover,
- &:focus {
- background: none;
- cursor: pointer;
+ .fa-circle {
+ position: absolute;
+ left: 7px;
+ top: 18px;
+ font-size: 4px;
+ color: @pink;
}
- .controls-container.dropdown-menu {
- top: 0;
- left: unset;
- right: 10px;
+ .refresh-tooltip {
+ display: block;
+ height: 16px;
+ margin: 3px 0;
+ color: @gray;
+ }
+}
- &.is-open {
- display: block;
- }
+.dashboard .chart-header,
+.dashboard .dashboard-header {
+ .dropdown-menu {
+ padding: 9px 0;
+ }
- & li {
- white-space: nowrap;
+ .dropdown-menu li a {
+ padding: 3px 16px;
+ color: @almost-black;
+ line-height: 16px;
+ font-size: 14px;
+ letter-spacing: 0.4px;
+
+ &:hover,
+ &:focus {
+ background: @menu-hover;
+ color: @almost-black;
}
}
}
+.slice-header-controls-trigger {
+ padding: 0 16px;
+ position: absolute;
+ top: 0;
+ right: -22px;
+
+ &:hover {
+ cursor: pointer;
+ }
+}
+
+.dot {
+ height: 4px;
+ width: 4px;
+ background-color: @gray;
+ border-radius: 50%;
+ margin: 2px 0;
+ display: inline-block;
+
+ .is-cached & {
+ background-color: @pink;
+ margin-right: 6px;
+ }
+
+ .vertical-dots-container & {
+ display: block;
+ }
+}
+
+
.modal img.loading {
width: 50px;
margin: 0;
diff --git a/superset/assets/src/dashboard/stylesheets/dnd.less b/superset/assets/src/dashboard/stylesheets/dnd.less
index 835b62b..0a10c61 100644
--- a/superset/assets/src/dashboard/stylesheets/dnd.less
+++ b/superset/assets/src/dashboard/stylesheets/dnd.less
@@ -65,11 +65,11 @@
float: left;
height: 2px;
margin: 1px;
- width: 2px
+ width: 2px;
}
.drag-handle-dot:after {
- content: "";
+ content: '';
background: #aaa;
float: left;
height: 2px;
diff --git a/superset/assets/src/dashboard/stylesheets/grid.less b/superset/assets/src/dashboard/stylesheets/grid.less
index a12ac97..9d09ac7 100644
--- a/superset/assets/src/dashboard/stylesheets/grid.less
+++ b/superset/assets/src/dashboard/stylesheets/grid.less
@@ -20,11 +20,16 @@
}
/* gutters between rows */
-.grid-content > div:not(:only-child):not(:last-child):not(.empty-grid-droptarget) {
+.grid-content
+ > div:not(:only-child):not(:last-child):not(.empty-grid-droptarget--bottom):not(.empty-grid-droptarget--top) {
margin-bottom: 16px;
}
-.empty-grid-droptarget {
+.grid-content > .empty-grid-droptarget--top {
+ height: 24px;
+ margin-top: -24px;
+}
+.empty-grid-droptarget--bottom {
width: 100%;
height: 100%;
}
diff --git a/superset/assets/src/dashboard/stylesheets/hover-menu.less b/superset/assets/src/dashboard/stylesheets/hover-menu.less
index 77edb06..4f62401 100644
--- a/superset/assets/src/dashboard/stylesheets/hover-menu.less
+++ b/superset/assets/src/dashboard/stylesheets/hover-menu.less
@@ -1,14 +1,16 @@
.hover-menu {
opacity: 0;
position: absolute;
- z-index: 2;
+ z-index: 10;
+ font-size: 14px;
}
.hover-menu--left {
width: 24px;
- height: 100%;
- top: 0;
+ top: 50%;
+ transform: translate(0, -50%);
left: -24px;
+ padding: 8px 0;
display: flex;
flex-direction: column;
justify-content: center;
@@ -19,21 +21,52 @@
margin-bottom: 12px;
}
-.dragdroppable-row .dragdroppable-row .hover-menu--left {
- left: 1px;
-}
-
.hover-menu--top {
- width: 100%;
height: 24px;
- top: 0;
- left: 0;
+ top: -24px;
+ left: 50%;
+ transform: translate(-50%);
+ padding: 0 8px;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
}
+/* Special cases */
+
+/* A row within a column has inset hover menu */
+.dragdroppable-column .dragdroppable-row .hover-menu--left {
+ left: -12px;
+ background: white;
+ border: 1px solid @gray-light;
+}
+
+/* A column within a column or tabs has inset hover menu */
+.dragdroppable-column .dragdroppable-column .hover-menu--top,
+.dashboard-component-tabs .dragdroppable-column .hover-menu--top {
+ top: -12px;
+ background: white;
+ border: 1px solid @gray-light;
+}
+
+/* move Tabs hover menu to top near actual Tabs */
+.dashboard-component-tabs > .hover-menu--left {
+ top: 0;
+ transform: unset;
+ background: transparent;
+}
+
+/* push Chart actions to upper right */
+.dragdroppable-column .dashboard-component-chart-holder > .hover-menu--top {
+ right: 8px;
+ top: 8px;
+ background: transparent;
+ border: none;
+ transform: unset;
+ left: unset;
+}
+
.hover-menu--top > :nth-child(n):not(:only-child):not(:last-child) {
margin-right: 12px;
}
diff --git a/superset/assets/src/dashboard/stylesheets/popover-menu.less b/superset/assets/src/dashboard/stylesheets/popover-menu.less
index 848949b..d69006c 100644
--- a/superset/assets/src/dashboard/stylesheets/popover-menu.less
+++ b/superset/assets/src/dashboard/stylesheets/popover-menu.less
@@ -3,13 +3,14 @@
outline: none;
}
-.grid-row.grid-row--empty .with-popover-menu { /* drop indicator doesn't show up without this */
+.grid-row.grid-row--empty .with-popover-menu {
+ /* drop indicator doesn't show up without this */
width: 100%;
height: 100%;
}
.with-popover-menu--focused:after {
- content: "";
+ content: '';
position: absolute;
top: 1;
left: -1;
@@ -34,15 +35,15 @@
box-shadow: 0 1px 2px 1px rgba(0, 0, 0, 0.2);
font-size: 14px;
cursor: default;
- z-index: 10;
+ z-index: 1000;
}
/* the focus menu doesn't account for parent padding */
.dashboard-component-tabs li .with-popover-menu--focused:after {
top: -12px;
- left: -2px;
- width: ~"calc(100% + 4px)"; /* escape for .less */
- height: ~"calc(100% + 28px)";
+ left: -8px;
+ width: ~'calc(100% + 16px)'; /* escape for .less */
+ height: ~'calc(100% + 28px)';
}
.dashboard-component-tabs li .popover-menu {
@@ -57,7 +58,7 @@
/* vertical spacer after each menu item */
.popover-menu .menu-item:not(:only-child):not(:last-child):after {
- content: "";
+ content: '';
width: 1;
height: 100%;
background: @gray-light;
@@ -86,12 +87,12 @@
background: @gray-light;
}
-.popover-dropdown .caret { /* without this the caret doesn't take up full width / is clipped */
+.popover-dropdown .caret {
+ /* without this the caret doesn't take up full width / is clipped */
width: auto;
border-top-color: transparent;
}
-
.hover-dropdown li.dropdown-item.active a,
.popover-menu li.dropdown-item.active a {
background: white;
@@ -105,7 +106,7 @@
}
.background-style-option:before {
- content: "";
+ content: '';
width: 1em;
height: 1em;
margin-right: 8px;
@@ -124,7 +125,10 @@
}
.background-style-option.background--transparent:before {
- background-image: linear-gradient(45deg, @gray 25%, transparent 25%), linear-gradient(-45deg, @gray 25%, transparent 25%), linear-gradient(45deg, transparent 75%, @gray 75%), linear-gradient(-45deg, transparent 75%, @gray 75%);
+ background-image: linear-gradient(45deg, @gray 25%, transparent 25%),
+ linear-gradient(-45deg, @gray 25%, transparent 25%),
+ linear-gradient(45deg, transparent 75%, @gray 75%),
+ linear-gradient(-45deg, transparent 75%, @gray 75%);
background-size: 8px 8px;
- background-position: 0 0, 0 4px, 4px -4px, -4px 0px
+ background-position: 0 0, 0 4px, 4px -4px, -4px 0px;
}
diff --git a/superset/assets/src/dashboard/stylesheets/variables.less b/superset/assets/src/dashboard/stylesheets/variables.less
index 254af23..8f53f99 100644
--- a/superset/assets/src/dashboard/stylesheets/variables.less
+++ b/superset/assets/src/dashboard/stylesheets/variables.less
@@ -5,6 +5,10 @@
@gray: #879399;
@gray-light: #CFD8DC;
@gray-bg: #f5f5f5;
+@menu-hover: #F2F3F5;
+
+/* builder component pane */
+@builder-pane-width: 374px;
/* toasts */
@pink: #E32364;
diff --git a/superset/assets/src/dashboard/util/constants.js b/superset/assets/src/dashboard/util/constants.js
index f35614c..d682687 100644
--- a/superset/assets/src/dashboard/util/constants.js
+++ b/superset/assets/src/dashboard/util/constants.js
@@ -2,6 +2,7 @@
export const DASHBOARD_GRID_ID = 'DASHBOARD_GRID_ID';
export const DASHBOARD_HEADER_ID = 'DASHBOARD_HEADER_ID';
export const DASHBOARD_ROOT_ID = 'DASHBOARD_ROOT_ID';
+export const DASHBOARD_VERSION_KEY = 'DASHBOARD_VERSION_KEY';
export const NEW_COMPONENTS_SOURCE_ID = 'NEW_COMPONENTS_SOURCE_ID';
export const NEW_CHART_ID = 'NEW_CHART_ID';
@@ -37,3 +38,6 @@ export const INFO_TOAST = 'INFO_TOAST';
export const SUCCESS_TOAST = 'SUCCESS_TOAST';
export const WARNING_TOAST = 'WARNING_TOAST';
export const DANGER_TOAST = 'DANGER_TOAST';
+
+// undo-redo
+export const UNDO_LIMIT = 50;
diff --git a/superset/assets/src/dashboard/util/dashboardLayoutConverter.js b/superset/assets/src/dashboard/util/dashboardLayoutConverter.js
index f04b50e..f3f6061 100644
--- a/superset/assets/src/dashboard/util/dashboardLayoutConverter.js
+++ b/superset/assets/src/dashboard/util/dashboardLayoutConverter.js
@@ -5,14 +5,14 @@ import {
ROW_TYPE,
COLUMN_TYPE,
CHART_TYPE,
- DASHBOARD_HEADER_TYPE,
DASHBOARD_ROOT_TYPE,
DASHBOARD_GRID_TYPE,
} from './componentTypes';
+
import {
DASHBOARD_GRID_ID,
- DASHBOARD_HEADER_ID,
DASHBOARD_ROOT_ID,
+ DASHBOARD_VERSION_KEY,
} from './constants';
const MAX_RECURSIVE_LEVEL = 6;
@@ -55,7 +55,6 @@ function getBoundary(positions) {
function getRowContainer() {
return {
- version: 'v2',
type: ROW_TYPE,
id: `DASHBOARD_ROW_TYPE-${generateId()}`,
children: [],
@@ -67,7 +66,6 @@ function getRowContainer() {
function getColContainer() {
return {
- version: 'v2',
type: COLUMN_TYPE,
id: `DASHBOARD_COLUMN_TYPE-${generateId()}`,
children: [],
@@ -78,24 +76,19 @@ function getColContainer() {
}
function getChartHolder(item) {
- const { row, col, size_x, size_y, slice_id } = item;
- const converted = {
- row: Math.round(row / GRID_RATIO),
- col: Math.floor((col - 1) / GRID_RATIO) + 1,
- size_x: Math.max(1, Math.floor(size_x / GRID_RATIO)),
- size_y: Math.max(1, Math.round(size_y / GRID_RATIO)),
- slice_id,
- };
+ const { size_x, size_y, slice_id } = item;
+
+ const width = Math.max(1, Math.floor(size_x / GRID_RATIO));
+ const height = Math.max(1, Math.round(size_y / GRID_RATIO));
return {
- version: 'v2',
type: CHART_TYPE,
id: `DASHBOARD_CHART_TYPE-${generateId()}`,
children: [],
meta: {
- width: converted.size_x,
- height: Math.round(converted.size_y * 100 / ROW_HEIGHT),
- chartId: slice_id,
+ width,
+ height: Math.round(height * 100 / ROW_HEIGHT),
+ chartId: parseInt(slice_id, 10),
},
};
}
@@ -111,21 +104,6 @@ function getChildrenSum(items, attr, layout) {
);
}
-// function getChildrenMax(items, attr, layout) {
-// return Math.max.apply(null, items.map((childId) => {
-// const child = layout[childId];
-// if (child.type === ROW_TYPE && attr === 'width') {
-// // rows don't have widths themselves
-// return getChildrenSum(child.children, attr, layout);
-// } else if (child.type === COLUMN_TYPE && attr === 'height') {
-// // columns don't have heights themselves
-// return getChildrenSum(child.children, attr, layout);
-// }
-//
-// return child.meta[attr];
-// }));
-// }
-
function sortByRowId(item1, item2) {
return item1.row - item2.row;
}
@@ -289,10 +267,10 @@ export default function(dashboard) {
// position data clean up. some dashboard didn't have position_json
let { position_json } = dashboard;
- const posDict = {};
+ const positionDict = {};
if (Array.isArray(position_json)) {
position_json.forEach(position => {
- posDict[position.slice_id] = position;
+ positionDict[position.slice_id] = position;
});
} else {
position_json = [];
@@ -303,25 +281,25 @@ export default function(dashboard) {
Math.max.apply(null, position_json.map(pos => pos.row + pos.size_y)),
);
let newSliceCounter = 0;
- dashboard.slices.forEach(slice => {
- const sliceId = slice.slice_id;
- let pos = posDict[sliceId];
- if (!pos) {
+ dashboard.slices.forEach(({ slice_id }) => {
+ let position = positionDict[slice_id];
+ if (!position) {
// append new slices to dashboard bottom, 3 slices per row
- pos = {
+ position = {
col: (newSliceCounter % 3) * 16 + 1,
row: lastRowId + Math.floor(newSliceCounter / 3) * 16,
size_x: 16,
size_y: 16,
- slice_id: String(sliceId),
+ slice_id,
};
newSliceCounter += 1;
}
- positions.push(pos);
+ positions.push(position);
});
const root = {
+ [DASHBOARD_VERSION_KEY]: 'v2',
[DASHBOARD_ROOT_ID]: {
type: DASHBOARD_ROOT_TYPE,
id: DASHBOARD_ROOT_ID,
@@ -332,11 +310,8 @@ export default function(dashboard) {
id: DASHBOARD_GRID_ID,
children: [],
},
- [DASHBOARD_HEADER_ID]: {
- type: DASHBOARD_HEADER_TYPE,
- id: DASHBOARD_HEADER_ID,
- },
};
+
doConvert(positions, 0, root[DASHBOARD_GRID_ID], root);
// remove row's width/height and col's height
diff --git a/superset/assets/src/dashboard/util/isValidChild.js b/superset/assets/src/dashboard/util/isValidChild.js
index d789f45..a885c31 100644
--- a/superset/assets/src/dashboard/util/isValidChild.js
+++ b/superset/assets/src/dashboard/util/isValidChild.js
@@ -33,6 +33,7 @@ const depthOne = rootDepth + 1;
const depthTwo = rootDepth + 2;
const depthThree = rootDepth + 3;
const depthFour = rootDepth + 4;
+const depthFive = rootDepth + 5;
// when moving components around the depth of child is irrelevant, note these are parent depths
const parentMaxDepthLookup = {
@@ -53,7 +54,7 @@ const parentMaxDepthLookup = {
[ROW_TYPE]: {
[CHART_TYPE]: depthFour,
[MARKDOWN_TYPE]: depthFour,
- [COLUMN_TYPE]: depthTwo,
+ [COLUMN_TYPE]: depthFour,
},
[TABS_TYPE]: {
@@ -70,9 +71,9 @@ const parentMaxDepthLookup = {
},
[COLUMN_TYPE]: {
- [CHART_TYPE]: depthThree,
- [HEADER_TYPE]: depthThree,
- [MARKDOWN_TYPE]: depthThree,
+ [CHART_TYPE]: depthFive,
+ [HEADER_TYPE]: depthFive,
+ [MARKDOWN_TYPE]: depthFive,
[ROW_TYPE]: depthThree,
},
diff --git a/superset/assets/src/dashboard/util/newComponentFactory.js b/superset/assets/src/dashboard/util/newComponentFactory.js
index 4e2de37..8d259af 100644
--- a/superset/assets/src/dashboard/util/newComponentFactory.js
+++ b/superset/assets/src/dashboard/util/newComponentFactory.js
@@ -34,7 +34,6 @@ function uuid(type) {
export default function entityFactory(type, meta) {
return {
- version: 'v0',
type,
id: uuid(type),
children: [],
diff --git a/superset/assets/src/dashboard/util/propShapes.jsx b/superset/assets/src/dashboard/util/propShapes.jsx
index 73a10b0..c8e1981 100644
--- a/superset/assets/src/dashboard/util/propShapes.jsx
+++ b/superset/assets/src/dashboard/util/propShapes.jsx
@@ -66,7 +66,6 @@ export const slicePropShape = PropTypes.shape({
});
export const dashboardStatePropShape = PropTypes.shape({
- title: PropTypes.string.isRequired,
sliceIds: PropTypes.object.isRequired,
refresh: PropTypes.bool.isRequired,
filters: PropTypes.object,
diff --git a/superset/assets/src/theme.js b/superset/assets/src/theme.js
index 68a7a8a..34fc0c0 100644
--- a/superset/assets/src/theme.js
+++ b/superset/assets/src/theme.js
@@ -1,3 +1,2 @@
-import '../stylesheets/less/index.less';
import '../stylesheets/react-select/select.less';
import '../stylesheets/superset.less';
diff --git a/superset/assets/src/visualizations/nvd3_vis.js b/superset/assets/src/visualizations/nvd3_vis.js
index bf87287..1621458 100644
--- a/superset/assets/src/visualizations/nvd3_vis.js
+++ b/superset/assets/src/visualizations/nvd3_vis.js
@@ -458,6 +458,8 @@ export default function nvd3Vis(slice, payload) {
customizeToolTip(chart, xAxisFormatter, [yAxisFormatter1, yAxisFormatter2]);
chart.showLegend(width > BREAKPOINTS.small);
}
+ // This is needed for correct chart dimensions if a chart is rendered in a hidden container
+ chart.width(width);
chart.height(height);
slice.container.css('height', height + 'px');
diff --git a/superset/assets/stylesheets/superset.less b/superset/assets/stylesheets/superset.less
index d756551..0e8ffad 100644
--- a/superset/assets/stylesheets/superset.less
+++ b/superset/assets/stylesheets/superset.less
@@ -114,7 +114,6 @@ span.title-block {
}
.nvtooltip {
- //position: relative !important;
z-index: 888;
transition: opacity 0ms linear;
-moz-transition: opacity 0ms linear;
@@ -238,13 +237,14 @@ table.table-no-hover tr:hover {
line-height: inherit;
white-space: normal;
text-align: left;
+ cursor: initial;
}
-.editable-title.editable-title--editable {
+.editable-title.editable-title--editable input[type="button"] {
cursor: pointer;
}
-.editable-title.editable-title--editing {
+.editable-title.editable-title--editing input[type="button"] {
cursor: text;
}
diff --git a/superset/views/core.py b/superset/views/core.py
index acedd77..fde5be7 100755
--- a/superset/views/core.py
+++ b/superset/views/core.py
@@ -1554,10 +1554,16 @@ class Superset(BaseSupersetView):
session.add(new_slice)
session.flush()
new_slice.dashboards.append(dash)
- old_to_new_sliceids['{}'.format(slc.id)] =\
- '{}'.format(new_slice.id)
- for d in data['positions']:
- d['slice_id'] = old_to_new_sliceids[d['slice_id']]
+ old_to_new_sliceids[slc.id] = new_slice.id
+
+ # update chartId of layout entities
+ for value in data['positions'].values():
+ if isinstance(value, dict) and value.get('meta') \
+ and value.get('meta').get('chartId'):
+
+ old_id = value.get('meta').get('chartId')
+ new_id = old_to_new_sliceids[old_id]
+ value['meta']['chartId'] = new_id
else:
dash.slices = original_dash.slices
dash.params = original_dash.params
@@ -1580,6 +1586,7 @@ class Superset(BaseSupersetView):
.filter_by(id=dashboard_id).first())
check_ownership(dash, raise_if_false=True)
data = json.loads(request.form.get('data'))
+ original_slice_names = {(slc.id): slc.slice_name for slc in dash.slices}
self._set_dash_metadata(dash, data)
session.merge(dash)
session.commit()
@@ -1591,15 +1598,30 @@ class Superset(BaseSupersetView):
positions = data['positions']
# find slices in the position data
slice_ids = []
+ slice_id_to_name = {}
for value in positions.values():
- if value.get('meta') and value.get('meta').get('chartId'):
- slice_ids.append(int(value.get('meta').get('chartId')))
+ if isinstance(value, dict) and value.get('meta') \
+ and value.get('meta').get('chartId'):
+
+ slice_id = value.get('meta').get('chartId')
+ slice_ids.append(slice_id)
+ slice_id_to_name[slice_id] = value.get('meta').get('chartName')
+
session = db.session()
Slice = models.Slice # noqa
current_slices = session.query(Slice).filter(
Slice.id.in_(slice_ids)).all()
dashboard.slices = current_slices
+
+ # update slice names. this assumes user has permissions to update the slice
+ for slc in dashboard.slices:
+ new_name = slice_id_to_name[slc.id]
+ if slc.slice_name != new_name:
+ slc.slice_name = new_name
+ session.merge(slc)
+ session.flush()
+
dashboard.position_json = json.dumps(positions, indent=4, sort_keys=True)
md = dashboard.params_dict
dashboard.css = data.get('css')
--
To stop receiving notification emails like this one, please contact
ccwilliams@apache.org.