From abc3ec076af5fcd61e5544d743288f8b326e851f Mon Sep 17 00:00:00 2001 From: Chris Williams Date: Fri, 23 Mar 2018 10:53:48 -0700 Subject: [PATCH] [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 +-- .../v2/components/DashboardBuilder.jsx | 97 ++++++++++- .../dashboard/v2/components/DashboardGrid.jsx | 64 +++---- .../v2/components/DashboardHeader.jsx | 71 ++++++-- .../dashboard/v2/components/IconButton.jsx | 12 +- .../v2/components/dnd/DragDroppable.jsx | 41 +++-- .../v2/components/dnd/dragDroppableConfig.js | 11 +- .../dashboard/v2/components/dnd/handleDrop.js | 20 ++- .../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 +-- .../v2/components/gridComponents/Row.jsx | 27 ++- .../v2/components/gridComponents/Spacer.jsx | 9 +- .../v2/components/gridComponents/Tab.jsx | 31 ++-- .../v2/components/gridComponents/Tabs.jsx | 61 ++++--- .../new/DraggableNewComponent.jsx | 5 +- ...opdown.jsx => BackgroundStyleDropdown.jsx} | 12 +- .../v2/components/menu/WithPopoverMenu.jsx | 10 +- .../resizable/ResizableContainer.jsx | 41 +++-- .../v2/containers/DashboardBuilder.jsx | 23 +++ .../v2/containers/DashboardComponent.jsx | 33 ++-- .../dashboard/v2/containers/DashboardGrid.jsx | 12 +- .../v2/containers/DashboardHeader.jsx | 31 ++++ .../v2/fixtures/emptyDashboardLayout.js | 36 ++++ .../dashboard/v2/fixtures/testLayout.js | 161 ------------------ .../dashboard/v2/reducers/dashboard.js | 146 ++++++++++++++-- .../dashboard/v2/reducers/index.js | 9 +- .../dashboard/v2/stylesheets/builder.less | 64 +++++++ .../dashboard/v2/stylesheets/buttons.less | 8 +- .../components/DashboardBuilder.jsx | 127 ++++++++++++++ .../v2/stylesheets/components/column.less | 10 +- .../stylesheets/components/new-component.less | 1 + .../v2/stylesheets/components/row.less | 6 +- .../v2/stylesheets/components/tabs.less | 39 +++-- .../dashboard/v2/stylesheets/dnd.less | 54 ++++-- .../dashboard/v2/stylesheets/grid.less | 43 ++++- .../dashboard/v2/stylesheets/hover-menu.less | 14 +- .../dashboard/v2/stylesheets/index.less | 1 + .../v2/stylesheets/popover-menu.less | 24 ++- .../dashboard/v2/stylesheets/resizable.less | 12 +- .../v2/util/backgroundStyleOptions.js | 7 + .../dashboard/v2/util/componentTypes.js | 10 +- .../dashboard/v2/util/constants.js | 11 +- .../v2/util/countChildRowsAndColumns.js | 14 -- .../dashboard/v2/util/dnd-reorder.js | 18 +- .../dashboard/v2/util/findParentId.js | 15 ++ .../dashboard/v2/util/getChildWidth.js | 16 ++ .../dashboard/v2/util/getDropPosition.js | 16 +- .../dashboard/v2/util/isValidChild.js | 96 +++++++---- .../dashboard/v2/util/newComponentFactory.js | 12 +- .../dashboard/v2/util/newEntitiesFromDrop.js | 20 +-- .../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 + .../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, 1322 insertions(+), 690 deletions(-) rename superset/assets/javascripts/dashboard/v2/components/menu/{RowStyleDropdown.jsx => BackgroundStyleDropdown.jsx} (65%) create mode 100644 superset/assets/javascripts/dashboard/v2/containers/DashboardBuilder.jsx create mode 100644 superset/assets/javascripts/dashboard/v2/containers/DashboardHeader.jsx create mode 100644 superset/assets/javascripts/dashboard/v2/fixtures/emptyDashboardLayout.js delete mode 100644 superset/assets/javascripts/dashboard/v2/fixtures/testLayout.js create mode 100644 superset/assets/javascripts/dashboard/v2/stylesheets/builder.less create mode 100644 superset/assets/javascripts/dashboard/v2/stylesheets/components/DashboardBuilder.jsx create mode 100644 superset/assets/javascripts/dashboard/v2/util/backgroundStyleOptions.js delete mode 100644 superset/assets/javascripts/dashboard/v2/util/countChildRowsAndColumns.js create mode 100644 superset/assets/javascripts/dashboard/v2/util/findParentId.js create mode 100644 superset/assets/javascripts/dashboard/v2/util/getChildWidth.js delete mode 100644 superset/assets/javascripts/dashboard/v2/util/rowStyleOptions.js delete mode 100644 superset/assets/stylesheets/dashboard-v2.css diff --git a/superset/assets/javascripts/dashboard/v2/actions/index.js b/superset/assets/javascripts/dashboard/v2/actions/index.js index 005a77e5dccd4..a6c7b77223945 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 a2ed1a00ccdd0..ffd1280f9a941 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 ( -
- - - {true ? - : } -
- ); + // @TODO delete this component? + return ; } } diff --git a/superset/assets/javascripts/dashboard/v2/components/DashboardBuilder.jsx b/superset/assets/javascripts/dashboard/v2/components/DashboardBuilder.jsx index 1878db634670b..f3717187c13dd 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 ( -
- - +
+ {topLevelTabs ? ( // you cannot drop on/displace tabs if they already exist + + ) : ( + + {({ dropIndicatorProps }) => ( +
+ + {dropIndicatorProps &&
} +
+ )} + )} + + {topLevelTabs && + , + ]} + > + + } + +
+ + +
); } diff --git a/superset/assets/javascripts/dashboard/v2/components/DashboardGrid.jsx b/superset/assets/javascripts/dashboard/v2/components/DashboardGrid.jsx index c92161a3f40b7..cfe99c71a26af 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 ( -
{ this.grid = ref; }} - className={cx( - 'grid-container', - isResizing && 'grid-container--resizing', - )} - > +
{ this.grid = ref; }}> {({ 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 : ( -
- {(rootComponent.children || []).map((id, index) => ( +
+ {gridComponent.children.map((id, index) => ( ))} - {rootComponent.children.length === 0 && + {/* render an empty drop target */} + {gridComponent.children.length === 0 && - {({ dropIndicatorProps }) => ( -
- {dropIndicatorProps &&
} -
- )} + {({ dropIndicatorProps }) => dropIndicatorProps && +
} } {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 8ffe677a3ed8e..e0d14c4712131 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 (

{}} + title={component.meta.text} + onSaveTitle={this.handleChangeText} showTooltip={false} + canEdit={editMode} />

+ + + + + Action 1 Action 2 @@ -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 98044c92029be..18fd3b1030535 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 (
+ > + + {label && {label}} +
); } } diff --git a/superset/assets/javascripts/dashboard/v2/components/dnd/DragDroppable.jsx b/superset/assets/javascripts/dashboard/v2/components/dnd/DragDroppable.jsx index 320872b549209..89664e56acaf7 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 (
{ - 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', + ), }, })}
diff --git a/superset/assets/javascripts/dashboard/v2/components/dnd/dragDroppableConfig.js b/superset/assets/javascripts/dashboard/v2/components/dnd/dragDroppableConfig.js index e6d55338b9003..55d7e1d3d8b25 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 cf790da5bfe2f..2207ca6e1dc9d 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 1eadef4ff9675..a303e133f0841 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 9daa8cfddc659..7ca506d753d2f 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 }) => ( ({ 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 ( {({ 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} > -
, + ]} > - - - - - - {columnItems.map((componentId, itemIndex) => { - if (componentId === GUTTER) { - return
; - } - - return ( - + + + + - ); - })} - - {dropIndicatorProps &&
} -
+
+ + {columnItems.map((componentId, itemIndex) => { + if (componentId === GUTTER) { + return
; + } + + return ( + + ); + })} + + {dropIndicatorProps &&
} +
+ )} diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Divider.jsx b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Divider.jsx index 29437e1a0d60a..ff29c3f776c93 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 967b483eacf09..d8744d6f0dba3 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`} />, - , , ]} diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Row.jsx b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Row.jsx index 3386f8cefc82c..a60524f959932 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={[ - , ]} > -
@@ -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 faac589bcf2bf..7a287d8668589 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 ( {({ 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 74cd9ae1464bb..9548a4bb5d43c 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 1e2e64cd8700c..cc5f637b04325 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 { {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 && } + title={
} />} + {/* 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 &&
}
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 c4d8d6252ac2d..778f58ecd8404 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 ( {({ dragSourceRef }) => (
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 d3c7eff965774..41cf1df72aa3a 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 ( -
+
{`${option.label} background`}
); @@ -21,19 +21,19 @@ function renderButton(option) { function renderOption(option) { return ( -
+
{option.label}
); } -export default class RowStyleDropdown extends React.PureComponent { +export default class BackgroundStyleDropdown extends React.PureComponent { render() { const { id, value, onChange } = this.props; return ( 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 5e436786947d8..fbb7d1d0974d8 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 ( {children} diff --git a/superset/assets/javascripts/dashboard/v2/containers/DashboardBuilder.jsx b/superset/assets/javascripts/dashboard/v2/containers/DashboardBuilder.jsx new file mode 100644 index 0000000000000..6bd865817d6fb --- /dev/null +++ b/superset/assets/javascripts/dashboard/v2/containers/DashboardBuilder.jsx @@ -0,0 +1,23 @@ +import { bindActionCreators } from 'redux'; +import { connect } from 'react-redux'; +import DashboardBuilder from '../components/DashboardBuilder'; + +import { + deleteTopLevelTabs, + handleComponentDrop, +} from '../actions'; + +function mapStateToProps({ dashboard: undoableDashboard }) { + return { + dashboard: undoableDashboard.present, + }; +} + +function mapDispatchToProps(dispatch) { + return bindActionCreators({ + deleteTopLevelTabs, + handleComponentDrop, + }, dispatch); +} + +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 1340781bbff67..f7e86cc1ca68c 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 741151b780e82..eb01616d05b8a 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 0000000000000..52e7e7ae219e7 --- /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 0000000000000..7816cc2965cc7 --- /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 c3ce897f5cee1..0000000000000 --- 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 19fa9d799d77d..9b0386161c684 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 103fda0178890..9c0575e2720fe 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 0000000000000..5f1a5b06bddfc --- /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 a8dd6617327e0..41ca47897890f 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 0000000000000..e011ad47023c5 --- /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 ( +
+ {topLevelTabs ? ( // you cannot drop on/displace tabs if they already exist + + ) : ( + + {({ dropIndicatorProps }) => ( +
+ + {dropIndicatorProps &&
} +
+ )} + )} + + {topLevelTabs && + , + ]} + > + + } + +
+ + +
+
+ ); + } +} + +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 b96b14ba33ddc..31ae21d72dd5a 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 31e84cb8b48d8..e36fee2eef6f8 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 88599268cc7e4..2036815335cb9 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 23e0469f8bd66..f67c1510074eb 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 to
  • 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 fb010e08a82bc..45a9784721485 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 c26ee0ad0926f..7c55deee856fb 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 bc2935cf3bbb8..77edb0675a263 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 125c8945218fd..d2a41a8118b91 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 f68cf136ad3fc..a36ab1c79a532 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 0ccd2f8d7b545..3ce5cfd31f773 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 0000000000000..cda678f6dd581 --- /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 fd5d2940a7afa..c667138d2c14f 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 44a0f0e823b87..e8924564c1d62 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 dbc63cd85b3fc..0000000000000 --- 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 5ebca8cf92034..9a0dedfd6dee7 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 0000000000000..0ca15a66a9568 --- /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 0000000000000..516624dc5ab5b --- /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 e1dfbd36021c2..6a3bd0e8204cf 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 c8921ec9e91e6..9c6ae8e31e857 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 c1ed03e0aa548..9bc01a7d95713 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 a0d92fa7449c7..9e49643650660 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 be84965957163..d701cc28ede2c 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 40e9af68bbb0f..f94914ee31a47 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 ad42492296cfb..0000000000000 --- 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 487e247808e19..e7e648cf1bf64 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 b3379f333af18..75f9504d1675e 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 14976766975ac..a7e3f17f1e35b 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 c9236bd24feea..bb21a4303b3c6 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 534a17eafa7f2..0000000000000 --- 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 2c405cdc09263..e9f508bbb7b3e 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 acb292c283b55..77248f03ac05c 100644 --- a/superset/templates/appbuilder/navbar.html +++ b/superset/templates/appbuilder/navbar.html @@ -29,21 +29,6 @@