You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by cc...@apache.org on 2018/06/22 00:54:18 UTC
[incubator-superset] 03/26: [dashboard-builder] add top-level tabs
+ undo-redo (#4626)
This is an automated email from the ASF dual-hosted git repository.
ccwilliams pushed a commit to branch dashboard-builder
in repository https://gitbox.apache.org/repos/asf/incubator-superset.git
commit abc3ec076af5fcd61e5544d743288f8b326e851f
Author: Chris Williams <wi...@users.noreply.github.com>
AuthorDate: Fri Mar 23 10:53:48 2018 -0700
[dashboard-builder] add top-level tabs + undo-redo (#4626)
* [top-level-tabs] initial working version of top-level tabs
* [top-level-tabs] simplify redux and disable ability to displace top-level tabs with other tabs
* [top-level-tabs] improve tab drag and drop css
* [undo-redo] add redux undo redo
* [dnd] clean up dropResult shape, add new component source id + type, use css for drop indicator instead of styles and fix tab indicators.
* [top-level-tabs] add 'Collapse tab content' to delete tabs button
* [dnd] add depth validation to drag and drop logic
* [dashboard-builder] add resize action, enforce minimum width of columns, column children inherit column size when necessary, meta.rowStyle => meta.background, add background to columns
* [dashboard-builder] make sure getChildWidth returns a number
---
.../javascripts/dashboard/v2/actions/index.js | 113 +++++++++++++--
.../dashboard/v2/components/Dashboard.jsx | 21 +--
.../dashboard/v2/components/DashboardBuilder.jsx | 97 ++++++++++++-
.../dashboard/v2/components/DashboardGrid.jsx | 64 +++-----
.../dashboard/v2/components/DashboardHeader.jsx | 71 +++++++--
.../dashboard/v2/components/IconButton.jsx | 12 +-
.../dashboard/v2/components/dnd/DragDroppable.jsx | 41 ++++--
.../v2/components/dnd/dragDroppableConfig.js | 11 +-
.../dashboard/v2/components/dnd/handleDrop.js | 20 ++-
.../dashboard/v2/components/dnd/handleHover.js | 18 +--
.../v2/components/gridComponents/Chart.jsx | 5 +-
.../v2/components/gridComponents/Column.jsx | 132 ++++++++++++-----
.../v2/components/gridComponents/Divider.jsx | 3 +
.../v2/components/gridComponents/Header.jsx | 23 +--
.../dashboard/v2/components/gridComponents/Row.jsx | 27 ++--
.../v2/components/gridComponents/Spacer.jsx | 9 +-
.../dashboard/v2/components/gridComponents/Tab.jsx | 31 ++--
.../v2/components/gridComponents/Tabs.jsx | 61 +++++---
.../gridComponents/new/DraggableNewComponent.jsx | 5 +-
...yleDropdown.jsx => BackgroundStyleDropdown.jsx} | 12 +-
.../v2/components/menu/WithPopoverMenu.jsx | 10 +-
.../v2/components/resizable/ResizableContainer.jsx | 41 ++++--
.../{DashboardGrid.jsx => DashboardBuilder.jsx} | 12 +-
.../dashboard/v2/containers/DashboardComponent.jsx | 33 +++--
.../dashboard/v2/containers/DashboardGrid.jsx | 12 +-
.../dashboard/v2/containers/DashboardHeader.jsx | 31 ++++
.../dashboard/v2/fixtures/emptyDashboardLayout.js | 36 +++++
.../dashboard/v2/fixtures/testLayout.js | 161 ---------------------
.../javascripts/dashboard/v2/reducers/dashboard.js | 146 +++++++++++++++++--
.../javascripts/dashboard/v2/reducers/index.js | 9 +-
.../dashboard/v2/stylesheets/builder.less | 64 ++++++++
.../dashboard/v2/stylesheets/buttons.less | 8 +-
.../v2/stylesheets/components/DashboardBuilder.jsx | 127 ++++++++++++++++
.../v2/stylesheets/components/column.less | 10 +-
.../v2/stylesheets/components/new-component.less | 1 +
.../dashboard/v2/stylesheets/components/row.less | 6 +-
.../dashboard/v2/stylesheets/components/tabs.less | 39 +++--
.../javascripts/dashboard/v2/stylesheets/dnd.less | 54 ++++---
.../javascripts/dashboard/v2/stylesheets/grid.less | 43 +++++-
.../dashboard/v2/stylesheets/hover-menu.less | 14 +-
.../dashboard/v2/stylesheets/index.less | 1 +
.../dashboard/v2/stylesheets/popover-menu.less | 24 ++-
.../dashboard/v2/stylesheets/resizable.less | 12 +-
.../dashboard/v2/util/backgroundStyleOptions.js | 7 +
.../dashboard/v2/util/componentTypes.js | 10 +-
.../javascripts/dashboard/v2/util/constants.js | 11 +-
.../dashboard/v2/util/countChildRowsAndColumns.js | 14 --
.../javascripts/dashboard/v2/util/dnd-reorder.js | 18 +--
.../javascripts/dashboard/v2/util/findParentId.js | 15 ++
.../javascripts/dashboard/v2/util/getChildWidth.js | 16 ++
.../dashboard/v2/util/getDropPosition.js | 16 +-
.../javascripts/dashboard/v2/util/isValidChild.js | 96 +++++++-----
.../dashboard/v2/util/newComponentFactory.js | 12 +-
.../dashboard/v2/util/newEntitiesFromDrop.js | 20 +--
.../javascripts/dashboard/v2/util/propShapes.jsx | 4 +-
.../dashboard/v2/util/resizableConfig.js | 7 +-
.../dashboard/v2/util/rowStyleOptions.js | 7 -
.../dashboard/v2/util/shouldWrapChildInRow.js | 4 +-
superset/assets/package.json | 1 +
superset/assets/src/components/EditableTitle.jsx | 6 +-
superset/assets/src/dashboard/index.jsx | 8 +-
superset/assets/stylesheets/dashboard-v2.css | 42 ------
superset/assets/stylesheets/superset.less | 2 +-
superset/templates/appbuilder/navbar.html | 15 --
64 files changed, 1305 insertions(+), 696 deletions(-)
diff --git a/superset/assets/javascripts/dashboard/v2/actions/index.js b/superset/assets/javascripts/dashboard/v2/actions/index.js
index 005a77e..a6c7b77 100644
--- a/superset/assets/javascripts/dashboard/v2/actions/index.js
+++ b/superset/assets/javascripts/dashboard/v2/actions/index.js
@@ -1,3 +1,12 @@
+import { DASHBOARD_ROOT_ID, NEW_COMPONENTS_SOURCE_ID } from '../util/constants';
+import findParentId from '../util/findParentId';
+import {
+ CHART_TYPE,
+ MARKDOWN_TYPE,
+ TABS_TYPE,
+} from '../util/componentTypes';
+
+// Component CRUD -------------------------------------------------------------
export const UPDATE_COMPONENTS = 'UPDATE_COMPONENTS';
export function updateComponents(nextComponents) {
return {
@@ -29,6 +38,67 @@ export function createComponent(dropResult) {
};
}
+// Tabs -----------------------------------------------------------------------
+export const CREATE_TOP_LEVEL_TABS = 'CREATE_TOP_LEVEL_TABS';
+export function createTopLevelTabs(dropResult) {
+ return {
+ type: CREATE_TOP_LEVEL_TABS,
+ payload: {
+ dropResult,
+ },
+ };
+}
+
+export const DELETE_TOP_LEVEL_TABS = 'DELETE_TOP_LEVEL_TABS';
+export function deleteTopLevelTabs() {
+ return {
+ type: DELETE_TOP_LEVEL_TABS,
+ payload: {},
+ };
+}
+
+// Resize ---------------------------------------------------------------------
+export const RESIZE_COMPONENT = 'RESIZE_COMPONENT';
+export function resizeComponent({ id, width, height }) {
+ return (dispatch, getState) => {
+ const { dashboard: undoableDashboard } = getState();
+ const { present: dashboard } = undoableDashboard;
+ const component = dashboard[id];
+
+ if (
+ component &&
+ (component.meta.width !== width || component.meta.height !== height)
+ ) {
+ // update the size of this component + any resizable children
+ const updatedComponents = {
+ [id]: {
+ ...component,
+ meta: {
+ ...component.meta,
+ width: width || component.meta.width,
+ height: height || component.meta.height,
+ },
+ },
+ };
+
+ component.children.forEach((childId) => {
+ const child = dashboard[childId];
+ if ([CHART_TYPE, MARKDOWN_TYPE].includes(child.type)) {
+ updatedComponents[childId] = {
+ ...child,
+ meta: {
+ ...child.meta,
+ width: width || component.meta.width,
+ height: height || component.meta.height,
+ },
+ };
+ }
+ });
+
+ dispatch(updateComponents(updatedComponents));
+ }
+ };
+}
// Drag and drop --------------------------------------------------------------
export const MOVE_COMPONENT = 'MOVE_COMPONENT';
@@ -43,27 +113,38 @@ export function moveComponent(dropResult) {
export const HANDLE_COMPONENT_DROP = 'HANDLE_COMPONENT_DROP';
export function handleComponentDrop(dropResult) {
- return (dispatch) => {
- if (
- dropResult.destination
- && dropResult.source
+ return (dispatch, getState) => {
+ const { source, destination } = dropResult;
+ const droppedOnRoot = destination && destination.id === DASHBOARD_ROOT_ID;
+ const isNewComponent = source.id === NEW_COMPONENTS_SOURCE_ID;
+
+ if (droppedOnRoot) {
+ dispatch(createTopLevelTabs(dropResult));
+ } else if (destination && isNewComponent) {
+ dispatch(createComponent(dropResult));
+ } else if (
+ destination
+ && source
&& !( // ensure it has moved
- dropResult.destination.droppableId === dropResult.source.droppableId
- && dropResult.destination.index === dropResult.source.index
+ destination.id === source.id
+ && destination.index === source.index
)
) {
- return dispatch(moveComponent(dropResult));
+ dispatch(moveComponent(dropResult));
+ }
- // new components don't have a source
- } else if (dropResult.destination && !dropResult.source) {
- return dispatch(createComponent(dropResult));
+ // if we moved a tab and the parent tabs no longer has children, delete it.
+ if (!isNewComponent) {
+ const { dashboard: undoableDashboard } = getState();
+ const { present: dashboard } = undoableDashboard;
+ const sourceComponent = dashboard[source.id];
+
+ if (sourceComponent.type === TABS_TYPE && sourceComponent.children.length === 0) {
+ const parentId = findParentId({ childId: source.id, components: dashboard });
+ dispatch(deleteComponent(source.id, parentId));
+ }
}
+
return null;
};
}
-
-// Resize ---------------------------------------------------------------------
-
-// export function dashboardComponentResizeStart() {}
-// export function dashboardComponentResize() {}
-// export function dashboardComponentResizeStop() {}
diff --git a/superset/assets/javascripts/dashboard/v2/components/Dashboard.jsx b/superset/assets/javascripts/dashboard/v2/components/Dashboard.jsx
index a2ed1a0..ffd1280 100644
--- a/superset/assets/javascripts/dashboard/v2/components/Dashboard.jsx
+++ b/superset/assets/javascripts/dashboard/v2/components/Dashboard.jsx
@@ -1,11 +1,8 @@
import React from 'react';
import PropTypes from 'prop-types';
-import DashboardBuilder from './DashboardBuilder';
-import StaticDashboard from './StaticDashboard';
-import DashboardHeader from './DashboardHeader';
+import DashboardBuilder from '../containers/DashboardBuilder';
-import '../../../../stylesheets/dashboard-v2.css';
import '../stylesheets/index.less';
const propTypes = {
@@ -22,20 +19,8 @@ const defaultProps = {
class Dashboard extends React.Component {
render() {
- const { editMode, actions } = this.props;
- const { setEditMode, updateDashboardTitle } = actions;
- return (
- <div className="dashboard-v2">
- <DashboardHeader
- editMode={true}
- setEditMode={setEditMode}
- updateDashboardTitle={updateDashboardTitle}
- />
-
- {true ?
- <DashboardBuilder /> : <StaticDashboard />}
- </div>
- );
+ // @TODO delete this component?
+ return <DashboardBuilder />;
}
}
diff --git a/superset/assets/javascripts/dashboard/v2/components/DashboardBuilder.jsx b/superset/assets/javascripts/dashboard/v2/components/DashboardBuilder.jsx
index 1878db6..f371718 100644
--- a/superset/assets/javascripts/dashboard/v2/components/DashboardBuilder.jsx
+++ b/superset/assets/javascripts/dashboard/v2/components/DashboardBuilder.jsx
@@ -2,13 +2,28 @@ import React from 'react';
import PropTypes from 'prop-types';
import HTML5Backend from 'react-dnd-html5-backend';
import { DragDropContext } from 'react-dnd';
-import cx from 'classnames';
import BuilderComponentPane from './BuilderComponentPane';
+import DashboardHeader from '../containers/DashboardHeader';
import DashboardGrid from '../containers/DashboardGrid';
+import IconButton from './IconButton';
+import DragDroppable from './dnd/DragDroppable';
+import DashboardComponent from '../containers/DashboardComponent';
+import WithPopoverMenu from './menu/WithPopoverMenu';
+
+import {
+ DASHBOARD_GRID_ID,
+ DASHBOARD_ROOT_ID,
+ DASHBOARD_ROOT_DEPTH,
+} from '../util/constants';
const propTypes = {
editMode: PropTypes.bool,
+
+ // redux
+ dashboard: PropTypes.object.isRequired,
+ deleteTopLevelTabs: PropTypes.func.isRequired,
+ handleComponentDrop: PropTypes.func.isRequired,
};
const defaultProps = {
@@ -16,17 +31,87 @@ const defaultProps = {
};
class DashboardBuilder extends React.Component {
+ static shouldFocusTabs(event, container) {
+ // don't focus the tabs when we click on a tab
+ return event.target.tagName === 'UL' || (
+ /icon-button/.test(event.target.className) && container.contains(event.target)
+ );
+ }
+
constructor(props) {
super(props);
- // this component might control the state of the side pane etc. in the future
- this.state = {};
+ this.state = {
+ tabIndex: 0, // top-level tabs
+ };
+ this.handleChangeTab = this.handleChangeTab.bind(this);
+ }
+
+ handleChangeTab({ tabIndex }) {
+ this.setState(() => ({ tabIndex }));
}
render() {
+ const { tabIndex } = this.state;
+ const { handleComponentDrop, dashboard, deleteTopLevelTabs } = this.props;
+ const dashboardRoot = dashboard[DASHBOARD_ROOT_ID];
+ const rootChildId = dashboardRoot.children[0];
+ const topLevelTabs = rootChildId !== DASHBOARD_GRID_ID && dashboard[rootChildId];
+
+ const gridComponentId = topLevelTabs
+ ? topLevelTabs.children[Math.min(topLevelTabs.children.length - 1, tabIndex)]
+ : DASHBOARD_GRID_ID;
+
+ const gridComponent = dashboard[gridComponentId];
+
return (
- <div className={cx('dashboard-builder')}>
- <DashboardGrid />
- <BuilderComponentPane />
+ <div className="dashboard-v2">
+ {topLevelTabs ? ( // you cannot drop on/displace tabs if they already exist
+ <DashboardHeader />
+ ) : (
+ <DragDroppable
+ component={dashboardRoot}
+ parentComponent={null}
+ depth={DASHBOARD_ROOT_DEPTH}
+ index={0}
+ orientation="column"
+ onDrop={topLevelTabs ? null : handleComponentDrop}
+ >
+ {({ dropIndicatorProps }) => (
+ <div>
+ <DashboardHeader />
+ {dropIndicatorProps && <div {...dropIndicatorProps} />}
+ </div>
+ )}
+ </DragDroppable>)}
+
+ {topLevelTabs &&
+ <WithPopoverMenu
+ shouldFocus={DashboardBuilder.shouldFocusTabs}
+ menuItems={[
+ <IconButton
+ className="fa fa-level-down"
+ label="Collapse tab content"
+ onClick={deleteTopLevelTabs}
+ />,
+ ]}
+ >
+ <DashboardComponent
+ id={topLevelTabs.id}
+ parentId={DASHBOARD_ROOT_ID}
+ depth={DASHBOARD_ROOT_DEPTH + 1}
+ index={0}
+ renderTabContent={false}
+ onChangeTab={this.handleChangeTab}
+ />
+ </WithPopoverMenu>}
+
+ <div className="dashboard-builder">
+ <DashboardGrid
+ gridComponent={gridComponent}
+ depth={DASHBOARD_ROOT_DEPTH + 1}
+ />
+ <BuilderComponentPane />
+ </div>
</div>
);
}
diff --git a/superset/assets/javascripts/dashboard/v2/components/DashboardGrid.jsx b/superset/assets/javascripts/dashboard/v2/components/DashboardGrid.jsx
index c92161a..cfe99c7 100644
--- a/superset/assets/javascripts/dashboard/v2/components/DashboardGrid.jsx
+++ b/superset/assets/javascripts/dashboard/v2/components/DashboardGrid.jsx
@@ -1,21 +1,21 @@
import React from 'react';
import PropTypes from 'prop-types';
import ParentSize from '@vx/responsive/build/components/ParentSize';
-import cx from 'classnames';
-import DragDroppable from './dnd/DragDroppable';
+import { componentShape } from '../util/propShapes';
import DashboardComponent from '../containers/DashboardComponent';
+import DragDroppable from './dnd/DragDroppable';
import {
- DASHBOARD_ROOT_ID,
GRID_GUTTER_SIZE,
GRID_COLUMN_COUNT,
} from '../util/constants';
const propTypes = {
- dashboard: PropTypes.object.isRequired,
- updateComponents: PropTypes.func.isRequired,
+ depth: PropTypes.number.isRequired,
+ gridComponent: componentShape.isRequired,
handleComponentDrop: PropTypes.func.isRequired,
+ resizeComponent: PropTypes.func.isRequired,
};
const defaultProps = {
@@ -60,24 +60,9 @@ class DashboardGrid extends React.PureComponent {
}
}
- handleResizeStop({ id, widthMultiple, heightMultiple }) {
- const { dashboard: components, updateComponents } = this.props;
- const component = components[id];
- if (
- component &&
- (component.meta.width !== widthMultiple || component.meta.height !== heightMultiple)
- ) {
- updateComponents({
- [id]: {
- ...component,
- meta: {
- ...component.meta,
- width: widthMultiple || component.meta.width,
- height: heightMultiple || component.meta.height,
- },
- },
- });
- }
+ handleResizeStop({ id, widthMultiple: width, heightMultiple: height }) {
+ this.props.resizeComponent({ id, width, height });
+
this.setState(() => ({
isResizing: false,
rowGuideTop: null,
@@ -85,18 +70,11 @@ class DashboardGrid extends React.PureComponent {
}
render() {
- const { dashboard: components, handleComponentDrop } = this.props;
+ const { gridComponent, handleComponentDrop, depth } = this.props;
const { isResizing, rowGuideTop } = this.state;
- const rootComponent = components[DASHBOARD_ROOT_ID];
return (
- <div
- ref={(ref) => { this.grid = ref; }}
- className={cx(
- 'grid-container',
- isResizing && 'grid-container--resizing',
- )}
- >
+ <div className="grid-container" ref={(ref) => { this.grid = ref; }}>
<ParentSize>
{({ width }) => {
// account for (COLUMN_COUNT - 1) gutters
@@ -104,13 +82,13 @@ class DashboardGrid extends React.PureComponent {
const columnWidth = columnPlusGutterWidth - GRID_GUTTER_SIZE;
return width < 50 ? null : (
- <div style={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
- {(rootComponent.children || []).map((id, index) => (
+ <div className="grid-content">
+ {gridComponent.children.map((id, index) => (
<DashboardComponent
key={id}
id={id}
- parentId={rootComponent.id}
- depth={0}
+ parentId={gridComponent.id}
+ depth={depth + 1}
index={index}
availableColumnCount={GRID_COLUMN_COUNT}
columnWidth={columnWidth}
@@ -120,19 +98,19 @@ class DashboardGrid extends React.PureComponent {
/>
))}
- {rootComponent.children.length === 0 &&
+ {/* render an empty drop target */}
+ {gridComponent.children.length === 0 &&
<DragDroppable
- component={rootComponent}
+ component={gridComponent}
+ depth={depth}
parentComponent={null}
index={0}
orientation="column"
onDrop={handleComponentDrop}
+ className="empty-grid-droptarget"
>
- {({ dropIndicatorProps }) => (
- <div style={{ width: '100%', height: '100%' }}>
- {dropIndicatorProps && <div {...dropIndicatorProps} />}
- </div>
- )}
+ {({ dropIndicatorProps }) => dropIndicatorProps &&
+ <div {...dropIndicatorProps} />}
</DragDroppable>}
{isResizing && Array(GRID_COLUMN_COUNT).fill(null).map((_, i) => (
diff --git a/superset/assets/javascripts/dashboard/v2/components/DashboardHeader.jsx b/superset/assets/javascripts/dashboard/v2/components/DashboardHeader.jsx
index 8ffe677..e0d14c4 100644
--- a/superset/assets/javascripts/dashboard/v2/components/DashboardHeader.jsx
+++ b/superset/assets/javascripts/dashboard/v2/components/DashboardHeader.jsx
@@ -1,44 +1,83 @@
import React from 'react';
import PropTypes from 'prop-types';
-import { ButtonToolbar, DropdownButton, MenuItem } from 'react-bootstrap';
+import { ButtonGroup, ButtonToolbar, DropdownButton, MenuItem } from 'react-bootstrap';
import Button from '../../../components/Button';
+import { componentShape } from '../util/propShapes';
import EditableTitle from '../../../components/EditableTitle';
const propTypes = {
- updateDashboardTitle: PropTypes.func,
- editMode: PropTypes.bool.isRequired,
- setEditMode: PropTypes.func.isRequired,
+ // editMode: PropTypes.bool.isRequired,
+ // setEditMode: PropTypes.func.isRequired,
+ component: componentShape.isRequired,
+
+ // redux
+ updateComponents: PropTypes.func.isRequired,
+ onUndo: PropTypes.func.isRequired,
+ onRedo: PropTypes.func.isRequired,
+ canUndo: PropTypes.bool.isRequired,
+ canRedo: PropTypes.bool.isRequired,
};
-class Header extends React.Component {
+class DashboardHeader extends React.Component {
constructor(props) {
super(props);
- this.handleSaveTitle = this.handleSaveTitle.bind(this);
+ this.handleChangeText = this.handleChangeText.bind(this);
this.toggleEditMode = this.toggleEditMode.bind(this);
}
- handleSaveTitle(title) {
- this.props.updateDashboardTitle(title);
+ toggleEditMode() {
+ console.log('@TODO toggleEditMode');
+ // this.props.setEditMode(!this.props.editMode);
}
- toggleEditMode() {
- this.props.setEditMode(!this.props.editMode);
+ handleChangeText(nextText) {
+ const { updateComponents, component } = this.props;
+ if (nextText && component.meta.text !== nextText) {
+ updateComponents({
+ [component.id]: {
+ ...component,
+ meta: {
+ ...component.meta,
+ text: nextText,
+ },
+ },
+ });
+ }
}
render() {
- const { editMode } = this.props;
+ const { component, onUndo, onRedo, canUndo, canRedo } = this.props;
+ const editMode = true;
+
return (
<div className="dashboard-header">
<h1>
<EditableTitle
- title={'Example header'}
- canEdit={false}
- onSaveTitle={() => {}}
+ title={component.meta.text}
+ onSaveTitle={this.handleChangeText}
showTooltip={false}
+ canEdit={editMode}
/>
</h1>
<ButtonToolbar>
+ <ButtonGroup>
+ <Button
+ bsSize="small"
+ onClick={onUndo}
+ disabled={!canUndo}
+ >
+ Undo
+ </Button>
+ <Button
+ bsSize="small"
+ onClick={onRedo}
+ disabled={!canRedo}
+ >
+ Redo
+ </Button>
+ </ButtonGroup>
+
<DropdownButton title="Actions" bsSize="small" id="btn-dashboard-actions">
<MenuItem>Action 1</MenuItem>
<MenuItem>Action 2</MenuItem>
@@ -57,6 +96,6 @@ class Header extends React.Component {
}
}
-Header.propTypes = propTypes;
+DashboardHeader.propTypes = propTypes;
-export default Header;
+export default DashboardHeader;
diff --git a/superset/assets/javascripts/dashboard/v2/components/IconButton.jsx b/superset/assets/javascripts/dashboard/v2/components/IconButton.jsx
index 98044c9..18fd3b1 100644
--- a/superset/assets/javascripts/dashboard/v2/components/IconButton.jsx
+++ b/superset/assets/javascripts/dashboard/v2/components/IconButton.jsx
@@ -1,14 +1,15 @@
import React from 'react';
import PropTypes from 'prop-types';
-import cx from 'classnames';
const propTypes = {
onClick: PropTypes.func.isRequired,
className: PropTypes.string,
+ label: PropTypes.string,
};
const defaultProps = {
className: null,
+ label: null,
};
export default class IconButton extends React.PureComponent {
@@ -24,14 +25,17 @@ export default class IconButton extends React.PureComponent {
}
render() {
- const { className } = this.props;
+ const { className, label } = this.props;
return (
<div
- className={cx('icon-button', className)}
+ className="icon-button"
onClick={this.handleClick}
tabIndex="0"
role="button"
- />
+ >
+ <span className={className} />
+ {label && <span className="icon-button-label">{label}</span>}
+ </div>
);
}
}
diff --git a/superset/assets/javascripts/dashboard/v2/components/dnd/DragDroppable.jsx b/superset/assets/javascripts/dashboard/v2/components/dnd/DragDroppable.jsx
index 320872b..89664e5 100644
--- a/superset/assets/javascripts/dashboard/v2/components/dnd/DragDroppable.jsx
+++ b/superset/assets/javascripts/dashboard/v2/components/dnd/DragDroppable.jsx
@@ -3,17 +3,21 @@ import PropTypes from 'prop-types';
import { DragSource, DropTarget } from 'react-dnd';
import cx from 'classnames';
-import { dragConfig, dropConfig } from './dragDroppableConfig';
import { componentShape } from '../../util/propShapes';
-
+import { dragConfig, dropConfig } from './dragDroppableConfig';
+import { DROP_TOP, DROP_RIGHT, DROP_BOTTOM, DROP_LEFT } from '../../util/getDropPosition';
const propTypes = {
children: PropTypes.func,
+ className: PropTypes.string,
component: componentShape.isRequired,
parentComponent: componentShape,
+ depth: PropTypes.number.isRequired,
disableDragDrop: PropTypes.bool,
orientation: PropTypes.oneOf(['row', 'column']),
index: PropTypes.number.isRequired,
+ style: PropTypes.object,
+ onDrop: PropTypes.func,
// from react-dnd
isDragging: PropTypes.bool.isRequired,
@@ -22,12 +26,11 @@ const propTypes = {
droppableRef: PropTypes.func.isRequired,
dragSourceRef: PropTypes.func.isRequired,
dragPreviewRef: PropTypes.func.isRequired,
-
- // from redux
- onDrop: PropTypes.func,
};
const defaultProps = {
+ className: null,
+ style: null,
parentComponent: null,
disableDragDrop: false,
children() {},
@@ -41,6 +44,7 @@ class DragDroppable extends React.Component {
this.state = {
dropIndicator: null, // this gets set/modified by the react-dnd HOCs
};
+ this.setRef = this.setRef.bind(this);
}
componentDidMount() {
@@ -51,38 +55,47 @@ class DragDroppable extends React.Component {
this.mounted = false;
}
+ setRef(ref) {
+ this.ref = ref;
+ this.props.dragPreviewRef(ref);
+ this.props.droppableRef(ref);
+ }
+
render() {
const {
children,
+ className,
orientation,
- droppableRef,
dragSourceRef,
- dragPreviewRef,
isDragging,
isDraggingOver,
+ style,
} = this.props;
const { dropIndicator } = this.state;
return (
<div
- ref={(ref) => {
- this.ref = ref;
- dragPreviewRef(ref);
- droppableRef(ref);
- }}
+ style={style}
+ ref={this.setRef}
className={cx(
'dragdroppable',
orientation === 'row' && 'dragdroppable-row',
orientation === 'column' && 'dragdroppable-column',
isDragging && 'dragdroppable--dragging',
+ className,
)}
>
{children({
dragSourceRef,
dropIndicatorProps: isDraggingOver && dropIndicator && {
- className: 'drop-indicator',
- style: dropIndicator,
+ className: cx(
+ 'drop-indicator',
+ dropIndicator === DROP_TOP && 'drop-indicator--top',
+ dropIndicator === DROP_BOTTOM && 'drop-indicator--bottom',
+ dropIndicator === DROP_LEFT && 'drop-indicator--left',
+ dropIndicator === DROP_RIGHT && 'drop-indicator--right',
+ ),
},
})}
</div>
diff --git a/superset/assets/javascripts/dashboard/v2/components/dnd/dragDroppableConfig.js b/superset/assets/javascripts/dashboard/v2/components/dnd/dragDroppableConfig.js
index e6d5533..55d7e1d 100644
--- a/superset/assets/javascripts/dashboard/v2/components/dnd/dragDroppableConfig.js
+++ b/superset/assets/javascripts/dashboard/v2/components/dnd/dragDroppableConfig.js
@@ -10,13 +10,16 @@ export const dragConfig = [
canDrag(props) {
return !props.disableDragDrop;
},
+
+ // this defines the dragging item object returned by monitor.getItem()
beginDrag(props /* , monitor, component */) {
- const { component, index, parentComponent } = props;
+ const { component, index, parentComponent = {} } = props;
return {
- draggableId: component.id,
- index,
- parentId: parentComponent && parentComponent.id,
type: component.type,
+ id: component.id,
+ index,
+ parentId: parentComponent.id,
+ parentType: parentComponent.type,
};
},
},
diff --git a/superset/assets/javascripts/dashboard/v2/components/dnd/handleDrop.js b/superset/assets/javascripts/dashboard/v2/components/dnd/handleDrop.js
index cf790da..2207ca6 100644
--- a/superset/assets/javascripts/dashboard/v2/components/dnd/handleDrop.js
+++ b/superset/assets/javascripts/dashboard/v2/components/dnd/handleDrop.js
@@ -2,7 +2,7 @@ import getDropPosition, { DROP_TOP, DROP_RIGHT, DROP_BOTTOM, DROP_LEFT } from '.
export default function handleDrop(props, monitor, Component) {
// this may happen due to throttling
- if (!Component.mounted) return undefined;
+ if (!Component.mounted || !Component.props.onDrop) return undefined;
Component.setState(() => ({ dropIndicator: null }));
const dropPosition = getDropPosition(monitor, Component);
@@ -27,17 +27,22 @@ export default function handleDrop(props, monitor, Component) {
? 'sibling' : 'child';
const dropResult = {
- source: draggingItem.parentId ? {
- droppableId: draggingItem.parentId,
+ source: {
+ id: draggingItem.parentId,
+ type: draggingItem.parentType,
index: draggingItem.index,
- } : null,
- draggableId: draggingItem.draggableId,
+ },
+ dragging: {
+ id: draggingItem.id,
+ type: draggingItem.type,
+ },
};
// simplest case, append as child
if (dropAsChildOrSibling === 'child') {
dropResult.destination = {
- droppableId: component.id,
+ id: component.id,
+ type: component.type,
index: component.children.length,
};
} else {
@@ -52,7 +57,8 @@ export default function handleDrop(props, monitor, Component) {
}
dropResult.destination = {
- droppableId: parentComponent.id,
+ id: parentComponent.id,
+ type: parentComponent.type,
index: nextIndex,
};
}
diff --git a/superset/assets/javascripts/dashboard/v2/components/dnd/handleHover.js b/superset/assets/javascripts/dashboard/v2/components/dnd/handleHover.js
index 1eadef4..a303e13 100644
--- a/superset/assets/javascripts/dashboard/v2/components/dnd/handleHover.js
+++ b/superset/assets/javascripts/dashboard/v2/components/dnd/handleHover.js
@@ -1,5 +1,5 @@
import throttle from 'lodash.throttle';
-import getDropPosition, { DROP_TOP, DROP_RIGHT, DROP_BOTTOM, DROP_LEFT } from '../../util/getDropPosition';
+import getDropPosition from '../../util/getDropPosition';
const HOVER_THROTTLE_MS = 200;
@@ -14,22 +14,8 @@ function handleHover(props, monitor, Component) {
return;
}
- // @TODO
- // drop-indicator
- // drop-indicator--top/right/bottom/left
Component.setState(() => ({
- dropIndicator: {
- top: dropPosition === DROP_BOTTOM ? '100%' : 0,
- left: dropPosition === DROP_RIGHT ? '100%' : 0,
- height: dropPosition === DROP_LEFT || dropPosition === DROP_RIGHT ? '100%' : 3,
- width: dropPosition === DROP_TOP || dropPosition === DROP_BOTTOM ? '100%' : 3,
- minHeight: dropPosition === DROP_LEFT || dropPosition === DROP_RIGHT ? 16 : null,
- minWidth: dropPosition === DROP_TOP || dropPosition === DROP_BOTTOM ? 16 : null,
- margin: 'auto',
- backgroundColor: '#44C0FF',
- position: 'absolute',
- zIndex: 10,
- },
+ dropIndicator: dropPosition,
}));
}
diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Chart.jsx b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Chart.jsx
index 9daa8cf..7ca506d 100644
--- a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Chart.jsx
+++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Chart.jsx
@@ -8,7 +8,7 @@ import HoverMenu from '../menu/HoverMenu';
import ResizableContainer from '../resizable/ResizableContainer';
import WithPopoverMenu from '../menu/WithPopoverMenu';
import { componentShape } from '../../util/propShapes';
-
+import { ROW_TYPE } from '../../util/componentTypes';
import {
GRID_MIN_COLUMN_COUNT,
GRID_MIN_ROW_UNITS,
@@ -79,13 +79,14 @@ class Chart extends React.Component {
parentComponent={parentComponent}
orientation={depth % 2 === 1 ? 'column' : 'row'}
index={index}
+ depth={depth}
onDrop={handleComponentDrop}
disableDragDrop={isFocused}
>
{({ dropIndicatorProps, dragSourceRef }) => (
<ResizableContainer
id={component.id}
- adjustableWidth={depth <= 1}
+ adjustableWidth={parentComponent.type === ROW_TYPE}
adjustableHeight
widthStep={columnWidth}
widthMultiple={component.meta.width}
diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Column.jsx b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Column.jsx
index 8409bc1..d51870d 100644
--- a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Column.jsx
+++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Column.jsx
@@ -7,10 +7,18 @@ import DeleteComponentButton from '../DeleteComponentButton';
import DragDroppable from '../dnd/DragDroppable';
import DragHandle from '../dnd/DragHandle';
import HoverMenu from '../menu/HoverMenu';
+import IconButton from '../IconButton';
import ResizableContainer from '../resizable/ResizableContainer';
+import BackgroundStyleDropdown from '../menu/BackgroundStyleDropdown';
+import WithPopoverMenu from '../menu/WithPopoverMenu';
+
+import backgroundStyleOptions from '../../util/backgroundStyleOptions';
import { componentShape } from '../../util/propShapes';
-import { GRID_GUTTER_SIZE, GRID_MIN_COLUMN_COUNT } from '../../util/constants';
+import {
+ BACKGROUND_TRANSPARENT,
+ GRID_GUTTER_SIZE,
+} from '../../util/constants';
const GUTTER = 'GUTTER';
@@ -21,11 +29,11 @@ const propTypes = {
parentComponent: componentShape.isRequired,
index: PropTypes.number.isRequired,
depth: PropTypes.number.isRequired,
- // occupiedRowCount: PropTypes.number,
// grid related
availableColumnCount: PropTypes.number.isRequired,
columnWidth: PropTypes.number.isRequired,
+ minColumnWidth: PropTypes.number.isRequired,
onResizeStart: PropTypes.func.isRequired,
onResize: PropTypes.func.isRequired,
onResizeStop: PropTypes.func.isRequired,
@@ -33,15 +41,20 @@ const propTypes = {
// dnd
deleteComponent: PropTypes.func.isRequired,
handleComponentDrop: PropTypes.func.isRequired,
+ updateComponents: PropTypes.func.isRequired,
};
const defaultProps = {
- // occupiedRowCount: null,
};
class Column extends React.PureComponent {
constructor(props) {
super(props);
+ this.state = {
+ isFocused: false,
+ };
+ this.handleChangeBackground = this.handleUpdateMeta.bind(this, 'background');
+ this.handleChangeFocus = this.handleChangeFocus.bind(this);
this.handleDeleteComponent = this.handleDeleteComponent.bind(this);
}
@@ -50,6 +63,25 @@ class Column extends React.PureComponent {
deleteComponent(id, parentId);
}
+ handleChangeFocus(nextFocus) {
+ this.setState(() => ({ isFocused: Boolean(nextFocus) }));
+ }
+
+ handleUpdateMeta(metaKey, nextValue) {
+ const { updateComponents, component } = this.props;
+ if (nextValue && component.meta[metaKey] !== nextValue) {
+ updateComponents({
+ [component.id]: {
+ ...component,
+ meta: {
+ ...component.meta,
+ [metaKey]: nextValue,
+ },
+ },
+ });
+ }
+ }
+
render() {
const {
component: columnComponent,
@@ -57,7 +89,7 @@ class Column extends React.PureComponent {
index,
availableColumnCount,
columnWidth,
- // occupiedRowCount,
+ minColumnWidth,
depth,
onResizeStart,
onResize,
@@ -74,12 +106,19 @@ class Column extends React.PureComponent {
}
});
+ const backgroundStyle = backgroundStyleOptions.find(
+ opt => opt.value === (columnComponent.meta.background || BACKGROUND_TRANSPARENT),
+ );
+
+ console.log('occupied/avail cols', columnComponent.meta.width, '/', availableColumnCount, 'min width', minColumnWidth)
+
return (
<DragDroppable
component={columnComponent}
parentComponent={parentComponent}
orientation="column"
index={index}
+ depth={depth}
onDrop={handleComponentDrop}
>
{({ dropIndicatorProps, dragSourceRef }) => (
@@ -89,47 +128,64 @@ class Column extends React.PureComponent {
adjustableHeight={false}
widthStep={columnWidth}
widthMultiple={columnComponent.meta.width}
- // heightMultiple={occupiedRowCount}
- minWidthMultiple={GRID_MIN_COLUMN_COUNT}
+ minWidthMultiple={minColumnWidth}
maxWidthMultiple={availableColumnCount + (columnComponent.meta.width || 0)}
onResizeStart={onResizeStart}
onResize={onResize}
onResizeStop={onResizeStop}
>
- <div
- className={cx(
- 'grid-column',
- columnItems.length === 0 && 'grid-column--empty',
- )}
+ <WithPopoverMenu
+ isFocused={this.state.isFocused}
+ onChangeFocus={this.handleChangeFocus}
+ disableClick
+ menuItems={[
+ <BackgroundStyleDropdown
+ id={`${columnComponent.id}-background`}
+ value={columnComponent.meta.background}
+ onChange={this.handleChangeBackground}
+ />,
+ ]}
>
- <HoverMenu innerRef={dragSourceRef} position="top">
- <DragHandle position="top" />
- <DeleteComponentButton onDelete={this.handleDeleteComponent} />
- </HoverMenu>
-
- {columnItems.map((componentId, itemIndex) => {
- if (componentId === GUTTER) {
- return <div key={`gutter-${itemIndex}`} style={{ height: GRID_GUTTER_SIZE }} />;
- }
-
- return (
- <DashboardComponent
- key={componentId}
- id={componentId}
- parentId={columnComponent.id}
- depth={depth + 1}
- index={itemIndex / 2} // account for gutters!
- availableColumnCount={availableColumnCount}
- columnWidth={columnWidth}
- onResizeStart={onResizeStart}
- onResize={onResize}
- onResizeStop={onResizeStop}
+ <div
+ className={cx(
+ 'grid-column',
+ columnItems.length === 0 && 'grid-column--empty',
+ backgroundStyle.className,
+ )}
+ >
+ <HoverMenu innerRef={dragSourceRef} position="top">
+ <DragHandle position="top" />
+ <DeleteComponentButton onDelete={this.handleDeleteComponent} />
+ <IconButton
+ onClick={this.handleChangeFocus}
+ className="fa fa-cog"
/>
- );
- })}
-
- {dropIndicatorProps && <div {...dropIndicatorProps} />}
- </div>
+ </HoverMenu>
+
+ {columnItems.map((componentId, itemIndex) => {
+ if (componentId === GUTTER) {
+ return <div key={`gutter-${itemIndex}`} style={{ height: GRID_GUTTER_SIZE }} />;
+ }
+
+ return (
+ <DashboardComponent
+ key={componentId}
+ id={componentId}
+ parentId={columnComponent.id}
+ depth={depth + 1}
+ index={itemIndex / 2} // account for gutters!
+ availableColumnCount={columnComponent.meta.width}
+ columnWidth={columnWidth}
+ onResizeStart={onResizeStart}
+ onResize={onResize}
+ onResizeStop={onResizeStop}
+ />
+ );
+ })}
+
+ {dropIndicatorProps && <div {...dropIndicatorProps} />}
+ </div>
+ </WithPopoverMenu>
</ResizableContainer>
)}
</DragDroppable>
diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Divider.jsx b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Divider.jsx
index 29437e1..ff29c3f 100644
--- a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Divider.jsx
+++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Divider.jsx
@@ -10,6 +10,7 @@ const propTypes = {
id: PropTypes.string.isRequired,
parentId: PropTypes.string.isRequired,
component: componentShape.isRequired,
+ depth: PropTypes.number.isRequired,
parentComponent: componentShape.isRequired,
index: PropTypes.number.isRequired,
handleComponentDrop: PropTypes.func.isRequired,
@@ -30,6 +31,7 @@ class Divider extends React.PureComponent {
render() {
const {
component,
+ depth,
parentComponent,
index,
handleComponentDrop,
@@ -41,6 +43,7 @@ class Divider extends React.PureComponent {
parentComponent={parentComponent}
orientation="row"
index={index}
+ depth={depth}
onDrop={handleComponentDrop}
>
{({ dropIndicatorProps, dragSourceRef }) => (
diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Header.jsx b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Header.jsx
index 967b483..d8744d6 100644
--- a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Header.jsx
+++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Header.jsx
@@ -7,18 +7,19 @@ import DragHandle from '../dnd/DragHandle';
import EditableTitle from '../../../../components/EditableTitle';
import HoverMenu from '../menu/HoverMenu';
import WithPopoverMenu from '../menu/WithPopoverMenu';
-import RowStyleDropdown from '../menu/RowStyleDropdown';
+import BackgroundStyleDropdown from '../menu/BackgroundStyleDropdown';
import DeleteComponentButton from '../DeleteComponentButton';
import PopoverDropdown from '../menu/PopoverDropdown';
import headerStyleOptions from '../../util/headerStyleOptions';
-import rowStyleOptions from '../../util/rowStyleOptions';
+import backgroundStyleOptions from '../../util/backgroundStyleOptions';
import { componentShape } from '../../util/propShapes';
-import { SMALL_HEADER, ROW_TRANSPARENT } from '../../util/constants';
+import { SMALL_HEADER, BACKGROUND_TRANSPARENT } from '../../util/constants';
const propTypes = {
id: PropTypes.string.isRequired,
parentId: PropTypes.string.isRequired,
component: componentShape.isRequired,
+ depth: PropTypes.number.isRequired,
parentComponent: componentShape.isRequired,
index: PropTypes.number.isRequired,
@@ -41,7 +42,7 @@ class Header extends React.PureComponent {
this.handleChangeFocus = this.handleChangeFocus.bind(this);
this.handleUpdateMeta = this.handleUpdateMeta.bind(this);
this.handleChangeSize = this.handleUpdateMeta.bind(this, 'headerSize');
- this.handleChangeRowStyle = this.handleUpdateMeta.bind(this, 'rowStyle');
+ this.handleChangeBackground = this.handleUpdateMeta.bind(this, 'background');
this.handleChangeText = this.handleUpdateMeta.bind(this, 'text');
}
@@ -74,6 +75,7 @@ class Header extends React.PureComponent {
const {
component,
+ depth,
parentComponent,
index,
handleComponentDrop,
@@ -83,8 +85,8 @@ class Header extends React.PureComponent {
opt => opt.value === (component.meta.headerSize || SMALL_HEADER),
);
- const rowStyle = rowStyleOptions.find(
- opt => opt.value === (component.meta.rowStyle || ROW_TRANSPARENT),
+ const rowStyle = backgroundStyleOptions.find(
+ opt => opt.value === (component.meta.background || BACKGROUND_TRANSPARENT),
);
return (
@@ -93,6 +95,7 @@ class Header extends React.PureComponent {
parentComponent={parentComponent}
orientation="row"
index={index}
+ depth={depth}
onDrop={handleComponentDrop}
disableDragDrop={isFocused}
>
@@ -112,10 +115,10 @@ class Header extends React.PureComponent {
onChange={this.handleChangeSize}
renderTitle={option => `${option.label} header`}
/>,
- <RowStyleDropdown
- id={`${component.id}-row-style`}
- value={component.meta.rowStyle}
- onChange={this.handleChangeRowStyle}
+ <BackgroundStyleDropdown
+ id={`${component.id}-background`}
+ value={component.meta.background}
+ onChange={this.handleChangeBackground}
/>,
<DeleteComponentButton onDelete={this.handleDeleteComponent} />,
]}
diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Row.jsx b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Row.jsx
index 3386f8c..a60524f 100644
--- a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Row.jsx
+++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Row.jsx
@@ -8,12 +8,12 @@ import DashboardComponent from '../../containers/DashboardComponent';
import DeleteComponentButton from '../DeleteComponentButton';
import HoverMenu from '../menu/HoverMenu';
import IconButton from '../IconButton';
-import RowStyleDropdown from '../menu/RowStyleDropdown';
+import BackgroundStyleDropdown from '../menu/BackgroundStyleDropdown';
import WithPopoverMenu from '../menu/WithPopoverMenu';
import { componentShape } from '../../util/propShapes';
-import rowStyleOptions from '../../util/rowStyleOptions';
-import { GRID_GUTTER_SIZE, ROW_TRANSPARENT } from '../../util/constants';
+import backgroundStyleOptions from '../../util/backgroundStyleOptions';
+import { GRID_GUTTER_SIZE, BACKGROUND_TRANSPARENT } from '../../util/constants';
const GUTTER = 'GUTTER';
@@ -29,7 +29,6 @@ const propTypes = {
availableColumnCount: PropTypes.number.isRequired,
columnWidth: PropTypes.number.isRequired,
occupiedColumnCount: PropTypes.number.isRequired,
- occupiedRowCount: PropTypes.number.isRequired,
onResizeStart: PropTypes.func.isRequired,
onResize: PropTypes.func.isRequired,
onResizeStop: PropTypes.func.isRequired,
@@ -52,7 +51,7 @@ class Row extends React.PureComponent {
};
this.handleDeleteComponent = this.handleDeleteComponent.bind(this);
this.handleUpdateMeta = this.handleUpdateMeta.bind(this);
- this.handleChangeRowStyle = this.handleUpdateMeta.bind(this, 'rowStyle');
+ this.handleChangeBackground = this.handleUpdateMeta.bind(this, 'background');
this.handleChangeFocus = this.handleChangeFocus.bind(this);
}
@@ -88,7 +87,6 @@ class Row extends React.PureComponent {
availableColumnCount,
columnWidth,
occupiedColumnCount,
- occupiedRowCount,
depth,
onResizeStart,
onResize,
@@ -106,8 +104,8 @@ class Row extends React.PureComponent {
}
});
- const rowStyle = rowStyleOptions.find(
- opt => opt.value === (rowComponent.meta.rowStyle || ROW_TRANSPARENT),
+ const backgroundStyle = backgroundStyleOptions.find(
+ opt => opt.value === (rowComponent.meta.background || BACKGROUND_TRANSPARENT),
);
return (
@@ -116,6 +114,7 @@ class Row extends React.PureComponent {
parentComponent={parentComponent}
orientation="row"
index={index}
+ depth={depth}
onDrop={handleComponentDrop}
>
{({ dropIndicatorProps, dragSourceRef }) => (
@@ -124,19 +123,18 @@ class Row extends React.PureComponent {
onChangeFocus={this.handleChangeFocus}
disableClick
menuItems={[
- <RowStyleDropdown
- id={`${rowComponent.id}-row-style`}
- value={rowComponent.meta.rowStyle}
- onChange={this.handleChangeRowStyle}
+ <BackgroundStyleDropdown
+ id={`${rowComponent.id}-background`}
+ value={rowComponent.meta.background}
+ onChange={this.handleChangeBackground}
/>,
]}
>
-
<div
className={cx(
'grid-row',
rowItems.length === 0 && 'grid-row--empty',
- rowStyle.className,
+ backgroundStyle.className,
)}
>
<HoverMenu innerRef={dragSourceRef} position="left">
@@ -161,7 +159,6 @@ class Row extends React.PureComponent {
depth={depth + 1}
index={itemIndex / 2} // account for gutters!
availableColumnCount={availableColumnCount - occupiedColumnCount}
- occupiedRowCount={occupiedRowCount}
columnWidth={columnWidth}
onResizeStart={onResizeStart}
onResize={onResize}
diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Spacer.jsx b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Spacer.jsx
index faac589..7a287d8 100644
--- a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Spacer.jsx
+++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Spacer.jsx
@@ -18,7 +18,6 @@ const propTypes = {
// grid related
availableColumnCount: PropTypes.number.isRequired,
columnWidth: PropTypes.number.isRequired,
- occupiedRowCount: PropTypes.number,
onResizeStart: PropTypes.func.isRequired,
onResize: PropTypes.func.isRequired,
onResizeStop: PropTypes.func.isRequired,
@@ -29,7 +28,6 @@ const propTypes = {
};
const defaultProps = {
- occupiedRowCount: null,
};
class Spacer extends React.PureComponent {
@@ -51,7 +49,6 @@ class Spacer extends React.PureComponent {
depth,
availableColumnCount,
columnWidth,
- occupiedRowCount,
onResizeStart,
onResize,
onResizeStop,
@@ -63,12 +60,15 @@ class Spacer extends React.PureComponent {
const adjustableWidth = orientation === 'column';
const adjustableHeight = orientation === 'row';
+ console.log('spacer', availableColumnCount)
+
return (
<DragDroppable
component={component}
parentComponent={parentComponent}
orientation={orientation}
index={index}
+ depth={depth}
onDrop={handleComponentDrop}
>
{({ dropIndicatorProps, dragSourceRef }) => (
@@ -77,9 +77,8 @@ class Spacer extends React.PureComponent {
adjustableWidth={adjustableWidth}
adjustableHeight={adjustableHeight}
widthStep={columnWidth}
- widthMultiple={component.meta.width}
+ widthMultiple={component.meta.width || 1}
heightMultiple={adjustableHeight ? component.meta.height || 1 : undefined}
- staticHeightMultiple={!adjustableHeight ? occupiedRowCount || 5 : undefined}
minWidthMultiple={1}
minHeightMultiple={1}
maxWidthMultiple={availableColumnCount + (component.meta.width || 0)}
diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Tab.jsx b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Tab.jsx
index 74cd9ae..9548a4b 100644
--- a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Tab.jsx
+++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Tab.jsx
@@ -20,13 +20,14 @@ const propTypes = {
depth: PropTypes.number.isRequired,
renderType: PropTypes.oneOf([RENDER_TAB, RENDER_TAB_CONTENT]).isRequired,
onDropOnTab: PropTypes.func,
+ onDeleteTab: PropTypes.func,
// grid related
- availableColumnCount: PropTypes.number.isRequired,
- columnWidth: PropTypes.number.isRequired,
- onResizeStart: PropTypes.func.isRequired,
- onResize: PropTypes.func.isRequired,
- onResizeStop: PropTypes.func.isRequired,
+ availableColumnCount: PropTypes.number,
+ columnWidth: PropTypes.number,
+ onResizeStart: PropTypes.func,
+ onResize: PropTypes.func,
+ onResizeStop: PropTypes.func,
// redux
handleComponentDrop: PropTypes.func.isRequired,
@@ -35,7 +36,13 @@ const propTypes = {
};
const defaultProps = {
- onDropOnTab: null,
+ availableColumnCount: 0,
+ columnWidth: 0,
+ onDropOnTab() {},
+ onDeleteTab() {},
+ onResizeStart() {},
+ onResize() {},
+ onResizeStop() {},
};
export default class Tab extends React.PureComponent {
@@ -70,14 +77,14 @@ export default class Tab extends React.PureComponent {
}
handleDeleteComponent() {
- const { deleteComponent, id, parentId } = this.props;
+ const { onDeleteTab, index, deleteComponent, id, parentId } = this.props;
deleteComponent(id, parentId);
+ onDeleteTab(index);
}
handleDrop(dropResult) {
- const { handleComponentDrop, onDropOnTab } = this.props;
- handleComponentDrop(dropResult);
- if (onDropOnTab) onDropOnTab(dropResult);
+ this.props.handleComponentDrop(dropResult);
+ this.props.onDropOnTab(dropResult);
}
renderTabContent() {
@@ -98,7 +105,7 @@ export default class Tab extends React.PureComponent {
key={componentId}
id={componentId}
parentId={tabComponent.id}
- depth={depth}
+ depth={depth} // see isValidChild.js for why tabs don't increment child depth
index={componentIndex}
onDrop={this.handleDrop}
availableColumnCount={availableColumnCount}
@@ -118,6 +125,7 @@ export default class Tab extends React.PureComponent {
component,
parentComponent,
index,
+ depth,
} = this.props;
return (
@@ -126,6 +134,7 @@ export default class Tab extends React.PureComponent {
parentComponent={parentComponent}
orientation="column"
index={index}
+ depth={depth}
onDrop={this.handleDrop}
disableDragDrop={isFocused}
>
diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Tabs.jsx b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Tabs.jsx
index 1e2e64c..cc5f637 100644
--- a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Tabs.jsx
+++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Tabs.jsx
@@ -8,8 +8,9 @@ import DashboardComponent from '../../containers/DashboardComponent';
import DeleteComponentButton from '../DeleteComponentButton';
import HoverMenu from '../menu/HoverMenu';
import { componentShape } from '../../util/propShapes';
-import { NEW_TAB_ID } from '../../util/constants';
+import { NEW_TAB_ID, DASHBOARD_ROOT_ID } from '../../util/constants';
import { RENDER_TAB, RENDER_TAB_CONTENT } from './Tab';
+import { TAB_TYPE } from '../../util/componentTypes';
const NEW_TAB_INDEX = -1;
const MAX_TAB_COUNT = 5;
@@ -21,13 +22,14 @@ const propTypes = {
parentComponent: componentShape.isRequired,
index: PropTypes.number.isRequired,
depth: PropTypes.number.isRequired,
+ renderTabContent: PropTypes.bool,
// grid related
- availableColumnCount: PropTypes.number.isRequired,
- columnWidth: PropTypes.number.isRequired,
- onResizeStart: PropTypes.func.isRequired,
- onResize: PropTypes.func.isRequired,
- onResizeStop: PropTypes.func.isRequired,
+ availableColumnCount: PropTypes.number,
+ columnWidth: PropTypes.number,
+ onResizeStart: PropTypes.func,
+ onResize: PropTypes.func,
+ onResizeStop: PropTypes.func,
// dnd
createComponent: PropTypes.func.isRequired,
@@ -40,6 +42,12 @@ const propTypes = {
const defaultProps = {
onChangeTab: null,
children: null,
+ renderTabContent: true,
+ availableColumnCount: 0,
+ columnWidth: 0,
+ onResizeStart() {},
+ onResize() {},
+ onResizeStop() {},
};
class Tabs extends React.PureComponent {
@@ -48,8 +56,9 @@ class Tabs extends React.PureComponent {
this.state = {
tabIndex: 0,
};
- this.handleClicKTab = this.handleClicKTab.bind(this);
+ this.handleClickTab = this.handleClickTab.bind(this);
this.handleDeleteComponent = this.handleDeleteComponent.bind(this);
+ this.handleDeleteTab = this.handleDeleteTab.bind(this);
this.handleDropOnTab = this.handleDropOnTab.bind(this);
}
@@ -60,7 +69,7 @@ class Tabs extends React.PureComponent {
}
}
- handleClicKTab(tabIndex) {
+ handleClickTab(tabIndex) {
const { onChangeTab, component, createComponent } = this.props;
if (tabIndex !== NEW_TAB_INDEX && tabIndex !== this.state.tabIndex) {
@@ -71,10 +80,14 @@ class Tabs extends React.PureComponent {
} else if (tabIndex === NEW_TAB_INDEX) {
createComponent({
destination: {
- droppableId: component.id,
+ id: component.id,
+ type: component.type,
index: component.children.length,
},
- draggableId: NEW_TAB_ID,
+ dragging: {
+ id: NEW_TAB_ID,
+ type: TAB_TYPE,
+ },
});
}
}
@@ -84,19 +97,23 @@ class Tabs extends React.PureComponent {
deleteComponent(id, parentId);
}
+ handleDeleteTab(tabIndex) {
+ this.handleClickTab(Math.max(0, tabIndex - 1));
+ }
+
handleDropOnTab(dropResult) {
const { component } = this.props;
// Ensure dropped tab is visible
const { destination } = dropResult;
if (destination) {
- const dropTabIndex = destination.droppableId === component.id
+ const dropTabIndex = destination.id === component.id
? destination.index // dropped ON tabs
- : component.children.indexOf(destination.droppableId); // dropped IN tab
+ : component.children.indexOf(destination.id); // dropped IN tab
if (dropTabIndex > -1) {
setTimeout(() => {
- this.handleClicKTab(dropTabIndex);
+ this.handleClickTab(dropTabIndex);
}, 30);
}
}
@@ -114,6 +131,7 @@ class Tabs extends React.PureComponent {
onResize,
onResizeStop,
handleComponentDrop,
+ renderTabContent,
} = this.props;
const { tabIndex: selectedTabIndex } = this.state;
@@ -125,6 +143,7 @@ class Tabs extends React.PureComponent {
parentComponent={parentComponent}
orientation="row"
index={index}
+ depth={depth}
onDrop={handleComponentDrop}
>
{({ dropIndicatorProps: tabsDropIndicatorProps, dragSourceRef: tabsDragSourceRef }) => (
@@ -137,7 +156,7 @@ class Tabs extends React.PureComponent {
<BootstrapTabs
id={tabsComponent.id}
activeKey={selectedTabIndex}
- onSelect={this.handleClicKTab}
+ onSelect={this.handleClickTab}
animation={false}
>
{tabIds.map((tabId, tabIndex) => (
@@ -156,10 +175,8 @@ class Tabs extends React.PureComponent {
renderType={RENDER_TAB}
availableColumnCount={availableColumnCount}
columnWidth={columnWidth}
- onResizeStart={onResizeStart}
- onResize={onResize}
- onResizeStop={onResizeStop}
onDropOnTab={this.handleDropOnTab}
+ onDeleteTab={this.handleDeleteTab}
/>
}
>
@@ -168,11 +185,11 @@ class Tabs extends React.PureComponent {
render potentially-expensive charts (this also enables lazy loading
their content)
*/}
- {tabIndex === selectedTabIndex &&
+ {tabIndex === selectedTabIndex && renderTabContent &&
<DashboardComponent
id={tabId}
parentId={tabsComponent.id}
- depth={depth}
+ depth={depth} // see isValidChild.js for why tabs don't increment child depth
index={tabIndex}
renderType={RENDER_TAB_CONTENT}
availableColumnCount={availableColumnCount}
@@ -188,14 +205,14 @@ class Tabs extends React.PureComponent {
{tabIds.length < MAX_TAB_COUNT &&
<BootstrapTab
eventKey={NEW_TAB_INDEX}
- title={<div className="fa fa-plus-square" />}
+ title={<div className="fa fa-plus" />}
/>}
</BootstrapTabs>
+ {/* don't indicate that a drop on root is allowed when tabs already exist */}
{tabsDropIndicatorProps
- && tabsDropIndicatorProps.style
- && tabsDropIndicatorProps.style.width === '100%'
+ && parentComponent.id !== DASHBOARD_ROOT_ID
&& <div {...tabsDropIndicatorProps} />}
</div>
diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/new/DraggableNewComponent.jsx b/superset/assets/javascripts/dashboard/v2/components/gridComponents/new/DraggableNewComponent.jsx
index c4d8d62..778f58e 100644
--- a/superset/assets/javascripts/dashboard/v2/components/gridComponents/new/DraggableNewComponent.jsx
+++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/new/DraggableNewComponent.jsx
@@ -3,6 +3,8 @@ import PropTypes from 'prop-types';
import cx from 'classnames';
import DragDroppable from '../../dnd/DragDroppable';
+import { NEW_COMPONENTS_SOURCE_ID } from '../../../util/constants';
+import { NEW_COMPONENT_SOURCE_TYPE } from '../../../util/componentTypes';
const propTypes = {
id: PropTypes.string.isRequired,
@@ -21,8 +23,9 @@ export default class DraggableNewComponent extends React.PureComponent {
return (
<DragDroppable
component={{ type, id }}
- parentComponent={null}
+ parentComponent={{ id: NEW_COMPONENTS_SOURCE_ID, type: NEW_COMPONENT_SOURCE_TYPE }}
index={0}
+ depth={0}
>
{({ dragSourceRef }) => (
<div ref={dragSourceRef} className="new-component">
diff --git a/superset/assets/javascripts/dashboard/v2/components/menu/RowStyleDropdown.jsx b/superset/assets/javascripts/dashboard/v2/components/menu/BackgroundStyleDropdown.jsx
similarity index 65%
rename from superset/assets/javascripts/dashboard/v2/components/menu/RowStyleDropdown.jsx
rename to superset/assets/javascripts/dashboard/v2/components/menu/BackgroundStyleDropdown.jsx
index d3c7eff..41cf1df 100644
--- a/superset/assets/javascripts/dashboard/v2/components/menu/RowStyleDropdown.jsx
+++ b/superset/assets/javascripts/dashboard/v2/components/menu/BackgroundStyleDropdown.jsx
@@ -2,7 +2,7 @@ import React from 'react';
import PropTypes from 'prop-types';
import cx from 'classnames';
-import rowStyleOptions from '../../util/rowStyleOptions';
+import backgroundStyleOptions from '../../util/backgroundStyleOptions';
import PopoverDropdown from './PopoverDropdown';
const propTypes = {
@@ -13,7 +13,7 @@ const propTypes = {
function renderButton(option) {
return (
- <div className={cx('row-style-option', option.className)}>
+ <div className={cx('background-style-option', option.className)}>
{`${option.label} background`}
</div>
);
@@ -21,19 +21,19 @@ function renderButton(option) {
function renderOption(option) {
return (
- <div className={cx('row-style-option', option.className)}>
+ <div className={cx('background-style-option', option.className)}>
{option.label}
</div>
);
}
-export default class RowStyleDropdown extends React.PureComponent {
+export default class BackgroundStyleDropdown extends React.PureComponent {
render() {
const { id, value, onChange } = this.props;
return (
<PopoverDropdown
id={id}
- options={rowStyleOptions}
+ options={backgroundStyleOptions}
value={value}
onChange={onChange}
renderButton={renderButton}
@@ -43,4 +43,4 @@ export default class RowStyleDropdown extends React.PureComponent {
}
}
-RowStyleDropdown.propTypes = propTypes;
+BackgroundStyleDropdown.propTypes = propTypes;
diff --git a/superset/assets/javascripts/dashboard/v2/components/menu/WithPopoverMenu.jsx b/superset/assets/javascripts/dashboard/v2/components/menu/WithPopoverMenu.jsx
index 7fb24cd..2054090 100644
--- a/superset/assets/javascripts/dashboard/v2/components/menu/WithPopoverMenu.jsx
+++ b/superset/assets/javascripts/dashboard/v2/components/menu/WithPopoverMenu.jsx
@@ -8,6 +8,7 @@ const propTypes = {
menuItems: PropTypes.arrayOf(PropTypes.node),
onChangeFocus: PropTypes.func,
isFocused: PropTypes.bool,
+ shouldFocus: PropTypes.func,
};
const defaultProps = {
@@ -17,6 +18,7 @@ const defaultProps = {
onPressDelete() {},
menuItems: [],
isFocused: false,
+ shouldFocus: (event, container) => container.contains(event.target),
};
class WithPopoverMenu extends React.PureComponent {
@@ -47,8 +49,10 @@ class WithPopoverMenu extends React.PureComponent {
}
handleClick(event) {
- const { onChangeFocus } = this.props;
- if (!this.state.isFocused) {
+ const { onChangeFocus, shouldFocus: shouldFocusThunk } = this.props;
+ const shouldFocus = shouldFocusThunk(event, this.container);
+
+ if (shouldFocus && !this.state.isFocused) {
// if not focused, set focus and add a window event listener to capture outside clicks
// this enables us to not set a click listener for ever item on a dashboard
document.addEventListener('click', this.handleClick, true);
@@ -57,7 +61,7 @@ class WithPopoverMenu extends React.PureComponent {
if (onChangeFocus) {
onChangeFocus(true);
}
- } else if (!this.container.contains(event.target)) {
+ } else if (!shouldFocus && this.state.isFocused) {
document.removeEventListener('click', this.handleClick, true);
document.removeEventListener('drag', this.handleClick, true);
this.setState(() => ({ isFocused: false }));
diff --git a/superset/assets/javascripts/dashboard/v2/components/resizable/ResizableContainer.jsx b/superset/assets/javascripts/dashboard/v2/components/resizable/ResizableContainer.jsx
index 5e43678..fbb7d1d 100644
--- a/superset/assets/javascripts/dashboard/v2/components/resizable/ResizableContainer.jsx
+++ b/superset/assets/javascripts/dashboard/v2/components/resizable/ResizableContainer.jsx
@@ -50,9 +50,9 @@ const defaultProps = {
onResizeStart: null,
};
-// because columns are not actually multiples of a single variable (width = n*cols + (n-1)*gutters)
-// we snap to the base unit and then snap to actual column multiples on stop
-const snapToGrid = [GRID_BASE_UNIT, GRID_BASE_UNIT];
+// because columns are not multiples of a single variable (width = n*cols + (n-1) * gutters)
+// we snap to the base unit and then snap to _actual_ column multiples on stop
+const SNAP_TO_GRID = [GRID_BASE_UNIT, GRID_BASE_UNIT];
class ResizableContainer extends React.PureComponent {
constructor(props) {
@@ -120,9 +120,12 @@ class ResizableContainer extends React.PureComponent {
adjustableHeight,
widthStep,
heightStep,
- staticHeightMultiple,
widthMultiple,
heightMultiple,
+ staticHeight,
+ staticHeightMultiple,
+ staticWidth,
+ staticWidthMultiple,
minWidthMultiple,
maxWidthMultiple,
minHeightMultiple,
@@ -132,42 +135,48 @@ class ResizableContainer extends React.PureComponent {
const size = {
width: adjustableWidth
- ? ((widthStep + gutterWidth) * widthMultiple) - gutterWidth : undefined,
+ ? ((widthStep + gutterWidth) * widthMultiple) - gutterWidth
+ : (staticWidthMultiple && staticWidthMultiple * widthStep)
+ || staticWidth
+ || undefined,
height: adjustableHeight
? heightStep * heightMultiple
- : (staticHeightMultiple && staticHeightMultiple * heightStep) || undefined,
+ : (staticHeightMultiple && staticHeightMultiple * heightStep)
+ || staticHeight
+ || undefined,
};
- let enableConfig = resizableConfig.widthAndHeight;
- if (!adjustableHeight) enableConfig = resizableConfig.widthOnly;
- else if (!adjustableWidth) enableConfig = resizableConfig.heightOnly;
+ let enableConfig = resizableConfig.notAdjustable;
+ if (adjustableWidth && adjustableHeight) enableConfig = resizableConfig.widthAndHeight;
+ else if (adjustableWidth) enableConfig = resizableConfig.widthOnly;
+ else if (adjustableHeight) enableConfig = resizableConfig.heightOnly;
const { isResizing } = this.state;
return (
<Resizable
enable={enableConfig}
- grid={snapToGrid}
+ grid={SNAP_TO_GRID}
minWidth={adjustableWidth
? (minWidthMultiple * (widthStep + gutterWidth)) - gutterWidth
- : size.width}
+ : undefined}
minHeight={adjustableHeight
? (minHeightMultiple * heightStep)
- : size.height}
+ : undefined}
maxWidth={adjustableWidth
? (maxWidthMultiple * (widthStep + gutterWidth)) - gutterWidth
- : size.width}
+ : undefined}
maxHeight={adjustableHeight
? (maxHeightMultiple * heightStep)
- : size.height}
+ : undefined}
size={size}
onResizeStart={this.handleResizeStart}
onResize={this.handleResize}
onResizeStop={this.handleResizeStop}
handleComponent={ResizableHandle}
className={cx(
- 'grid-resizable-container',
- isResizing && 'grid-resizable-container--resizing',
+ 'resizable-container',
+ isResizing && 'resizable-container--resizing',
)}
>
{children}
diff --git a/superset/assets/javascripts/dashboard/v2/containers/DashboardGrid.jsx b/superset/assets/javascripts/dashboard/v2/containers/DashboardBuilder.jsx
similarity index 59%
copy from superset/assets/javascripts/dashboard/v2/containers/DashboardGrid.jsx
copy to superset/assets/javascripts/dashboard/v2/containers/DashboardBuilder.jsx
index 741151b..6bd8658 100644
--- a/superset/assets/javascripts/dashboard/v2/containers/DashboardGrid.jsx
+++ b/superset/assets/javascripts/dashboard/v2/containers/DashboardBuilder.jsx
@@ -1,23 +1,23 @@
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
-import DashboardGrid from '../components/DashboardGrid';
+import DashboardBuilder from '../components/DashboardBuilder';
import {
- updateComponents,
+ deleteTopLevelTabs,
handleComponentDrop,
} from '../actions';
-function mapStateToProps({ dashboard = {} }) {
+function mapStateToProps({ dashboard: undoableDashboard }) {
return {
- dashboard,
+ dashboard: undoableDashboard.present,
};
}
function mapDispatchToProps(dispatch) {
return bindActionCreators({
- updateComponents,
+ deleteTopLevelTabs,
handleComponentDrop,
}, dispatch);
}
-export default connect(mapStateToProps, mapDispatchToProps)(DashboardGrid);
+export default connect(mapStateToProps, mapDispatchToProps)(DashboardBuilder);
diff --git a/superset/assets/javascripts/dashboard/v2/containers/DashboardComponent.jsx b/superset/assets/javascripts/dashboard/v2/containers/DashboardComponent.jsx
index 1340781..f7e86cc 100644
--- a/superset/assets/javascripts/dashboard/v2/containers/DashboardComponent.jsx
+++ b/superset/assets/javascripts/dashboard/v2/containers/DashboardComponent.jsx
@@ -4,9 +4,10 @@ import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import ComponentLookup from '../components/gridComponents';
-import countChildRowsAndColumns from '../util/countChildRowsAndColumns';
+import getTotalChildWidth from '../util/getChildWidth';
import { componentShape } from '../util/propShapes';
-import { ROW_TYPE } from '../util/componentTypes';
+import { COLUMN_TYPE, ROW_TYPE } from '../util/componentTypes';
+import { GRID_MIN_COLUMN_COUNT } from '../util/constants';
import {
createComponent,
@@ -24,23 +25,31 @@ const propTypes = {
handleComponentDrop: PropTypes.func.isRequired,
};
-function mapStateToProps({ dashboard = {} }, ownProps) {
+function mapStateToProps({ dashboard: undoableDashboard }, ownProps) {
+ const components = undoableDashboard.present;
const { id, parentId } = ownProps;
+ const component = components[id];
const props = {
- component: dashboard[id],
- parentComponent: dashboard[parentId],
+ component,
+ parentComponent: components[parentId],
};
- // row is a special component that needs extra dims about its children
+ // rows and columns need more data about their child dimensions
// doing this allows us to not pass the entire component lookup to all Components
if (props.component.type === ROW_TYPE) {
- const { rowCount, columnCount } = countChildRowsAndColumns({
- component: props.component,
- components: dashboard,
- });
+ props.occupiedColumnCount = getTotalChildWidth({ id, components });
+ } else if (props.component.type === COLUMN_TYPE) {
+ props.minColumnWidth = GRID_MIN_COLUMN_COUNT;
- props.occupiedRowCount = rowCount;
- props.occupiedColumnCount = columnCount;
+ component.children.forEach((childId) => {
+ // rows don't have widths, so find the width of its children
+ if (components[childId].type === ROW_TYPE) {
+ props.minColumnWidth = Math.max(
+ props.minColumnWidth,
+ getTotalChildWidth({ id: childId, components }),
+ );
+ }
+ });
}
return props;
diff --git a/superset/assets/javascripts/dashboard/v2/containers/DashboardGrid.jsx b/superset/assets/javascripts/dashboard/v2/containers/DashboardGrid.jsx
index 741151b..eb01616 100644
--- a/superset/assets/javascripts/dashboard/v2/containers/DashboardGrid.jsx
+++ b/superset/assets/javascripts/dashboard/v2/containers/DashboardGrid.jsx
@@ -3,21 +3,15 @@ import { connect } from 'react-redux';
import DashboardGrid from '../components/DashboardGrid';
import {
- updateComponents,
handleComponentDrop,
+ resizeComponent,
} from '../actions';
-function mapStateToProps({ dashboard = {} }) {
- return {
- dashboard,
- };
-}
-
function mapDispatchToProps(dispatch) {
return bindActionCreators({
- updateComponents,
handleComponentDrop,
+ resizeComponent,
}, dispatch);
}
-export default connect(mapStateToProps, mapDispatchToProps)(DashboardGrid);
+export default connect(null, mapDispatchToProps)(DashboardGrid);
diff --git a/superset/assets/javascripts/dashboard/v2/containers/DashboardHeader.jsx b/superset/assets/javascripts/dashboard/v2/containers/DashboardHeader.jsx
new file mode 100644
index 0000000..52e7e7a
--- /dev/null
+++ b/superset/assets/javascripts/dashboard/v2/containers/DashboardHeader.jsx
@@ -0,0 +1,31 @@
+import { ActionCreators as UndoActionCreators } from 'redux-undo'
+import { bindActionCreators } from 'redux';
+import { connect } from 'react-redux';
+
+import DashboardHeader from '../components/DashboardHeader';
+import { DASHBOARD_HEADER_ID } from '../util/constants';
+
+import {
+ updateComponents,
+ handleComponentDrop,
+} from '../actions';
+
+function mapStateToProps({ dashboard: undoableDashboard }) {
+ return {
+ component: undoableDashboard.present[DASHBOARD_HEADER_ID],
+ canUndo: undoableDashboard.past.length > 0,
+ canRedo: undoableDashboard.future.length > 0,
+ };
+}
+
+function mapDispatchToProps(dispatch) {
+ return bindActionCreators({
+ updateComponents,
+ handleComponentDrop,
+ onUndo: UndoActionCreators.undo,
+ onRedo: UndoActionCreators.redo,
+ }, dispatch);
+}
+
+
+export default connect(mapStateToProps, mapDispatchToProps)(DashboardHeader);
diff --git a/superset/assets/javascripts/dashboard/v2/fixtures/emptyDashboardLayout.js b/superset/assets/javascripts/dashboard/v2/fixtures/emptyDashboardLayout.js
new file mode 100644
index 0000000..7816cc2
--- /dev/null
+++ b/superset/assets/javascripts/dashboard/v2/fixtures/emptyDashboardLayout.js
@@ -0,0 +1,36 @@
+import {
+ DASHBOARD_GRID_TYPE,
+ DASHBOARD_HEADER_TYPE,
+ DASHBOARD_ROOT_TYPE,
+} from '../util/componentTypes';
+
+import {
+ DASHBOARD_ROOT_ID,
+ DASHBOARD_HEADER_ID,
+ DASHBOARD_GRID_ID,
+} from '../util/constants';
+
+export default {
+ [DASHBOARD_ROOT_ID]: {
+ type: DASHBOARD_ROOT_TYPE,
+ id: DASHBOARD_ROOT_ID,
+ children: [
+ DASHBOARD_GRID_ID,
+ ],
+ },
+
+ [DASHBOARD_GRID_ID]: {
+ type: DASHBOARD_GRID_TYPE,
+ id: DASHBOARD_GRID_ID,
+ children: [],
+ meta: {},
+ },
+
+ [DASHBOARD_HEADER_ID]: {
+ type: DASHBOARD_HEADER_TYPE,
+ id: DASHBOARD_HEADER_ID,
+ meta: {
+ text: 'New dashboard',
+ },
+ },
+};
diff --git a/superset/assets/javascripts/dashboard/v2/fixtures/testLayout.js b/superset/assets/javascripts/dashboard/v2/fixtures/testLayout.js
deleted file mode 100644
index c3ce897..0000000
--- a/superset/assets/javascripts/dashboard/v2/fixtures/testLayout.js
+++ /dev/null
@@ -1,161 +0,0 @@
-import {
- COLUMN_TYPE,
- HEADER_TYPE,
- ROW_TYPE,
- SPACER_TYPE,
- TAB_TYPE,
- TABS_TYPE,
- CHART_TYPE,
- DIVIDER_TYPE,
- GRID_ROOT_TYPE,
-} from '../util/componentTypes';
-
-import { DASHBOARD_ROOT_ID } from '../util/constants';
-
-export default {
- [DASHBOARD_ROOT_ID]: {
- type: GRID_ROOT_TYPE,
- id: DASHBOARD_ROOT_ID,
- children: [
- // 'header0',
- // 'row0',
- // 'divider0',
- // 'row1',
- // 'tabs0',
- // 'divider1',
- ],
- },
- // row0: {
- // id: 'row0',
- // type: INVISIBLE_ROW_TYPE,
- // children: [
- // // 'charta',
- // // 'chartb',
- // // 'chartc',
- // ],
- // },
- // row1: {
- // id: 'row1',
- // type: ROW_TYPE,
- // children: [
- // 'header1',
- // ],
- // },
- // row2: {
- // id: 'row2',
- // type: ROW_TYPE,
- // children: [
- // 'chartd',
- // 'spacer0',
- // 'charte',
- // ],
- // },
- // tabs0: {
- // id: 'tabs0',
- // type: TABS_TYPE,
- // children: [
- // 'tab0',
- // 'tab1',
- // 'tab3',
- // ],
- // meta: {
- // },
- // },
- // tab0: {
- // id: 'tab0',
- // type: TAB_TYPE,
- // children: [
- // // 'row2',
- // ],
- // meta: {
- // text: 'Tab A',
- // },
- // },
- // tab1: {
- // id: 'tab1',
- // type: TAB_TYPE,
- // children: [
- // ],
- // meta: {
- // text: 'Tab B',
- // },
- // },
- // tab3: {
- // id: 'tab3',
- // type: TAB_TYPE,
- // children: [
- // ],
- // meta: {
- // text: 'Tab C',
- // },
- // },
- // header0: {
- // id: 'header0',
- // type: HEADER_TYPE,
- // meta: {
- // text: 'Header 1',
- // },
- // },
- // header1: {
- // id: 'header1',
- // type: HEADER_TYPE,
- // meta: {
- // text: 'Header 2',
- // },
- // },
- // divider0: {
- // id: 'divider0',
- // type: DIVIDER_TYPE,
- // },
- // divider1: {
- // id: 'divider1',
- // type: DIVIDER_TYPE,
- // },
- // charta: {
- // id: 'charta',
- // type: CHART_TYPE,
- // meta: {
- // width: 3,
- // height: 10,
- // },
- // },
- // chartb: {
- // id: 'chartb',
- // type: CHART_TYPE,
- // meta: {
- // width: 3,
- // height: 10,
- // },
- // },
- // chartc: {
- // id: 'chartc',
- // type: CHART_TYPE,
- // meta: {
- // width: 3,
- // height: 10,
- // },
- // },
- // chartd: {
- // id: 'chartd',
- // type: CHART_TYPE,
- // meta: {
- // width: 3,
- // height: 10,
- // },
- // },
- // charte: {
- // id: 'charte',
- // type: CHART_TYPE,
- // meta: {
- // width: 3,
- // height: 10,
- // },
- // },
- // spacer0: {
- // id: 'spacer0',
- // type: SPACER_TYPE,
- // meta: {
- // width: 1,
- // },
- // },
-};
diff --git a/superset/assets/javascripts/dashboard/v2/reducers/dashboard.js b/superset/assets/javascripts/dashboard/v2/reducers/dashboard.js
index 19fa9d7..9b03861 100644
--- a/superset/assets/javascripts/dashboard/v2/reducers/dashboard.js
+++ b/superset/assets/javascripts/dashboard/v2/reducers/dashboard.js
@@ -1,14 +1,25 @@
+import { DASHBOARD_ROOT_ID, DASHBOARD_GRID_ID, NEW_COMPONENTS_SOURCE_ID } from '../util/constants';
import newComponentFactory from '../util/newComponentFactory';
import newEntitiesFromDrop from '../util/newEntitiesFromDrop';
import reorderItem from '../util/dnd-reorder';
import shouldWrapChildInRow from '../util/shouldWrapChildInRow';
-import { ROW_TYPE } from '../util/componentTypes';
+import {
+ CHART_TYPE,
+ COLUMN_TYPE,
+ MARKDOWN_TYPE,
+ ROW_TYPE,
+ TAB_TYPE,
+ TABS_TYPE,
+
+} from '../util/componentTypes';
import {
UPDATE_COMPONENTS,
DELETE_COMPONENT,
CREATE_COMPONENT,
MOVE_COMPONENT,
+ CREATE_TOP_LEVEL_TABS,
+ DELETE_TOP_LEVEL_TABS,
} from '../actions';
const actionHandlers = {
@@ -28,12 +39,11 @@ const actionHandlers = {
const nextComponents = { ...state };
// recursively find children to remove
- let deleteCount = 0;
function recursivelyDeleteChildren(componentId, componentParentId) {
// delete child and it's children
const component = nextComponents[componentId];
delete nextComponents[componentId];
- deleteCount += 1;
+
const { children = [] } = component;
children.forEach((childId) => { recursivelyDeleteChildren(childId, componentId); });
@@ -52,14 +62,30 @@ const actionHandlers = {
}
recursivelyDeleteChildren(id, parentId);
- console.log('deleted', deleteCount, 'total components', nextComponents);
return nextComponents;
},
[CREATE_COMPONENT](state, action) {
const { payload: { dropResult } } = action;
+ const { destination, dragging } = dropResult;
const newEntities = newEntitiesFromDrop({ dropResult, components: state });
+
+ // inherit the width of a column parent
+ if (destination.type === COLUMN_TYPE && [CHART_TYPE, MARKDOWN_TYPE].includes(dragging.type)) {
+ const newEntitiesArray = Object.values(newEntities);
+ const component = newEntitiesArray.find(entity => entity.type === dragging.type);
+ const parentColumn = newEntities[destination.id];
+
+ newEntities[component.id] = {
+ ...component,
+ meta: {
+ ...component.meta,
+ width: parentColumn.meta.width,
+ },
+ };
+ }
+
return {
...state,
...newEntities,
@@ -68,9 +94,9 @@ const actionHandlers = {
[MOVE_COMPONENT](state, action) {
const { payload: { dropResult } } = action;
- const { source, destination, draggableId } = dropResult;
+ const { source, destination, dragging } = dropResult;
- if (!source || !destination || !draggableId) return state;
+ if (!source || !destination || !dragging) return state;
const nextEntities = reorderItem({
entitiesMap: state,
@@ -78,16 +104,14 @@ const actionHandlers = {
destination,
});
- // wrap the dragged component in a row depening on destination type
- const destinationType = (state[destination.droppableId] || {}).type;
- const draggableType = (state[draggableId] || {}).type;
+ // wrap the dragged component in a row depending on destination type
const wrapInRow = shouldWrapChildInRow({
- parentType: destinationType,
- childType: draggableType,
+ parentType: destination.type,
+ childType: dragging.type,
});
if (wrapInRow) {
- const destinationEntity = nextEntities[destination.droppableId];
+ const destinationEntity = nextEntities[destination.id];
const destinationChildren = destinationEntity.children;
const newRow = newComponentFactory(ROW_TYPE);
newRow.children = [destinationChildren[destination.index]];
@@ -95,11 +119,109 @@ const actionHandlers = {
nextEntities[newRow.id] = newRow;
}
+ // inherit the width of a column parent
+ if (destination.type === COLUMN_TYPE && [CHART_TYPE, MARKDOWN_TYPE].includes(dragging.type)) {
+ const component = nextEntities[dragging.id];
+ const parentColumn = nextEntities[destination.id];
+ nextEntities[dragging.id] = {
+ ...component,
+ meta: {
+ ...component.meta,
+ width: parentColumn.meta.width,
+ },
+ };
+ }
+
return {
...state,
...nextEntities,
};
},
+
+ [CREATE_TOP_LEVEL_TABS](state, action) {
+ const { payload: { dropResult } } = action;
+ const { source, dragging } = dropResult;
+
+ // move children of current root to be children of the dragging tab
+ const rootComponent = state[DASHBOARD_ROOT_ID];
+ const topLevelId = rootComponent.children[0];
+ const topLevelComponent = state[topLevelId];
+
+ if (source.id !== NEW_COMPONENTS_SOURCE_ID) {
+ // component already exists
+ const draggingTabs = state[dragging.id];
+ const draggingTabId = draggingTabs.children[0];
+ const draggingTab = state[draggingTabId];
+
+ // move all children except the one that is dragging
+ const childrenToMove = [...topLevelComponent.children].filter(id => id !== dragging.id);
+
+ return {
+ ...state,
+ [DASHBOARD_ROOT_ID]: {
+ ...rootComponent,
+ children: [dragging.id],
+ },
+ [topLevelId]: {
+ ...topLevelComponent,
+ children: [],
+ },
+ [draggingTabId]: {
+ ...draggingTab,
+ children: [
+ ...draggingTab.children,
+ ...childrenToMove,
+ ],
+ },
+ };
+ }
+
+ // create new component
+ const newEntities = newEntitiesFromDrop({ dropResult, components: state });
+ const newEntitiesArray = Object.values(newEntities);
+ const tabComponent = newEntitiesArray.find(component => component.type === TAB_TYPE);
+ const tabsComponent = newEntitiesArray.find(component => component.type === TABS_TYPE);
+
+ tabComponent.children = [...topLevelComponent.children];
+ newEntities[topLevelId] = { ...topLevelComponent, children: [] };
+ newEntities[DASHBOARD_ROOT_ID] = { ...rootComponent, children: [tabsComponent.id] };
+
+ return {
+ ...state,
+ ...newEntities,
+ };
+ },
+
+ [DELETE_TOP_LEVEL_TABS](state) {
+ const rootComponent = state[DASHBOARD_ROOT_ID];
+ const topLevelId = rootComponent.children[0];
+ const topLevelTabs = state[topLevelId];
+
+ if (topLevelTabs.type !== TABS_TYPE) return state;
+
+ let childrenToMove = [];
+ const nextEntities = { ...state };
+
+ topLevelTabs.children.forEach((tabId) => {
+ const tabComponent = state[tabId];
+ childrenToMove = [...childrenToMove, ...tabComponent.children];
+ delete nextEntities[tabId];
+ });
+
+ delete nextEntities[topLevelId];
+
+ nextEntities[DASHBOARD_ROOT_ID] = {
+ ...rootComponent,
+ children: [DASHBOARD_GRID_ID],
+ };
+
+ nextEntities[DASHBOARD_GRID_ID] = {
+ ...(state[DASHBOARD_GRID_ID]),
+ children: childrenToMove,
+ };
+
+ return nextEntities;
+ },
};
export default function dashboardReducer(state = {}, action) {
diff --git a/superset/assets/javascripts/dashboard/v2/reducers/index.js b/superset/assets/javascripts/dashboard/v2/reducers/index.js
index 103fda0..9c0575e 100644
--- a/superset/assets/javascripts/dashboard/v2/reducers/index.js
+++ b/superset/assets/javascripts/dashboard/v2/reducers/index.js
@@ -1,6 +1,13 @@
import { combineReducers } from 'redux';
+import undoable, { distinctState } from 'redux-undo';
+
import dashboard from './dashboard';
+const undoableDashboard = undoable(dashboard, {
+ limit: 10,
+ filter: distinctState(),
+});
+
export default combineReducers({
- dashboard,
+ dashboard: undoableDashboard,
});
diff --git a/superset/assets/javascripts/dashboard/v2/stylesheets/builder.less b/superset/assets/javascripts/dashboard/v2/stylesheets/builder.less
new file mode 100644
index 0000000..5f1a5b0
--- /dev/null
+++ b/superset/assets/javascripts/dashboard/v2/stylesheets/builder.less
@@ -0,0 +1,64 @@
+.dashboard-v2 {
+ margin-top: -20px;
+ position: relative;
+ color: @almost-black;
+}
+
+.dashboard-header {
+ background: white;
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ justify-content: space-between;
+ padding: 0 24px;
+ box-shadow: 0 4px 4px 0 rgba(0, 0, 0, 0.1); /* @TODO color */
+}
+
+.dashboard-builder {
+ display: flex;
+ flex-direction: row;
+ flex-wrap: nowrap;
+ height: auto;
+}
+
+/* only top-level tabs have popover, give it more padding to match header + tabs */
+.dashboard-v2 > .with-popover-menu > .popover-menu {
+ left: 24px;
+}
+
+/* drop shadow for top-level tabs only */
+.dashboard-v2 .dashboard-component-tabs {
+ box-shadow: 0 4px 4px 0 rgba(0, 0, 0, 0.1);
+ padding-left: 8px; /* note this is added to tab-level padding, to match header */
+}
+
+.dashboard-builder .grid-container .dashboard-component-tabs {
+ box-shadow: none;
+ padding-left: 0;
+}
+
+.dashboard-builder > div:first-child {
+ width: 100%;
+ flex-grow: 1;
+ position: relative;
+}
+
+.dashboard-builder-sidepane {
+ background: white;
+ flex: 0 0 376px;
+ border: 1px solid @gray-light;
+ z-index: 1;
+}
+
+.dashboard-builder-sidepane-header {
+ font-size: 15px;
+ font-weight: 700;
+ border-bottom: 1px solid @gray-light;
+ padding: 14px;
+}
+
+/* @TODO remove upon new theme */
+.btn.btn-primary {
+ background: @almost-black !important;
+ color: white !important;
+}
diff --git a/superset/assets/javascripts/dashboard/v2/stylesheets/buttons.less b/superset/assets/javascripts/dashboard/v2/stylesheets/buttons.less
index a8dd661..41ca478 100644
--- a/superset/assets/javascripts/dashboard/v2/stylesheets/buttons.less
+++ b/superset/assets/javascripts/dashboard/v2/stylesheets/buttons.less
@@ -1,6 +1,6 @@
.icon-button {
color: @gray;
- font-size: 1em;
+ font-size: 1.2em;
display: flex;
flex-direction: row;
align-items: center;
@@ -15,3 +15,9 @@
outline: none;
text-decoration: none;
}
+
+.icon-button-label {
+ color: @gray-dark;
+ padding-left: 8px;
+ font-size: 0.9em;
+}
diff --git a/superset/assets/javascripts/dashboard/v2/stylesheets/components/DashboardBuilder.jsx b/superset/assets/javascripts/dashboard/v2/stylesheets/components/DashboardBuilder.jsx
new file mode 100644
index 0000000..e011ad4
--- /dev/null
+++ b/superset/assets/javascripts/dashboard/v2/stylesheets/components/DashboardBuilder.jsx
@@ -0,0 +1,127 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import HTML5Backend from 'react-dnd-html5-backend';
+import { DragDropContext } from 'react-dnd';
+
+import BuilderComponentPane from './BuilderComponentPane';
+import DashboardHeader from '../containers/DashboardHeader';
+import DashboardGrid from './DashboardGrid';
+import IconButton from './IconButton';
+import DragDroppable from './dnd/DragDroppable';
+import DashboardComponent from '../containers/DashboardComponent';
+import WithPopoverMenu from './menu/WithPopoverMenu';
+
+import {
+ DASHBOARD_GRID_ID,
+ DASHBOARD_ROOT_ID,
+ DASHBOARD_ROOT_DEPTH,
+} from '../util/constants';
+
+const propTypes = {
+ editMode: PropTypes.bool,
+
+ // redux
+ dashboard: PropTypes.object.isRequired,
+ deleteTopLevelTabs: PropTypes.func.isRequired,
+ updateComponents: PropTypes.func.isRequired,
+ handleComponentDrop: PropTypes.func.isRequired,
+};
+
+const defaultProps = {
+ editMode: true,
+};
+
+class DashboardBuilder extends React.Component {
+ static shouldFocusTabs(event, container) {
+ // don't focus the tabs when we click on a tab
+ return event.target.tagName === 'UL' || (
+ /icon-button/.test(event.target.className) && container.contains(event.target)
+ );
+ }
+
+ constructor(props) {
+ super(props);
+ this.state = {
+ tabIndex: 0, // top-level tabs
+ };
+ this.handleChangeTab = this.handleChangeTab.bind(this);
+ }
+
+ handleChangeTab({ tabIndex }) {
+ this.setState(() => ({ tabIndex }));
+ }
+
+ render() {
+ const { tabIndex } = this.state;
+ const { handleComponentDrop, updateComponents, dashboard, deleteTopLevelTabs } = this.props;
+ const dashboardRoot = dashboard[DASHBOARD_ROOT_ID];
+ const rootChildId = dashboardRoot.children[0];
+ const topLevelTabs = rootChildId !== DASHBOARD_GRID_ID && dashboard[rootChildId];
+
+ const gridComponentId = topLevelTabs
+ ? topLevelTabs.children[Math.min(topLevelTabs.children.length - 1, tabIndex)]
+ : DASHBOARD_GRID_ID;
+
+ const gridComponent = dashboard[gridComponentId];
+
+ return (
+ <div className="dashboard-v2">
+ {topLevelTabs ? ( // you cannot drop on/displace tabs if they already exist
+ <DashboardHeader />
+ ) : (
+ <DragDroppable
+ component={dashboardRoot}
+ parentComponent={null}
+ depth={DASHBOARD_ROOT_DEPTH}
+ index={0}
+ orientation="column"
+ onDrop={topLevelTabs ? null : handleComponentDrop}
+ >
+ {({ dropIndicatorProps }) => (
+ <div>
+ <DashboardHeader />
+ {dropIndicatorProps && <div {...dropIndicatorProps} />}
+ </div>
+ )}
+ </DragDroppable>)}
+
+ {topLevelTabs &&
+ <WithPopoverMenu
+ shouldFocus={DashboardBuilder.shouldFocusTabs}
+ menuItems={[
+ <IconButton
+ className="fa fa-level-down"
+ label="Collapse tab content"
+ onClick={deleteTopLevelTabs}
+ />,
+ ]}
+ >
+ <DashboardComponent
+ id={topLevelTabs.id}
+ parentId={DASHBOARD_ROOT_ID}
+ depth={DASHBOARD_ROOT_DEPTH + 1}
+ index={0}
+ renderTabContent={false}
+ onChangeTab={this.handleChangeTab}
+ />
+ </WithPopoverMenu>}
+
+ <div className="dashboard-builder">
+ <DashboardGrid
+ gridComponent={gridComponent}
+ dashboard={dashboard}
+ handleComponentDrop={handleComponentDrop}
+ updateComponents={updateComponents}
+ depth={DASHBOARD_ROOT_DEPTH + 1}
+ />
+ <BuilderComponentPane />
+ </div>
+ </div>
+ );
+ }
+}
+
+DashboardBuilder.propTypes = propTypes;
+DashboardBuilder.defaultProps = defaultProps;
+
+export default DragDropContext(HTML5Backend)(DashboardBuilder);
diff --git a/superset/assets/javascripts/dashboard/v2/stylesheets/components/column.less b/superset/assets/javascripts/dashboard/v2/stylesheets/components/column.less
index b96b14b..31ae21d 100644
--- a/superset/assets/javascripts/dashboard/v2/stylesheets/components/column.less
+++ b/superset/assets/javascripts/dashboard/v2/stylesheets/components/column.less
@@ -7,7 +7,15 @@
top: -20px;
}
-.grid-column--empty:after {
+.grid-column.background--transparent {
+ background-color: transparent;
+}
+
+.grid-column.background--white {
+ background-color: white;
+}
+
+.grid-column--empty:before {
content: "Empty column";
position: absolute;
top: 0;
diff --git a/superset/assets/javascripts/dashboard/v2/stylesheets/components/new-component.less b/superset/assets/javascripts/dashboard/v2/stylesheets/components/new-component.less
index 31e84cb..e36fee2 100644
--- a/superset/assets/javascripts/dashboard/v2/stylesheets/components/new-component.less
+++ b/superset/assets/javascripts/dashboard/v2/stylesheets/components/new-component.less
@@ -5,6 +5,7 @@
align-items: center;
padding: 16px;
background: white;
+ cursor: move;
}
.new-component-placeholder {
diff --git a/superset/assets/javascripts/dashboard/v2/stylesheets/components/row.less b/superset/assets/javascripts/dashboard/v2/stylesheets/components/row.less
index 8859926..2036815 100644
--- a/superset/assets/javascripts/dashboard/v2/stylesheets/components/row.less
+++ b/superset/assets/javascripts/dashboard/v2/stylesheets/components/row.less
@@ -8,11 +8,11 @@
background-color: transparent;
}
-.grid-row--transparent {
+.grid-row.background--transparent {
background-color: transparent;
}
-.grid-row--white {
+.grid-row.background--white {
background-color: white;
}
@@ -25,7 +25,7 @@
height: 80px;
}
-.grid-row--empty:after {
+.grid-row--empty:before {
position: absolute;
top: 0;
left: 0;
diff --git a/superset/assets/javascripts/dashboard/v2/stylesheets/components/tabs.less b/superset/assets/javascripts/dashboard/v2/stylesheets/components/tabs.less
index 23e0469..f67c151 100644
--- a/superset/assets/javascripts/dashboard/v2/stylesheets/components/tabs.less
+++ b/superset/assets/javascripts/dashboard/v2/stylesheets/components/tabs.less
@@ -2,6 +2,7 @@
width: 100%;
background-color: white;
}
+
.dashboard-component-tabs .dashboard-component-tabs-content {
min-height: 48px;
margin-top: 1px;
@@ -13,13 +14,15 @@
/* by moving padding from <a/> to <li/> we can restrict the selected tab indicator to text width */
.dashboard-component-tabs .nav-tabs > li {
- padding: 0 16px;
+ margin: 0 16px;
}
.dashboard-component-tabs .nav-tabs > li > a {
- color: #263238;
+ color: @almost-black;
border: none;
padding: 12px 0 14px 0;
+ font-size: 15px;
+ margin-right: 0;
}
.dashboard-component-tabs .nav-tabs > li.active > a {
@@ -38,7 +41,7 @@
.dashboard-component-tabs .nav-tabs > li > a:hover {
border: none;
background: inherit;
- color: #000000;
+ color: @almost-black;
}
.dashboard-component-tabs .nav-tabs > li > a:focus {
@@ -51,15 +54,27 @@
}
.dashboard-component-tabs .nav-tabs > li .drop-indicator {
- height: 40px !important;
- top: -10px !important;
- opacity: 0.5;
+ top: -12px !important;
+ height: ~"calc(100% + 24px)" !important;
+}
+
+.dashboard-component-tabs .nav-tabs > li .drop-indicator--left {
+ left: -12px !important;
+}
+
+.dashboard-component-tabs .nav-tabs > li .drop-indicator--right {
+ right: -12px !important;
+}
+
+.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 */
+ opacity: 0.4;
}
-.dashboard-component-tabs .fa-plus-square {
- background: linear-gradient(135deg, #E32464, #2C2261);
- -webkit-background-clip: text;
- -webkit-text-fill-color: transparent;
- display: initial;
- font-size: 16px;
+.dashboard-component-tabs li .fa-plus {
+ color: @gray-dark;
+ font-size: 14px;
+ margin-top: 3px;
}
diff --git a/superset/assets/javascripts/dashboard/v2/stylesheets/dnd.less b/superset/assets/javascripts/dashboard/v2/stylesheets/dnd.less
index fb010e0..45a9784 100644
--- a/superset/assets/javascripts/dashboard/v2/stylesheets/dnd.less
+++ b/superset/assets/javascripts/dashboard/v2/stylesheets/dnd.less
@@ -3,32 +3,54 @@
}
.dragdroppable--dragging {
- opacity: 0.25;
+ opacity: 0.15;
}
.dragdroppable-row {
width: 100%;
}
-.grid-container .dragdroppable-row:after,
-.grid-container .dragdroppable-column:after {
- border: 1px dashed transparent;
- content: "";
+/* drop indicators */
+.drop-indicator {
+ margin: auto;
+ background-color: @indicator-color;
position: absolute;
+ z-index: 10;
+}
+
+.drop-indicator--top {
+ top: 0;
+ left: 0;
+ height: 4px;
width: 100%;
- height: 100%;
- top: 1px;
+ min-width: 16px;
+}
+
+.drop-indicator--bottom {
+ top: 100%;
left: 0;
- z-index: 1;
- pointer-events: none;
+ height: 4px;
+ width: 100%;
+ min-width: 16px;
}
- .grid-container .dragdroppable-row:hover:after,
- .grid-container .dragdroppable-column:hover:after {
- border: 1px dashed #aaa;
- }
+.drop-indicator--right {
+ top: 0;
+ left: 100%;
+ height: 100%;
+ width: 4px;
+ min-height: 16px;
+}
+
+.drop-indicator--left {
+ top: 0;
+ left: 0;
+ height: 100%;
+ width: 4px;
+ min-height: 16px;
+}
-/* Drag handle */
+/* drag handles */
.drag-handle {
overflow: hidden;
width: 16px;
@@ -39,10 +61,6 @@
width: 8px;
}
-.drag-handle--top {
- /*margin: 10px auto;*/
-}
-
.drag-handle-dot {
float: left;
height: 2px;
diff --git a/superset/assets/javascripts/dashboard/v2/stylesheets/grid.less b/superset/assets/javascripts/dashboard/v2/stylesheets/grid.less
index c26ee0a..7c55dee 100644
--- a/superset/assets/javascripts/dashboard/v2/stylesheets/grid.less
+++ b/superset/assets/javascripts/dashboard/v2/stylesheets/grid.less
@@ -1,9 +1,17 @@
.grid-container {
- flex-grow: 1;
- min-width: 66%;
- margin: 24px 32px;
- height: 100%;
- position: relative;
+ position: relative;
+ margin: 24px;
+}
+
+.grid-content {
+ height: 100%;
+ display: flex;
+ flex-direction: column;
+}
+
+.empty-grid-droptarget {
+ width: 100%;
+ height: 100%;
}
/* Editing guides */
@@ -19,7 +27,28 @@
.grid-row-guide {
position: absolute;
left: 0;
- height: 1;
- background-color: var(--indicator-color);
+ bottom: 2;
+ height: 2;
+ background-color: @indicator-color;
pointer-events: none;
+ z-index: 10;
+}
+
+
+.grid-container .grid-row:after,
+.grid-container .grid-column:after {
+ border: 1px dashed transparent;
+ content: "";
+ position: absolute;
+ width: 100%;
+ height: 100%;
+ top: 1px;
+ left: 0;
+ z-index: 1;
+ pointer-events: none;
+}
+
+.grid-container .grid-row:hover:after,
+.grid-container .grid-column:hover:after {
+ border: 1px solid @gray-light;
}
diff --git a/superset/assets/javascripts/dashboard/v2/stylesheets/hover-menu.less b/superset/assets/javascripts/dashboard/v2/stylesheets/hover-menu.less
index bc2935c..77edb06 100644
--- a/superset/assets/javascripts/dashboard/v2/stylesheets/hover-menu.less
+++ b/superset/assets/javascripts/dashboard/v2/stylesheets/hover-menu.less
@@ -5,10 +5,10 @@
}
.hover-menu--left {
- width: 20px;
+ width: 24px;
height: 100%;
top: 0;
- left: -20px;
+ left: -24px;
display: flex;
flex-direction: column;
justify-content: center;
@@ -16,7 +16,7 @@
}
.hover-menu--left > :nth-child(n):not(:only-child):not(:last-child) {
- margin-bottom: 8px;
+ margin-bottom: 12px;
}
.dragdroppable-row .dragdroppable-row .hover-menu--left {
@@ -25,7 +25,7 @@
.hover-menu--top {
width: 100%;
- height: 20px;
+ height: 24px;
top: 0;
left: 0;
display: flex;
@@ -35,10 +35,10 @@
}
.hover-menu--top > :nth-child(n):not(:only-child):not(:last-child) {
- margin-right: 8px;
+ margin-right: 12px;
}
-.dragdroppable:hover .hover-menu,
-.dragdroppable .hover-menu:hover {
+div:hover > .hover-menu,
+.hover-menu:hover {
opacity: 1;
}
diff --git a/superset/assets/javascripts/dashboard/v2/stylesheets/index.less b/superset/assets/javascripts/dashboard/v2/stylesheets/index.less
index 125c894..d2a41a8 100644
--- a/superset/assets/javascripts/dashboard/v2/stylesheets/index.less
+++ b/superset/assets/javascripts/dashboard/v2/stylesheets/index.less
@@ -1,5 +1,6 @@
@import './variables.less';
+@import './builder.less';
@import './buttons.less';
@import './dnd.less';
@import './grid.less';
diff --git a/superset/assets/javascripts/dashboard/v2/stylesheets/popover-menu.less b/superset/assets/javascripts/dashboard/v2/stylesheets/popover-menu.less
index f68cf13..a36ab1c 100644
--- a/superset/assets/javascripts/dashboard/v2/stylesheets/popover-menu.less
+++ b/superset/assets/javascripts/dashboard/v2/stylesheets/popover-menu.less
@@ -37,6 +37,18 @@
z-index: 10;
}
+/* 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)";
+}
+
+.dashboard-component-tabs li .popover-menu {
+ top: -56px;
+}
+
.popover-menu .menu-item {
display: flex;
flex-direction: row;
@@ -87,12 +99,12 @@
color: @almost-black;
}
-/* row style menu */
-.row-style-option {
+/* background style menu */
+.background-style-option {
display: inline-block;
}
-.row-style-option:before {
+.background-style-option:before {
content: "";
width: 1em;
height: 1em;
@@ -101,16 +113,16 @@
vertical-align: middle;
}
-.row-style-option.grid-row--white {
+.background-style-option.background--white {
padding-left: 0;
background: transparent;
}
-.row-style-option.grid-row--white:before {
+.background-style-option.background--white:before {
background: white;
border: 1px solid @gray-light;
}
-.row-style-option.grid-row--transparent:before {
+.background-style-option.background--transparent:before {
background: @gray-light;
}
diff --git a/superset/assets/javascripts/dashboard/v2/stylesheets/resizable.less b/superset/assets/javascripts/dashboard/v2/stylesheets/resizable.less
index 0ccd2f8..3ce5cfd 100644
--- a/superset/assets/javascripts/dashboard/v2/stylesheets/resizable.less
+++ b/superset/assets/javascripts/dashboard/v2/stylesheets/resizable.less
@@ -1,10 +1,10 @@
-.grid-resizable-container {
+.resizable-container {
background-color: transparent;
position: relative;
}
/* after ensures border visibility on top of any children */
-.grid-resizable-container--resizing:after {
+.resizable-container--resizing:after {
content: "";
position: absolute;
top: 0;
@@ -18,8 +18,8 @@
opacity: 0;
}
- .grid-resizable-container:hover .resize-handle,
- .grid-resizable-container--resizing .resize-handle {
+ .resizable-container:hover .resize-handle,
+ .resizable-container--resizing .resize-handle {
opacity: 1;
}
@@ -59,14 +59,14 @@
border-bottom: 1px solid @gray;
}
-.grid-resizable-container--resizing > span .resize-handle {
+.resizable-container--resizing > span .resize-handle {
border-color: @indicator-color;
}
/* re-resizable sets an empty div to 100% width and height, which doesn't
play well with many 100% height containers we need
*/
-.grid-resizable-container ~ div {
+.resizable-container ~ div {
width: auto !important;
height: auto !important;
}
diff --git a/superset/assets/javascripts/dashboard/v2/util/backgroundStyleOptions.js b/superset/assets/javascripts/dashboard/v2/util/backgroundStyleOptions.js
new file mode 100644
index 0000000..cda678f
--- /dev/null
+++ b/superset/assets/javascripts/dashboard/v2/util/backgroundStyleOptions.js
@@ -0,0 +1,7 @@
+import { t } from '../../../locales';
+import { BACKGROUND_TRANSPARENT, BACKGROUND_WHITE } from './constants';
+
+export default [
+ { value: BACKGROUND_TRANSPARENT, label: t('Transparent'), className: 'background--transparent' },
+ { value: BACKGROUND_WHITE, label: t('White'), className: 'background--white' },
+];
diff --git a/superset/assets/javascripts/dashboard/v2/util/componentTypes.js b/superset/assets/javascripts/dashboard/v2/util/componentTypes.js
index fd5d294..c667138 100644
--- a/superset/assets/javascripts/dashboard/v2/util/componentTypes.js
+++ b/superset/assets/javascripts/dashboard/v2/util/componentTypes.js
@@ -1,9 +1,12 @@
export const CHART_TYPE = 'DASHBOARD_CHART_TYPE';
export const COLUMN_TYPE = 'DASHBOARD_COLUMN_TYPE';
+export const DASHBOARD_GRID_TYPE = 'DASHBOARD_GRID_TYPE';
+export const DASHBOARD_HEADER_TYPE = 'DASHBOARD_DASHBOARD_HEADER_TYPE';
+export const DASHBOARD_ROOT_TYPE = 'DASHBOARD_ROOT_TYPE';
export const DIVIDER_TYPE = 'DASHBOARD_DIVIDER_TYPE';
-export const GRID_ROOT_TYPE = 'DASHBOARD_GRID_ROOT_TYPE';
export const HEADER_TYPE = 'DASHBOARD_HEADER_TYPE';
export const MARKDOWN_TYPE = 'DASHBOARD_MARKDOWN_TYPE';
+export const NEW_COMPONENT_SOURCE_TYPE = 'NEW_COMPONENT_SOURCE_TYPE';
export const ROW_TYPE = 'DASHBOARD_ROW_TYPE';
export const SPACER_TYPE = 'DASHBOARD_SPACER_TYPE';
export const TABS_TYPE = 'DASHBOARD_TABS_TYPE';
@@ -12,10 +15,13 @@ export const TAB_TYPE = 'DASHBOARD_TAB_TYPE';
export default {
CHART_TYPE,
COLUMN_TYPE,
+ DASHBOARD_GRID_TYPE,
+ DASHBOARD_HEADER_TYPE,
+ DASHBOARD_ROOT_TYPE,
DIVIDER_TYPE,
- GRID_ROOT_TYPE,
HEADER_TYPE,
MARKDOWN_TYPE,
+ NEW_COMPONENT_SOURCE_TYPE,
ROW_TYPE,
SPACER_TYPE,
TABS_TYPE,
diff --git a/superset/assets/javascripts/dashboard/v2/util/constants.js b/superset/assets/javascripts/dashboard/v2/util/constants.js
index 44a0f0e..e892456 100644
--- a/superset/assets/javascripts/dashboard/v2/util/constants.js
+++ b/superset/assets/javascripts/dashboard/v2/util/constants.js
@@ -1,5 +1,9 @@
// Ids
+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 NEW_COMPONENTS_SOURCE_ID = 'NEW_COMPONENTS_SOURCE_ID';
export const NEW_CHART_ID = 'NEW_CHART_ID';
export const NEW_COLUMN_ID = 'NEW_COLUMN_ID';
export const NEW_DIVIDER_ID = 'NEW_DIVIDER_ID';
@@ -11,6 +15,7 @@ export const NEW_TAB_ID = 'NEW_TAB_ID';
export const NEW_TABS_ID = 'NEW_TABS_ID';
// grid constants
+export const DASHBOARD_ROOT_DEPTH = 0;
export const GRID_BASE_UNIT = 8;
export const GRID_GUTTER_SIZE = 2 * GRID_BASE_UNIT;
export const GRID_ROW_HEIGHT_UNIT = 2 * GRID_BASE_UNIT;
@@ -25,6 +30,6 @@ export const SMALL_HEADER = 'SMALL_HEADER';
export const MEDIUM_HEADER = 'MEDIUM_HEADER';
export const LARGE_HEADER = 'LARGE_HEADER';
-// Row types
-export const ROW_WHITE = 'ROW_WHITE';
-export const ROW_TRANSPARENT = 'ROW_TRANSPARENT';
+// Style types
+export const BACKGROUND_WHITE = 'BACKGROUND_WHITE';
+export const BACKGROUND_TRANSPARENT = 'BACKGROUND_TRANSPARENT';
diff --git a/superset/assets/javascripts/dashboard/v2/util/countChildRowsAndColumns.js b/superset/assets/javascripts/dashboard/v2/util/countChildRowsAndColumns.js
deleted file mode 100644
index dbc63cd..0000000
--- a/superset/assets/javascripts/dashboard/v2/util/countChildRowsAndColumns.js
+++ /dev/null
@@ -1,14 +0,0 @@
-export default function countChildRowsAndColumns({ component, components }) {
- let columnCount = 0;
- let rowCount = 0;
-
- (component.children || []).forEach((childId) => {
- const childComponent = components[childId];
- columnCount += (childComponent.meta || {}).width || 0;
- if ((childComponent.meta || {}).height) {
- rowCount = Math.max(rowCount, childComponent.meta.height);
- }
- });
-
- return { columnCount, rowCount };
-}
diff --git a/superset/assets/javascripts/dashboard/v2/util/dnd-reorder.js b/superset/assets/javascripts/dashboard/v2/util/dnd-reorder.js
index 5ebca8c..9a0dedf 100644
--- a/superset/assets/javascripts/dashboard/v2/util/dnd-reorder.js
+++ b/superset/assets/javascripts/dashboard/v2/util/dnd-reorder.js
@@ -11,12 +11,12 @@ export default function reorderItem({
source,
destination,
}) {
- const current = [...entitiesMap[source.droppableId].children];
- const next = [...entitiesMap[destination.droppableId].children];
+ const current = [...entitiesMap[source.id].children];
+ const next = [...entitiesMap[destination.id].children];
const target = current[source.index];
// moving to same list
- if (source.droppableId === destination.droppableId) {
+ if (source.id === destination.id) {
const reordered = reorder(
current,
source.index,
@@ -25,8 +25,8 @@ export default function reorderItem({
const result = {
...entitiesMap,
- [source.droppableId]: {
- ...entitiesMap[source.droppableId],
+ [source.id]: {
+ ...entitiesMap[source.id],
children: reordered,
},
};
@@ -40,12 +40,12 @@ export default function reorderItem({
const result = {
...entitiesMap,
- [source.droppableId]: {
- ...entitiesMap[source.droppableId],
+ [source.id]: {
+ ...entitiesMap[source.id],
children: current,
},
- [destination.droppableId]: {
- ...entitiesMap[destination.droppableId],
+ [destination.id]: {
+ ...entitiesMap[destination.id],
children: next,
},
};
diff --git a/superset/assets/javascripts/dashboard/v2/util/findParentId.js b/superset/assets/javascripts/dashboard/v2/util/findParentId.js
new file mode 100644
index 0000000..0ca15a6
--- /dev/null
+++ b/superset/assets/javascripts/dashboard/v2/util/findParentId.js
@@ -0,0 +1,15 @@
+export default function findParentId({ childId, components = {} }) {
+ let parentId = null;
+
+ const ids = Object.keys(components);
+ for (let i = 0; i < ids.length - 1; i += 1) {
+ const id = ids[i];
+ const component = components[id] || {};
+ if (id !== childId && component.children && component.children.includes(childId)) {
+ parentId = id;
+ break;
+ }
+ }
+
+ return parentId;
+}
diff --git a/superset/assets/javascripts/dashboard/v2/util/getChildWidth.js b/superset/assets/javascripts/dashboard/v2/util/getChildWidth.js
new file mode 100644
index 0000000..516624d
--- /dev/null
+++ b/superset/assets/javascripts/dashboard/v2/util/getChildWidth.js
@@ -0,0 +1,16 @@
+export default function getTotalChildWidth({ id, components, recurse = false }) {
+ const component = components[id];
+ if (!component) return 0;
+
+ let width = 0;
+
+ (component.children || []).forEach((childId) => {
+ const child = components[childId];
+ width += child.meta.width || 0;
+ if (recurse) {
+ width += getTotalChildWidth({ id: childId, components, recurse }) || 0;
+ }
+ });
+
+ return width;
+}
diff --git a/superset/assets/javascripts/dashboard/v2/util/getDropPosition.js b/superset/assets/javascripts/dashboard/v2/util/getDropPosition.js
index e1dfbd3..6a3bd0e 100644
--- a/superset/assets/javascripts/dashboard/v2/util/getDropPosition.js
+++ b/superset/assets/javascripts/dashboard/v2/util/getDropPosition.js
@@ -1,14 +1,16 @@
import isValidChild from './isValidChild';
+import { TAB_TYPE, TABS_TYPE } from './componentTypes';
export const DROP_TOP = 'DROP_TOP';
export const DROP_RIGHT = 'DROP_RIGHT';
export const DROP_BOTTOM = 'DROP_BOTTOM';
export const DROP_LEFT = 'DROP_LEFT';
-const SIBLING_DROP_THRESHOLD = 10;
+const SIBLING_DROP_THRESHOLD = 15;
export default function getDropPosition(monitor, Component) {
const {
+ depth: componentDepth,
parentComponent,
component,
orientation,
@@ -18,17 +20,23 @@ export default function getDropPosition(monitor, Component) {
const draggingItem = monitor.getItem();
// if dropped self on self, do nothing
- if (!draggingItem || draggingItem.draggableId === component.id || !isDraggingOverShallow) {
+ if (!draggingItem || draggingItem.id === component.id || !isDraggingOverShallow) {
return null;
}
const validChild = isValidChild({
parentType: component.type,
+ parentDepth: componentDepth,
childType: draggingItem.type,
});
+ const parentType = parentComponent && parentComponent.type;
+ const parentDepth = // see isValidChild.js for why tabs don't increment child depth
+ componentDepth + (parentType === TAB_TYPE || parentType === TABS_TYPE ? 0 : -1);
+
const validSibling = isValidChild({
- parentType: parentComponent && parentComponent.type,
+ parentType,
+ parentDepth,
childType: draggingItem.type,
});
@@ -36,7 +44,7 @@ export default function getDropPosition(monitor, Component) {
return null;
}
- const hasChildren = component.children.length > 0;
+ const hasChildren = (component.children || []).length > 0;
const childDropOrientation = orientation === 'row' ? 'vertical' : 'horizontal';
const siblingDropOrientation = orientation === 'row' ? 'horizontal' : 'vertical';
diff --git a/superset/assets/javascripts/dashboard/v2/util/isValidChild.js b/superset/assets/javascripts/dashboard/v2/util/isValidChild.js
index c8921ec..9c6ae8e 100644
--- a/superset/assets/javascripts/dashboard/v2/util/isValidChild.js
+++ b/superset/assets/javascripts/dashboard/v2/util/isValidChild.js
@@ -1,9 +1,26 @@
+/* eslint max-len: 0 */
+/**
+ * When determining if a component is a valid child of another component we must consider both
+ * - parent + child component types
+ * - component depth, or depth of nesting of container components
+ *
+ * We consider types because some components aren't containers (e.g. a heading) and we consider
+ * depth to prevent infinite nesting of container components.
+ *
+ * The following example container nestings should be valid, which means that some containers
+ * don't increase the (depth) of their children, namely tabs and tab:
+ * (a) root (0) > grid (1) > row (2) > column (3) > row (4) > non-container (5)
+ * (b) root (0) > grid (1) > tabs (2) > tab (2) > row (2) > column (3) > row (4) > non-container (5)
+ * (c) root (0) > top-tab (1) > row (2) > column (3) > row (4) > non-container (5)
+ * (d) root (0) > top-tab (1) > tabs (2) > tab (2) > row (2) > column (3) > row (4) > non-container (5)
+ */
import {
CHART_TYPE,
COLUMN_TYPE,
+ DASHBOARD_GRID_TYPE,
+ DASHBOARD_ROOT_TYPE,
DIVIDER_TYPE,
HEADER_TYPE,
- GRID_ROOT_TYPE,
MARKDOWN_TYPE,
ROW_TYPE,
SPACER_TYPE,
@@ -11,59 +28,70 @@ import {
TAB_TYPE,
} from './componentTypes';
-const typeToValidChildType = {
- // while some components are wrapped in Rows, most types are valid root children
- [GRID_ROOT_TYPE]: {
- [CHART_TYPE]: true,
- [COLUMN_TYPE]: true,
- [DIVIDER_TYPE]: true,
- [HEADER_TYPE]: true,
- [ROW_TYPE]: true,
- [SPACER_TYPE]: true,
- [TABS_TYPE]: true,
+import { DASHBOARD_ROOT_DEPTH as rootDepth } from './constants';
+
+const depthOne = rootDepth + 1;
+const depthTwo = rootDepth + 2;
+const depthThree = rootDepth + 3;
+const depthFour = rootDepth + 4;
+
+// when moving components around the depth of child is irrelevant, note these are parent depths
+const parentMaxDepthLookup = {
+ [DASHBOARD_ROOT_TYPE]: {
+ [TABS_TYPE]: rootDepth,
+ [DASHBOARD_GRID_TYPE]: rootDepth,
+ },
+
+ [DASHBOARD_GRID_TYPE]: {
+ [CHART_TYPE]: depthOne,
+ [COLUMN_TYPE]: depthOne,
+ [DIVIDER_TYPE]: depthOne,
+ [HEADER_TYPE]: depthOne,
+ [ROW_TYPE]: depthOne,
+ [SPACER_TYPE]: depthOne,
+ [TABS_TYPE]: depthOne,
},
[ROW_TYPE]: {
- [CHART_TYPE]: true,
- [MARKDOWN_TYPE]: true,
- [COLUMN_TYPE]: true,
- [SPACER_TYPE]: true,
+ [CHART_TYPE]: depthFour,
+ [MARKDOWN_TYPE]: depthFour,
+ [COLUMN_TYPE]: depthTwo,
+ [SPACER_TYPE]: depthFour,
},
[TABS_TYPE]: {
- [TAB_TYPE]: true,
+ [TAB_TYPE]: depthTwo,
},
[TAB_TYPE]: {
- [CHART_TYPE]: true,
- [COLUMN_TYPE]: true,
- [DIVIDER_TYPE]: true,
- [HEADER_TYPE]: true,
- [ROW_TYPE]: true,
- [SPACER_TYPE]: true,
+ [CHART_TYPE]: depthTwo,
+ [COLUMN_TYPE]: depthTwo,
+ [DIVIDER_TYPE]: depthTwo,
+ [HEADER_TYPE]: depthTwo,
+ [ROW_TYPE]: depthTwo,
+ [SPACER_TYPE]: depthTwo,
+ [TABS_TYPE]: depthTwo,
},
[COLUMN_TYPE]: {
- [CHART_TYPE]: true,
- [MARKDOWN_TYPE]: true,
- [HEADER_TYPE]: true,
- [SPACER_TYPE]: true,
+ [CHART_TYPE]: depthThree,
+ [HEADER_TYPE]: depthThree,
+ [MARKDOWN_TYPE]: depthThree,
+ [ROW_TYPE]: depthThree,
+ [SPACER_TYPE]: depthThree,
},
// these have no valid children
[CHART_TYPE]: {},
- [MARKDOWN_TYPE]: {},
[DIVIDER_TYPE]: {},
[HEADER_TYPE]: {},
+ [MARKDOWN_TYPE]: {},
[SPACER_TYPE]: {},
};
-export default function isValidChild({ parentType, childType }) {
- if (!parentType || !childType) return false;
-
- const isValid = Boolean(
- typeToValidChildType[parentType][childType],
- );
+export default function isValidChild({ parentType, childType, parentDepth }) {
+ if (!parentType || !childType || typeof parentDepth !== 'number') return false;
+ const maxParentDepth = (parentMaxDepthLookup[parentType] || {})[childType];
- return isValid;
+ return typeof maxParentDepth === 'number' && parentDepth <= maxParentDepth;
}
diff --git a/superset/assets/javascripts/dashboard/v2/util/newComponentFactory.js b/superset/assets/javascripts/dashboard/v2/util/newComponentFactory.js
index c1ed03e..9bc01a7 100644
--- a/superset/assets/javascripts/dashboard/v2/util/newComponentFactory.js
+++ b/superset/assets/javascripts/dashboard/v2/util/newComponentFactory.js
@@ -12,16 +12,20 @@ import {
import {
MEDIUM_HEADER,
- ROW_TRANSPARENT,
+ BACKGROUND_TRANSPARENT,
} from './constants';
const typeToDefaultMetaData = {
[CHART_TYPE]: { width: 3, height: 15 },
- [COLUMN_TYPE]: { width: 3 },
+ [COLUMN_TYPE]: { width: 3, background: BACKGROUND_TRANSPARENT },
[DIVIDER_TYPE]: null,
- [HEADER_TYPE]: { text: 'New header', headerSize: MEDIUM_HEADER, rowStyle: ROW_TRANSPARENT },
+ [HEADER_TYPE]: {
+ text: 'New header',
+ headerSize: MEDIUM_HEADER,
+ background: BACKGROUND_TRANSPARENT,
+ },
[MARKDOWN_TYPE]: { width: 3, height: 15 },
- [ROW_TYPE]: { rowStyle: ROW_TRANSPARENT },
+ [ROW_TYPE]: { background: BACKGROUND_TRANSPARENT },
[SPACER_TYPE]: {},
[TABS_TYPE]: null,
[TAB_TYPE]: { text: 'New Tab' },
diff --git a/superset/assets/javascripts/dashboard/v2/util/newEntitiesFromDrop.js b/superset/assets/javascripts/dashboard/v2/util/newEntitiesFromDrop.js
index a0d92fa..9e49643 100644
--- a/superset/assets/javascripts/dashboard/v2/util/newEntitiesFromDrop.js
+++ b/superset/assets/javascripts/dashboard/v2/util/newEntitiesFromDrop.js
@@ -1,4 +1,3 @@
-import newComponentIdToType from './newComponentIdToType';
import shouldWrapChildInRow from './shouldWrapChildInRow';
import newComponentFactory from './newComponentFactory';
@@ -9,21 +8,10 @@ import {
} from './componentTypes';
export default function newEntitiesFromDrop({ dropResult, components }) {
- const { draggableId, destination } = dropResult;
-
- const dragType = newComponentIdToType[draggableId];
- const dropEntity = components[destination.droppableId];
-
- if (!dropEntity) {
- console.warn('Drop target entity', destination.droppableId, 'not found');
- return null;
- }
-
- if (!dragType) {
- console.warn('Drag type not found for id', draggableId);
- return null;
- }
+ const { dragging, destination } = dropResult;
+ const dragType = dragging.type;
+ const dropEntity = components[destination.id];
const dropType = dropEntity.type;
let newDropChild = newComponentFactory(dragType);
const wrapChildInRow = shouldWrapChildInRow({ parentType: dropType, childType: dragType });
@@ -46,7 +34,7 @@ export default function newEntitiesFromDrop({ dropResult, components }) {
const nextDropChildren = [...dropEntity.children];
nextDropChildren.splice(destination.index, 0, newDropChild.id);
- newEntities[destination.droppableId] = {
+ newEntities[destination.id] = {
...dropEntity,
children: nextDropChildren,
};
diff --git a/superset/assets/javascripts/dashboard/v2/util/propShapes.jsx b/superset/assets/javascripts/dashboard/v2/util/propShapes.jsx
index be84965..d701cc2 100644
--- a/superset/assets/javascripts/dashboard/v2/util/propShapes.jsx
+++ b/superset/assets/javascripts/dashboard/v2/util/propShapes.jsx
@@ -1,6 +1,6 @@
import PropTypes from 'prop-types';
import componentTypes from './componentTypes';
-import rowStyleOptions from './rowStyleOptions';
+import backgroundStyleOptions from './backgroundStyleOptions';
import headerStyleOptions from './headerStyleOptions';
export const componentShape = PropTypes.shape({ // eslint-disable-line
@@ -19,6 +19,6 @@ export const componentShape = PropTypes.shape({ // eslint-disable-line
headerSize: PropTypes.oneOf(headerStyleOptions.map(opt => opt.value)),
// Row
- rowStyle: PropTypes.oneOf(rowStyleOptions.map(opt => opt.value)),
+ background: PropTypes.oneOf(backgroundStyleOptions.map(opt => opt.value)),
}),
});
diff --git a/superset/assets/javascripts/dashboard/v2/util/resizableConfig.js b/superset/assets/javascripts/dashboard/v2/util/resizableConfig.js
index 40e9af6..f94914e 100644
--- a/superset/assets/javascripts/dashboard/v2/util/resizableConfig.js
+++ b/superset/assets/javascripts/dashboard/v2/util/resizableConfig.js
@@ -1,5 +1,4 @@
// config for a ResizableContainer
-
const adjustableWidthAndHeight = {
top: false,
right: false,
@@ -23,8 +22,14 @@ const adjustableHeight = {
bottomRight: false,
};
+const notAdjustable = {
+ ...adjustableWidthAndHeight,
+ bottomRight: false,
+};
+
export default {
widthAndHeight: adjustableWidthAndHeight,
widthOnly: adjustableWidth,
heightOnly: adjustableHeight,
+ notAdjustable,
};
diff --git a/superset/assets/javascripts/dashboard/v2/util/rowStyleOptions.js b/superset/assets/javascripts/dashboard/v2/util/rowStyleOptions.js
deleted file mode 100644
index ad42492..0000000
--- a/superset/assets/javascripts/dashboard/v2/util/rowStyleOptions.js
+++ /dev/null
@@ -1,7 +0,0 @@
-import { t } from '../../../locales';
-import { ROW_TRANSPARENT, ROW_WHITE } from './constants';
-
-export default [
- { value: ROW_TRANSPARENT, label: t('Transparent'), className: 'grid-row--transparent' },
- { value: ROW_WHITE, label: t('White'), className: 'grid-row--white' },
-];
diff --git a/superset/assets/javascripts/dashboard/v2/util/shouldWrapChildInRow.js b/superset/assets/javascripts/dashboard/v2/util/shouldWrapChildInRow.js
index 487e247..e7e648c 100644
--- a/superset/assets/javascripts/dashboard/v2/util/shouldWrapChildInRow.js
+++ b/superset/assets/javascripts/dashboard/v2/util/shouldWrapChildInRow.js
@@ -1,5 +1,5 @@
import {
- GRID_ROOT_TYPE,
+ DASHBOARD_GRID_TYPE,
CHART_TYPE,
COLUMN_TYPE,
MARKDOWN_TYPE,
@@ -7,7 +7,7 @@ import {
} from './componentTypes';
const typeToWrapChildLookup = {
- [GRID_ROOT_TYPE]: {
+ [DASHBOARD_GRID_TYPE]: {
[CHART_TYPE]: true,
[COLUMN_TYPE]: true,
[MARKDOWN_TYPE]: true,
diff --git a/superset/assets/package.json b/superset/assets/package.json
index b3379f3..75f9504 100644
--- a/superset/assets/package.json
+++ b/superset/assets/package.json
@@ -113,6 +113,7 @@
"redux": "^3.5.2",
"redux-localstorage": "^0.4.1",
"redux-thunk": "^2.1.0",
+ "redux-undo": "^0.6.1",
"shortid": "^2.2.6",
"sprintf-js": "^1.1.1",
"srcdoc-polyfill": "^1.0.0",
diff --git a/superset/assets/src/components/EditableTitle.jsx b/superset/assets/src/components/EditableTitle.jsx
index 1497676..a7e3f17 100644
--- a/superset/assets/src/components/EditableTitle.jsx
+++ b/superset/assets/src/components/EditableTitle.jsx
@@ -28,7 +28,7 @@ class EditableTitle extends React.PureComponent {
this.handleClick = this.handleClick.bind(this);
this.handleBlur = this.handleBlur.bind(this);
this.handleChange = this.handleChange.bind(this);
- this.handleKeyDown = this.handleKeyDown.bind(this);
+ this.handleKeyUp = this.handleKeyUp.bind(this);
this.handleKeyPress = this.handleKeyPress.bind(this);
}
@@ -79,7 +79,7 @@ class EditableTitle extends React.PureComponent {
}
}
- handleKeyDown(ev) {
+ handleKeyUp(ev) {
// this entire method exists to support using EditableTitle as the title of a
// react-bootstrap Tab, as a workaround for this line in react-bootstrap https://goo.gl/ZVLmv4
//
@@ -121,7 +121,7 @@ class EditableTitle extends React.PureComponent {
required
type={this.state.isEditing ? 'text' : 'button'}
value={this.state.title}
- onKeyDown={this.handleKeyDown}
+ onKeyUp={this.handleKeyUp}
onChange={this.handleChange}
onBlur={this.handleBlur}
onClick={this.handleClick}
diff --git a/superset/assets/src/dashboard/index.jsx b/superset/assets/src/dashboard/index.jsx
index c9236bd..bb21a43 100644
--- a/superset/assets/src/dashboard/index.jsx
+++ b/superset/assets/src/dashboard/index.jsx
@@ -10,7 +10,7 @@ import { initJQueryAjax } from '../modules/utils';
import DashboardContainer from './components/DashboardContainer';
// import rootReducer, { getInitialState } from './reducers';
-import testLayout from './v2/fixtures/testLayout';
+import emptyDashboardLayout from './v2/fixtures/emptyDashboardLayout';
import rootReducer from './v2/reducers/';
appSetup();
@@ -20,7 +20,11 @@ const appContainer = document.getElementById('app');
// const bootstrapData = JSON.parse(appContainer.getAttribute('data-bootstrap'));
// const initState = Object.assign({}, getInitialState(bootstrapData));
const initState = {
- dashboard: testLayout,
+ dashboard: {
+ past: [],
+ present: emptyDashboardLayout,
+ future: [],
+ },
};
const store = createStore(
diff --git a/superset/assets/stylesheets/dashboard-v2.css b/superset/assets/stylesheets/dashboard-v2.css
deleted file mode 100644
index 534a17e..0000000
--- a/superset/assets/stylesheets/dashboard-v2.css
+++ /dev/null
@@ -1,42 +0,0 @@
-.dashboard-v2 {
- margin-top: -20px;
- position: relative;
- color: #263238;
-}
-
-.dashboard-header {
- background: white;
- display: flex;
- flex-direction: row;
- align-items: center;
- justify-content: space-between;
- padding: 0 24px;
- box-shadow: 0 4px 4px 0 rgba(0, 0, 0, 0.1);
- margin-bottom: 2px;
-}
-
-.dashboard-builder {
- display: flex;
- flex-direction: row;
- flex-wrap: nowrap;
- height: auto;
-}
-
-.dashboard-builder-sidepane {
- background: white;
- flex: 0 0 376px;
- box-shadow: 0 0 0 1px #ccc; /* @TODO color */
-}
-
-.dashboard-builder-sidepane-header {
- font-size: 16;
- font-weight: 700;
- border-bottom: 1px solid #ccc;
- padding: 16px;
-}
-
-/* @TODO remove upon new theme */
-.btn.btn-primary {
- background: #263238 !important;
- color: white !important;
-}
diff --git a/superset/assets/stylesheets/superset.less b/superset/assets/stylesheets/superset.less
index 2c405cd..e9f508b 100644
--- a/superset/assets/stylesheets/superset.less
+++ b/superset/assets/stylesheets/superset.less
@@ -232,7 +232,7 @@ table.table-no-hover tr:hover {
background: transparent;
border: none;
box-shadow: none;
- padding-left: 0;
+ padding: 0;
}
.editable-title input[type="button"] {
diff --git a/superset/templates/appbuilder/navbar.html b/superset/templates/appbuilder/navbar.html
index acb292c..77248f0 100644
--- a/superset/templates/appbuilder/navbar.html
+++ b/superset/templates/appbuilder/navbar.html
@@ -29,21 +29,6 @@
</ul>
<ul class="nav navbar-nav navbar-right">
{% include 'appbuilder/navbar_right.html' %}
- <li>
- <a href="/static/assets/version_info.json" title="Version info">
- <i class="fa fa-code-fork"></i>
- </a>
- </li>
- <li>
- <a href="https://github.com/apache/incubator-superset" title="Superset's Github" target="_blank">
- <i class="fa fa-github"></i>
- </a>
- </li>
- <li>
- <a href="https://superset.incubator.apache.org" title="Documentation" target="_blank">
- <i class="fa fa-book"></i>
- </a>
- </li>
</ul>
</div>
</div>