From 3768a16ee76fc93ddde3092c3c9c599f188c39a8 Mon Sep 17 00:00:00 2001 From: Chris Williams Date: Wed, 17 Jan 2018 14:26:20 -0800 Subject: [PATCH 01/25] [grid] add , , and initial grid components. --- .../dashboard/v2/components/Box.jsx | 135 +++++++++++++++ .../dashboard/v2/components/Column.jsx | 35 ++++ .../dashboard/v2/components/Dashboard.jsx | 13 +- .../v2/components/DashboardBuilder.jsx | 15 +- .../dashboard/v2/components/DashboardGrid.jsx | 162 ++++++++++++++++++ .../v2/components/DashboardHeader.jsx | 62 +++++++ .../dashboard/v2/components/Divider.jsx | 36 ++++ .../dashboard/v2/components/Header.jsx | 65 +++---- .../v2/components/ResizableContainer.jsx | 160 +++++++++++++++++ .../dashboard/v2/components/Row.jsx | 138 +++++++++++++++ .../components/dnd/DraggableNewComponent.jsx | 8 +- .../v2/components/dnd/DraggableNewRow.jsx | 2 +- .../dashboard/v2/util/constants.js | 29 +++- superset/assets/package.json | 3 +- superset/assets/stylesheets/dashboard-v2.css | 16 ++ 15 files changed, 814 insertions(+), 65 deletions(-) create mode 100644 superset/assets/javascripts/dashboard/v2/components/Box.jsx create mode 100644 superset/assets/javascripts/dashboard/v2/components/Column.jsx create mode 100644 superset/assets/javascripts/dashboard/v2/components/DashboardGrid.jsx create mode 100644 superset/assets/javascripts/dashboard/v2/components/DashboardHeader.jsx create mode 100644 superset/assets/javascripts/dashboard/v2/components/Divider.jsx create mode 100644 superset/assets/javascripts/dashboard/v2/components/ResizableContainer.jsx create mode 100644 superset/assets/javascripts/dashboard/v2/components/Row.jsx diff --git a/superset/assets/javascripts/dashboard/v2/components/Box.jsx b/superset/assets/javascripts/dashboard/v2/components/Box.jsx new file mode 100644 index 0000000000000..2017d3e2cef22 --- /dev/null +++ b/superset/assets/javascripts/dashboard/v2/components/Box.jsx @@ -0,0 +1,135 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import Resizable from 're-resizable'; + +import { GRID_BASE_UNIT, GRID_ROW_HEIGHT_UNIT } from '../util/constants'; + +const propTypes = { + snapToWidth: PropTypes.number, + snapToHeight: PropTypes.number, + onResizeStart: PropTypes.func, + onResizeStop: PropTypes.func, + minWidth: PropTypes.number, + minHeight: PropTypes.number, + maxWidth: PropTypes.number, + maxHeight: PropTypes.number, + initialWidth: PropTypes.number, + initialHeight: PropTypes.number, + // width: PropTypes.number, + // height: PropTypes.number, +}; + +const defaultProps = { + snapToWidth: GRID_BASE_UNIT, + snapToHeight: 2 * GRID_ROW_HEIGHT_UNIT, + minWidth: 3 * GRID_BASE_UNIT, + minHeight: GRID_ROW_HEIGHT_UNIT, + maxWidth: Infinity, + maxHeight: Infinity, + initialWidth: 3 * GRID_BASE_UNIT, + initialHeight: GRID_ROW_HEIGHT_UNIT, + onResizeStop: null, + onResizeStart: null, + // width: null, + // height: null, +}; + +const ENABLE_CONFIG = { + top: false, + right: true, + bottom: true, + left: false, + topRight: false, + bottomRight: true, + bottomLeft: false, + topLeft: false, +}; + +class Box extends React.Component { + constructor(props) { + super(props); + const { initialWidth, initialHeight, minWidth, minHeight } = props; + + this.state = { + width: Math.max(initialWidth, minWidth), + height: Math.max(initialHeight, minHeight), + }; + + this.handleResizeStart = this.handleResizeStart.bind(this); + this.handleResizeStop = this.handleResizeStop.bind(this); + } + + handleResizeStart() { + if (this.props.onResizeStart) this.props.onResizeStart(); + } + + handleResizeStop(event, direction, ref, delta) { + console.log('resize delta', delta, direction) + const { onResizeStop, snapToWidth, snapToHeight, id } = this.props; + let nextState = {}; + + const callback = onResizeStop && (() => { + onResizeStop({ + ...nextState, + id, + widthMultiple: Math.floor(nextState.width / snapToWidth), + heightMultiple: Math.floor(nextState.height / snapToHeight), + }); + }); + + this.setState(({ width, height }) => { + nextState = { + width: width + delta.width, + height: height + delta.height, + }; + return nextState; + }, callback); + } + + render() { + const { + snapToWidth, + snapToHeight, + minWidth, + minHeight, + maxWidth, + maxHeight, + // width, + // height, + } = this.props; + + return ( + +
+
+
width (remaining)
+
{`${this.state.width}px (${maxWidth - this.state.width}px)`}
+
+
+ ); + } +} + +Box.propTypes = propTypes; +Box.defaultProps = defaultProps; + +export default Box; diff --git a/superset/assets/javascripts/dashboard/v2/components/Column.jsx b/superset/assets/javascripts/dashboard/v2/components/Column.jsx new file mode 100644 index 0000000000000..dde983ef26517 --- /dev/null +++ b/superset/assets/javascripts/dashboard/v2/components/Column.jsx @@ -0,0 +1,35 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +import { } from '../util/constants'; + +const propTypes = { +}; + +const defaultProps = { +}; + +class Column extends React.Component { + constructor(props) { + super(props); + this.state = { + }; + } + + render() { + return ( +
+ ); + } +} + +Column.propTypes = propTypes; +Column.defaultProps = defaultProps; + +export default Column; diff --git a/superset/assets/javascripts/dashboard/v2/components/Dashboard.jsx b/superset/assets/javascripts/dashboard/v2/components/Dashboard.jsx index 0de4e8b6fbdbd..5936006c8550f 100644 --- a/superset/assets/javascripts/dashboard/v2/components/Dashboard.jsx +++ b/superset/assets/javascripts/dashboard/v2/components/Dashboard.jsx @@ -3,7 +3,7 @@ import PropTypes from 'prop-types'; import DashboardBuilder from './DashboardBuilder'; import StaticDashboard from './StaticDashboard'; -import Header from './Header'; +import DashboardHeader from './DashboardHeader'; import '../../../../stylesheets/dashboard-v2.css'; @@ -15,19 +15,23 @@ const propTypes = { editMode: PropTypes.bool, }; +const defaultProps = { + editMode: true, +}; + class Dashboard extends React.Component { render() { const { editMode, actions } = this.props; const { setEditMode, updateDashboardTitle } = actions; return (
-
- {editMode ? + {true ? : }
); @@ -35,5 +39,6 @@ class Dashboard extends React.Component { } Dashboard.propTypes = propTypes; +Dashboard.defaultProps = defaultProps; export default Dashboard; diff --git a/superset/assets/javascripts/dashboard/v2/components/DashboardBuilder.jsx b/superset/assets/javascripts/dashboard/v2/components/DashboardBuilder.jsx index 62a65339a5093..23e5a72afa262 100644 --- a/superset/assets/javascripts/dashboard/v2/components/DashboardBuilder.jsx +++ b/superset/assets/javascripts/dashboard/v2/components/DashboardBuilder.jsx @@ -3,6 +3,7 @@ import PropTypes from 'prop-types'; import { DragDropContext, Droppable, Draggable } from 'react-beautiful-dnd'; import BuilderComponentPane from './BuilderComponentPane'; +import DashboardGrid from './DashboardGrid'; import { reorder, reorderRows } from '../util/dnd-reorder'; import { DROPPABLE_DASHBOARD_ROOT, DRAGGABLE_ROW_TYPE } from '../util/constants'; @@ -11,6 +12,10 @@ const propTypes = { editMode: PropTypes.bool, }; +const defaultProps = { + editMode: true, +}; + class DashboardBuilder extends React.Component { constructor(props) { super(props); @@ -75,10 +80,11 @@ class DashboardBuilder extends React.Component { onDragEnd={this.handleDragEnd} >
-
- + + {/* {(provided, snapshot) => ( @@ -95,7 +101,7 @@ class DashboardBuilder extends React.Component { {provided.placeholder}
)} - + */}
@@ -105,5 +111,6 @@ class DashboardBuilder extends React.Component { } DashboardBuilder.propTypes = propTypes; +DashboardBuilder.defaultProps = defaultProps; export default DashboardBuilder; diff --git a/superset/assets/javascripts/dashboard/v2/components/DashboardGrid.jsx b/superset/assets/javascripts/dashboard/v2/components/DashboardGrid.jsx new file mode 100644 index 0000000000000..dd2634baef3d4 --- /dev/null +++ b/superset/assets/javascripts/dashboard/v2/components/DashboardGrid.jsx @@ -0,0 +1,162 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import ParentSize from '@vx/responsive/build/components/ParentSize'; + +import Box from './Box'; +import Header from './Header'; +import Row from './Row'; +import Divider from './Divider'; + +import { + GRID_COLUMN_COUNT, + GRID_MIN_COLUMN_COUNT, + HEADER_TYPE, + ROW_TYPE, + CHART_TYPE, + DIVIDER_TYPE, +} from '../util/constants'; + +const propTypes = { +}; + +const defaultProps = { +}; + +class DashboardGrid extends React.Component { + constructor(props) { + super(props); + this.state = { + showGrid: false, + children: [ + 'header0', + 'row0', + 'divider0', + 'row1', + 'row2', + ], + entities: { + header0: { + id: 'header0', + type: HEADER_TYPE, + children: [], + meta: { + text: 'Section header', + }, + }, + divider0: { + id: 'divider0', + type: DIVIDER_TYPE, + children: [], + }, + row0: { + id: 'row0', + type: ROW_TYPE, + children: [], + }, + row1: { + id: 'row1', + type: ROW_TYPE, + children: [], + }, + row2: { + id: 'row2', + type: ROW_TYPE, + children: [ + 'chart0', + 'chart1', + ], + }, + chart0: { + id: 'chart0', + type: CHART_TYPE, + meta: { + width: 3, + height: 5, + }, + }, + chart1: { + id: 'chart1', + type: CHART_TYPE, + meta: { + width: 3, + height: 5, + }, + }, + }, + }; + + this.handleResizeStart = this.handleResizeStart.bind(this); + this.handleResizeStop = this.handleResizeStop.bind(this); + } + + handleResizeStart() { + this.setState(() => ({ showGrid: true })); + } + + handleResizeStop() { + this.setState(() => ({ showGrid: false })); + } + + render() { + const { showGrid, children, entities } = this.state; + + return ( + + {({ width, height }) => { + const columnWidth = width / GRID_COLUMN_COUNT; + const minElementWidth = Math.floor(GRID_MIN_COLUMN_COUNT * columnWidth); + + return width < 10 ? null : ( +
+ {children.map((id) => { + if (/header/i.test(id)) return
; + if (/divider/i.test(id)) return ; + if (/row/i.test(id)) { + return ( + + ); + } + return null; + })} + + {showGrid && Array(GRID_COLUMN_COUNT + 1).fill(null).map((_, i) => ( +
+ ))} +
+ ); + }} + + ); + } +} + +DashboardGrid.propTypes = propTypes; +DashboardGrid.defaultProps = defaultProps; + +export default DashboardGrid; diff --git a/superset/assets/javascripts/dashboard/v2/components/DashboardHeader.jsx b/superset/assets/javascripts/dashboard/v2/components/DashboardHeader.jsx new file mode 100644 index 0000000000000..8ffe677a3ed8e --- /dev/null +++ b/superset/assets/javascripts/dashboard/v2/components/DashboardHeader.jsx @@ -0,0 +1,62 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { ButtonToolbar, DropdownButton, MenuItem } from 'react-bootstrap'; + +import Button from '../../../components/Button'; +import EditableTitle from '../../../components/EditableTitle'; + +const propTypes = { + updateDashboardTitle: PropTypes.func, + editMode: PropTypes.bool.isRequired, + setEditMode: PropTypes.func.isRequired, +}; + +class Header extends React.Component { + constructor(props) { + super(props); + this.handleSaveTitle = this.handleSaveTitle.bind(this); + this.toggleEditMode = this.toggleEditMode.bind(this); + } + + handleSaveTitle(title) { + this.props.updateDashboardTitle(title); + } + + toggleEditMode() { + this.props.setEditMode(!this.props.editMode); + } + + render() { + const { editMode } = this.props; + return ( +
+

+ {}} + showTooltip={false} + /> +

+ + + Action 1 + Action 2 + Action 3 + + + + +
+ ); + } +} + +Header.propTypes = propTypes; + +export default Header; diff --git a/superset/assets/javascripts/dashboard/v2/components/Divider.jsx b/superset/assets/javascripts/dashboard/v2/components/Divider.jsx new file mode 100644 index 0000000000000..3389440991544 --- /dev/null +++ b/superset/assets/javascripts/dashboard/v2/components/Divider.jsx @@ -0,0 +1,36 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +import { GRID_BASE_UNIT } from '../util/constants'; + +const propTypes = { +}; + +const defaultProps = { +}; + +class Divider extends React.Component { + constructor(props) { + super(props); + this.state = { + }; + } + + render() { + return ( +
+ ); + } +} + +Divider.propTypes = propTypes; +Divider.defaultProps = defaultProps; + +export default Divider; diff --git a/superset/assets/javascripts/dashboard/v2/components/Header.jsx b/superset/assets/javascripts/dashboard/v2/components/Header.jsx index 8ffe677a3ed8e..efd063114e2f6 100644 --- a/superset/assets/javascripts/dashboard/v2/components/Header.jsx +++ b/superset/assets/javascripts/dashboard/v2/components/Header.jsx @@ -1,62 +1,45 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { ButtonToolbar, DropdownButton, MenuItem } from 'react-bootstrap'; -import Button from '../../../components/Button'; -import EditableTitle from '../../../components/EditableTitle'; +import { GRID_BASE_UNIT } from '../util/constants'; const propTypes = { - updateDashboardTitle: PropTypes.func, - editMode: PropTypes.bool.isRequired, - setEditMode: PropTypes.func.isRequired, + entity: PropTypes.shape({ + id: PropTypes.string.isRequired, + meta: PropTypes.shape({ + text: PropTypes.string, + }), + }), +}; + +const defaultProps = { + entity: {}, }; class Header extends React.Component { constructor(props) { super(props); - this.handleSaveTitle = this.handleSaveTitle.bind(this); - this.toggleEditMode = this.toggleEditMode.bind(this); - } - - handleSaveTitle(title) { - this.props.updateDashboardTitle(title); - } - - toggleEditMode() { - this.props.setEditMode(!this.props.editMode); + this.state = {}; } render() { - const { editMode } = this.props; - return ( -
-

- {}} - showTooltip={false} - /> -

- - - Action 1 - Action 2 - Action 3 - - - - + const { entity: { id, meta } } = this.props; + return !meta || !id ? null : ( +
+
+ {meta.text} +
); } } Header.propTypes = propTypes; +Header.defaultProps = defaultProps; export default Header; diff --git a/superset/assets/javascripts/dashboard/v2/components/ResizableContainer.jsx b/superset/assets/javascripts/dashboard/v2/components/ResizableContainer.jsx new file mode 100644 index 0000000000000..4f8ef4c66f5ac --- /dev/null +++ b/superset/assets/javascripts/dashboard/v2/components/ResizableContainer.jsx @@ -0,0 +1,160 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import Resizable from 're-resizable'; +import cx from 'classnames'; + +import { GRID_BASE_UNIT } from '../util/constants'; + +const propTypes = { + id: PropTypes.string.isRequired, + // adjustableWidth: PropTypes.bool, + adjustableHeight: PropTypes.bool, + widthStep: PropTypes.number, + heightStep: PropTypes.number, + widthMultiple: PropTypes.number, + heightMultiple: PropTypes.number, + minWidthMultiple: PropTypes.number, + maxWidthMultiple: PropTypes.number, + minHeightMultiple: PropTypes.number, + maxHeightMultiple: PropTypes.number, + onResizeStop: PropTypes.func, + onResizeStart: PropTypes.func, +}; + +const defaultProps = { + // adjustableWidth: true, + adjustableHeight: true, + widthStep: GRID_BASE_UNIT, + heightStep: GRID_BASE_UNIT, + widthMultiple: 1, + heightMultiple: 1, + minWidthMultiple: 1, + maxWidthMultiple: Infinity, + minHeightMultiple: 1, + maxHeightMultiple: Infinity, + onResizeStop: null, + onResizeStart: null, +}; + +const ADJUSTABLE_W_AND_H_CONFIG = { + top: false, + right: false, + bottom: false, + left: false, + topRight: false, + bottomRight: true, + bottomLeft: false, + topLeft: false, +}; + +const ADJUSTABLE_W_CONFIG = { + ...ADJUSTABLE_W_AND_H_CONFIG, + right: true, + bottomRight: false, +}; + +// These are preferrable to overriding classNames because we don't have to !important +const HANDLE_STYLES = { + right: { + width: 1, + height: 20, + right: 5, + top: '45%', + position: 'absolute', + borderLeft: '1px solid #484848', + borderRight: '1px solid #484848', + }, + bottomRight: { + border: 'solid #fff', + borderWidth: '0 1px 1px 0', + display: 'inline-block', + right: GRID_BASE_UNIT * 0.5, + bottom: GRID_BASE_UNIT * 0.5, + width: GRID_BASE_UNIT, + height: GRID_BASE_UNIT, + }, +}; + +class ResizableContainer extends React.Component { + constructor(props) { + super(props); + + this.state = { + isResizing: false, + }; + + this.handleResizeStart = this.handleResizeStart.bind(this); + this.handleResizeStop = this.handleResizeStop.bind(this); + } + + handleResizeStart() { + const { id, onResizeStart } = this.props; + if (onResizeStart) onResizeStart({ id }); + this.setState(() => ({ isResizing: true })); + } + + handleResizeStop(event, direction, ref, delta) { + const { id, onResizeStop, widthStep, heightStep, widthMultiple, heightMultiple } = this.props; + if (onResizeStop) { + const nextWidthMultiple = Math.round(widthMultiple + (delta.width / widthStep)); + const nextHeightMultiple = Math.round(heightMultiple + (delta.height / heightStep)); + + onResizeStop({ + id, + widthMultiple: nextWidthMultiple, + heightMultiple: nextHeightMultiple, + }); + this.setState(() => ({ isResizing: false })); + } + } + + render() { + const { + adjustableHeight, + widthStep, + heightStep, + widthMultiple, + heightMultiple, + minWidthMultiple, + maxWidthMultiple, + minHeightMultiple, + maxHeightMultiple, + } = this.props; + + const size = { + width: widthStep * widthMultiple, + height: heightStep * heightMultiple, + }; + + const enableConfig = adjustableHeight ? ADJUSTABLE_W_AND_H_CONFIG : ADJUSTABLE_W_CONFIG; + + return ( + +
+
width (remaining)
+
{`${widthMultiple} (${maxWidthMultiple - widthMultiple})`}
+
+
+ ); + } +} + +ResizableContainer.propTypes = propTypes; +ResizableContainer.defaultProps = defaultProps; + +export default ResizableContainer; diff --git a/superset/assets/javascripts/dashboard/v2/components/Row.jsx b/superset/assets/javascripts/dashboard/v2/components/Row.jsx new file mode 100644 index 0000000000000..0bc2bd656b751 --- /dev/null +++ b/superset/assets/javascripts/dashboard/v2/components/Row.jsx @@ -0,0 +1,138 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import ResizableContainer from './ResizableContainer'; + +import { + COLUMN_TYPE, + CHART_TYPE, + MARKDOWN_TYPE, + GRID_ROW_HEIGHT_UNIT, + GRID_MIN_ROW_HEIGHT, + GRID_COLUMN_COUNT, + GRID_MIN_COLUMN_COUNT, + GRID_MIN_ROW_UNITS, + GRID_MAX_ROW_UNITS, +} from '../util/constants'; + +const propTypes = { + row: PropTypes.object, + entities: PropTypes.object, + rowWidth: PropTypes.number, + columnWidth: PropTypes.number, + minElementWidth: PropTypes.number, + onResizeStart: PropTypes.func, + onResizeStop: PropTypes.func, +}; + +const defaultProps = { + row: {}, + entities: {}, + rowWidth: 0, + columnWidth: 0, + minElementWidth: 0, + onResizeStop: null, + onResizeStart: null, +}; + +class Row extends React.Component { + constructor(props) { + super(props); + this.state = { + modifiedEntities: { + ...props.entities, + }, + }; + this.handleResizeStart = this.handleResizeStart.bind(this); + this.handleResizeStop = this.handleResizeStop.bind(this); + } + + componentWillReceiveProps() { + // @TODO + } + + handleResizeStart({ id }) { + const { onResizeStart } = this.props; + if (onResizeStart) onResizeStart({ id }); + } + + handleResizeStop({ id, widthMultiple, heightMultiple }) { + const { onResizeStop } = this.props; + this.setState(({ modifiedEntities }) => { + const entity = modifiedEntities[id]; + if (entity.meta.width !== widthMultiple || entity.meta.height !== heightMultiple) { + return { + modifiedEntities: { + ...modifiedEntities, + [id]: { + ...entity, + meta: { + ...entity.meta, + width: widthMultiple, + height: heightMultiple, + }, + }, + }, + }; + } + return null; + }, onResizeStop); + } + + serializeRow() { + // @TODO + } + + render() { + const { row, rowWidth, columnWidth, minElementWidth } = this.props; + const { modifiedEntities } = this.state; + + const totalColumns = row.children.reduce((sum, curr) => ( + sum + modifiedEntities[curr].meta.width + ), 0); + + return ( +
+ {row.children.map((id) => { + const entity = modifiedEntities[id]; + const isResizable = [COLUMN_TYPE, CHART_TYPE, MARKDOWN_TYPE].indexOf(entity.type) > -1; + if (isResizable) { + return ( + + ); + } + return null; + })} + {!row.children.length && 'Empty row'} +
+ ); + } +} + +Row.propTypes = propTypes; +Row.defaultProps = defaultProps; + +export default Row; diff --git a/superset/assets/javascripts/dashboard/v2/components/dnd/DraggableNewComponent.jsx b/superset/assets/javascripts/dashboard/v2/components/dnd/DraggableNewComponent.jsx index a7a4b1caddb34..22f65675332bd 100644 --- a/superset/assets/javascripts/dashboard/v2/components/dnd/DraggableNewComponent.jsx +++ b/superset/assets/javascripts/dashboard/v2/components/dnd/DraggableNewComponent.jsx @@ -7,20 +7,18 @@ const propTypes = { id: PropTypes.string.isRequired, label: PropTypes.string.isRequired, index: PropTypes.number.isRequired, - draggableType: PropTypes.string, + type: PropTypes.string, }; export default class DraggableNewComponent extends React.Component { render() { - const { id, label, index, draggableType = undefined} = this.props; - if (!id) console.warn(`no 'id' provided for NewComponent ${type}`); - if (typeof index === 'undefined') console.warn(`no 'index' provided for NewComponent ${type}`); + const { id, label, index, type = undefined } = this.props; return ( {(provided, snapshot) => (
diff --git a/superset/assets/javascripts/dashboard/v2/components/dnd/DraggableNewRow.jsx b/superset/assets/javascripts/dashboard/v2/components/dnd/DraggableNewRow.jsx index dda588ec810d0..0333f2c109e29 100644 --- a/superset/assets/javascripts/dashboard/v2/components/dnd/DraggableNewRow.jsx +++ b/superset/assets/javascripts/dashboard/v2/components/dnd/DraggableNewRow.jsx @@ -15,7 +15,7 @@ export default class DraggableNewRow extends React.Component { ); diff --git a/superset/assets/javascripts/dashboard/v2/util/constants.js b/superset/assets/javascripts/dashboard/v2/util/constants.js index a46585786b7fd..e8c02eadd9ddc 100644 --- a/superset/assets/javascripts/dashboard/v2/util/constants.js +++ b/superset/assets/javascripts/dashboard/v2/util/constants.js @@ -1,19 +1,30 @@ -export const CHART = 'DASHBOARD_CHART'; -export const MARKDOWN = 'DASHBOARD_MARKDOWN'; -export const SEPARATOR = 'DASHBOARD_SEPARATOR'; -export const ROW = 'DASHBOARD_ROW'; -export const TABS = 'DASHBOARD_TABS'; -export const HEADER = 'DASHBOARD_HEADER'; +// Component types +export const CHART_TYPE = 'DASHBOARD_CHART_TYPE'; +export const MARKDOWN_TYPE = 'DASHBOARD_MARKDOWN_TYPE'; +export const DIVIDER_TYPE = 'DASHBOARD_DIVIDER_TYPE'; +export const ROW_TYPE = 'DASHBOARD_ROW_TYPE'; +export const COLUMN_TYPE = 'DASHBOARD_COLUMN_TYPE'; +export const TABS_TYPE = 'DASHBOARD_TABS_TYPE'; +export const HEADER_TYPE = 'DASHBOARD_HEADER_TYPE'; +// Drag and drop constants export const DROPPABLE_NEW_COMPONENT = 'DROPPABLE_NEW_COMPONENT'; export const DROPPABLE_DASHBOARD_ROOT = 'DASHBOARD_ROOT_DROPPABLE'; +export const DROPPABLE_DIRECTION_VERTICAL = 'vertical'; +export const DROPPABLE_DIRECTION_HORIZONTAL = 'horizontal'; export const DRAGGABLE_NEW_CHART = 'DRAGGABLE_NEW_CHART'; export const DRAGGABLE_NEW_DIVIDER = 'DRAGGABLE_NEW_DIVIDER'; export const DRAGGABLE_NEW_HEADER = 'DRAGGABLE_NEW_HEADER'; export const DRAGGABLE_NEW_ROW = 'DRAGGABLE_NEW_ROW'; -export const DRAGGABLE_ROW_TYPE = 'DRAGGABLE_ROW_TYPE'; +export const DRAGGABLE_TYPE_ROW = 'DRAGGABLE_TYPE_ROW'; -export const VERTICAL_DIRECTION = 'vertical'; -export const HORIZONTAL_DIRECTION = 'horizontal'; +// grid constants +export const GRID_BASE_UNIT = 8; +export const GRID_ROW_HEIGHT_UNIT = 2 * GRID_BASE_UNIT; +export const GRID_COLUMN_COUNT = 12; +export const GRID_MIN_COLUMN_COUNT = 3; +export const GRID_MIN_ROW_UNITS = 5; +export const GRID_MAX_ROW_UNITS = 100; +export const GRID_MIN_ROW_HEIGHT = GRID_ROW_HEIGHT_UNIT * GRID_MIN_ROW_UNITS; diff --git a/superset/assets/package.json b/superset/assets/package.json index d6af355764934..0028b656c8ac0 100644 --- a/superset/assets/package.json +++ b/superset/assets/package.json @@ -42,6 +42,7 @@ "dependencies": { "@data-ui/event-flow": "^0.0.8", "@data-ui/sparkline": "^0.0.49", + "@vx/responsive": "0.0.153", "babel-register": "^6.24.1", "bootstrap": "^3.3.6", "brace": "^0.10.0", @@ -71,6 +72,7 @@ "nvd3": "1.8.6", "po2json": "^0.4.5", "prop-types": "^15.6.0", + "re-resizable": "^4.3.0", "react": "^15.6.2", "react-ace": "^5.0.1", "react-addons-css-transition-group": "^15.6.0", @@ -86,7 +88,6 @@ "react-grid-layout": "^0.16.0", "react-map-gl": "^3.0.4", "react-redux": "^5.0.2", - "react-resizable": "^1.3.3", "react-select": "1.0.0-rc.10", "react-select-fast-filter-options": "^0.2.1", "react-sortable-hoc": "^0.6.7", diff --git a/superset/assets/stylesheets/dashboard-v2.css b/superset/assets/stylesheets/dashboard-v2.css index 0ad56d5d166cb..cdd012aa8e20f 100644 --- a/superset/assets/stylesheets/dashboard-v2.css +++ b/superset/assets/stylesheets/dashboard-v2.css @@ -40,6 +40,22 @@ box-shadow: 0 0 1px #fff; } +.grid-resizable-container { + width: 100%; + height: 100%; + background-color: #ff269e; + opacity: 0.9; + position: relative; +} + +.grid-resizable-container--resizing { + box-shadow: inset 0 0 0 2px #03A9F4; +} + +.grid-resizable-handle--right { + +} + /* @TODO remove upon new theme */ .btn.btn-primary { background: #484848 !important; From 0782ba043f8a3a5a196962d90b301d3f5298387f Mon Sep 17 00:00:00 2001 From: Chris Williams Date: Wed, 17 Jan 2018 18:17:31 -0800 Subject: [PATCH 02/25] [grid] gridComponents/ directory, add fixtures/ directory and test layout, add --- .../dashboard/v2/components/Box.jsx | 135 ------------------ .../dashboard/v2/components/DashboardGrid.jsx | 86 +++-------- .../v2/components/ResizableContainer.jsx | 85 ++++++++--- .../{Column.jsx => gridComponents/Chart.jsx} | 17 +-- .../v2/components/gridComponents/Column.jsx | 107 ++++++++++++++ .../{ => gridComponents}/Divider.jsx | 2 +- .../{ => gridComponents}/Header.jsx | 2 +- .../components/{ => gridComponents}/Row.jsx | 51 ++++--- .../v2/components/gridComponents/index.js | 27 ++++ .../dashboard/v2/fixtures/testLayout.js | 75 ++++++++++ .../dashboard/v2/testDashboardLayout.js | 3 - 11 files changed, 334 insertions(+), 256 deletions(-) delete mode 100644 superset/assets/javascripts/dashboard/v2/components/Box.jsx rename superset/assets/javascripts/dashboard/v2/components/{Column.jsx => gridComponents/Chart.jsx} (52%) create mode 100644 superset/assets/javascripts/dashboard/v2/components/gridComponents/Column.jsx rename superset/assets/javascripts/dashboard/v2/components/{ => gridComponents}/Divider.jsx (90%) rename superset/assets/javascripts/dashboard/v2/components/{ => gridComponents}/Header.jsx (93%) rename superset/assets/javascripts/dashboard/v2/components/{ => gridComponents}/Row.jsx (71%) create mode 100644 superset/assets/javascripts/dashboard/v2/components/gridComponents/index.js create mode 100644 superset/assets/javascripts/dashboard/v2/fixtures/testLayout.js delete mode 100644 superset/assets/javascripts/dashboard/v2/testDashboardLayout.js diff --git a/superset/assets/javascripts/dashboard/v2/components/Box.jsx b/superset/assets/javascripts/dashboard/v2/components/Box.jsx deleted file mode 100644 index 2017d3e2cef22..0000000000000 --- a/superset/assets/javascripts/dashboard/v2/components/Box.jsx +++ /dev/null @@ -1,135 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import Resizable from 're-resizable'; - -import { GRID_BASE_UNIT, GRID_ROW_HEIGHT_UNIT } from '../util/constants'; - -const propTypes = { - snapToWidth: PropTypes.number, - snapToHeight: PropTypes.number, - onResizeStart: PropTypes.func, - onResizeStop: PropTypes.func, - minWidth: PropTypes.number, - minHeight: PropTypes.number, - maxWidth: PropTypes.number, - maxHeight: PropTypes.number, - initialWidth: PropTypes.number, - initialHeight: PropTypes.number, - // width: PropTypes.number, - // height: PropTypes.number, -}; - -const defaultProps = { - snapToWidth: GRID_BASE_UNIT, - snapToHeight: 2 * GRID_ROW_HEIGHT_UNIT, - minWidth: 3 * GRID_BASE_UNIT, - minHeight: GRID_ROW_HEIGHT_UNIT, - maxWidth: Infinity, - maxHeight: Infinity, - initialWidth: 3 * GRID_BASE_UNIT, - initialHeight: GRID_ROW_HEIGHT_UNIT, - onResizeStop: null, - onResizeStart: null, - // width: null, - // height: null, -}; - -const ENABLE_CONFIG = { - top: false, - right: true, - bottom: true, - left: false, - topRight: false, - bottomRight: true, - bottomLeft: false, - topLeft: false, -}; - -class Box extends React.Component { - constructor(props) { - super(props); - const { initialWidth, initialHeight, minWidth, minHeight } = props; - - this.state = { - width: Math.max(initialWidth, minWidth), - height: Math.max(initialHeight, minHeight), - }; - - this.handleResizeStart = this.handleResizeStart.bind(this); - this.handleResizeStop = this.handleResizeStop.bind(this); - } - - handleResizeStart() { - if (this.props.onResizeStart) this.props.onResizeStart(); - } - - handleResizeStop(event, direction, ref, delta) { - console.log('resize delta', delta, direction) - const { onResizeStop, snapToWidth, snapToHeight, id } = this.props; - let nextState = {}; - - const callback = onResizeStop && (() => { - onResizeStop({ - ...nextState, - id, - widthMultiple: Math.floor(nextState.width / snapToWidth), - heightMultiple: Math.floor(nextState.height / snapToHeight), - }); - }); - - this.setState(({ width, height }) => { - nextState = { - width: width + delta.width, - height: height + delta.height, - }; - return nextState; - }, callback); - } - - render() { - const { - snapToWidth, - snapToHeight, - minWidth, - minHeight, - maxWidth, - maxHeight, - // width, - // height, - } = this.props; - - return ( - -
-
-
width (remaining)
-
{`${this.state.width}px (${maxWidth - this.state.width}px)`}
-
-
- ); - } -} - -Box.propTypes = propTypes; -Box.defaultProps = defaultProps; - -export default Box; diff --git a/superset/assets/javascripts/dashboard/v2/components/DashboardGrid.jsx b/superset/assets/javascripts/dashboard/v2/components/DashboardGrid.jsx index dd2634baef3d4..f489b03e7f31f 100644 --- a/superset/assets/javascripts/dashboard/v2/components/DashboardGrid.jsx +++ b/superset/assets/javascripts/dashboard/v2/components/DashboardGrid.jsx @@ -2,20 +2,18 @@ import React from 'react'; import PropTypes from 'prop-types'; import ParentSize from '@vx/responsive/build/components/ParentSize'; -import Box from './Box'; -import Header from './Header'; -import Row from './Row'; -import Divider from './Divider'; +import { Row, Header, Divider } from './gridComponents'; import { - GRID_COLUMN_COUNT, - GRID_MIN_COLUMN_COUNT, HEADER_TYPE, - ROW_TYPE, - CHART_TYPE, DIVIDER_TYPE, + ROW_TYPE, + GRID_COLUMN_COUNT, + GRID_MIN_COLUMN_COUNT, } from '../util/constants'; +import testLayout from '../fixtures/testLayout'; + const propTypes = { }; @@ -27,62 +25,7 @@ class DashboardGrid extends React.Component { super(props); this.state = { showGrid: false, - children: [ - 'header0', - 'row0', - 'divider0', - 'row1', - 'row2', - ], - entities: { - header0: { - id: 'header0', - type: HEADER_TYPE, - children: [], - meta: { - text: 'Section header', - }, - }, - divider0: { - id: 'divider0', - type: DIVIDER_TYPE, - children: [], - }, - row0: { - id: 'row0', - type: ROW_TYPE, - children: [], - }, - row1: { - id: 'row1', - type: ROW_TYPE, - children: [], - }, - row2: { - id: 'row2', - type: ROW_TYPE, - children: [ - 'chart0', - 'chart1', - ], - }, - chart0: { - id: 'chart0', - type: CHART_TYPE, - meta: { - width: 3, - height: 5, - }, - }, - chart1: { - id: 'chart1', - type: CHART_TYPE, - meta: { - width: 3, - height: 5, - }, - }, - }, + layout: testLayout, }; this.handleResizeStart = this.handleResizeStart.bind(this); @@ -98,7 +41,8 @@ class DashboardGrid extends React.Component { } render() { - const { showGrid, children, entities } = this.state; + const { showGrid, layout } = this.state; + const { children, entities } = layout; return ( @@ -112,17 +56,19 @@ class DashboardGrid extends React.Component { width, height, position: 'relative', - background: '#7affd2', + background: 'transparent', }} > {children.map((id) => { - if (/header/i.test(id)) return
; - if (/divider/i.test(id)) return ; - if (/row/i.test(id)) { + // @TODO In the future these will all be rows or invisible rows + const entity = entities[id]; + if (entity.type === HEADER_TYPE) return
; + if (entity.type === DIVIDER_TYPE) return ; + if (entity.type === ROW_TYPE) { return ( ({ isResizing: false })); } } render() { const { + children, + adjustableWidth, adjustableHeight, widthStep, heightStep, @@ -122,32 +157,38 @@ class ResizableContainer extends React.Component { } = this.props; const size = { - width: widthStep * widthMultiple, - height: heightStep * heightMultiple, + width: adjustableWidth ? widthStep * widthMultiple : '100%', + height: adjustableHeight ? heightStep * heightMultiple : '100%', }; - const enableConfig = adjustableHeight ? ADJUSTABLE_W_AND_H_CONFIG : ADJUSTABLE_W_CONFIG; + let enableConfig = ADJUSTABLE_W_AND_H_CONFIG; + if (!adjustableHeight) enableConfig = ADJUSTABLE_W_CONFIG; + else if (!adjustableWidth) enableConfig = ADJUSTABLE_H_CONFIG; return ( -
-
width (remaining)
-
{`${widthMultiple} (${maxWidthMultiple - widthMultiple})`}
+
+
+ {`width ${widthMultiple} (rem ${maxWidthMultiple - widthMultiple})`} +
+ {children}
); diff --git a/superset/assets/javascripts/dashboard/v2/components/Column.jsx b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Chart.jsx similarity index 52% rename from superset/assets/javascripts/dashboard/v2/components/Column.jsx rename to superset/assets/javascripts/dashboard/v2/components/gridComponents/Chart.jsx index dde983ef26517..3a972890be817 100644 --- a/superset/assets/javascripts/dashboard/v2/components/Column.jsx +++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Chart.jsx @@ -1,7 +1,7 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { } from '../util/constants'; +import { } from '../../util/constants'; const propTypes = { }; @@ -9,7 +9,7 @@ const propTypes = { const defaultProps = { }; -class Column extends React.Component { +class Chart extends React.Component { constructor(props) { super(props); this.state = { @@ -20,16 +20,17 @@ class Column extends React.Component { return (
+ >Chart
); } } -Column.propTypes = propTypes; -Column.defaultProps = defaultProps; +Chart.propTypes = propTypes; +Chart.defaultProps = defaultProps; -export default Column; +export default Chart; diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Column.jsx b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Column.jsx new file mode 100644 index 0000000000000..430875f5ecd4b --- /dev/null +++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Column.jsx @@ -0,0 +1,107 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +import ResizableContainer from '../ResizableContainer'; + +import { GRID_MIN_ROW_HEIGHT } from '../../util/constants'; + +const propTypes = { + entity: PropTypes.object, + entities: PropTypes.object, + onResizeStart: PropTypes.func, + onResizeStop: PropTypes.func, +}; + +const defaultProps = { + entity: {}, + entities: {}, + onResizeStop: null, + onResizeStart: null, +}; + +class Column extends React.Component { + constructor(props) { + super(props); + this.state = { + + }; + this.handleResizeStart = this.handleResizeStart.bind(this); + this.handleResizeStop = this.handleResizeStop.bind(this); + } + + handleResizeStart({ id }) { + const { onResizeStart } = this.props; + if (onResizeStart) onResizeStart({ id }); + } + + handleResizeStop({ id, heightMultiple }) { + // const { onResizeStop } = this.props; + // this.setState(({ modifiedEntities }) => { + // const entity = modifiedEntities[id]; + // if (entity.meta.width !== widthMultiple || entity.meta.height !== heightMultiple) { + // return { + // modifiedEntities: { + // ...modifiedEntities, + // [id]: { + // ...entity, + // meta: { + // ...entity.meta, + // width: widthMultiple, + // height: heightMultiple, + // }, + // }, + // }, + // }; + // } + // return null; + // }, onResizeStop); + } + + render() { + const { entity: columnEntity, entities, onResizeStop, onResizeStart } = this.props; + return ( +
+ {(columnEntity.children || []).map((id) => { + const entity = entities[id]; + const Component = COMPONENT_TYPE_LOOKUP[entity.type]; + const isResizable = [CHART_TYPE, MARKDOWN_TYPE].indexOf(entity.type) > -1; + + if (isResizable) { + return ( + + {} + + ); + } + return ; + })} + + {(!columnEntity.children || !columnEntity.children.length) + && 'Empty column'} +
+ ); + } +} + +Column.propTypes = propTypes; +Column.defaultProps = defaultProps; + +export default Column; diff --git a/superset/assets/javascripts/dashboard/v2/components/Divider.jsx b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Divider.jsx similarity index 90% rename from superset/assets/javascripts/dashboard/v2/components/Divider.jsx rename to superset/assets/javascripts/dashboard/v2/components/gridComponents/Divider.jsx index 3389440991544..d8fc5e641ce82 100644 --- a/superset/assets/javascripts/dashboard/v2/components/Divider.jsx +++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Divider.jsx @@ -1,7 +1,7 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { GRID_BASE_UNIT } from '../util/constants'; +import { GRID_BASE_UNIT } from '../../util/constants'; const propTypes = { }; diff --git a/superset/assets/javascripts/dashboard/v2/components/Header.jsx b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Header.jsx similarity index 93% rename from superset/assets/javascripts/dashboard/v2/components/Header.jsx rename to superset/assets/javascripts/dashboard/v2/components/gridComponents/Header.jsx index efd063114e2f6..7441814feca20 100644 --- a/superset/assets/javascripts/dashboard/v2/components/Header.jsx +++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Header.jsx @@ -1,7 +1,7 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { GRID_BASE_UNIT } from '../util/constants'; +import { GRID_BASE_UNIT } from '../../util/constants'; const propTypes = { entity: PropTypes.shape({ diff --git a/superset/assets/javascripts/dashboard/v2/components/Row.jsx b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Row.jsx similarity index 71% rename from superset/assets/javascripts/dashboard/v2/components/Row.jsx rename to superset/assets/javascripts/dashboard/v2/components/gridComponents/Row.jsx index 0bc2bd656b751..ec59326527f86 100644 --- a/superset/assets/javascripts/dashboard/v2/components/Row.jsx +++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Row.jsx @@ -1,6 +1,7 @@ import React from 'react'; import PropTypes from 'prop-types'; -import ResizableContainer from './ResizableContainer'; + +import ResizableContainer from '../ResizableContainer'; import { COLUMN_TYPE, @@ -12,10 +13,12 @@ import { GRID_MIN_COLUMN_COUNT, GRID_MIN_ROW_UNITS, GRID_MAX_ROW_UNITS, -} from '../util/constants'; +} from '../../util/constants'; + +import { COMPONENT_TYPE_LOOKUP } from './'; const propTypes = { - row: PropTypes.object, + entity: PropTypes.object, entities: PropTypes.object, rowWidth: PropTypes.number, columnWidth: PropTypes.number, @@ -25,7 +28,7 @@ const propTypes = { }; const defaultProps = { - row: {}, + entity: {}, entities: {}, rowWidth: 0, columnWidth: 0, @@ -67,8 +70,8 @@ class Row extends React.Component { ...entity, meta: { ...entity.meta, - width: widthMultiple, - height: heightMultiple, + width: widthMultiple || entity.meta.width, + height: heightMultiple || entity.meta.height, }, }, }, @@ -83,26 +86,39 @@ class Row extends React.Component { } render() { - const { row, rowWidth, columnWidth, minElementWidth } = this.props; + const { entity: rowEntity, columnWidth } = this.props; const { modifiedEntities } = this.state; - const totalColumns = row.children.reduce((sum, curr) => ( + const totalColumns = rowEntity.children.reduce((sum, curr) => ( sum + modifiedEntities[curr].meta.width ), 0); return (
- {row.children.map((id) => { + {(rowEntity.children || []).map((id) => { const entity = modifiedEntities[id]; + const Component = COMPONENT_TYPE_LOOKUP[entity.type]; + const ComponentInstance = ( + + ); const isResizable = [COLUMN_TYPE, CHART_TYPE, MARKDOWN_TYPE].indexOf(entity.type) > -1; if (isResizable) { return ( @@ -121,12 +137,15 @@ class Row extends React.Component { maxHeightMultiple={GRID_MAX_ROW_UNITS} onResizeStop={this.handleResizeStop} onResizeStart={this.handleResizeStart} - /> + > + {ComponentInstance} + ); } - return null; + return ComponentInstance; })} - {!row.children.length && 'Empty row'} + + {(!rowEntity.children || !rowEntity.children.length) && 'Empty row'}
); } diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/index.js b/superset/assets/javascripts/dashboard/v2/components/gridComponents/index.js new file mode 100644 index 0000000000000..d5df6133420ea --- /dev/null +++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/index.js @@ -0,0 +1,27 @@ +import { + CHART_TYPE, + COLUMN_TYPE, + DIVIDER_TYPE, + HEADER_TYPE, + ROW_TYPE, +} from '../../util/constants'; + +import Chart from './Chart'; +import Column from './Column'; +import Divider from './Divider'; +import Header from './Header'; +import Row from './Row'; + +export { default as Chart } from './Chart'; +export { default as Column } from './Column'; +export { default as Divider } from './Divider'; +export { default as Header } from './Header'; +export { default as Row } from './Row'; + +export const COMPONENT_TYPE_LOOKUP = { + [CHART_TYPE]: Chart, + [COLUMN_TYPE]: Column, + [DIVIDER_TYPE]: Divider, + [HEADER_TYPE]: Header, + [ROW_TYPE]: Row, +}; diff --git a/superset/assets/javascripts/dashboard/v2/fixtures/testLayout.js b/superset/assets/javascripts/dashboard/v2/fixtures/testLayout.js new file mode 100644 index 0000000000000..a0e31c0137dbf --- /dev/null +++ b/superset/assets/javascripts/dashboard/v2/fixtures/testLayout.js @@ -0,0 +1,75 @@ +import { + COLUMN_TYPE, + HEADER_TYPE, + ROW_TYPE, + CHART_TYPE, + DIVIDER_TYPE, +} from '../util/constants'; + +export default { + children: [ + 'header0', + 'row0', + 'divider0', + 'row1', + 'row2', + ], + entities: { + header0: { + id: 'header0', + type: HEADER_TYPE, + children: [], + meta: { + text: 'Section header', + }, + }, + divider0: { + id: 'divider0', + type: DIVIDER_TYPE, + children: [], + }, + row0: { + id: 'row0', + type: ROW_TYPE, + children: [], + }, + row1: { + id: 'row1', + type: ROW_TYPE, + children: [], + }, + row2: { + id: 'row2', + type: ROW_TYPE, + children: [ + 'column0', + 'chart0', + 'chart1', + ], + }, + chart0: { + id: 'chart0', + type: CHART_TYPE, + meta: { + width: 3, + height: 10, + }, + }, + chart1: { + id: 'chart1', + type: CHART_TYPE, + meta: { + width: 3, + height: 10, + }, + }, + column0: { + id: 'column0', + type: COLUMN_TYPE, + meta: { + width: 3, + height: 5, + }, + }, + }, +}; diff --git a/superset/assets/javascripts/dashboard/v2/testDashboardLayout.js b/superset/assets/javascripts/dashboard/v2/testDashboardLayout.js deleted file mode 100644 index ef096d7f71233..0000000000000 --- a/superset/assets/javascripts/dashboard/v2/testDashboardLayout.js +++ /dev/null @@ -1,3 +0,0 @@ -import { CHART, HEADER, ROW } from './builderTypes'; - -export default {}; From 625eba32c8d14d42931ce87803b6d64a99ec9301 Mon Sep 17 00:00:00 2001 From: Chris Williams Date: Mon, 22 Jan 2018 12:28:21 -0800 Subject: [PATCH 03/25] [grid] working grid with gutters --- .../v2/components/BuilderComponentPane.jsx | 10 +- .../v2/components/DashboardBuilder.jsx | 30 +--- .../dashboard/v2/components/DashboardGrid.jsx | 140 ++++++++++-------- .../v2/components/gridComponents/Chart.jsx | 2 +- .../v2/components/gridComponents/Column.jsx | 99 ++++++------- .../v2/components/gridComponents/Row.jsx | 75 +++++----- .../v2/components/gridComponents/Spacer.jsx | 34 +++++ .../v2/components/gridComponents/grid.css | 47 ++++++ .../v2/components/gridComponents/index.js | 4 + .../{ => resizable}/ResizableContainer.jsx | 125 ++++++---------- .../components/resizable/ResizableHandle.jsx | 25 ++++ .../v2/components/resizable/resizable.css | 64 ++++++++ .../dashboard/v2/fixtures/testLayout.js | 110 +++++++++++--- .../dashboard/v2/util/constants.js | 5 +- .../dashboard/v2/util/gridUtils.js | 15 ++ .../dashboard/v2/util/resizableConfig.js | 28 ++++ superset/assets/stylesheets/dashboard-v2.css | 35 +++-- 17 files changed, 534 insertions(+), 314 deletions(-) create mode 100644 superset/assets/javascripts/dashboard/v2/components/gridComponents/Spacer.jsx create mode 100644 superset/assets/javascripts/dashboard/v2/components/gridComponents/grid.css rename superset/assets/javascripts/dashboard/v2/components/{ => resizable}/ResizableContainer.jsx (57%) create mode 100644 superset/assets/javascripts/dashboard/v2/components/resizable/ResizableHandle.jsx create mode 100644 superset/assets/javascripts/dashboard/v2/components/resizable/resizable.css create mode 100644 superset/assets/javascripts/dashboard/v2/util/gridUtils.js create mode 100644 superset/assets/javascripts/dashboard/v2/util/resizableConfig.js diff --git a/superset/assets/javascripts/dashboard/v2/components/BuilderComponentPane.jsx b/superset/assets/javascripts/dashboard/v2/components/BuilderComponentPane.jsx index 3938a51a5fec3..e1139f1f3d841 100644 --- a/superset/assets/javascripts/dashboard/v2/components/BuilderComponentPane.jsx +++ b/superset/assets/javascripts/dashboard/v2/components/BuilderComponentPane.jsx @@ -17,7 +17,7 @@ class BuilderComponentPane extends React.Component { render() { return (
-
+
Insert components
{provided => ( -
+
diff --git a/superset/assets/javascripts/dashboard/v2/components/DashboardBuilder.jsx b/superset/assets/javascripts/dashboard/v2/components/DashboardBuilder.jsx index 23e5a72afa262..4e380823e684c 100644 --- a/superset/assets/javascripts/dashboard/v2/components/DashboardBuilder.jsx +++ b/superset/assets/javascripts/dashboard/v2/components/DashboardBuilder.jsx @@ -4,9 +4,9 @@ import PropTypes from 'prop-types'; import { DragDropContext, Droppable, Draggable } from 'react-beautiful-dnd'; import BuilderComponentPane from './BuilderComponentPane'; import DashboardGrid from './DashboardGrid'; -import { reorder, reorderRows } from '../util/dnd-reorder'; +// import { reorder, reorderRows } from '../util/dnd-reorder'; -import { DROPPABLE_DASHBOARD_ROOT, DRAGGABLE_ROW_TYPE } from '../util/constants'; +// import { DROPPABLE_DASHBOARD_ROOT, DRAGGABLE_ROW_TYPE } from '../util/constants'; const propTypes = { editMode: PropTypes.bool, @@ -79,30 +79,8 @@ class DashboardBuilder extends React.Component { onDragStart={this.handleDragStart} onDragEnd={this.handleDragEnd} > -
-
- - {/* - {(provided, snapshot) => ( -
- {this.state.rows.map(id => this.renderRow(id))} - {provided.placeholder} -
- )} -
*/} -
+
+
diff --git a/superset/assets/javascripts/dashboard/v2/components/DashboardGrid.jsx b/superset/assets/javascripts/dashboard/v2/components/DashboardGrid.jsx index f489b03e7f31f..bb2cfb7536acb 100644 --- a/superset/assets/javascripts/dashboard/v2/components/DashboardGrid.jsx +++ b/superset/assets/javascripts/dashboard/v2/components/DashboardGrid.jsx @@ -1,13 +1,12 @@ import React from 'react'; -import PropTypes from 'prop-types'; +// import PropTypes from 'prop-types'; import ParentSize from '@vx/responsive/build/components/ParentSize'; import { Row, Header, Divider } from './gridComponents'; +import './gridComponents/grid.css'; import { - HEADER_TYPE, - DIVIDER_TYPE, - ROW_TYPE, + GRID_GUTTER_SIZE, GRID_COLUMN_COUNT, GRID_MIN_COLUMN_COUNT, } from '../util/constants'; @@ -20,84 +19,101 @@ const propTypes = { const defaultProps = { }; -class DashboardGrid extends React.Component { +class DashboardGrid extends React.PureComponent { constructor(props) { super(props); this.state = { showGrid: false, layout: testLayout, + rowGuide: null, }; this.handleResizeStart = this.handleResizeStart.bind(this); + this.handleResize = this.handleResize.bind(this); this.handleResizeStop = this.handleResizeStop.bind(this); + this.getRowGuidePosition = this.getRowGuidePosition.bind(this); } - handleResizeStart() { - this.setState(() => ({ showGrid: true })); + getRowGuidePosition(resizeRef) { + if (resizeRef && this.grid) { + return resizeRef.getBoundingClientRect().bottom - this.grid.getBoundingClientRect().top - 1; + } + return null; + } + + handleResizeStart({ ref, direction }) { + let rowGuide = null; + if (direction === 'bottom' || direction === 'bottomRight') { + rowGuide = this.getRowGuidePosition() + } + + this.setState(() => ({ showGrid: true, rowGuide })); + } + + handleResize({ ref, direction }) { + if (direction === 'bottom' || direction === 'bottomRight') { + this.setState(() => ({ rowGuide: this.getRowGuidePosition(ref) })); + } } handleResizeStop() { - this.setState(() => ({ showGrid: false })); + this.setState(() => ({ showGrid: false, rowGuide: null })); } render() { - const { showGrid, layout } = this.state; + const { showGrid, layout, rowGuide } = this.state; const { children, entities } = layout; return ( - - {({ width, height }) => { - const columnWidth = width / GRID_COLUMN_COUNT; - const minElementWidth = Math.floor(GRID_MIN_COLUMN_COUNT * columnWidth); - - return width < 10 ? null : ( -
- {children.map((id) => { - // @TODO In the future these will all be rows or invisible rows - const entity = entities[id]; - if (entity.type === HEADER_TYPE) return
; - if (entity.type === DIVIDER_TYPE) return ; - if (entity.type === ROW_TYPE) { - return ( - - ); - } - return null; - })} - - {showGrid && Array(GRID_COLUMN_COUNT + 1).fill(null).map((_, i) => ( -
- ))} -
- ); - }} - +
+ + {({ width, height }) => { + const columnPlusGutterWidth = width / GRID_COLUMN_COUNT; + const extraGutterWidth = GRID_GUTTER_SIZE / GRID_COLUMN_COUNT; + const columnWidth = columnPlusGutterWidth - GRID_GUTTER_SIZE + extraGutterWidth; + + return width < 50 ? null : ( +
{ this.grid = ref; }} + style={{ width, height }} + > + {children.map(id => ( + + ))} + + {showGrid && Array(GRID_COLUMN_COUNT).fill(null).map((_, i) => ( +
+ ))} + + {showGrid && rowGuide && +
} +
+ ); + }} + +
); } } diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Chart.jsx b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Chart.jsx index 3a972890be817..50e295957c69e 100644 --- a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Chart.jsx +++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Chart.jsx @@ -22,7 +22,7 @@ class Chart extends React.Component { style={{ width: '100%', height: '100%', - backgroundColor: '#FFFCE1', + backgroundColor: '#fff', padding: 16, }} >Chart
diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Column.jsx b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Column.jsx index 430875f5ecd4b..9f5aae2ca98f8 100644 --- a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Column.jsx +++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Column.jsx @@ -1,14 +1,24 @@ import React from 'react'; import PropTypes from 'prop-types'; -import ResizableContainer from '../ResizableContainer'; +import ResizableContainer from '../resizable/ResizableContainer'; -import { GRID_MIN_ROW_HEIGHT } from '../../util/constants'; +import { COMPONENT_TYPE_LOOKUP } from './'; +import { componentIsResizable } from '../../util/gridUtils'; + +import { + SPACER_TYPE, + GRID_GUTTER_SIZE, + GRID_ROW_HEIGHT_UNIT, + GRID_MIN_ROW_UNITS, + GRID_MAX_ROW_UNITS, +} from '../../util/constants'; const propTypes = { entity: PropTypes.object, entities: PropTypes.object, onResizeStart: PropTypes.func, + onResize: PropTypes.func, onResizeStop: PropTypes.func, }; @@ -16,82 +26,61 @@ const defaultProps = { entity: {}, entities: {}, onResizeStop: null, + onResize: null, onResizeStart: null, }; -class Column extends React.Component { - constructor(props) { - super(props); - this.state = { - - }; - this.handleResizeStart = this.handleResizeStart.bind(this); - this.handleResizeStop = this.handleResizeStop.bind(this); - } +class Column extends React.PureComponent { + render() { + const { entity: columnEntity, entities, onResizeStop, onResize, onResizeStart } = this.props; - handleResizeStart({ id }) { - const { onResizeStart } = this.props; - if (onResizeStart) onResizeStart({ id }); - } + const columnItems = []; - handleResizeStop({ id, heightMultiple }) { - // const { onResizeStop } = this.props; - // this.setState(({ modifiedEntities }) => { - // const entity = modifiedEntities[id]; - // if (entity.meta.width !== widthMultiple || entity.meta.height !== heightMultiple) { - // return { - // modifiedEntities: { - // ...modifiedEntities, - // [id]: { - // ...entity, - // meta: { - // ...entity.meta, - // width: widthMultiple, - // height: heightMultiple, - // }, - // }, - // }, - // }; - // } - // return null; - // }, onResizeStop); - } + (columnEntity.children || []).forEach((id, index) => { + const entity = entities[id]; + columnItems.push(entity); + if (index < columnEntity.children.length - 1) columnItems.push(`gutter-${index}`); + }); - render() { - const { entity: columnEntity, entities, onResizeStop, onResizeStart } = this.props; return ( -
- {(columnEntity.children || []).map((id) => { - const entity = entities[id]; +
+ {columnItems.map((entity) => { + const id = entity.id || entity; const Component = COMPONENT_TYPE_LOOKUP[entity.type]; - const isResizable = [CHART_TYPE, MARKDOWN_TYPE].indexOf(entity.type) > -1; + const isResizable = componentIsResizable(entity); + + let ColumnItem = Component ? ( + + ) :
; if (isResizable) { - return ( + ColumnItem = ( - {} + {ColumnItem} ); } - return ; + return ColumnItem; })} {(!columnEntity.children || !columnEntity.children.length) diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Row.jsx b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Row.jsx index ec59326527f86..486cf80980ed2 100644 --- a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Row.jsx +++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Row.jsx @@ -1,14 +1,15 @@ import React from 'react'; import PropTypes from 'prop-types'; -import ResizableContainer from '../ResizableContainer'; +import ResizableContainer from '../resizable/ResizableContainer'; +import { componentIsResizable } from '../../util/gridUtils'; import { COLUMN_TYPE, - CHART_TYPE, - MARKDOWN_TYPE, + SPACER_TYPE, + GRID_BASE_UNIT, + GRID_GUTTER_SIZE, GRID_ROW_HEIGHT_UNIT, - GRID_MIN_ROW_HEIGHT, GRID_COLUMN_COUNT, GRID_MIN_COLUMN_COUNT, GRID_MIN_ROW_UNITS, @@ -20,20 +21,18 @@ import { COMPONENT_TYPE_LOOKUP } from './'; const propTypes = { entity: PropTypes.object, entities: PropTypes.object, - rowWidth: PropTypes.number, columnWidth: PropTypes.number, - minElementWidth: PropTypes.number, onResizeStart: PropTypes.func, + onResize: PropTypes.func, onResizeStop: PropTypes.func, }; const defaultProps = { entity: {}, entities: {}, - rowWidth: 0, columnWidth: 0, - minElementWidth: 0, onResizeStop: null, + onResize: null, onResizeStart: null, }; @@ -53,9 +52,9 @@ class Row extends React.Component { // @TODO } - handleResizeStart({ id }) { + handleResizeStart(args) { const { onResizeStart } = this.props; - if (onResizeStart) onResizeStart({ id }); + if (onResizeStart) onResizeStart(args); } handleResizeStop({ id, widthMultiple, heightMultiple }) { @@ -86,63 +85,65 @@ class Row extends React.Component { } render() { - const { entity: rowEntity, columnWidth } = this.props; + const { entity: rowEntity, columnWidth, onResize } = this.props; const { modifiedEntities } = this.state; - const totalColumns = rowEntity.children.reduce((sum, curr) => ( - sum + modifiedEntities[curr].meta.width - ), 0); + let totalColumns = 0; + const rowItems = []; + + (rowEntity.children || []).forEach((id, index) => { + const entity = modifiedEntities[id]; + totalColumns += (entity.meta || {}).width || 0; + rowItems.push(entity); + if (index < rowEntity.children.length - 1) rowItems.push(`gutter-${index}`); + }); return ( -
- {(rowEntity.children || []).map((id) => { - const entity = modifiedEntities[id]; + // @TODO row vs invisible row +
+ {rowItems.map((entity) => { + const id = entity.id || entity; const Component = COMPONENT_TYPE_LOOKUP[entity.type]; - const ComponentInstance = ( + const isSpacer = entity.type === SPACER_TYPE; + const isResizable = componentIsResizable(entity); + + let RowItem = Component ? ( - ); - const isResizable = [COLUMN_TYPE, CHART_TYPE, MARKDOWN_TYPE].indexOf(entity.type) > -1; + ) :
; + if (isResizable) { - return ( + RowItem = ( - {ComponentInstance} + {RowItem} ); } - return ComponentInstance; + return RowItem; })} {(!rowEntity.children || !rowEntity.children.length) && 'Empty row'} diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Spacer.jsx b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Spacer.jsx new file mode 100644 index 0000000000000..9cbcefcf3a399 --- /dev/null +++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Spacer.jsx @@ -0,0 +1,34 @@ +import React from 'react'; + +const propTypes = { +}; + +const defaultProps = { +}; + +class Spacer extends React.Component { + constructor(props) { + super(props); + this.state = { + }; + } + + render() { + return ( +
+ ); + } +} + +Spacer.propTypes = propTypes; +Spacer.defaultProps = defaultProps; + +export default Spacer; diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/grid.css b/superset/assets/javascripts/dashboard/v2/components/gridComponents/grid.css new file mode 100644 index 0000000000000..ecac7dfda6f80 --- /dev/null +++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/grid.css @@ -0,0 +1,47 @@ +.grid-container { + flex: 1; + min-width: 66%; + margin: 16px; + height: 100%; + overflow-y: auto; + position: relative; +} + +.grid-column { + width: 100%; + min-height: 16px; +} + +.grid-row { + display: flex; + flex-direction: row; + flex-wrap: nowrap; + align-items: flex-start; + min-height: 16px; + height: fit-content; + display: flex; + background-color: #D5BCDB; + box-shadow: 0 0 0 1px white; +} + + .grid-row--invisible { + background-color: transparent; + } + +/* Editing guides */ +.grid-column-guide { + position: absolute; + top: 0; + height: 100%; + background-color: #44C0FF; + opacity: 0.1; + pointer-events: none; +} + +.grid-row-guide { + position: absolute; + left: 0; + height: 1; + background-color: #44C0FF; + pointer-events: none; +} diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/index.js b/superset/assets/javascripts/dashboard/v2/components/gridComponents/index.js index d5df6133420ea..22dbcfee4cb31 100644 --- a/superset/assets/javascripts/dashboard/v2/components/gridComponents/index.js +++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/index.js @@ -4,6 +4,7 @@ import { DIVIDER_TYPE, HEADER_TYPE, ROW_TYPE, + SPACER_TYPE, } from '../../util/constants'; import Chart from './Chart'; @@ -11,12 +12,14 @@ import Column from './Column'; import Divider from './Divider'; import Header from './Header'; import Row from './Row'; +import Spacer from './Spacer'; export { default as Chart } from './Chart'; export { default as Column } from './Column'; export { default as Divider } from './Divider'; export { default as Header } from './Header'; export { default as Row } from './Row'; +export { default as Spacer } from './Spacer'; export const COMPONENT_TYPE_LOOKUP = { [CHART_TYPE]: Chart, @@ -24,4 +27,5 @@ export const COMPONENT_TYPE_LOOKUP = { [DIVIDER_TYPE]: Divider, [HEADER_TYPE]: Header, [ROW_TYPE]: Row, + [SPACER_TYPE]: Spacer, }; diff --git a/superset/assets/javascripts/dashboard/v2/components/ResizableContainer.jsx b/superset/assets/javascripts/dashboard/v2/components/resizable/ResizableContainer.jsx similarity index 57% rename from superset/assets/javascripts/dashboard/v2/components/ResizableContainer.jsx rename to superset/assets/javascripts/dashboard/v2/components/resizable/ResizableContainer.jsx index 2e33c05b41cb4..fbd7c68eb9c85 100644 --- a/superset/assets/javascripts/dashboard/v2/components/ResizableContainer.jsx +++ b/superset/assets/javascripts/dashboard/v2/components/resizable/ResizableContainer.jsx @@ -3,13 +3,17 @@ import PropTypes from 'prop-types'; import Resizable from 're-resizable'; import cx from 'classnames'; -import { GRID_BASE_UNIT } from '../util/constants'; +import ResizableHandle from './ResizableHandle'; +import resizableConfig from '../../util/resizableConfig'; +import { GRID_BASE_UNIT } from '../../util/constants'; +import './resizable.css'; const propTypes = { id: PropTypes.string.isRequired, children: PropTypes.node, adjustableWidth: PropTypes.bool, adjustableHeight: PropTypes.bool, + gutterWidth: PropTypes.number, widthStep: PropTypes.number, heightStep: PropTypes.number, widthMultiple: PropTypes.number, @@ -19,6 +23,7 @@ const propTypes = { minHeightMultiple: PropTypes.number, maxHeightMultiple: PropTypes.number, onResizeStop: PropTypes.func, + onResize: PropTypes.func, onResizeStart: PropTypes.func, }; @@ -26,6 +31,7 @@ const defaultProps = { children: null, adjustableWidth: true, adjustableHeight: true, + gutterWidth: 0, widthStep: GRID_BASE_UNIT, heightStep: GRID_BASE_UNIT, widthMultiple: 1, @@ -35,69 +41,13 @@ const defaultProps = { minHeightMultiple: 1, maxHeightMultiple: Infinity, onResizeStop: null, + onResize: null, onResizeStart: null, }; -const ADJUSTABLE_W_AND_H_CONFIG = { - top: false, - right: false, - bottom: false, - left: false, - topRight: false, - bottomRight: true, - bottomLeft: false, - topLeft: false, -}; - -const ADJUSTABLE_W_CONFIG = { - ...ADJUSTABLE_W_AND_H_CONFIG, - right: true, - bottomRight: false, -}; - -const ADJUSTABLE_H_CONFIG = { - ...ADJUSTABLE_W_AND_H_CONFIG, - bottom: true, - bottomRight: false, -}; - -// These are preferrable to overriding classNames because we don't have to !important -// @TODO move to utils -const HANDLE_STYLES = { - right: { - width: 1, - height: 20, - right: 5, - top: '45%', - position: 'absolute', - borderLeft: '1px solid #484848', - borderRight: '1px solid #484848', - }, - bottom: { - height: 1, - width: 20, - bottom: 5, - left: '45%', - position: 'absolute', - borderTop: '1px solid #484848', - borderBottom: '1px solid #484848', - }, - bottomRight: { - border: 'solid', - borderWidth: `${0}px 1.5px 1.5px ${0}px`, - // borderTopColor: 'transparent', - // borderLeftColor: 'transparent', - borderRightColor: '#484848', - borderBottomColor: '#484848', - display: 'inline-block', - right: GRID_BASE_UNIT, - bottom: GRID_BASE_UNIT, - width: GRID_BASE_UNIT, - height: GRID_BASE_UNIT, - }, -}; +const snapToGrid = [GRID_BASE_UNIT, GRID_BASE_UNIT]; -class ResizableContainer extends React.Component { +class ResizableContainer extends React.PureComponent { constructor(props) { super(props); @@ -106,15 +56,27 @@ class ResizableContainer extends React.Component { }; this.handleResizeStart = this.handleResizeStart.bind(this); + this.handleResize = this.handleResize.bind(this); this.handleResizeStop = this.handleResizeStop.bind(this); } - handleResizeStart() { + handleResizeStart(event, direction, ref) { const { id, onResizeStart } = this.props; - if (onResizeStart) onResizeStart({ id }); + + if (onResizeStart) { + onResizeStart({ id, direction, ref }); + } + this.setState(() => ({ isResizing: true })); } + handleResize(event, direction, ref) { + const { onResize, id } = this.props; + if (onResize) { + onResize({ id, direction, ref }); + } + } + handleResizeStop(event, direction, ref, delta) { const { id, @@ -154,42 +116,39 @@ class ResizableContainer extends React.Component { maxWidthMultiple, minHeightMultiple, maxHeightMultiple, + gutterWidth, } = this.props; const size = { - width: adjustableWidth ? widthStep * widthMultiple : '100%', + width: adjustableWidth ? (widthStep * widthMultiple) - gutterWidth : '100%', height: adjustableHeight ? heightStep * heightMultiple : '100%', }; - let enableConfig = ADJUSTABLE_W_AND_H_CONFIG; - if (!adjustableHeight) enableConfig = ADJUSTABLE_W_CONFIG; - else if (!adjustableWidth) enableConfig = ADJUSTABLE_H_CONFIG; + let enableConfig = resizableConfig.widthAndHeight; + if (!adjustableHeight) enableConfig = resizableConfig.widthOnly; + else if (!adjustableWidth) enableConfig = resizableConfig.heightOnly; + + const { isResizing } = this.state; return ( -
-
- {`width ${widthMultiple} (rem ${maxWidthMultiple - widthMultiple})`} -
- {children} -
+ {children}
); } diff --git a/superset/assets/javascripts/dashboard/v2/components/resizable/ResizableHandle.jsx b/superset/assets/javascripts/dashboard/v2/components/resizable/ResizableHandle.jsx new file mode 100644 index 0000000000000..9536f6bbf8bc5 --- /dev/null +++ b/superset/assets/javascripts/dashboard/v2/components/resizable/ResizableHandle.jsx @@ -0,0 +1,25 @@ +import React from 'react'; + +export function BottomRightResizeHandle() { + return ( +
+ ); +} + +export function RightResizeHandle() { + return ( +
+ ); +} + +export function BottomResizeHandle() { + return ( +
+ ); +} + +export default { + right: RightResizeHandle, + bottom: BottomResizeHandle, + bottomRight: BottomRightResizeHandle, +}; diff --git a/superset/assets/javascripts/dashboard/v2/components/resizable/resizable.css b/superset/assets/javascripts/dashboard/v2/components/resizable/resizable.css new file mode 100644 index 0000000000000..805d971fd05c5 --- /dev/null +++ b/superset/assets/javascripts/dashboard/v2/components/resizable/resizable.css @@ -0,0 +1,64 @@ +.grid-resizable-container { + background-color: transparent; + position: relative; +} + +/* after ensures border visibility on top of any children */ +.grid-resizable-container--resizing::after { + content: ""; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + box-shadow: inset 0 0 0 2px #44C0FF; +} + +.resize-handle { + opacity: 0; +} + + .grid-resizable-container:hover .resize-handle, + .grid-resizable-container--resizing .resize-handle { + opacity: 1; + } + +.resize-handle--bottom-right { + position: absolute; + border: solid; + border-width: 0 1.5px 1.5px 0; + border-right-color: #484848; + border-bottom-color: #484848; + right: 16; + bottom: 16; + width: 8px; + height: 8px; +} + +.resize-handle--right { + width: 2px; + height: 20px; + right: 10px; + top: 50%; + position: absolute; + border-left: 1px solid #484848; + border-right: 1px solid #484848; +} + + .grid-spacer + span .resize-handle--right { + right: 50%; + } + +.resize-handle--bottom { + height: 2px; + width: 20px; + bottom: 10px; + left: 50%; + position: absolute; + border-top: 1px solid #484848; + border-bottom: 1px solid #484848; +} + +.grid-resizable-container--resizing .resize-handle:first-child { + border-color: #44C0FF; +} diff --git a/superset/assets/javascripts/dashboard/v2/fixtures/testLayout.js b/superset/assets/javascripts/dashboard/v2/fixtures/testLayout.js index a0e31c0137dbf..e7abdf097b4f9 100644 --- a/superset/assets/javascripts/dashboard/v2/fixtures/testLayout.js +++ b/superset/assets/javascripts/dashboard/v2/fixtures/testLayout.js @@ -2,73 +2,137 @@ import { COLUMN_TYPE, HEADER_TYPE, ROW_TYPE, + INVISIBLE_ROW_TYPE, + SPACER_TYPE, CHART_TYPE, DIVIDER_TYPE, } from '../util/constants'; export default { children: [ - 'header0', 'row0', - 'divider0', 'row1', 'row2', + 'row3', + 'row4', ], entities: { - header0: { - id: 'header0', - type: HEADER_TYPE, - children: [], - meta: { - text: 'Section header', - }, - }, - divider0: { - id: 'divider0', - type: DIVIDER_TYPE, - children: [], - }, row0: { id: 'row0', type: ROW_TYPE, - children: [], + children: ['header0'], }, row1: { id: 'row1', type: ROW_TYPE, - children: [], + children: [ + 'divider0', + ], }, row2: { id: 'row2', type: ROW_TYPE, + children: [], + }, + row3: { + id: 'row3', + type: ROW_TYPE, + children: [ + 'header1', + ], + }, + row4: { + id: 'row4', + type: ROW_TYPE, children: [ 'column0', - 'chart0', - 'chart1', + 'chart3', + 'spacer0', + 'chart4', ], }, + header0: { + id: 'header0', + type: HEADER_TYPE, + meta: { + text: 'Section header', + }, + }, + header1: { + id: 'header1', + type: HEADER_TYPE, + meta: { + text: 'Header in row', + }, + }, + divider0: { + id: 'divider0', + type: DIVIDER_TYPE, + children: [], + }, + chart0: { id: 'chart0', type: CHART_TYPE, meta: { - width: 3, - height: 10, + height: 6, }, }, chart1: { id: 'chart1', type: CHART_TYPE, + meta: { + height: 6, + }, + }, + chart2: { + id: 'chart2', + type: CHART_TYPE, + meta: { + height: 6, + }, + }, + chart3: { + id: 'chart3', + type: CHART_TYPE, + meta: { + width: 3, + height: 20, + }, + }, + chart4: { + id: 'chart4', + type: CHART_TYPE, meta: { width: 3, - height: 10, + height: 20, }, }, column0: { id: 'column0', type: COLUMN_TYPE, + children: [ + 'chart0', + 'chart1', + 'spacer1', + 'chart2', + ], meta: { width: 3, - height: 5, + }, + }, + spacer0: { + id: 'spacer0', + type: SPACER_TYPE, + meta: { + width: 1, + }, + }, + spacer1: { + id: 'spacer1', + type: SPACER_TYPE, + meta: { + height: 1, }, }, }, diff --git a/superset/assets/javascripts/dashboard/v2/util/constants.js b/superset/assets/javascripts/dashboard/v2/util/constants.js index e8c02eadd9ddc..19198a1eebc2a 100644 --- a/superset/assets/javascripts/dashboard/v2/util/constants.js +++ b/superset/assets/javascripts/dashboard/v2/util/constants.js @@ -3,9 +3,11 @@ export const CHART_TYPE = 'DASHBOARD_CHART_TYPE'; export const MARKDOWN_TYPE = 'DASHBOARD_MARKDOWN_TYPE'; export const DIVIDER_TYPE = 'DASHBOARD_DIVIDER_TYPE'; export const ROW_TYPE = 'DASHBOARD_ROW_TYPE'; +export const INVISIBLE_ROW_TYPE = 'DASHBOARD_INVISIBLE_ROW_TYPE'; export const COLUMN_TYPE = 'DASHBOARD_COLUMN_TYPE'; export const TABS_TYPE = 'DASHBOARD_TABS_TYPE'; export const HEADER_TYPE = 'DASHBOARD_HEADER_TYPE'; +export const SPACER_TYPE = 'DASHBOARD_SPACER_TYPE'; // Drag and drop constants export const DROPPABLE_NEW_COMPONENT = 'DROPPABLE_NEW_COMPONENT'; @@ -22,9 +24,10 @@ export const DRAGGABLE_TYPE_ROW = 'DRAGGABLE_TYPE_ROW'; // grid constants 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; export const GRID_COLUMN_COUNT = 12; export const GRID_MIN_COLUMN_COUNT = 3; export const GRID_MIN_ROW_UNITS = 5; export const GRID_MAX_ROW_UNITS = 100; -export const GRID_MIN_ROW_HEIGHT = GRID_ROW_HEIGHT_UNIT * GRID_MIN_ROW_UNITS; +export const GRID_MIN_ROW_HEIGHT = GRID_GUTTER_SIZE; diff --git a/superset/assets/javascripts/dashboard/v2/util/gridUtils.js b/superset/assets/javascripts/dashboard/v2/util/gridUtils.js new file mode 100644 index 0000000000000..2c483b4445a74 --- /dev/null +++ b/superset/assets/javascripts/dashboard/v2/util/gridUtils.js @@ -0,0 +1,15 @@ +import { + SPACER_TYPE, + COLUMN_TYPE, + CHART_TYPE, + MARKDOWN_TYPE, +} from './constants'; + +export function componentIsResizable(entity) { + return [ + SPACER_TYPE, + COLUMN_TYPE, + CHART_TYPE, + MARKDOWN_TYPE, + ].indexOf(entity.type) > -1; +} diff --git a/superset/assets/javascripts/dashboard/v2/util/resizableConfig.js b/superset/assets/javascripts/dashboard/v2/util/resizableConfig.js new file mode 100644 index 0000000000000..2c566595750b6 --- /dev/null +++ b/superset/assets/javascripts/dashboard/v2/util/resizableConfig.js @@ -0,0 +1,28 @@ +const adjustableWidthAndHeight = { + top: false, + right: false, + bottom: false, + left: false, + topRight: false, + bottomRight: true, + bottomLeft: false, + topLeft: false, +}; + +const adjustableWidth = { + ...adjustableWidthAndHeight, + right: true, + bottomRight: false, +}; + +const adjustableHeight = { + ...adjustableWidthAndHeight, + bottom: true, + bottomRight: false, +}; + +export default { + widthAndHeight: adjustableWidthAndHeight, + widthOnly: adjustableWidth, + heightOnly: adjustableHeight, +}; diff --git a/superset/assets/stylesheets/dashboard-v2.css b/superset/assets/stylesheets/dashboard-v2.css index cdd012aa8e20f..6e871dc030b18 100644 --- a/superset/assets/stylesheets/dashboard-v2.css +++ b/superset/assets/stylesheets/dashboard-v2.css @@ -14,9 +14,24 @@ margin-bottom: 2px; } +.dashboard-builder-container { + display: flex; + flex-direction: row; + flex-wrap: wrap-reverse; + height: auto; +} + .dashboard-builder-sidepane { background: white; - box-shadow: 0 2px 6px #aaa; /* @TODO color */ + 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; } .new-draggable-component { @@ -24,7 +39,7 @@ flex-direction: row; flex-wrap: nowrap; align-items: center; - padding: 16; + padding: 16px; background: white; } @@ -40,22 +55,6 @@ box-shadow: 0 0 1px #fff; } -.grid-resizable-container { - width: 100%; - height: 100%; - background-color: #ff269e; - opacity: 0.9; - position: relative; -} - -.grid-resizable-container--resizing { - box-shadow: inset 0 0 0 2px #03A9F4; -} - -.grid-resizable-handle--right { - -} - /* @TODO remove upon new theme */ .btn.btn-primary { background: #484848 !important; From 72d03e7b1fb66291e54bdab930e62550a621fbf6 Mon Sep 17 00:00:00 2001 From: Chris Williams Date: Tue, 23 Jan 2018 13:46:39 -0800 Subject: [PATCH 04/25] [grid] design tweaks and polish, add --- .../dashboard/v2/components/DashboardGrid.jsx | 16 ++--- .../v2/components/gridComponents/Chart.jsx | 5 ++ .../v2/components/gridComponents/Divider.jsx | 31 +-------- .../v2/components/gridComponents/Header.jsx | 15 +---- .../v2/components/gridComponents/Row.jsx | 27 ++++++-- .../v2/components/gridComponents/Spacer.jsx | 29 +------- .../v2/components/gridComponents/Tabs.jsx | 64 ++++++++++++++++++ .../components/gridComponents/components.css | 66 +++++++++++++++++++ .../v2/components/gridComponents/grid.css | 25 +++---- .../v2/components/gridComponents/index.js | 6 ++ .../resizable/ResizableContainer.jsx | 14 ++-- .../v2/components/resizable/resizable.css | 26 ++++---- .../dashboard/v2/fixtures/testLayout.js | 60 ++++++++++++++--- superset/assets/stylesheets/dashboard-v2.css | 9 +-- 14 files changed, 268 insertions(+), 125 deletions(-) create mode 100644 superset/assets/javascripts/dashboard/v2/components/gridComponents/Tabs.jsx create mode 100644 superset/assets/javascripts/dashboard/v2/components/gridComponents/components.css diff --git a/superset/assets/javascripts/dashboard/v2/components/DashboardGrid.jsx b/superset/assets/javascripts/dashboard/v2/components/DashboardGrid.jsx index bb2cfb7536acb..8874e805fe911 100644 --- a/superset/assets/javascripts/dashboard/v2/components/DashboardGrid.jsx +++ b/superset/assets/javascripts/dashboard/v2/components/DashboardGrid.jsx @@ -2,13 +2,12 @@ import React from 'react'; // import PropTypes from 'prop-types'; import ParentSize from '@vx/responsive/build/components/ParentSize'; -import { Row, Header, Divider } from './gridComponents'; +import { Row } from './gridComponents'; import './gridComponents/grid.css'; import { GRID_GUTTER_SIZE, GRID_COLUMN_COUNT, - GRID_MIN_COLUMN_COUNT, } from '../util/constants'; import testLayout from '../fixtures/testLayout'; @@ -67,16 +66,13 @@ class DashboardGrid extends React.PureComponent { return (
- {({ width, height }) => { - const columnPlusGutterWidth = width / GRID_COLUMN_COUNT; - const extraGutterWidth = GRID_GUTTER_SIZE / GRID_COLUMN_COUNT; - const columnWidth = columnPlusGutterWidth - GRID_GUTTER_SIZE + extraGutterWidth; + {({ width }) => { + // account for (COLUMN_COUNT - 1) gutters + const columnPlusGutterWidth = (width + GRID_GUTTER_SIZE) / GRID_COLUMN_COUNT; + const columnWidth = columnPlusGutterWidth - GRID_GUTTER_SIZE; return width < 50 ? null : ( -
{ this.grid = ref; }} - style={{ width, height }} - > +
{ this.grid = ref; }}> {children.map(id => ( Chart
); diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Divider.jsx b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Divider.jsx index d8fc5e641ce82..4c12e655fb2ba 100644 --- a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Divider.jsx +++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Divider.jsx @@ -1,36 +1,9 @@ import React from 'react'; -import PropTypes from 'prop-types'; - -import { GRID_BASE_UNIT } from '../../util/constants'; - -const propTypes = { -}; - -const defaultProps = { -}; - -class Divider extends React.Component { - constructor(props) { - super(props); - this.state = { - }; - } +class Divider extends React.PureComponent { render() { - return ( -
- ); + return
; } } -Divider.propTypes = propTypes; -Divider.defaultProps = defaultProps; - export default Divider; diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Header.jsx b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Header.jsx index 7441814feca20..034b20d64bcc2 100644 --- a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Header.jsx +++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Header.jsx @@ -1,8 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { GRID_BASE_UNIT } from '../../util/constants'; - const propTypes = { entity: PropTypes.shape({ id: PropTypes.string.isRequired, @@ -16,7 +14,7 @@ const defaultProps = { entity: {}, }; -class Header extends React.Component { +class Header extends React.PureComponent { constructor(props) { super(props); this.state = {}; @@ -25,15 +23,8 @@ class Header extends React.Component { render() { const { entity: { id, meta } } = this.props; return !meta || !id ? null : ( -
-
- {meta.text} -
+
+ {meta.text}
); } diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Row.jsx b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Row.jsx index 486cf80980ed2..de29adece7ab2 100644 --- a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Row.jsx +++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Row.jsx @@ -1,5 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; +import cx from 'classnames'; import ResizableContainer from '../resizable/ResizableContainer'; import { componentIsResizable } from '../../util/gridUtils'; @@ -7,7 +8,7 @@ import { componentIsResizable } from '../../util/gridUtils'; import { COLUMN_TYPE, SPACER_TYPE, - GRID_BASE_UNIT, + INVISIBLE_ROW_TYPE, GRID_GUTTER_SIZE, GRID_ROW_HEIGHT_UNIT, GRID_COLUMN_COUNT, @@ -89,6 +90,7 @@ class Row extends React.Component { const { modifiedEntities } = this.state; let totalColumns = 0; + let maxItemHeight = 0; const rowItems = []; (rowEntity.children || []).forEach((id, index) => { @@ -96,17 +98,23 @@ class Row extends React.Component { totalColumns += (entity.meta || {}).width || 0; rowItems.push(entity); if (index < rowEntity.children.length - 1) rowItems.push(`gutter-${index}`); + if ((entity.meta || {}).height) maxItemHeight = Math.max(maxItemHeight, entity.meta.height); }); return ( - // @TODO row vs invisible row -
+
{rowItems.map((entity) => { const id = entity.id || entity; const Component = COMPONENT_TYPE_LOOKUP[entity.type]; const isSpacer = entity.type === SPACER_TYPE; const isResizable = componentIsResizable(entity); + // Rows may have Column children which are resizable let RowItem = Component ? ( + Empty row +
}
); } diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Spacer.jsx b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Spacer.jsx index 9cbcefcf3a399..2cd2755ede8e2 100644 --- a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Spacer.jsx +++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Spacer.jsx @@ -1,34 +1,9 @@ import React from 'react'; -const propTypes = { -}; - -const defaultProps = { -}; - -class Spacer extends React.Component { - constructor(props) { - super(props); - this.state = { - }; - } - +class Spacer extends React.PureComponent { render() { - return ( -
- ); + return
; } } -Spacer.propTypes = propTypes; -Spacer.defaultProps = defaultProps; - export default Spacer; diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Tabs.jsx b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Tabs.jsx new file mode 100644 index 0000000000000..4a197e26118c3 --- /dev/null +++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Tabs.jsx @@ -0,0 +1,64 @@ +import React from 'react'; +import { Tabs as BootstrapTabs, Tab } from 'react-bootstrap'; +import PropTypes from 'prop-types'; + +const propTypes = { + id: PropTypes.string.isRequired, + tabs: PropTypes.arrayOf( + PropTypes.shape({ + label: PropTypes.string, + }), + ), + onChangeTab: PropTypes.func, +}; + +const defaultProps = { + tabs: [ + { label: 'Section Tab' }, + { label: 'Section Tab' }, + { label: 'Section Tab' }, + ], + onChangeTab: null, +}; + +class Tabs extends React.Component { + constructor(props) { + super(props); + this.state = { + tabIndex: 0, + }; + this.handleClicKTab = this.handleClicKTab.bind(this); + } + + handleClicKTab(tabIndex) { + const { onChangeTab, tabs } = this.props; + this.setState(() => ({ tabIndex })); + if (onChangeTab) { + onChangeTab({ tabIndex, tab: tabs[tabIndex] }); + } + } + + render() { + const { tabs, id } = this.props; + const { tabIndex } = this.state; + return ( +
+ + {tabs.map((tab, i) => ( + {tab.label}
} /> + ))} + +
+ ); + } +} + +Tabs.propTypes = propTypes; +Tabs.defaultProps = defaultProps; + +export default Tabs; diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/components.css b/superset/assets/javascripts/dashboard/v2/components/gridComponents/components.css new file mode 100644 index 0000000000000..c54cd5da89e72 --- /dev/null +++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/components.css @@ -0,0 +1,66 @@ +/* Header */ +.dashboard-component-header { + width: 100%; + font-size: 24px; /* @TODO will need different sizes */ + line-height: 24px; + font-weight: 700; + background-color: inherit; + padding: 16px 0; + color: #263238; +} + +.grid-row-container .dashboard-component-header { + padding-left: 16px; +} + +/* Divider */ +.dashboard-component-divider { + width: 100%; + height: 1px; + margin: 24px 0; + background-color: #CFD8DC; +} + +/* Tabs -- this overwrites Superset bootstrap theme tab styling */ +.dashboard-component-tabs { + width: 100%; + background-color: inherit; +} + +.grid-row-container .dashboard-component-tabs { + /*padding-bottom: 16px;*/ +} + +.grid-row-container .dashboard-component-tabs .nav-tabs { + border-bottom: 1px solid #CFD8DC; +} + +/* by moving padding from to
  • we can restrict the selected tab indicator to text width */ +.grid-row-container .dashboard-component-tabs .nav-tabs > li { + padding: 0 16px; +} + +.grid-row-container .dashboard-component-tabs .nav-tabs > li > a { + color: #263238; + border: none; + padding: 12px 0 14px 0; +} + +.grid-row-container .dashboard-component-tabs .nav-tabs > li.active > a { + border: none; +} + +.grid-row-container .dashboard-component-tabs .nav-tabs > li.active > a:after { + content: ""; + position: absolute; + height: 3px; + width: 100%; + bottom: 0; + background: linear-gradient(to right, #E32464, #2C2261); +} + +.grid-row-container .dashboard-component-tabs .nav-tabs > li > a:hover { + border: none; + background: inherit; + color: #000000; +} diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/grid.css b/superset/assets/javascripts/dashboard/v2/components/gridComponents/grid.css index ecac7dfda6f80..3689e8068ff14 100644 --- a/superset/assets/javascripts/dashboard/v2/components/gridComponents/grid.css +++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/grid.css @@ -1,9 +1,8 @@ .grid-container { - flex: 1; + flex-grow: 9999; min-width: 66%; - margin: 16px; + margin: 24px; height: 100%; - overflow-y: auto; position: relative; } @@ -19,23 +18,27 @@ align-items: flex-start; min-height: 16px; height: fit-content; - display: flex; - background-color: #D5BCDB; - box-shadow: 0 0 0 1px white; + background-color: transparent; } - .grid-row--invisible { - background-color: transparent; - } +.grid-row-container { + background-color: #fff; +} + +.grid-spacer { + width: 100%; + height: 100%; + background-color: transparent; +} /* Editing guides */ .grid-column-guide { position: absolute; top: 0; height: 100%; - background-color: #44C0FF; - opacity: 0.1; + background-color: rgba(68, 192, 255, 0.05); pointer-events: none; + box-shadow: inset 0 0 0 1px rgba(68, 192, 255, 0.5); } .grid-row-guide { diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/index.js b/superset/assets/javascripts/dashboard/v2/components/gridComponents/index.js index 22dbcfee4cb31..7c635f14b546c 100644 --- a/superset/assets/javascripts/dashboard/v2/components/gridComponents/index.js +++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/index.js @@ -1,3 +1,5 @@ +import './components.css'; + import { CHART_TYPE, COLUMN_TYPE, @@ -5,6 +7,7 @@ import { HEADER_TYPE, ROW_TYPE, SPACER_TYPE, + TABS_TYPE, } from '../../util/constants'; import Chart from './Chart'; @@ -13,6 +16,7 @@ import Divider from './Divider'; import Header from './Header'; import Row from './Row'; import Spacer from './Spacer'; +import Tabs from './Tabs'; export { default as Chart } from './Chart'; export { default as Column } from './Column'; @@ -20,6 +24,7 @@ export { default as Divider } from './Divider'; export { default as Header } from './Header'; export { default as Row } from './Row'; export { default as Spacer } from './Spacer'; +export { default as Tabs } from './Tabs'; export const COMPONENT_TYPE_LOOKUP = { [CHART_TYPE]: Chart, @@ -28,4 +33,5 @@ export const COMPONENT_TYPE_LOOKUP = { [HEADER_TYPE]: Header, [ROW_TYPE]: Row, [SPACER_TYPE]: Spacer, + [TABS_TYPE]: Tabs, }; diff --git a/superset/assets/javascripts/dashboard/v2/components/resizable/ResizableContainer.jsx b/superset/assets/javascripts/dashboard/v2/components/resizable/ResizableContainer.jsx index fbd7c68eb9c85..92d93e44ce16a 100644 --- a/superset/assets/javascripts/dashboard/v2/components/resizable/ResizableContainer.jsx +++ b/superset/assets/javascripts/dashboard/v2/components/resizable/ResizableContainer.jsx @@ -45,6 +45,8 @@ 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 columns on resize stop const snapToGrid = [GRID_BASE_UNIT, GRID_BASE_UNIT]; class ResizableContainer extends React.PureComponent { @@ -120,8 +122,8 @@ class ResizableContainer extends React.PureComponent { } = this.props; const size = { - width: adjustableWidth ? (widthStep * widthMultiple) - gutterWidth : '100%', - height: adjustableHeight ? heightStep * heightMultiple : '100%', + width: adjustableWidth ? (widthStep * widthMultiple) - gutterWidth : 'auto', + height: (adjustableHeight || heightMultiple) ? heightStep * heightMultiple : 'auto', }; let enableConfig = resizableConfig.widthAndHeight; @@ -134,10 +136,10 @@ class ResizableContainer extends React.PureComponent { span .resize-handle { border-color: #44C0FF; } diff --git a/superset/assets/javascripts/dashboard/v2/fixtures/testLayout.js b/superset/assets/javascripts/dashboard/v2/fixtures/testLayout.js index e7abdf097b4f9..f70775598bd4a 100644 --- a/superset/assets/javascripts/dashboard/v2/fixtures/testLayout.js +++ b/superset/assets/javascripts/dashboard/v2/fixtures/testLayout.js @@ -4,6 +4,7 @@ import { ROW_TYPE, INVISIBLE_ROW_TYPE, SPACER_TYPE, + TABS_TYPE, CHART_TYPE, DIVIDER_TYPE, } from '../util/constants'; @@ -15,24 +16,29 @@ export default { 'row2', 'row3', 'row4', + 'row5', ], entities: { row0: { id: 'row0', - type: ROW_TYPE, + type: INVISIBLE_ROW_TYPE, children: ['header0'], }, row1: { id: 'row1', - type: ROW_TYPE, + type: INVISIBLE_ROW_TYPE, children: [ - 'divider0', + 'charta', + 'chartb', + 'chartc', ], }, row2: { id: 'row2', - type: ROW_TYPE, - children: [], + type: INVISIBLE_ROW_TYPE, + children: [ + 'divider0', + ], }, row3: { id: 'row3', @@ -44,10 +50,18 @@ export default { row4: { id: 'row4', type: ROW_TYPE, + children: [ + 'tabs0', + ], + }, + row5: { + id: 'row5', + type: ROW_TYPE, children: [ 'column0', - 'chart3', 'spacer0', + 'chart3', + 'spacer1', 'chart4', ], }, @@ -108,13 +122,36 @@ export default { height: 20, }, }, + charta: { + id: 'charta', + type: CHART_TYPE, + meta: { + width: 3, + height: 20, + }, + }, + chartb: { + id: 'chartb', + type: CHART_TYPE, + meta: { + width: 6, + height: 20, + }, + }, + chartc: { + id: 'chartc', + type: CHART_TYPE, + meta: { + width: 3, + height: 20, + }, + }, column0: { id: 'column0', type: COLUMN_TYPE, children: [ 'chart0', 'chart1', - 'spacer1', 'chart2', ], meta: { @@ -132,7 +169,14 @@ export default { id: 'spacer1', type: SPACER_TYPE, meta: { - height: 1, + width: 1, + }, + }, + tabs0: { + id: 'tabs0', + type: TABS_TYPE, + meta: { + width: 1, }, }, }, diff --git a/superset/assets/stylesheets/dashboard-v2.css b/superset/assets/stylesheets/dashboard-v2.css index 6e871dc030b18..033fbbac395ed 100644 --- a/superset/assets/stylesheets/dashboard-v2.css +++ b/superset/assets/stylesheets/dashboard-v2.css @@ -1,6 +1,7 @@ .dashboard-v2 { margin-top: -20px; position: relative; + color: #263238; } .dashboard-header { @@ -9,8 +10,8 @@ flex-direction: row; align-items: center; justify-content: space-between; - padding: 0 16px; - box-shadow: 0 0px 6px #aaa; /* @TODO color */ + padding: 0 24px; + box-shadow: 0 4px 4px 0 rgba(0, 0, 0, 0.1); margin-bottom: 2px; } @@ -23,7 +24,7 @@ .dashboard-builder-sidepane { background: white; - flex: 0 0 376px; + flex: 1 0 376px; box-shadow: 0 0 0 1px #ccc; /* @TODO color */ } @@ -57,6 +58,6 @@ /* @TODO remove upon new theme */ .btn.btn-primary { - background: #484848 !important; + background: #263238 !important; color: white !important; } From d4a08db89af5df975a31fa78964a4af2c1685d22 Mon Sep 17 00:00:00 2001 From: Chris Williams Date: Tue, 23 Jan 2018 18:49:15 -0800 Subject: [PATCH 05/25] [header] add gradient header logo and favicon --- superset/assets/images/favicon.png | Bin 6927 -> 18166 bytes superset/assets/images/superset-logo@2x.png | Bin 4132 -> 0 bytes .../dashboard/v2/components/DashboardGrid.jsx | 2 +- .../components/gridComponents/components.css | 4 ---- superset/assets/stylesheets/superset.less | 11 +++++++++++ superset/config.py | 2 +- superset/templates/appbuilder/navbar.html | 5 +++-- 7 files changed, 16 insertions(+), 8 deletions(-) delete mode 100644 superset/assets/images/superset-logo@2x.png diff --git a/superset/assets/images/favicon.png b/superset/assets/images/favicon.png index 55316fa7c5e582ab33c9f71ca5ccfd8d47da2b6c..f03cd5c7325e2ac09cd2d7558a2437a1f4c05b3d 100644 GIT binary patch literal 18166 zcmV)GK)%0;P)_TEIRRp!v*1B#>t4eiI+qINZ)Kc`e zE^V($FQwNK`wfcL)uLL}#jPMpt+7N162uZ&k@ekX{@?HKobQ?6JoC)F@0-MJ@8^Gb z=C?2BcYe=#o_S{8Nukhq6d~f#Z1hFP?5SA7(K2()F%w_gV$VYA?0zyj;AhNgn*qmI&h4ec!B90x#PFr?JAcG zJTO?ga?1GiF~_i;Hr9Xjx;20-!?6@4Se=&0|%XVbYkiuYi4TM(`k~I3oWC&dTKH@$IvMhHh%T`FA`QT#TPa^ z?mglhzuoTS#dOS?cD01McO$V=K6&O6%C{c5bxXNYJdixr8pB>#TMni_03ETo15Osg ziMoL+&#oYS;$JUX`<%VL`_2+{!ct_&j#!G0@bsh?uNe$q9jY;-bfdN(Q5{uRbq1Uc zht9Ge-G9Ld)iVp>bXEGnQBlT(4AO?9JwgTLQd7AT2XW4hI_x6Zldp9lOpbwvUIwHg z5AV1KG!M><&gfJ+6!t~zMqyVg>O?(24_)A zXf?FlL!W;3@;%AFW~VdeFKR8WN*NnDY(PDQYahLs3(s0x>FAJy5nIv6JezDR!|A)I z6-Q1_tGBbgeA|WP#{b1TQL27lwghEoEuQs+5YW;aU&qNO$sGgy*ut~ zUE!2AUuyE?m_@>1N!aM*S32=(8FSA-i7=QMby&}KIt=@=S(}2rK$r%zn!LsFi^;g> z~?n(Z{@} zg$WCXuD}8=MhCoG>HJw`;E-=KA~&9}0hQwh6zKb3xSD$f_vmgpX66HM44S<8@pHM8 zg&i(*#SxEP!i}G&YvHur&iHdjxiHr6XbkeF=da`@MW?o>sND!zZ?+2$?gQkphIGh! zvyC`7J08^GppG^vI<@`6J0{M|wZVADG>q$N4(U|E$)__NK zgXoYx{n+uJyMCw3?+jgF>vkku z7FyvX4?76&i%3(p-_y3oF-Y}+emHU54tYKf-{IS{Rqb%A#tBr;fbKasbk{Y8Kpxr?` zx)Te--u#aTe=-QF^r3{AQ0u|lowSH@UVjY6E9iq$uNelpHf-LHq?1jXYa4oaA^0}B zQ61haE0NHX@dU%kV?7KnmMreb!>$qAg4N~$PfQ{#IdA#H#j#5Bgp-`IpfhLT)J>

    *y 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%0DkwqheuV-SRBka7R?}p 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 literal 6927 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 i-%t|}5D*Zk{`fDN;USF=?Yy-B0000 ({ showGrid: true, rowGuide })); diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/components.css b/superset/assets/javascripts/dashboard/v2/components/gridComponents/components.css index c54cd5da89e72..0fc4a65b10243 100644 --- a/superset/assets/javascripts/dashboard/v2/components/gridComponents/components.css +++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/components.css @@ -27,10 +27,6 @@ background-color: inherit; } -.grid-row-container .dashboard-component-tabs { - /*padding-bottom: 16px;*/ -} - .grid-row-container .dashboard-component-tabs .nav-tabs { border-bottom: 1px solid #CFD8DC; } diff --git a/superset/assets/stylesheets/superset.less b/superset/assets/stylesheets/superset.less index a89c0e09ac10b..72a657252df66 100644 --- a/superset/assets/stylesheets/superset.less +++ b/superset/assets/stylesheets/superset.less @@ -203,6 +203,17 @@ div.widget { } } } +/* brand icon */ +.navbar-brand > img.logo { + margin-left: 15px; + width: 36px; + display: inline; +} +.navbar-brand > span { + margin-left: 2px; + font-size: 15px; + font-weight: bold; +} .navbar .alert { padding: 5px 10px; diff --git a/superset/config.py b/superset/config.py index c2da1db8b55c7..a3fb4d3c622dd 100644 --- a/superset/config.py +++ b/superset/config.py @@ -95,7 +95,7 @@ APP_NAME = 'Superset' # Uncomment to setup an App icon -APP_ICON = '/static/assets/images/superset-logo@2x.png' +APP_ICON = '/static/assets/images/favicon.png' # Druid query timezone # tz.tzutc() : Using utc timezone diff --git a/superset/templates/appbuilder/navbar.html b/superset/templates/appbuilder/navbar.html index 0ea2daec5fb15..0a946fe2bdcd6 100644 --- a/superset/templates/appbuilder/navbar.html +++ b/superset/templates/appbuilder/navbar.html @@ -12,9 +12,11 @@ + Superset

  • - From 30787cf585eb17a0a58543f64dabd7349d12f92c Mon Sep 17 00:00:00 2001 From: Chris Williams Date: Fri, 2 Feb 2018 16:57:41 -0800 Subject: [PATCH 06/25] [dnd] begin adding dnd functionality --- .../v2/components/BuilderComponentPane.jsx | 8 +- .../v2/components/DashboardBuilder.jsx | 102 +++++--- .../dashboard/v2/components/DashboardGrid.jsx | 152 ++++++++++-- .../v2/components/dnd/DraggableNewChart.jsx | 13 +- .../components/dnd/DraggableNewComponent.jsx | 13 +- .../v2/components/dnd/DraggableNewDivider.jsx | 11 +- .../v2/components/dnd/DraggableNewHeader.jsx | 9 +- .../v2/components/dnd/DraggableNewRow.jsx | 10 +- .../dashboard/v2/components/dnd/dnd.css | 37 +++ .../v2/components/gridComponents/Row.jsx | 230 ++++++++++-------- .../v2/components/gridComponents/grid.css | 2 +- .../v2/components/gridComponents/index.js | 2 + .../v2/components/resizable/resizable.css | 6 +- .../dashboard/v2/fixtures/testLayout.js | 28 ++- .../dashboard/v2/util/constants.js | 20 +- .../dashboard/v2/util/dnd-reorder.js | 14 +- .../dashboard/v2/util/resizableConfig.js | 2 + superset/assets/stylesheets/dashboard-v2.css | 6 +- 18 files changed, 431 insertions(+), 234 deletions(-) create mode 100644 superset/assets/javascripts/dashboard/v2/components/dnd/dnd.css diff --git a/superset/assets/javascripts/dashboard/v2/components/BuilderComponentPane.jsx b/superset/assets/javascripts/dashboard/v2/components/BuilderComponentPane.jsx index e1139f1f3d841..5c3f07bc4665f 100644 --- a/superset/assets/javascripts/dashboard/v2/components/BuilderComponentPane.jsx +++ b/superset/assets/javascripts/dashboard/v2/components/BuilderComponentPane.jsx @@ -27,10 +27,10 @@ class BuilderComponentPane extends React.Component { > {provided => (
    - - - - + + + + {provided.placeholder}
    )} diff --git a/superset/assets/javascripts/dashboard/v2/components/DashboardBuilder.jsx b/superset/assets/javascripts/dashboard/v2/components/DashboardBuilder.jsx index 4e380823e684c..8751e38878e29 100644 --- a/superset/assets/javascripts/dashboard/v2/components/DashboardBuilder.jsx +++ b/superset/assets/javascripts/dashboard/v2/components/DashboardBuilder.jsx @@ -1,12 +1,17 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { DragDropContext, Droppable, Draggable } from 'react-beautiful-dnd'; +import { DragDropContext } from 'react-beautiful-dnd'; +import cx from 'classnames'; + import BuilderComponentPane from './BuilderComponentPane'; import DashboardGrid from './DashboardGrid'; -// import { reorder, reorderRows } from '../util/dnd-reorder'; +// import getNewGridEntity from '../util/getNewGridEntity'; +import { reorderRows } from '../util/dnd-reorder'; + +import './dnd/dnd.css'; +import testLayout from '../fixtures/testLayout'; -// import { DROPPABLE_DASHBOARD_ROOT, DRAGGABLE_ROW_TYPE } from '../util/constants'; const propTypes = { editMode: PropTypes.bool, @@ -20,67 +25,90 @@ class DashboardBuilder extends React.Component { constructor(props) { super(props); this.state = { - rows: [], - entities: {}, + layout: testLayout, + draggingEntity: null, }; this.handleDragEnd = this.handleDragEnd.bind(this); this.handleDragStart = this.handleDragStart.bind(this); - this.handleReorder = this.handleReorder.bind(this); + this.handleMoveEntity = this.handleMoveEntity.bind(this); this.handleNewEntity = this.handleNewEntity.bind(this); + this.handleUpdateEntity = this.handleUpdateEntity.bind(this); } handleDragEnd(dropResult) { console.log('drag end', dropResult); if (dropResult.destination) { - // if (isNewEntity(dropResult.draggableId)) { - // this.handleNewEntity(dropResult); - // } else { - // this.handleReorder(dropResult); - // } + if (/new/gi.test(dropResult.draggableId)) { + this.handleNewEntity(dropResult); + } else { + this.handleMoveEntity(dropResult); + } } } - handleDragStart(obj) { - console.log('drag start', obj); + handleDragStart(result) { + console.log('drag start', result); + const { layout } = this.state; + const draggingEntity = layout.entities[result.draggableId] || { type: result.draggableId }; + this.setState(() => ({ draggingEntity })); } handleNewEntity() { + console.log('new entity'); + } + + handleMoveEntity({ source, destination, draggableId }) { + console.log('source', source, 'destination', destination); + this.setState(({ layout }) => { + const { entities } = layout; + + const nextEntities = reorderRows({ + entitiesMap: entities, + source, + destination, + }); + + return { + layout: { + ...layout, + entities: { + ...nextEntities, + }, + }, + }; + }); } - handleReorder({ source, destination, draggableId }) { - // this.setState(({ rows, entities }) => { - // const { type } = entities[draggableId]; - // - // if (isRowType(type)) { // re-ordering rows - // const nextRows = reorder( - // rows, - // source.index, - // destination.index, - // ); - // return { rows: nextRows }; - // } - // - // // moving items between rows - // const nextEntities = reorderRows({ - // entitiesMap: entities, - // source, - // destination, - // }); - // - // return { entities: nextEntities }; - // }); + handleUpdateEntity(nextEntity) { + console.log('update entity', nextEntity); + + this.setState(({ layout }) => ({ + layout: { + ...layout, + entities: { + ...layout.entities, + [nextEntity.id]: nextEntity, + }, + }, + })); } render() { + const { draggingEntity, layout } = this.state; + return ( -
    - +
    +
    diff --git a/superset/assets/javascripts/dashboard/v2/components/DashboardGrid.jsx b/superset/assets/javascripts/dashboard/v2/components/DashboardGrid.jsx index bdedc579fb34f..fb327a4186849 100644 --- a/superset/assets/javascripts/dashboard/v2/components/DashboardGrid.jsx +++ b/superset/assets/javascripts/dashboard/v2/components/DashboardGrid.jsx @@ -1,21 +1,36 @@ import React from 'react'; -// import PropTypes from 'prop-types'; +import PropTypes from 'prop-types'; import ParentSize from '@vx/responsive/build/components/ParentSize'; +import cx from 'classnames'; +import { Droppable, Draggable } from 'react-beautiful-dnd'; -import { Row } from './gridComponents'; -import './gridComponents/grid.css'; +import isValidChild from '../util/isValidChild'; import { + DROPPABLE_ID_DASHBOARD_ROOT, GRID_GUTTER_SIZE, GRID_COLUMN_COUNT, + GRID_ROOT_TYPE, } from '../util/constants'; -import testLayout from '../fixtures/testLayout'; +import { COMPONENT_TYPE_LOOKUP } from './gridComponents'; +import './gridComponents/grid.css'; const propTypes = { + layout: PropTypes.object, + draggingEntity: PropTypes.shape({ + type: PropTypes.string.isRequired, // @TODO enumerate + }), + updateEntity: PropTypes.func, }; const defaultProps = { + layout: { + children: [], + entities: {}, + }, + draggingEntity: null, + updateEntity() {}, }; class DashboardGrid extends React.PureComponent { @@ -23,10 +38,13 @@ class DashboardGrid extends React.PureComponent { super(props); this.state = { showGrid: false, - layout: testLayout, rowGuide: null, + disableDrop: false, + disableDrag: false, + selectedEntityId: null, }; + this.handleToggleSelectEntityId = this.handleToggleSelectEntityId.bind(this); this.handleResizeStart = this.handleResizeStart.bind(this); this.handleResize = this.handleResize.bind(this); this.handleResizeStop = this.handleResizeStop.bind(this); @@ -41,27 +59,72 @@ class DashboardGrid extends React.PureComponent { } handleResizeStart({ ref, direction }) { + console.log('resize start'); let rowGuide = null; if (direction === 'bottom' || direction === 'bottomRight') { rowGuide = this.getRowGuidePosition(ref); } - this.setState(() => ({ showGrid: true, rowGuide })); + this.setState(() => ({ + showGrid: true, + rowGuide, + disableDrag: true, + disableDrop: true, + })); } handleResize({ ref, direction }) { + console.log('resize'); if (direction === 'bottom' || direction === 'bottomRight') { this.setState(() => ({ rowGuide: this.getRowGuidePosition(ref) })); } } - handleResizeStop() { - this.setState(() => ({ showGrid: false, rowGuide: null })); + handleResizeStop({ id, widthMultiple, heightMultiple }) { + console.log('resize stop'); + const { layout, updateEntity } = this.props; + const entity = layout.entities[id]; + debugger; + if (entity && (entity.meta.width !== widthMultiple || entity.meta.height !== heightMultiple)) { + updateEntity({ + ...entity, + meta: { + ...entity.meta, + width: widthMultiple || entity.meta.width, + height: heightMultiple || entity.meta.height, + }, + }); + } + this.setState(() => ({ + showGrid: false, + rowGuide: null, + disableDrag: false, + disableDrop: false, + })); + } + + handleToggleSelectEntityId(id) { + // only enable selection if no drag is occurring + if (!this.props.draggingEntity) { + this.setState(({ selectedEntityId }) => { + const nextSelectedEntityId = id === selectedEntityId ? null : id; + const disableDragDrop = Boolean(nextSelectedEntityId); + return { + selectedEntityId: nextSelectedEntityId, + disableDrop: disableDragDrop, + disableDrag: disableDragDrop, + }; + }); + } } render() { - const { showGrid, layout, rowGuide } = this.state; - const { children, entities } = layout; + const { layout, draggingEntity } = this.props; + const { showGrid, rowGuide, disableDrop, disableDrag, selectedEntityId } = this.state; + const { entities } = layout; + const rootEntity = entities[DROPPABLE_ID_DASHBOARD_ROOT]; + + console.log('dragging', draggingEntity, 'selected', selectedEntityId); return (
    @@ -73,18 +136,63 @@ class DashboardGrid extends React.PureComponent { return width < 50 ? null : (
    { this.grid = ref; }}> - {children.map(id => ( - - ))} + + {(droppableProvided, droppableSnapshot) => ( +
    + {rootEntity.children.map((id, index) => { + const entity = entities[id] || {}; + const Component = COMPONENT_TYPE_LOOKUP[entity.type]; + return ( + + {draggableProvided => ( +
    + {draggableProvided.placeholder} +
    +
    + +
    +
    + )} + + ); + })} + {droppableProvided.placeholder} +
    + )} + {showGrid && Array(GRID_COLUMN_COUNT).fill(null).map((_, i) => (
    ; + return ( + + ); } } diff --git a/superset/assets/javascripts/dashboard/v2/components/dnd/DraggableNewComponent.jsx b/superset/assets/javascripts/dashboard/v2/components/dnd/DraggableNewComponent.jsx index 22f65675332bd..1f6e8495674e7 100644 --- a/superset/assets/javascripts/dashboard/v2/components/dnd/DraggableNewComponent.jsx +++ b/superset/assets/javascripts/dashboard/v2/components/dnd/DraggableNewComponent.jsx @@ -6,20 +6,13 @@ import { Draggable } from 'react-beautiful-dnd'; const propTypes = { id: PropTypes.string.isRequired, label: PropTypes.string.isRequired, - index: PropTypes.number.isRequired, - type: PropTypes.string, }; -export default class DraggableNewComponent extends React.Component { +export default class DraggableNewComponent extends React.PureComponent { render() { - const { id, label, index, type = undefined } = this.props; - + const { id, label } = this.props; return ( - + {(provided, snapshot) => (
    + ); } } diff --git a/superset/assets/javascripts/dashboard/v2/components/dnd/DraggableNewHeader.jsx b/superset/assets/javascripts/dashboard/v2/components/dnd/DraggableNewHeader.jsx index 004693ecc8eb5..8242c9032574a 100644 --- a/superset/assets/javascripts/dashboard/v2/components/dnd/DraggableNewHeader.jsx +++ b/superset/assets/javascripts/dashboard/v2/components/dnd/DraggableNewHeader.jsx @@ -1,18 +1,19 @@ import React from 'react'; -import PropTypes from 'prop-types'; +// import PropTypes from 'prop-types'; import { DRAGGABLE_NEW_HEADER } from '../../util/constants'; import DraggableNewComponent from './DraggableNewComponent'; const propTypes = { - index: PropTypes.number.isRequired, }; export default class DraggableNewHeader extends React.Component { render() { - const { index } = this.props; return ( - + ); } } diff --git a/superset/assets/javascripts/dashboard/v2/components/dnd/DraggableNewRow.jsx b/superset/assets/javascripts/dashboard/v2/components/dnd/DraggableNewRow.jsx index 0333f2c109e29..e989f0b10a92e 100644 --- a/superset/assets/javascripts/dashboard/v2/components/dnd/DraggableNewRow.jsx +++ b/superset/assets/javascripts/dashboard/v2/components/dnd/DraggableNewRow.jsx @@ -1,22 +1,18 @@ import React from 'react'; -import PropTypes from 'prop-types'; +// import PropTypes from 'prop-types'; -import { DRAGGABLE_NEW_ROW, DRAGGABLE_ROW_TYPE } from '../../util/constants'; +import { DRAGGABLE_NEW_ROW } from '../../util/constants'; import DraggableNewComponent from './DraggableNewComponent'; const propTypes = { - index: PropTypes.number.isRequired, }; -export default class DraggableNewRow extends React.Component { +export default class DraggableNewRow extends React.PureComponent { render() { - const { index } = this.props; return ( ); } diff --git a/superset/assets/javascripts/dashboard/v2/components/dnd/dnd.css b/superset/assets/javascripts/dashboard/v2/components/dnd/dnd.css new file mode 100644 index 0000000000000..97cff9d8865db --- /dev/null +++ b/superset/assets/javascripts/dashboard/v2/components/dnd/dnd.css @@ -0,0 +1,37 @@ +.draggable-row, +.draggable-row-item { + position: relative; +} + +.draggable-row-handle { + opacity: 0; + position: absolute; + width: 5px; + height: 100%; + top: 0; + left: -10px; + cursor: move; + background: #44C0FF; + z-index: 1; +} + +.draggable-row-item-handle { + opacity: 0; + position: absolute; + width: 100%; + height: 5px; + top: -10px; + left: 0; + cursor: move; + background: #44C0FF; + z-index: 1; +} + +:not(.dashboard-builder--dragging) .draggable-row:hover .draggable-row-handle, +:not(.dashboard-builder--dragging) .draggable-row-handle:hover, +:not(.dashboard-builder--dragging) .draggable-row-item:hover .draggable-row-item-handle, +:not(.dashboard-builder--dragging) .draggable-row-item-handle:hover, +:not(.dashboard-builder--dragging) .draggable-column-item:hover .draggable-column-item-handle, +:not(.dashboard-builder--dragging) .draggable-column-item-handle:hover { + opacity: 1; +} diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Row.jsx b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Row.jsx index de29adece7ab2..f229eabc9adcd 100644 --- a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Row.jsx +++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Row.jsx @@ -1,8 +1,10 @@ import React from 'react'; import PropTypes from 'prop-types'; import cx from 'classnames'; +import { Draggable, Droppable } from 'react-beautiful-dnd'; import ResizableContainer from '../resizable/ResizableContainer'; +import isValidChild from '../../util/isValidChild'; import { componentIsResizable } from '../../util/gridUtils'; import { @@ -15,6 +17,7 @@ import { GRID_MIN_COLUMN_COUNT, GRID_MIN_ROW_UNITS, GRID_MAX_ROW_UNITS, + DROPPABLE_DIRECTION_HORIZONTAL, } from '../../util/constants'; import { COMPONENT_TYPE_LOOKUP } from './'; @@ -37,130 +40,141 @@ const defaultProps = { onResizeStart: null, }; -class Row extends React.Component { - constructor(props) { - super(props); - this.state = { - modifiedEntities: { - ...props.entities, - }, - }; - this.handleResizeStart = this.handleResizeStart.bind(this); - this.handleResizeStop = this.handleResizeStop.bind(this); - } - - componentWillReceiveProps() { - // @TODO - } - - handleResizeStart(args) { - const { onResizeStart } = this.props; - if (onResizeStart) onResizeStart(args); - } - - handleResizeStop({ id, widthMultiple, heightMultiple }) { - const { onResizeStop } = this.props; - this.setState(({ modifiedEntities }) => { - const entity = modifiedEntities[id]; - if (entity.meta.width !== widthMultiple || entity.meta.height !== heightMultiple) { - return { - modifiedEntities: { - ...modifiedEntities, - [id]: { - ...entity, - meta: { - ...entity.meta, - width: widthMultiple || entity.meta.width, - height: heightMultiple || entity.meta.height, - }, - }, - }, - }; - } - return null; - }, onResizeStop); - } - - serializeRow() { - // @TODO - } +class Row extends React.PureComponent { + // shouldComponentUpdate() { + // // @TODO check foro updates to this row only + // } render() { - const { entity: rowEntity, columnWidth, onResize } = this.props; - const { modifiedEntities } = this.state; + const { + entities, + entity: rowEntity, + columnWidth, + onResizeStart, + onResize, + onResizeStop, + draggingEntity, + disableDrop, + disableDrag, + } = this.props; let totalColumns = 0; let maxItemHeight = 0; const rowItems = []; (rowEntity.children || []).forEach((id, index) => { - const entity = modifiedEntities[id]; + const entity = entities[id]; totalColumns += (entity.meta || {}).width || 0; rowItems.push(entity); if (index < rowEntity.children.length - 1) rowItems.push(`gutter-${index}`); if ((entity.meta || {}).height) maxItemHeight = Math.max(maxItemHeight, entity.meta.height); }); + if (!rowEntity.children || !rowEntity.children.length) { + return ( +
    + Empty row +
    + ); + } + return ( -
    - {rowItems.map((entity) => { - const id = entity.id || entity; - const Component = COMPONENT_TYPE_LOOKUP[entity.type]; - const isSpacer = entity.type === SPACER_TYPE; - const isResizable = componentIsResizable(entity); - - // Rows may have Column children which are resizable - let RowItem = Component ? ( - - ) :
    ; - - if (isResizable) { - RowItem = ( - - {RowItem} - - ); - } - return RowItem; + - Empty row -
    } -
    + direction={DROPPABLE_DIRECTION_HORIZONTAL} + > + {(droppableProvided, droppableSnapshot) => ( +
    + {droppableProvided.placeholder} + + {rowItems.map((entity, index) => { + const id = entity.id || entity; + const Component = COMPONENT_TYPE_LOOKUP[entity.type]; + const isSpacer = entity.type === SPACER_TYPE; + const isResizable = componentIsResizable(entity); + + let RowItem = Component ? ( + + ) :
    ; + + if (isResizable && Component) { + RowItem = ( + + {RowItem} + + ); + } + + if (Component) { + return ( + + {draggableProvided => ( +
    + {draggableProvided.placeholder} +
    +
    + {RowItem} +
    +
    + )} + + ); + } + + return RowItem; + })} +
    + )} + ); } } diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/grid.css b/superset/assets/javascripts/dashboard/v2/components/gridComponents/grid.css index 3689e8068ff14..19eaaacd9dc69 100644 --- a/superset/assets/javascripts/dashboard/v2/components/gridComponents/grid.css +++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/grid.css @@ -1,5 +1,5 @@ .grid-container { - flex-grow: 9999; + flex-grow: 1; min-width: 66%; margin: 24px; height: 100%; diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/index.js b/superset/assets/javascripts/dashboard/v2/components/gridComponents/index.js index 7c635f14b546c..144eae366da0e 100644 --- a/superset/assets/javascripts/dashboard/v2/components/gridComponents/index.js +++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/index.js @@ -5,6 +5,7 @@ import { COLUMN_TYPE, DIVIDER_TYPE, HEADER_TYPE, + INVISIBLE_ROW_TYPE, ROW_TYPE, SPACER_TYPE, TABS_TYPE, @@ -31,6 +32,7 @@ export const COMPONENT_TYPE_LOOKUP = { [COLUMN_TYPE]: Column, [DIVIDER_TYPE]: Divider, [HEADER_TYPE]: Header, + [INVISIBLE_ROW_TYPE]: Row, [ROW_TYPE]: Row, [SPACER_TYPE]: Spacer, [TABS_TYPE]: Tabs, diff --git a/superset/assets/javascripts/dashboard/v2/components/resizable/resizable.css b/superset/assets/javascripts/dashboard/v2/components/resizable/resizable.css index b79071bc4714e..3a9acd07367c6 100644 --- a/superset/assets/javascripts/dashboard/v2/components/resizable/resizable.css +++ b/superset/assets/javascripts/dashboard/v2/components/resizable/resizable.css @@ -14,7 +14,7 @@ box-shadow: inset 0 0 0 2px #44C0FF; } -.grid-resizable-container:hover .grid-spacer { +.grid-resizable-container .grid-spacer { box-shadow: inset 0 0 0 1px #eaeaea; } @@ -40,7 +40,7 @@ } .resize-handle--right { - width: 2px; + width: 3px; height: 20px; right: -2px; top: 47%; @@ -54,7 +54,7 @@ } .resize-handle--bottom { - height: 2px; + height: 3px; width: 20px; bottom: 10px; left: 47%; diff --git a/superset/assets/javascripts/dashboard/v2/fixtures/testLayout.js b/superset/assets/javascripts/dashboard/v2/fixtures/testLayout.js index f70775598bd4a..c4e3899808be7 100644 --- a/superset/assets/javascripts/dashboard/v2/fixtures/testLayout.js +++ b/superset/assets/javascripts/dashboard/v2/fixtures/testLayout.js @@ -7,23 +7,27 @@ import { TABS_TYPE, CHART_TYPE, DIVIDER_TYPE, + DROPPABLE_ID_DASHBOARD_ROOT, } from '../util/constants'; export default { - children: [ - 'row0', - 'row1', - 'row2', - 'row3', - 'row4', - 'row5', - ], entities: { - row0: { - id: 'row0', - type: INVISIBLE_ROW_TYPE, - children: ['header0'], + [DROPPABLE_ID_DASHBOARD_ROOT]: { + id: DROPPABLE_ID_DASHBOARD_ROOT, + children: [ + 'header0', + 'row1', + 'row2', + 'row3', + 'row4', + 'row5', + ], }, + // row0: { + // id: 'row0', + // type: INVISIBLE_ROW_TYPE, + // children: ['header0'], + // }, row1: { id: 'row1', type: INVISIBLE_ROW_TYPE, diff --git a/superset/assets/javascripts/dashboard/v2/util/constants.js b/superset/assets/javascripts/dashboard/v2/util/constants.js index 19198a1eebc2a..4f185f01fce7b 100644 --- a/superset/assets/javascripts/dashboard/v2/util/constants.js +++ b/superset/assets/javascripts/dashboard/v2/util/constants.js @@ -1,26 +1,32 @@ // Component types export const CHART_TYPE = 'DASHBOARD_CHART_TYPE'; -export const MARKDOWN_TYPE = 'DASHBOARD_MARKDOWN_TYPE'; -export const DIVIDER_TYPE = 'DASHBOARD_DIVIDER_TYPE'; -export const ROW_TYPE = 'DASHBOARD_ROW_TYPE'; -export const INVISIBLE_ROW_TYPE = 'DASHBOARD_INVISIBLE_ROW_TYPE'; export const COLUMN_TYPE = 'DASHBOARD_COLUMN_TYPE'; -export const TABS_TYPE = 'DASHBOARD_TABS_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 INVISIBLE_ROW_TYPE = 'DASHBOARD_INVISIBLE_ROW_TYPE'; +export const MARKDOWN_TYPE = 'DASHBOARD_MARKDOWN_TYPE'; +export const ROW_TYPE = 'DASHBOARD_ROW_TYPE'; export const SPACER_TYPE = 'DASHBOARD_SPACER_TYPE'; +export const TABS_TYPE = 'DASHBOARD_TABS_TYPE'; // Drag and drop constants -export const DROPPABLE_NEW_COMPONENT = 'DROPPABLE_NEW_COMPONENT'; -export const DROPPABLE_DASHBOARD_ROOT = 'DASHBOARD_ROOT_DROPPABLE'; export const DROPPABLE_DIRECTION_VERTICAL = 'vertical'; export const DROPPABLE_DIRECTION_HORIZONTAL = 'horizontal'; +// Ids for new components +export const DROPPABLE_NEW_COMPONENT = 'DROPPABLE_NEW_COMPONENT'; +export const DROPPABLE_ID_DASHBOARD_ROOT = 'DROPPABLE_ID_DASHBOARD_ROOT'; + export const DRAGGABLE_NEW_CHART = 'DRAGGABLE_NEW_CHART'; export const DRAGGABLE_NEW_DIVIDER = 'DRAGGABLE_NEW_DIVIDER'; export const DRAGGABLE_NEW_HEADER = 'DRAGGABLE_NEW_HEADER'; export const DRAGGABLE_NEW_ROW = 'DRAGGABLE_NEW_ROW'; +export const DRAGGABLE_NEW_SPACER = 'DRAGGABLE_NEW_SPACER'; +// Draggable types export const DRAGGABLE_TYPE_ROW = 'DRAGGABLE_TYPE_ROW'; +export const DRAGGABLE_TYPE_ROW_ITEM = 'DRAGGABLE_TYPE_ROW_ITEM'; // grid constants export const GRID_BASE_UNIT = 8; diff --git a/superset/assets/javascripts/dashboard/v2/util/dnd-reorder.js b/superset/assets/javascripts/dashboard/v2/util/dnd-reorder.js index 81d479ae1e65e..5bcaefd408558 100644 --- a/superset/assets/javascripts/dashboard/v2/util/dnd-reorder.js +++ b/superset/assets/javascripts/dashboard/v2/util/dnd-reorder.js @@ -1,22 +1,23 @@ -export const reorder = (list, startIndex, endIndex) => { +export function reorder(list, startIndex, endIndex) { const result = [...list]; const [removed] = result.splice(startIndex, 1); result.splice(endIndex, 0, removed); - + if (result.some(v => !v)) debugger; return result; -}; +} -export const reorderRows = ({ +export function reorderRows({ entitiesMap, source, destination, -}) => { +}) { const current = [...entitiesMap[source.droppableId].children]; const next = [...entitiesMap[destination.droppableId].children]; const target = current[source.index]; // moving to same list if (source.droppableId === destination.droppableId) { + console.log('within list', source.index, destination.index); const reordered = reorder( current, source.index, @@ -38,6 +39,7 @@ export const reorderRows = ({ current.splice(source.index, 1); // remove from original next.splice(destination.index, 0, target); // insert into next + console.log('between list', source.index, destination.index); const result = { ...entitiesMap, [source.droppableId]: { @@ -51,4 +53,4 @@ export const reorderRows = ({ }; return result; -}; +} diff --git a/superset/assets/javascripts/dashboard/v2/util/resizableConfig.js b/superset/assets/javascripts/dashboard/v2/util/resizableConfig.js index 2c566595750b6..40e9af68bbb0f 100644 --- a/superset/assets/javascripts/dashboard/v2/util/resizableConfig.js +++ b/superset/assets/javascripts/dashboard/v2/util/resizableConfig.js @@ -1,3 +1,5 @@ +// config for a ResizableContainer + const adjustableWidthAndHeight = { top: false, right: false, diff --git a/superset/assets/stylesheets/dashboard-v2.css b/superset/assets/stylesheets/dashboard-v2.css index 033fbbac395ed..c30f9e12535bf 100644 --- a/superset/assets/stylesheets/dashboard-v2.css +++ b/superset/assets/stylesheets/dashboard-v2.css @@ -15,16 +15,16 @@ margin-bottom: 2px; } -.dashboard-builder-container { +.dashboard-builder { display: flex; flex-direction: row; - flex-wrap: wrap-reverse; + flex-wrap: nowrap; height: auto; } .dashboard-builder-sidepane { background: white; - flex: 1 0 376px; + flex: 0 0 376px; box-shadow: 0 0 0 1px #ccc; /* @TODO color */ } From 27d67fb3705a9d0021c682766658d16e630252f5 Mon Sep 17 00:00:00 2001 From: Chris Williams Date: Fri, 2 Feb 2018 16:58:16 -0800 Subject: [PATCH 07/25] [dnd] add util/isValidChild.js --- .../dashboard/v2/util/isValidChild.js | 90 +++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 superset/assets/javascripts/dashboard/v2/util/isValidChild.js diff --git a/superset/assets/javascripts/dashboard/v2/util/isValidChild.js b/superset/assets/javascripts/dashboard/v2/util/isValidChild.js new file mode 100644 index 0000000000000..904fb41b0ab09 --- /dev/null +++ b/superset/assets/javascripts/dashboard/v2/util/isValidChild.js @@ -0,0 +1,90 @@ +import { + CHART_TYPE, + COLUMN_TYPE, + DIVIDER_TYPE, + HEADER_TYPE, + GRID_ROOT_TYPE, + INVISIBLE_ROW_TYPE, + MARKDOWN_TYPE, + ROW_TYPE, + SPACER_TYPE, + TABS_TYPE, + + DRAGGABLE_NEW_CHART, + DRAGGABLE_NEW_DIVIDER, + DRAGGABLE_NEW_HEADER, + DRAGGABLE_NEW_ROW, + DRAGGABLE_NEW_SPACER, +} from './constants'; + +const typeToValidChildType = { + // root + [GRID_ROOT_TYPE]: { + [ROW_TYPE]: true, + [INVISIBLE_ROW_TYPE]: true, + [TABS_TYPE]: true, + + [DRAGGABLE_NEW_CHART]: true, + [DRAGGABLE_NEW_DIVIDER]: true, + [DRAGGABLE_NEW_HEADER]: true, + [DRAGGABLE_NEW_ROW]: true, + }, + + // row types + [ROW_TYPE]: { + [CHART_TYPE]: true, + [MARKDOWN_TYPE]: true, + [COLUMN_TYPE]: true, + [SPACER_TYPE]: true, + [DRAGGABLE_NEW_SPACER]: true, + [DRAGGABLE_NEW_CHART]: true, + [DRAGGABLE_NEW_HEADER]: true, + }, + + [INVISIBLE_ROW_TYPE]: { + [CHART_TYPE]: true, + [MARKDOWN_TYPE]: true, + [COLUMN_TYPE]: true, + [SPACER_TYPE]: true, + [DRAGGABLE_NEW_SPACER]: true, + [DRAGGABLE_NEW_CHART]: true, + [DRAGGABLE_NEW_HEADER]: true, + }, + + [TABS_TYPE]: { + [ROW_TYPE]: true, + [CHART_TYPE]: true, + [MARKDOWN_TYPE]: true, + [SPACER_TYPE]: true, + [COLUMN_TYPE]: true, + + [DRAGGABLE_NEW_SPACER]: true, + }, + + [COLUMN_TYPE]: { + [CHART_TYPE]: true, + [MARKDOWN_TYPE]: true, + [HEADER_TYPE]: true, + [SPACER_TYPE]: true, + + [DRAGGABLE_NEW_SPACER]: true, + [DRAGGABLE_NEW_CHART]: true, + [DRAGGABLE_NEW_HEADER]: true, + // divider? + // row? + }, + + // these have no valid children + [CHART_TYPE]: {}, + [MARKDOWN_TYPE]: {}, + [DIVIDER_TYPE]: {}, + [HEADER_TYPE]: {}, + [SPACER_TYPE]: {}, +}; + +export default function isValidChild({ parentType, childType }) { + if (!parentType || !childType) return false; + return Boolean( + typeToValidChildType[parentType][childType], + ); +} From 95c836176d7e2c58494fa010af033a0220be4889 Mon Sep 17 00:00:00 2001 From: Chris Williams Date: Mon, 5 Feb 2018 13:05:21 -0800 Subject: [PATCH 08/25] [react-beautiful-dnd] iterate on dnd until blocked --- .../v2/components/DashboardBuilder.jsx | 12 +- .../dashboard/v2/components/DashboardGrid.jsx | 156 +++++++++--------- .../dashboard/v2/components/dnd/dnd.css | 61 +++++-- .../v2/components/gridComponents/Chart.jsx | 18 +- .../v2/components/gridComponents/Divider.jsx | 6 +- .../v2/components/gridComponents/Row.jsx | 28 ++-- .../v2/components/gridComponents/Tabs.jsx | 67 +++++++- .../components/gridComponents/components.css | 39 ++++- .../v2/components/gridComponents/grid.css | 2 +- .../v2/components/resizable/resizable.css | 10 +- .../dashboard/v2/fixtures/testLayout.js | 24 +-- .../dashboard/v2/util/dnd-reorder.js | 6 +- .../dashboard/v2/util/isValidChild.js | 30 ++-- 13 files changed, 292 insertions(+), 167 deletions(-) diff --git a/superset/assets/javascripts/dashboard/v2/components/DashboardBuilder.jsx b/superset/assets/javascripts/dashboard/v2/components/DashboardBuilder.jsx index 8751e38878e29..18c6cb57e58a8 100644 --- a/superset/assets/javascripts/dashboard/v2/components/DashboardBuilder.jsx +++ b/superset/assets/javascripts/dashboard/v2/components/DashboardBuilder.jsx @@ -7,7 +7,7 @@ import cx from 'classnames'; import BuilderComponentPane from './BuilderComponentPane'; import DashboardGrid from './DashboardGrid'; // import getNewGridEntity from '../util/getNewGridEntity'; -import { reorderRows } from '../util/dnd-reorder'; +import { reorderItem } from '../util/dnd-reorder'; import './dnd/dnd.css'; import testLayout from '../fixtures/testLayout'; @@ -45,6 +45,7 @@ class DashboardBuilder extends React.Component { this.handleMoveEntity(dropResult); } } + this.setState(() => ({ draggingEntity: null })); } handleDragStart(result) { @@ -64,7 +65,7 @@ class DashboardBuilder extends React.Component { this.setState(({ layout }) => { const { entities } = layout; - const nextEntities = reorderRows({ + const nextEntities = reorderItem({ entitiesMap: entities, source, destination, @@ -103,7 +104,12 @@ class DashboardBuilder extends React.Component { onDragStart={this.handleDragStart} onDragEnd={this.handleDragEnd} > -
    +
    +
    { this.grid = ref; }} + className={cx('grid-container', showGrid && 'grid-container--resizing')} + > {({ width }) => { // account for (COLUMN_COUNT - 1) gutters @@ -135,85 +138,82 @@ class DashboardGrid extends React.PureComponent { const columnWidth = columnPlusGutterWidth - GRID_GUTTER_SIZE; return width < 50 ? null : ( -
    { this.grid = ref; }}> - - {(droppableProvided, droppableSnapshot) => ( -
    - {rootEntity.children.map((id, index) => { - const entity = entities[id] || {}; - const Component = COMPONENT_TYPE_LOOKUP[entity.type]; - return ( - - {draggableProvided => ( -
    - {draggableProvided.placeholder} + + {(droppableProvided, droppableSnapshot) => ( +
    + {rootEntity.children.map((id, index) => { + const entity = entities[id] || {}; + const Component = COMPONENT_TYPE_LOOKUP[entity.type]; + return ( + + {draggableProvided => ( +
    +
    -
    - -
    + className={cx(!disableDrag && 'draggable-row-handle')} + {...draggableProvided.dragHandleProps} + /> +
    - )} - - ); - })} - {droppableProvided.placeholder} -
    - )} - - - {showGrid && Array(GRID_COLUMN_COUNT).fill(null).map((_, i) => ( -
    - ))} - - {showGrid && rowGuide && -
    } -
    + {draggableProvided.placeholder} +
    + )} + + ); + })} + {droppableProvided.placeholder} + {showGrid && Array(GRID_COLUMN_COUNT).fill(null).map((_, i) => ( +
    + ))} + + {showGrid && rowGuide && +
    } +
    + )} + ); }} diff --git a/superset/assets/javascripts/dashboard/v2/components/dnd/dnd.css b/superset/assets/javascripts/dashboard/v2/components/dnd/dnd.css index 97cff9d8865db..8a158fd1158cc 100644 --- a/superset/assets/javascripts/dashboard/v2/components/dnd/dnd.css +++ b/superset/assets/javascripts/dashboard/v2/components/dnd/dnd.css @@ -1,37 +1,76 @@ +.draggable-row { + width: 100%; +} + .draggable-row, .draggable-row-item { position: relative; } +.draggable-row--dragging, +.draggable-row-item--dragging { + box-shadow: inset 0 0 0 1px #CFD8DC; +} + .draggable-row-handle { opacity: 0; position: absolute; - width: 5px; + width: 10px; height: 100%; top: 0; left: -10px; cursor: move; - background: #44C0FF; z-index: 1; + display: flex; + flex-direction: column; + justify-content: space-around; +} + +.draggable-row-handle:after { + content: ""; + background: #44C0FF; + display: block; + width: 5px; + height: 20%; + min-height: 20px; + margin: auto -5px auto 0; } .draggable-row-item-handle { opacity: 0; position: absolute; width: 100%; - height: 5px; - top: -10px; + height: 10px; + top: 0; left: 0; cursor: move; - background: #44C0FF; z-index: 1; } -:not(.dashboard-builder--dragging) .draggable-row:hover .draggable-row-handle, -:not(.dashboard-builder--dragging) .draggable-row-handle:hover, -:not(.dashboard-builder--dragging) .draggable-row-item:hover .draggable-row-item-handle, -:not(.dashboard-builder--dragging) .draggable-row-item-handle:hover, -:not(.dashboard-builder--dragging) .draggable-column-item:hover .draggable-column-item-handle, -:not(.dashboard-builder--dragging) .draggable-column-item-handle:hover { +.draggable-row-item-handle:after { + content: ""; + background: #44C0FF; + display: block; + margin: 5px auto 0 auto; + width: 20%; + min-width: 20px; + height: 5px; +} + +.draggable-row:hover .draggable-row-handle, +.draggable-row-handle:hover, +.draggable-row-item:hover .draggable-row-item-handle, +.draggable-row-item-handle:hover, +.draggable-column-item:hover .draggable-column-item-handle, +.draggable-column-item-handle:hover { opacity: 1; } + +.dashboard-builder--dragging .draggable-row:hover .draggable-row-handle, +.dashboard-builder--dragging .draggable-row-handle:hover, +.dashboard-builder--dragging .draggable-row-item:hover .draggable-row-item-handle, +.dashboard-builder--dragging .draggable-row-item-handle:hover, +.dashboard-builder--dragging .draggable-column-item:hover .draggable-column-item-handle, +.dashboard-builder--dragging .draggable-column-item-handle:hover { + opacity: 0; +} diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Chart.jsx b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Chart.jsx index f5e2a3a593c71..8133db8b25c31 100644 --- a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Chart.jsx +++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Chart.jsx @@ -1,7 +1,5 @@ import React from 'react'; -import PropTypes from 'prop-types'; - -import { } from '../../util/constants'; +// import PropTypes from 'prop-types'; const propTypes = { }; @@ -18,19 +16,7 @@ class Chart extends React.Component { render() { return ( -
    Chart
    +
    Chart
    ); } } diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Divider.jsx b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Divider.jsx index 4c12e655fb2ba..d6fe42f4bb4d7 100644 --- a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Divider.jsx +++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Divider.jsx @@ -2,7 +2,11 @@ import React from 'react'; class Divider extends React.PureComponent { render() { - return
    ; + return ( +
    +
    +
    + ); } } diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Row.jsx b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Row.jsx index f229eabc9adcd..cc38e44ce7c3c 100644 --- a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Row.jsx +++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Row.jsx @@ -23,7 +23,7 @@ import { import { COMPONENT_TYPE_LOOKUP } from './'; const propTypes = { - entity: PropTypes.object, + entity: PropTypes.object, // @TODO shape entities: PropTypes.object, columnWidth: PropTypes.number, onResizeStart: PropTypes.func, @@ -42,7 +42,7 @@ const defaultProps = { class Row extends React.PureComponent { // shouldComponentUpdate() { - // // @TODO check foro updates to this row only + // // @TODO check for updates to this row only // } render() { @@ -62,6 +62,7 @@ class Row extends React.PureComponent { let maxItemHeight = 0; const rowItems = []; + // this adds a gutter between each child in the row. (rowEntity.children || []).forEach((id, index) => { const entity = entities[id]; totalColumns += (entity.meta || {}).width || 0; @@ -70,14 +71,6 @@ class Row extends React.PureComponent { if ((entity.meta || {}).height) maxItemHeight = Math.max(maxItemHeight, entity.meta.height); }); - if (!rowEntity.children || !rowEntity.children.length) { - return ( -
    - Empty row -
    - ); - } - return ( (
    - {droppableProvided.placeholder} - {rowItems.map((entity, index) => { const id = entity.id || entity; const Component = COMPONENT_TYPE_LOOKUP[entity.type]; @@ -152,8 +144,12 @@ class Row extends React.PureComponent { isDragDisabled={disableDrag} > {draggableProvided => ( -
    - {draggableProvided.placeholder} +
    {RowItem}
    + {draggableProvided.placeholder}
    )} @@ -172,6 +169,7 @@ class Row extends React.PureComponent { return RowItem; })} + {droppableProvided.placeholder}
    )} diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Tabs.jsx b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Tabs.jsx index 4a197e26118c3..df14d08f64878 100644 --- a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Tabs.jsx +++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Tabs.jsx @@ -1,6 +1,12 @@ import React from 'react'; -import { Tabs as BootstrapTabs, Tab } from 'react-bootstrap'; import PropTypes from 'prop-types'; +import cx from 'classnames'; +import { Tabs as BootstrapTabs, Tab } from 'react-bootstrap'; +import { Draggable, Droppable } from 'react-beautiful-dnd'; + +import { DROPPABLE_DIRECTION_VERTICAL } from '../../util/constants'; +import isValidChild from '../../util/isValidChild'; +import { COMPONENT_TYPE_LOOKUP } from './'; const propTypes = { id: PropTypes.string.isRequired, @@ -10,6 +16,10 @@ const propTypes = { }), ), onChangeTab: PropTypes.func, + entity: PropTypes.shape({ + children: PropTypes.arrayOf(PropTypes.string), + }), + entities: PropTypes.object, // @TODO shape }; const defaultProps = { @@ -19,6 +29,7 @@ const defaultProps = { { label: 'Section Tab' }, ], onChangeTab: null, + children: null, }; class Tabs extends React.Component { @@ -39,12 +50,13 @@ class Tabs extends React.Component { } render() { - const { tabs, id } = this.props; + const { tabs, id: tabId, entity: tabEntity, ...restProps } = this.props; + const { entities, draggingEntity, disableDrop, disableDrag } = restProps; const { tabIndex } = this.state; return (
    {tab.label}
    } /> ))} + + + {(droppableProvided, droppableSnapshot) => ( +
    + {(tabEntity.children || []).map((id, index) => { + const entity = entities[id] || {}; + const Component = COMPONENT_TYPE_LOOKUP[entity.type]; + return Component && ( + + {draggableProvided => ( +
    +
    +
    + +
    + {draggableProvided.placeholder} +
    + )} + + ); + })} + {droppableProvided.placeholder} +
    + )} +
    ); } diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/components.css b/superset/assets/javascripts/dashboard/v2/components/gridComponents/components.css index 0fc4a65b10243..a7197f64c16ea 100644 --- a/superset/assets/javascripts/dashboard/v2/components/gridComponents/components.css +++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/components.css @@ -13,40 +13,63 @@ padding-left: 16px; } +/* Chart */ +.dashboard-component-chart { + width: 100%; + height: 100%; + color: #879399; + background-color: #fff; + padding: 16px; + display: flex; + align-items: center; + justify-content: center; +} + +.grid-container--resizing .dashboard-component-chart, +.dashboard-builder--dragging .dashboard-component-chart, +.dashboard-component-chart:hover { + box-shadow: inset 0 0 0 1px #CFD8DC; +} + /* Divider */ .dashboard-component-divider { width: 100%; + padding: 24px 0; /* this is padding not margin to enable a larger mouse target */ + background-color: transparent; +} + +.dashboard-component-divider > div { height: 1px; - margin: 24px 0; + width: 100%; background-color: #CFD8DC; } /* Tabs -- this overwrites Superset bootstrap theme tab styling */ .dashboard-component-tabs { width: 100%; - background-color: inherit; + background-color: white; } -.grid-row-container .dashboard-component-tabs .nav-tabs { +.dashboard-component-tabs .nav-tabs { border-bottom: 1px solid #CFD8DC; } /* by moving padding from to
  • we can restrict the selected tab indicator to text width */ -.grid-row-container .dashboard-component-tabs .nav-tabs > li { +.dashboard-component-tabs .nav-tabs > li { padding: 0 16px; } -.grid-row-container .dashboard-component-tabs .nav-tabs > li > a { +.dashboard-component-tabs .nav-tabs > li > a { color: #263238; border: none; padding: 12px 0 14px 0; } -.grid-row-container .dashboard-component-tabs .nav-tabs > li.active > a { +.dashboard-component-tabs .nav-tabs > li.active > a { border: none; } -.grid-row-container .dashboard-component-tabs .nav-tabs > li.active > a:after { +.dashboard-component-tabs .nav-tabs > li.active > a:after { content: ""; position: absolute; height: 3px; @@ -55,7 +78,7 @@ background: linear-gradient(to right, #E32464, #2C2261); } -.grid-row-container .dashboard-component-tabs .nav-tabs > li > a:hover { +.dashboard-component-tabs .nav-tabs > li > a:hover { border: none; background: inherit; color: #000000; diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/grid.css b/superset/assets/javascripts/dashboard/v2/components/gridComponents/grid.css index 19eaaacd9dc69..e0bd7b4b0f9f7 100644 --- a/superset/assets/javascripts/dashboard/v2/components/gridComponents/grid.css +++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/grid.css @@ -14,7 +14,7 @@ .grid-row { display: flex; flex-direction: row; - flex-wrap: nowrap; + flex-wrap: wrap; align-items: flex-start; min-height: 16px; height: fit-content; diff --git a/superset/assets/javascripts/dashboard/v2/components/resizable/resizable.css b/superset/assets/javascripts/dashboard/v2/components/resizable/resizable.css index 3a9acd07367c6..a16f434d5c351 100644 --- a/superset/assets/javascripts/dashboard/v2/components/resizable/resizable.css +++ b/superset/assets/javascripts/dashboard/v2/components/resizable/resizable.css @@ -14,8 +14,10 @@ box-shadow: inset 0 0 0 2px #44C0FF; } -.grid-resizable-container .grid-spacer { - box-shadow: inset 0 0 0 1px #eaeaea; +.grid-container--resizing .grid-spacer, +.dashboard-builder--dragging .grid-spacer, +.grid-spacer:hover { + box-shadow: inset 0 0 0 1px #CFD8DC; } .resize-handle { @@ -40,7 +42,7 @@ } .resize-handle--right { - width: 3px; + width: 2px; height: 20px; right: -2px; top: 47%; @@ -54,7 +56,7 @@ } .resize-handle--bottom { - height: 3px; + height: 2px; width: 20px; bottom: 10px; left: 47%; diff --git a/superset/assets/javascripts/dashboard/v2/fixtures/testLayout.js b/superset/assets/javascripts/dashboard/v2/fixtures/testLayout.js index c4e3899808be7..026c1fff2dff7 100644 --- a/superset/assets/javascripts/dashboard/v2/fixtures/testLayout.js +++ b/superset/assets/javascripts/dashboard/v2/fixtures/testLayout.js @@ -17,10 +17,10 @@ export default { children: [ 'header0', 'row1', - 'row2', + 'divider0', 'row3', - 'row4', - 'row5', + 'tabs0', + // 'row5', ], }, // row0: { @@ -37,13 +37,13 @@ export default { 'chartc', ], }, - row2: { - id: 'row2', - type: INVISIBLE_ROW_TYPE, - children: [ - 'divider0', - ], - }, + // row2: { + // id: 'row2', + // type: INVISIBLE_ROW_TYPE, + // children: [ + // 'divider0', + // ], + // }, row3: { id: 'row3', type: ROW_TYPE, @@ -179,8 +179,10 @@ export default { tabs0: { id: 'tabs0', type: TABS_TYPE, + children: [ + 'row5', + ], meta: { - width: 1, }, }, }, diff --git a/superset/assets/javascripts/dashboard/v2/util/dnd-reorder.js b/superset/assets/javascripts/dashboard/v2/util/dnd-reorder.js index 5bcaefd408558..868062dec0104 100644 --- a/superset/assets/javascripts/dashboard/v2/util/dnd-reorder.js +++ b/superset/assets/javascripts/dashboard/v2/util/dnd-reorder.js @@ -2,11 +2,11 @@ export function reorder(list, startIndex, endIndex) { const result = [...list]; const [removed] = result.splice(startIndex, 1); result.splice(endIndex, 0, removed); - if (result.some(v => !v)) debugger; + return result; } -export function reorderRows({ +export function reorderItem({ entitiesMap, source, destination, @@ -17,7 +17,6 @@ export function reorderRows({ // moving to same list if (source.droppableId === destination.droppableId) { - console.log('within list', source.index, destination.index); const reordered = reorder( current, source.index, @@ -39,7 +38,6 @@ export function reorderRows({ current.splice(source.index, 1); // remove from original next.splice(destination.index, 0, target); // insert into next - console.log('between list', source.index, destination.index); const result = { ...entitiesMap, [source.droppableId]: { diff --git a/superset/assets/javascripts/dashboard/v2/util/isValidChild.js b/superset/assets/javascripts/dashboard/v2/util/isValidChild.js index 904fb41b0ab09..b3449c6ce205c 100644 --- a/superset/assets/javascripts/dashboard/v2/util/isValidChild.js +++ b/superset/assets/javascripts/dashboard/v2/util/isValidChild.js @@ -23,19 +23,21 @@ const typeToValidChildType = { [ROW_TYPE]: true, [INVISIBLE_ROW_TYPE]: true, [TABS_TYPE]: true, - + [DIVIDER_TYPE]: true, + [HEADER_TYPE]: true, [DRAGGABLE_NEW_CHART]: true, [DRAGGABLE_NEW_DIVIDER]: true, [DRAGGABLE_NEW_HEADER]: true, [DRAGGABLE_NEW_ROW]: true, }, - // row types [ROW_TYPE]: { [CHART_TYPE]: true, [MARKDOWN_TYPE]: true, [COLUMN_TYPE]: true, [SPACER_TYPE]: true, + [HEADER_TYPE]: true, + [DRAGGABLE_NEW_SPACER]: true, [DRAGGABLE_NEW_CHART]: true, [DRAGGABLE_NEW_HEADER]: true, @@ -53,12 +55,14 @@ const typeToValidChildType = { [TABS_TYPE]: { [ROW_TYPE]: true, - [CHART_TYPE]: true, - [MARKDOWN_TYPE]: true, - [SPACER_TYPE]: true, - [COLUMN_TYPE]: true, - - [DRAGGABLE_NEW_SPACER]: true, + [INVISIBLE_ROW_TYPE]: true, + [DIVIDER_TYPE]: true, + [HEADER_TYPE]: true, + // [CHART_TYPE]: true, + // [MARKDOWN_TYPE]: true, + // [SPACER_TYPE]: true, + // [COLUMN_TYPE]: true, + // [DRAGGABLE_NEW_SPACER]: true, }, [COLUMN_TYPE]: { @@ -66,12 +70,11 @@ const typeToValidChildType = { [MARKDOWN_TYPE]: true, [HEADER_TYPE]: true, [SPACER_TYPE]: true, - + [DIVIDER_TYPE]: true, + [DRAGGABLE_NEW_DIVIDER]: true, [DRAGGABLE_NEW_SPACER]: true, [DRAGGABLE_NEW_CHART]: true, [DRAGGABLE_NEW_HEADER]: true, - // divider? - // row? }, // these have no valid children @@ -84,7 +87,10 @@ const typeToValidChildType = { export default function isValidChild({ parentType, childType }) { if (!parentType || !childType) return false; - return Boolean( + const isValid = Boolean( typeToValidChildType[parentType][childType], ); + + console.log(`${parentType} > ${childType} -> ${isValid}`); + return isValid; } From c6fbc30eae51b2dd1456b5c9e3a9797c55301951 Mon Sep 17 00:00:00 2001 From: Chris Williams Date: Thu, 8 Feb 2018 18:25:04 -0800 Subject: [PATCH 09/25] [dnd] refactor to use react-dnd --- .../v2/components/BuilderComponentPane.jsx | 22 +- .../v2/components/DashboardBuilder.jsx | 100 +++--- .../dashboard/v2/components/DashboardGrid.jsx | 166 ++++----- .../v2/components/StaticDashboard.jsx | 2 +- .../dashboard/v2/components/dnd/Draggable.jsx | 62 ++++ .../v2/components/dnd/DraggableColumn.jsx | 332 ++++++++++++++++++ .../v2/components/dnd/DraggableNewChart.jsx | 4 +- .../components/dnd/DraggableNewComponent.jsx | 40 +-- .../v2/components/dnd/DraggableNewDivider.jsx | 4 +- .../v2/components/dnd/DraggableNewHeader.jsx | 4 +- .../v2/components/dnd/DraggableNewRow.jsx | 5 +- .../v2/components/dnd/DraggableRow.jsx | 274 +++++++++++++++ .../dashboard/v2/components/dnd/dnd.css | 19 +- .../v2/components/gridComponents/Column.jsx | 5 +- .../v2/components/gridComponents/Row.jsx | 164 ++------- .../v2/components/gridComponents/Tabs.jsx | 82 ++--- .../components/gridComponents/components.css | 7 +- .../v2/components/gridComponents/grid.css | 2 + .../v2/components/gridComponents/index.js | 2 +- .../v2/components/resizable/resizable.css | 6 - .../dashboard/v2/fixtures/testLayout.js | 274 ++++++--------- .../{gridUtils.js => componentIsResizable.js} | 4 +- .../dashboard/v2/util/componentTypes.js | 23 ++ .../dashboard/v2/util/constants.js | 31 +- .../dashboard/v2/util/isValidChild.js | 29 +- .../dashboard/v2/util/newEntitiesFromDrop.js | 84 +++++ .../dashboard/v2/util/propShapes.jsx | 12 + superset/assets/package.json | 3 +- 28 files changed, 1122 insertions(+), 640 deletions(-) create mode 100644 superset/assets/javascripts/dashboard/v2/components/dnd/Draggable.jsx create mode 100644 superset/assets/javascripts/dashboard/v2/components/dnd/DraggableColumn.jsx create mode 100644 superset/assets/javascripts/dashboard/v2/components/dnd/DraggableRow.jsx rename superset/assets/javascripts/dashboard/v2/util/{gridUtils.js => componentIsResizable.js} (69%) create mode 100644 superset/assets/javascripts/dashboard/v2/util/componentTypes.js create mode 100644 superset/assets/javascripts/dashboard/v2/util/newEntitiesFromDrop.js create mode 100644 superset/assets/javascripts/dashboard/v2/util/propShapes.jsx diff --git a/superset/assets/javascripts/dashboard/v2/components/BuilderComponentPane.jsx b/superset/assets/javascripts/dashboard/v2/components/BuilderComponentPane.jsx index 5c3f07bc4665f..6f90a38a3c7fe 100644 --- a/superset/assets/javascripts/dashboard/v2/components/BuilderComponentPane.jsx +++ b/superset/assets/javascripts/dashboard/v2/components/BuilderComponentPane.jsx @@ -1,14 +1,11 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { Droppable } from 'react-beautiful-dnd'; import DraggableNewChart from './dnd/DraggableNewChart'; import DraggableNewDivider from './dnd/DraggableNewDivider'; import DraggableNewHeader from './dnd/DraggableNewHeader'; import DraggableNewRow from './dnd/DraggableNewRow'; -import { DROPPABLE_NEW_COMPONENT } from '../util/constants'; - const propTypes = { editMode: PropTypes.bool, }; @@ -20,21 +17,10 @@ class BuilderComponentPane extends React.Component {
    Insert components
    - - {provided => ( -
    - - - - - {provided.placeholder} -
    - )} -
    + + + +
  • ); } diff --git a/superset/assets/javascripts/dashboard/v2/components/DashboardBuilder.jsx b/superset/assets/javascripts/dashboard/v2/components/DashboardBuilder.jsx index 18c6cb57e58a8..33979121e97d0 100644 --- a/superset/assets/javascripts/dashboard/v2/components/DashboardBuilder.jsx +++ b/superset/assets/javascripts/dashboard/v2/components/DashboardBuilder.jsx @@ -1,12 +1,12 @@ import React from 'react'; import PropTypes from 'prop-types'; - -import { DragDropContext } from 'react-beautiful-dnd'; +import HTML5Backend from 'react-dnd-html5-backend'; +import { DragDropContext } from 'react-dnd'; import cx from 'classnames'; import BuilderComponentPane from './BuilderComponentPane'; import DashboardGrid from './DashboardGrid'; -// import getNewGridEntity from '../util/getNewGridEntity'; +import newEntitiesFromDrop from '../util/newEntitiesFromDrop'; import { reorderItem } from '../util/dnd-reorder'; import './dnd/dnd.css'; @@ -26,47 +26,49 @@ class DashboardBuilder extends React.Component { super(props); this.state = { layout: testLayout, - draggingEntity: null, }; - this.handleDragEnd = this.handleDragEnd.bind(this); - this.handleDragStart = this.handleDragStart.bind(this); + // @TODO most of this can probably be moved into redux + this.handleDrop = this.handleDrop.bind(this); this.handleMoveEntity = this.handleMoveEntity.bind(this); this.handleNewEntity = this.handleNewEntity.bind(this); this.handleUpdateEntity = this.handleUpdateEntity.bind(this); } - handleDragEnd(dropResult) { - console.log('drag end', dropResult); - if (dropResult.destination) { - if (/new/gi.test(dropResult.draggableId)) { - this.handleNewEntity(dropResult); - } else { - this.handleMoveEntity(dropResult); - } + handleDrop(dropResult) { + console.log('builder handleDrop', dropResult); + + if ( + dropResult.destination + && dropResult.source + && !( // ensure it has moved + dropResult.destination.droppableId === dropResult.source.droppableId + && dropResult.destination.index === dropResult.source.index + ) + ) { + this.handleMoveEntity(dropResult); + } else if (dropResult.destination && !dropResult.source) { + this.handleNewEntity(dropResult); } - this.setState(() => ({ draggingEntity: null })); - } - - handleDragStart(result) { - console.log('drag start', result); - const { layout } = this.state; - const draggingEntity = layout.entities[result.draggableId] || { type: result.draggableId }; - this.setState(() => ({ draggingEntity })); } - handleNewEntity() { + handleNewEntity(dropResult) { console.log('new entity'); + this.setState(({ layout }) => { + const newEntities = newEntitiesFromDrop({ dropResult, entitiesMap: layout }); + return { + layout: { + ...layout, + ...newEntities, + }, + }; + }); } handleMoveEntity({ source, destination, draggableId }) { - console.log('source', source, 'destination', destination); - this.setState(({ layout }) => { - const { entities } = layout; - const nextEntities = reorderItem({ - entitiesMap: entities, + entitiesMap: layout, source, destination, }); @@ -74,9 +76,7 @@ class DashboardBuilder extends React.Component { return { layout: { ...layout, - entities: { - ...nextEntities, - }, + ...nextEntities, }, }; }); @@ -84,40 +84,28 @@ class DashboardBuilder extends React.Component { handleUpdateEntity(nextEntity) { console.log('update entity', nextEntity); - this.setState(({ layout }) => ({ layout: { ...layout, - entities: { - ...layout.entities, - [nextEntity.id]: nextEntity, - }, + [nextEntity.id]: nextEntity, }, })); } render() { - const { draggingEntity, layout } = this.state; + const { layout } = this.state; return ( - -
    - - -
    -
    +
    + + +
    ); } } @@ -125,4 +113,4 @@ class DashboardBuilder extends React.Component { DashboardBuilder.propTypes = propTypes; DashboardBuilder.defaultProps = defaultProps; -export default DashboardBuilder; +export default DragDropContext(HTML5Backend)(DashboardBuilder); diff --git a/superset/assets/javascripts/dashboard/v2/components/DashboardGrid.jsx b/superset/assets/javascripts/dashboard/v2/components/DashboardGrid.jsx index 8b7ce725fc3c3..7959226bb7d8f 100644 --- a/superset/assets/javascripts/dashboard/v2/components/DashboardGrid.jsx +++ b/superset/assets/javascripts/dashboard/v2/components/DashboardGrid.jsx @@ -2,34 +2,23 @@ import React from 'react'; import PropTypes from 'prop-types'; import ParentSize from '@vx/responsive/build/components/ParentSize'; import cx from 'classnames'; -import { Droppable, Draggable } from 'react-beautiful-dnd'; - -import isValidChild from '../util/isValidChild'; +import DraggableRow from './dnd/DraggableRow'; import { - DROPPABLE_ID_DASHBOARD_ROOT, + DASHBOARD_ROOT_ID, GRID_GUTTER_SIZE, GRID_COLUMN_COUNT, - GRID_ROOT_TYPE, } from '../util/constants'; -import { COMPONENT_TYPE_LOOKUP } from './gridComponents'; import './gridComponents/grid.css'; const propTypes = { layout: PropTypes.object, - draggingEntity: PropTypes.shape({ - type: PropTypes.string.isRequired, // @TODO enumerate - }), updateEntity: PropTypes.func, }; const defaultProps = { - layout: { - children: [], - entities: {}, - }, - draggingEntity: null, + layout: {}, updateEntity() {}, }; @@ -38,10 +27,11 @@ class DashboardGrid extends React.PureComponent { super(props); this.state = { showGrid: false, - rowGuide: null, + rowGuideTop: null, disableDrop: false, disableDrag: false, selectedEntityId: null, + dropIndicatorTop: null, }; this.handleToggleSelectEntityId = this.handleToggleSelectEntityId.bind(this); @@ -60,14 +50,14 @@ class DashboardGrid extends React.PureComponent { handleResizeStart({ ref, direction }) { console.log('resize start'); - let rowGuide = null; + let rowGuideTop = null; if (direction === 'bottom' || direction === 'bottomRight') { - rowGuide = this.getRowGuidePosition(ref); + rowGuideTop = this.getRowGuidePosition(ref); } this.setState(() => ({ showGrid: true, - rowGuide, + rowGuideTop, disableDrag: true, disableDrop: true, })); @@ -76,15 +66,15 @@ class DashboardGrid extends React.PureComponent { handleResize({ ref, direction }) { console.log('resize'); if (direction === 'bottom' || direction === 'bottomRight') { - this.setState(() => ({ rowGuide: this.getRowGuidePosition(ref) })); + this.setState(() => ({ rowGuideTop: this.getRowGuidePosition(ref) })); } } handleResizeStop({ id, widthMultiple, heightMultiple }) { console.log('resize stop'); - const { layout, updateEntity } = this.props; - const entity = layout.entities[id]; - debugger; + + const { layout: entities, updateEntity } = this.props; + const entity = entities[id]; if (entity && (entity.meta.width !== widthMultiple || entity.meta.height !== heightMultiple)) { updateEntity({ ...entity, @@ -97,7 +87,7 @@ class DashboardGrid extends React.PureComponent { } this.setState(() => ({ showGrid: false, - rowGuide: null, + rowGuideTop: null, disableDrag: false, disableDrop: false, })); @@ -119,12 +109,16 @@ class DashboardGrid extends React.PureComponent { } render() { - const { layout, draggingEntity } = this.props; - const { showGrid, rowGuide, disableDrop, disableDrag, selectedEntityId } = this.state; - const { entities } = layout; - const rootEntity = entities[DROPPABLE_ID_DASHBOARD_ROOT]; + const { layout: entities, onDrop, canDrop } = this.props; + const { + showGrid, + rowGuideTop, + disableDrop, + disableDrag, + // selectedEntityId, + } = this.state; - console.log('dragging', draggingEntity, 'selected', selectedEntityId); + const rootEntity = entities[DASHBOARD_ROOT_ID]; return (
    - {(droppableProvided, droppableSnapshot) => ( +
    + {rootEntity.children.map((id, index) => ( + + ))} + + {showGrid && Array(GRID_COLUMN_COUNT).fill(null).map((_, i) => ( +
    + ))} + + {showGrid && rowGuideTop &&
    - {rootEntity.children.map((id, index) => { - const entity = entities[id] || {}; - const Component = COMPONENT_TYPE_LOOKUP[entity.type]; - return ( - - {draggableProvided => ( -
    -
    -
    - -
    - {draggableProvided.placeholder} -
    - )} - - ); - })} - {droppableProvided.placeholder} - {showGrid && Array(GRID_COLUMN_COUNT).fill(null).map((_, i) => ( -
    - ))} - - {showGrid && rowGuide && -
    } -
    - )} - + className="grid-row-guide" + style={{ + top: rowGuideTop, + width, + }} + />} +
    ); }} diff --git a/superset/assets/javascripts/dashboard/v2/components/StaticDashboard.jsx b/superset/assets/javascripts/dashboard/v2/components/StaticDashboard.jsx index 1d0b356893b06..4fd239779d7bd 100644 --- a/superset/assets/javascripts/dashboard/v2/components/StaticDashboard.jsx +++ b/superset/assets/javascripts/dashboard/v2/components/StaticDashboard.jsx @@ -1,5 +1,5 @@ import React from 'react'; -import PropTypes from 'prop-types'; +// import PropTypes from 'prop-types'; const propTypes = { }; diff --git a/superset/assets/javascripts/dashboard/v2/components/dnd/Draggable.jsx b/superset/assets/javascripts/dashboard/v2/components/dnd/Draggable.jsx new file mode 100644 index 0000000000000..b15449eb8d716 --- /dev/null +++ b/superset/assets/javascripts/dashboard/v2/components/dnd/Draggable.jsx @@ -0,0 +1,62 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { DragSource } from 'react-dnd'; + +const propTypes = { + draggableId: PropTypes.string.isRequired, + index: PropTypes.number.isRequired, + children: PropTypes.func.isRequired, + beginDrag: PropTypes.func, + endDrag: PropTypes.func, + canDrag: PropTypes.func, + // from react-dnd + dragSourceRef: PropTypes.func.isRequired, + dragPreviewRef: PropTypes.func.isRequired, + isDragging: PropTypes.bool, +}; + +const defaultProps = { + beginDrag: null, + endDrag: null, + canDrag: null, + isDragging: false, +}; + +class Draggable extends React.Component { + render() { + const { children, dragSourceRef, dragPreviewRef, isDragging } = this.props; + + return children({ + dragSourceRef, + dragPreviewRef, + isDragging, + }); + } +} + +Draggable.propTypes = propTypes; +Draggable.defaultProps = defaultProps; + +export default DragSource( + 'DEFAULT', // @TODO this should be a constant, not a useful hook for us + { + beginDrag(props, monitor, component) { + return props.beginDrag ? props.beginDrag(props, monitor, component) : ({ + draggableId: props.draggableId, + index: props.index, + type: props.type, + }); + }, + endDrag(props, monitor, component) { + return props.endDrag && props.endDrag(props, monitor, component); + }, + canDrag(props, monitor) { return props.canDrag ? props.canDrag(props, monitor) : true; }, + }, + function dndStateToProps(connect, monitor) { + return { + dragSourceRef: connect.dragSource(), // @TODO non-ref if this doesn't work + dragPreviewRef: connect.dragPreview(), + isDragging: monitor.isDragging(), + }; + }, +)(Draggable); diff --git a/superset/assets/javascripts/dashboard/v2/components/dnd/DraggableColumn.jsx b/superset/assets/javascripts/dashboard/v2/components/dnd/DraggableColumn.jsx new file mode 100644 index 0000000000000..bc25a5e4e672c --- /dev/null +++ b/superset/assets/javascripts/dashboard/v2/components/dnd/DraggableColumn.jsx @@ -0,0 +1,332 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { DragSource, DropTarget } from 'react-dnd'; +import cx from 'classnames'; +import throttle from 'lodash.throttle'; + +import ResizableContainer from '../resizable/ResizableContainer'; +import componentIsResizable from '../../util/componentIsResizable'; + +import { + GRID_GUTTER_SIZE, + GRID_ROW_HEIGHT_UNIT, + GRID_COLUMN_COUNT, + GRID_MIN_COLUMN_COUNT, + GRID_MIN_ROW_UNITS, + GRID_MAX_ROW_UNITS, +} from '../../util/constants'; + +import { SPACER_TYPE, COLUMN_TYPE } from '../../util/componentTypes'; +import { COMPONENT_TYPE_LOOKUP } from '../gridComponents'; + +import isValidChild from '../../util/isValidChild'; + +const HOVER_THROTTLE_MS = 200; + +const propTypes = { + // depth: PropTypes.number.isRequired, + entity: PropTypes.object.isRequired, + index: PropTypes.number.isRequired, + parentId: PropTypes.string.isRequired, + disableDrop: PropTypes.bool, + disableDrag: PropTypes.bool, + + // from HOCs + isDragging: PropTypes.bool.isRequired, + droppableRef: PropTypes.func.isRequired, + dragSourceRef: PropTypes.func.isRequired, + dragPreviewRef: PropTypes.func.isRequired, +}; + +const defaultProps = { + disableDrag: false, + disableDrop: false, +}; + +class DraggableColumn extends React.PureComponent { + constructor(props) { + super(props); + this.state = { + horizontalDropIndicator: null, + }; + + this.handleBeginDrag = this.handleBeginDrag.bind(this); + this.handleHover = throttle(this.hover.bind(this), HOVER_THROTTLE_MS).bind(this); + this.handleDrop = this.handleDrop.bind(this); + + this.renderDropIndicator = this.renderDropIndicator.bind(this); + this.renderComponent = this.renderComponent.bind(this); + } + + handleBeginDrag() { + const { entity, index, parentId } = this.props; + return { draggableId: entity.id, index, parentId, type: entity.type }; + } + + hover(props, monitor, component) { + const { entity } = this.props; + const draggingItem = monitor.getItem(); + + if (!draggingItem || draggingItem.draggableId === entity.id) { + return; + } + + const draggingItemIsValidChild = isValidChild({ + parentType: entity.type, + childType: draggingItem.type, + }); + + if (draggingItemIsValidChild) { // vertical indicator, append to container + this.setState(() => ({ + dropIndicator: { + position: 'relative', + width: 'auto', + minWidth: 20, + margin: 'auto', + left: 0, + right: 0, + }, + })); + } else { // vertical indicator, left or right + const colBoundingRect = this.ref.getBoundingClientRect(); + const clientOffset = monitor.getClientOffset(); + + if (clientOffset) { + const colMiddleX = colBoundingRect.left + + (colBoundingRect.right - colBoundingRect.left) / 2; + + let dropIndicatorLeft = 0; + if (clientOffset.x >= colMiddleX) { // place indicator right of col + dropIndicatorLeft = colBoundingRect.width; + } + + this.setState(() => ({ + dropIndicator: { + left: dropIndicatorLeft, + height: '100%', + width: 3, + }, + })); + } + } + } + + handleDrop(props, monitor, component) { + console.log('column drop'); + // check if a nested drop target handled the drop + if (monitor.didDrop()) return; + + const { entity, parentId: columnParentId, index: columnIndex } = this.props; + const draggingItem = monitor.getItem(); + + // if dropped self on self, do nothing + if (draggingItem && draggingItem.draggableId === entity.id) return; + + // append to self, or parent + const acceptChild = isValidChild({ parentType: entity.type, childType: draggingItem.type }); + + // if (!acceptChild) return undefined; + + const dropResult = { + source: draggingItem.parentId ? { + droppableId: draggingItem.parentId, + index: draggingItem.index, + } : null, + draggableId: draggingItem.draggableId, + }; + + if (acceptChild) { // if it's a valid child, append it to entity.children + dropResult.destination = { + droppableId: entity.id, + index: entity.children.length, + }; + } else { + let nextIndex = columnIndex; + const colBoundingRect = this.ref.getBoundingClientRect(); + const clientOffset = monitor.getClientOffset(); + + if (clientOffset) { + const colMiddleX = colBoundingRect.left + + (colBoundingRect.right - colBoundingRect.left) / 2; + + if (clientOffset.x >= colMiddleX) { // right of column + nextIndex += 1; + } + } + + if (draggingItem.parentId === columnParentId && draggingItem.index < columnIndex) { + nextIndex = Math.max(0, nextIndex - 1); + } + + dropResult.destination = { + droppableId: columnParentId, + index: nextIndex, + }; + } + // @TODO this should be a redux action + this.props.onDrop(dropResult); + + return dropResult; + } + + renderResizableContainer(children) { + const { entity, gridProps } = this.props; + const isResizable = componentIsResizable(entity); + const isSpacer = entity.type === SPACER_TYPE; + + if (!isResizable) return children; + + const { + onResizeStart, + onResize, + onResizeStop, + columnWidth, + occupiedColumnCount, + currentRowHeight, + } = gridProps; + + return ( + + {children} + + ); + } + + renderComponent() { + const { entity, entities, gridProps } = this.props; + const Component = COMPONENT_TYPE_LOOKUP[entity.type]; + + return ( + + ); + } + + renderDropIndicator() { + const { dropIndicator } = this.state; + const { isDraggingOverShallow } = this.props; + return dropIndicator && isDraggingOverShallow && ( +
    + ); + } + + render() { + const { + droppableRef, + dragSourceRef, + dragPreviewRef, + isDragging, + disableDrag, + // isDraggingOverShallow + } = this.props; + + return ( +
    { + this.ref = ref; + dragPreviewRef(ref); + droppableRef(ref); + }} + className={cx( + 'draggable', + 'draggable-row-item', // @TODO update to draggable-column + isDragging && 'draggable--dragging', + disableDrag && 'draggable--disabled', + )} + style={{ + display: 'flex', + flexDirection: 'row', + opacity: isDragging && 0.25, + position: 'relative', + backgroundColor: isDragging ? '#ccc' : null, + }} + > + +
    + + {this.renderResizableContainer( + this.renderComponent(), + )} + + {this.renderDropIndicator()} +
    + ); + } +} + +DraggableColumn.propTypes = propTypes; +DraggableColumn.defaultProps = defaultProps; + +const dragConfig = [ + 'DEFAULT', + { + beginDrag(props, monitor, component) { + return component.handleBeginDrag(monitor); + }, + }, + function dragStateToProps(connect, monitor) { + return { + dragSourceRef: connect.dragSource(), // @TODO non-ref if this doesn't work + dragPreviewRef: connect.dragPreview(), + isDragging: monitor.isDragging(), + }; + }, + +]; + +const dropConfig = [ + 'DEFAULT', + { + hover(props, monitor, component) { + if (monitor.isOver({ shallow: true })) { + component.decoratedComponentInstance.handleHover(props, monitor, component); + } + }, + drop(props, monitor, component) { + if (!component) return undefined + return component.decoratedComponentInstance.handleDrop(props, monitor, component); + }, + }, + function dropStateToProps(connect, monitor) { + return { + droppableRef: connect.dropTarget(), + isDraggingOverShallow: monitor.isOver({ shallow: true }), + draggableId: (monitor.getItem() || {}).draggableId || null, + }; + }, +]; + +// note that the composition order here determines using +// component.method() vs decoratedComponentInstance.method() in the above config +export default DropTarget(...dropConfig)( + DragSource(...dragConfig)(DraggableColumn), +); diff --git a/superset/assets/javascripts/dashboard/v2/components/dnd/DraggableNewChart.jsx b/superset/assets/javascripts/dashboard/v2/components/dnd/DraggableNewChart.jsx index 9591f1e1069d2..5c36742c28169 100644 --- a/superset/assets/javascripts/dashboard/v2/components/dnd/DraggableNewChart.jsx +++ b/superset/assets/javascripts/dashboard/v2/components/dnd/DraggableNewChart.jsx @@ -1,7 +1,7 @@ import React from 'react'; // import PropTypes from 'prop-types'; -import { DRAGGABLE_NEW_CHART } from '../../util/constants'; +import { CHART_TYPE } from '../../util/componentTypes'; import DraggableNewComponent from './DraggableNewComponent'; const propTypes = { @@ -11,7 +11,7 @@ export default class DraggableNewChart extends React.PureComponent { render() { return ( ); diff --git a/superset/assets/javascripts/dashboard/v2/components/dnd/DraggableNewComponent.jsx b/superset/assets/javascripts/dashboard/v2/components/dnd/DraggableNewComponent.jsx index 1f6e8495674e7..971ba0aa5ca80 100644 --- a/superset/assets/javascripts/dashboard/v2/components/dnd/DraggableNewComponent.jsx +++ b/superset/assets/javascripts/dashboard/v2/components/dnd/DraggableNewComponent.jsx @@ -1,36 +1,32 @@ import React from 'react'; import PropTypes from 'prop-types'; import cx from 'classnames'; -import { Draggable } from 'react-beautiful-dnd'; +import Draggable from './Draggable'; const propTypes = { - id: PropTypes.string.isRequired, + // id: PropTypes.string.isRequired, + type: PropTypes.string.isRequired, label: PropTypes.string.isRequired, }; export default class DraggableNewComponent extends React.PureComponent { render() { - const { id, label } = this.props; + const { type, label } = this.props; return ( - - {(provided, snapshot) => ( -
    -
    -
    -
    - {label} -
    -
    - {provided.placeholder} + + {({ dragSourceRef, dragPreviewRef, isDragging }) => ( +
    { + dragSourceRef(ref); + dragPreviewRef(ref); + }} + className={cx( + 'new-draggable-component', + isDragging && 'draggable--dragging', + )} + > +
    + {label}
    )} diff --git a/superset/assets/javascripts/dashboard/v2/components/dnd/DraggableNewDivider.jsx b/superset/assets/javascripts/dashboard/v2/components/dnd/DraggableNewDivider.jsx index 022e768990b4e..e2b3f99814b05 100644 --- a/superset/assets/javascripts/dashboard/v2/components/dnd/DraggableNewDivider.jsx +++ b/superset/assets/javascripts/dashboard/v2/components/dnd/DraggableNewDivider.jsx @@ -1,7 +1,7 @@ import React from 'react'; // import PropTypes from 'prop-types'; -import { DRAGGABLE_NEW_DIVIDER } from '../../util/constants'; +import { DIVIDER_TYPE } from '../../util/componentTypes'; import DraggableNewComponent from './DraggableNewComponent'; const propTypes = { @@ -11,7 +11,7 @@ export default class DraggableNewDivider extends React.PureComponent { render() { return ( ); diff --git a/superset/assets/javascripts/dashboard/v2/components/dnd/DraggableNewHeader.jsx b/superset/assets/javascripts/dashboard/v2/components/dnd/DraggableNewHeader.jsx index 8242c9032574a..6d1c76f4300a2 100644 --- a/superset/assets/javascripts/dashboard/v2/components/dnd/DraggableNewHeader.jsx +++ b/superset/assets/javascripts/dashboard/v2/components/dnd/DraggableNewHeader.jsx @@ -1,7 +1,7 @@ import React from 'react'; // import PropTypes from 'prop-types'; -import { DRAGGABLE_NEW_HEADER } from '../../util/constants'; +import { HEADER_TYPE } from '../../util/componentTypes'; import DraggableNewComponent from './DraggableNewComponent'; const propTypes = { @@ -11,7 +11,7 @@ export default class DraggableNewHeader extends React.Component { render() { return ( ); diff --git a/superset/assets/javascripts/dashboard/v2/components/dnd/DraggableNewRow.jsx b/superset/assets/javascripts/dashboard/v2/components/dnd/DraggableNewRow.jsx index e989f0b10a92e..a4ecf0d8c9c34 100644 --- a/superset/assets/javascripts/dashboard/v2/components/dnd/DraggableNewRow.jsx +++ b/superset/assets/javascripts/dashboard/v2/components/dnd/DraggableNewRow.jsx @@ -1,7 +1,6 @@ import React from 'react'; -// import PropTypes from 'prop-types'; -import { DRAGGABLE_NEW_ROW } from '../../util/constants'; +import { ROW_TYPE } from '../../util/componentTypes'; import DraggableNewComponent from './DraggableNewComponent'; const propTypes = { @@ -11,7 +10,7 @@ export default class DraggableNewRow extends React.PureComponent { render() { return ( ); diff --git a/superset/assets/javascripts/dashboard/v2/components/dnd/DraggableRow.jsx b/superset/assets/javascripts/dashboard/v2/components/dnd/DraggableRow.jsx new file mode 100644 index 0000000000000..8e53c4ffdb1ff --- /dev/null +++ b/superset/assets/javascripts/dashboard/v2/components/dnd/DraggableRow.jsx @@ -0,0 +1,274 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { DragSource, DropTarget } from 'react-dnd'; +import cx from 'classnames'; +import throttle from 'lodash.throttle'; + +import isValidChild from '../../util/isValidChild'; +import { COMPONENT_TYPE_LOOKUP } from '../gridComponents'; + +const HOVER_THROTTLE_MS = 200; + +const propTypes = { + // depth: PropTypes.number.isRequired, + entity: PropTypes.object.isRequired, + parentId: PropTypes.string.isRequired, + index: PropTypes.number.isRequired, + disableDrop: PropTypes.bool, + disableDrag: PropTypes.bool, + + // from HOCs + isDragging: PropTypes.bool.isRequired, + droppableRef: PropTypes.func.isRequired, + dragSourceRef: PropTypes.func.isRequired, + dragPreviewRef: PropTypes.func.isRequired, +}; + +const defaultProps = { + disableDrag: false, + disableDrop: false, +}; + +class DraggableRow extends React.PureComponent { + constructor(props) { + super(props); + this.state = { + dropIndicator: null, + }; + + this.handleBeginDrag = this.handleBeginDrag.bind(this); + this.handleHover = throttle(this.hover.bind(this), HOVER_THROTTLE_MS).bind(this); + this.handleDrop = this.handleDrop.bind(this); + this.renderDropIndicator = this.renderDropIndicator.bind(this); + this.renderComponent = this.renderComponent.bind(this); + } + + handleBeginDrag() { + const { entity, index, parentId, type } = this.props; + return { draggableId: entity.id, index, parentId, type }; + } + + hover(props, monitor, component) { + const { entity } = this.props; + const draggingItem = monitor.getItem(); + + if (!draggingItem || draggingItem.draggableId === entity.id) { + return; + } + + const draggingItemIsValidChild = isValidChild({ + parentType: entity.type, + childType: draggingItem.type, + }); + + if (draggingItemIsValidChild) { // vertical indicator, append to container + this.setState(() => ({ + dropIndicator: { + height: '50%', + minHeight: 16, + top: 0, + right: 24, + width: 3, + }, + })); + } else { // horizontal indicator, top or bottom + const rowBoundingRect = this.ref.getBoundingClientRect(); + const clientOffset = monitor.getClientOffset(); + if (clientOffset) { + const rowMiddleY = rowBoundingRect.top + (rowBoundingRect.bottom - rowBoundingRect.top) / 2; + + let dropIndicatorTop = null; + if (clientOffset.y < rowMiddleY) { // place indicator above row + dropIndicatorTop = 0; + } else if (clientOffset.y >= rowMiddleY) { // place indicator below row + dropIndicatorTop = rowBoundingRect.height; + } + + this.setState(() => ({ + dropIndicator: { + top: dropIndicatorTop, + width: '100%', + height: 3, + }, + })); + } + } + } + + handleDrop(props, monitor, component) { + console.log('handle drop'); + // check if a nested drop target handled the drop + if (monitor.didDrop()) return; + + const { entity, entities, parentId: rowParentId, index: rowIndex } = this.props; + const draggingItem = monitor.getItem(); + + // if dropped self on self, do nothing + if (!draggingItem || draggingItem.draggableId === entity.id) return; + + // append to self, or parent + const acceptChild = isValidChild({ parentType: entity.type, childType: draggingItem.type }); + + const dropResult = { + source: draggingItem.parentId ? { + droppableId: draggingItem.parentId, + index: draggingItem.index, + } : null, + draggableId: draggingItem.draggableId, + }; + + if (acceptChild) { // if it's a valid child, append it to entity.children + dropResult.destination = { + droppableId: entity.id, + index: entity.children.length, + }; + } else { + let nextIndex = rowIndex; + const rowBoundingRect = this.ref.getBoundingClientRect(); + const clientOffset = monitor.getClientOffset(); + + if (clientOffset) { + const rowMiddleY = rowBoundingRect.top + (rowBoundingRect.bottom - rowBoundingRect.top) / 2; + + if (clientOffset.y >= rowMiddleY) { // place indicator below row + nextIndex += 1; + } + } + + if (draggingItem.parentId === rowParentId && draggingItem.index < rowIndex) { + nextIndex = Math.max(0, nextIndex - 1); + } + + dropResult.destination = { + droppableId: rowParentId, + index: nextIndex, + }; + } + // @TODO this should be a redux action + this.props.onDrop(dropResult); + + return dropResult; + } + + renderComponent() { + const { + entity, + droppableRef, + dragSourceRef, + dragPreviewRef, + ...restProps + } = this.props; + + const Component = COMPONENT_TYPE_LOOKUP[entity.type]; + + return ( + + ); + } + + renderDropIndicator() { + const { dropIndicator } = this.state; + const { isDraggingOverShallow } = this.props; + return dropIndicator && isDraggingOverShallow && ( +
    + ); + } + + render() { + const { + droppableRef, + dragSourceRef, + dragPreviewRef, + isDragging, + disableDrag, + // isDraggingOver, + // isDraggingOverShallow + } = this.props; + + return ( +
    { + this.ref = ref; + dragPreviewRef(ref); + droppableRef(ref); + }} + className={cx( + 'draggable', + 'draggable-row', + isDragging && 'draggable--dragging', + disableDrag && 'draggable--disabled', + )} + style={{ + opacity: isDragging && 0.25, + position: 'relative', + backgroundColor: isDragging ? '#ccc' : null, + }} + > + +
    + + {this.renderComponent()} + {this.renderDropIndicator()} +
    + ); + } +} + +DraggableRow.propTypes = propTypes; +DraggableRow.defaultProps = defaultProps; + +const dragConfig = [ + 'DEFAULT', + { + beginDrag(props, monitor, component) { + return component.handleBeginDrag(monitor); + }, + }, + function dragStateToProps(connect, monitor) { + return { + dragSourceRef: connect.dragSource(), // @TODO non-ref if this doesn't work + dragPreviewRef: connect.dragPreview(), + isDragging: monitor.isDragging(), + }; + }, + +]; + +const dropConfig = [ + 'DEFAULT', + { + hover(props, monitor, component) { + if (monitor.isOver({ shallow: true })) { + component.decoratedComponentInstance.handleHover(props, monitor, component); + } + }, + drop(props, monitor, component) { + if (!component) return undefined; + return component.decoratedComponentInstance.handleDrop(props, monitor, component); + }, + }, + function dropStateToProps(connect, monitor) { + return { + droppableRef: connect.dropTarget(), + isDraggingOverShallow: monitor.isOver({ shallow: true }), + draggableId: (monitor.getItem() || {}).draggableId || null, + }; + }, +]; + +// note that the composition order here determines using +// component.method() vs decoratedComponentInstance.method() in the above config +export default DropTarget(...dropConfig)( + DragSource(...dragConfig)(DraggableRow), +); diff --git a/superset/assets/javascripts/dashboard/v2/components/dnd/dnd.css b/superset/assets/javascripts/dashboard/v2/components/dnd/dnd.css index 8a158fd1158cc..6b0c2b6a1d221 100644 --- a/superset/assets/javascripts/dashboard/v2/components/dnd/dnd.css +++ b/superset/assets/javascripts/dashboard/v2/components/dnd/dnd.css @@ -12,6 +12,11 @@ box-shadow: inset 0 0 0 1px #CFD8DC; } +/*.draggable-row:hover { + box-shadow: 0 0 5px 2px rgba(0, 0, 0, 0.1); + z-index: 10; +}*/ + .draggable-row-handle { opacity: 0; position: absolute; @@ -26,6 +31,11 @@ justify-content: space-around; } + /* offset nested row handles */ + .draggable-row .draggable-row .draggable-row-handle { + left: 10px; + } + .draggable-row-handle:after { content: ""; background: #44C0FF; @@ -65,12 +75,3 @@ .draggable-column-item-handle:hover { opacity: 1; } - -.dashboard-builder--dragging .draggable-row:hover .draggable-row-handle, -.dashboard-builder--dragging .draggable-row-handle:hover, -.dashboard-builder--dragging .draggable-row-item:hover .draggable-row-item-handle, -.dashboard-builder--dragging .draggable-row-item-handle:hover, -.dashboard-builder--dragging .draggable-column-item:hover .draggable-column-item-handle, -.dashboard-builder--dragging .draggable-column-item-handle:hover { - opacity: 0; -} diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Column.jsx b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Column.jsx index 9f5aae2ca98f8..7142733bf8c8e 100644 --- a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Column.jsx +++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Column.jsx @@ -4,16 +4,17 @@ import PropTypes from 'prop-types'; import ResizableContainer from '../resizable/ResizableContainer'; import { COMPONENT_TYPE_LOOKUP } from './'; -import { componentIsResizable } from '../../util/gridUtils'; +import componentIsResizable from '../../util/componentIsResizable'; import { - SPACER_TYPE, GRID_GUTTER_SIZE, GRID_ROW_HEIGHT_UNIT, GRID_MIN_ROW_UNITS, GRID_MAX_ROW_UNITS, } from '../../util/constants'; +import { SPACER_TYPE } from '../../util/componentTypes'; + const propTypes = { entity: PropTypes.object, entities: PropTypes.object, diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Row.jsx b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Row.jsx index cc38e44ce7c3c..76c5f1ce6b1e2 100644 --- a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Row.jsx +++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Row.jsx @@ -1,26 +1,10 @@ import React from 'react'; import PropTypes from 'prop-types'; import cx from 'classnames'; -import { Draggable, Droppable } from 'react-beautiful-dnd'; -import ResizableContainer from '../resizable/ResizableContainer'; -import isValidChild from '../../util/isValidChild'; -import { componentIsResizable } from '../../util/gridUtils'; - -import { - COLUMN_TYPE, - SPACER_TYPE, - INVISIBLE_ROW_TYPE, - GRID_GUTTER_SIZE, - GRID_ROW_HEIGHT_UNIT, - GRID_COLUMN_COUNT, - GRID_MIN_COLUMN_COUNT, - GRID_MIN_ROW_UNITS, - GRID_MAX_ROW_UNITS, - DROPPABLE_DIRECTION_HORIZONTAL, -} from '../../util/constants'; - -import { COMPONENT_TYPE_LOOKUP } from './'; +import DraggableColumn from '../dnd/DraggableColumn'; +import { GRID_GUTTER_SIZE } from '../../util/constants'; +import { INVISIBLE_ROW_TYPE } from '../../util/componentTypes'; const propTypes = { entity: PropTypes.object, // @TODO shape @@ -49,130 +33,54 @@ class Row extends React.PureComponent { const { entities, entity: rowEntity, - columnWidth, - onResizeStart, - onResize, - onResizeStop, - draggingEntity, disableDrop, disableDrag, + onDrop, + gridProps, } = this.props; - let totalColumns = 0; - let maxItemHeight = 0; + let occupiedColumnCount = 0; + let currentRowHeight = 0; const rowItems = []; // this adds a gutter between each child in the row. (rowEntity.children || []).forEach((id, index) => { const entity = entities[id]; - totalColumns += (entity.meta || {}).width || 0; + occupiedColumnCount += (entity.meta || {}).width || 0; rowItems.push(entity); if (index < rowEntity.children.length - 1) rowItems.push(`gutter-${index}`); - if ((entity.meta || {}).height) maxItemHeight = Math.max(maxItemHeight, entity.meta.height); + if ((entity.meta || {}).height) { + currentRowHeight = Math.max(currentRowHeight, entity.meta.height); + } }); - return ( - - {(droppableProvided, droppableSnapshot) => ( -
    - {rowItems.map((entity, index) => { - const id = entity.id || entity; - const Component = COMPONENT_TYPE_LOOKUP[entity.type]; - const isSpacer = entity.type === SPACER_TYPE; - const isResizable = componentIsResizable(entity); - - let RowItem = Component ? ( - - ) :
    ; + const modifiedGridProps = { ...gridProps, occupiedColumnCount, currentRowHeight }; - if (isResizable && Component) { - RowItem = ( - - {RowItem} - - ); - } - - if (Component) { - return ( - - {draggableProvided => ( -
    -
    -
    - {RowItem} -
    - {draggableProvided.placeholder} -
    - )} - - ); - } - - return RowItem; - })} - {droppableProvided.placeholder} -
    + return ( +
    + > + {rowItems.map((entity, index) => ( + !entity.id ? ( +
    + ) : ( + + ) + ))} +
    ); } } diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Tabs.jsx b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Tabs.jsx index df14d08f64878..52f8bed54bf56 100644 --- a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Tabs.jsx +++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Tabs.jsx @@ -2,14 +2,10 @@ import React from 'react'; import PropTypes from 'prop-types'; import cx from 'classnames'; import { Tabs as BootstrapTabs, Tab } from 'react-bootstrap'; -import { Draggable, Droppable } from 'react-beautiful-dnd'; -import { DROPPABLE_DIRECTION_VERTICAL } from '../../util/constants'; -import isValidChild from '../../util/isValidChild'; -import { COMPONENT_TYPE_LOOKUP } from './'; +import DraggableRow from '../dnd/DraggableRow'; const propTypes = { - id: PropTypes.string.isRequired, tabs: PropTypes.arrayOf( PropTypes.shape({ label: PropTypes.string, @@ -50,14 +46,21 @@ class Tabs extends React.Component { } render() { - const { tabs, id: tabId, entity: tabEntity, ...restProps } = this.props; - const { entities, draggingEntity, disableDrop, disableDrag } = restProps; - const { tabIndex } = this.state; + const { + tabs, + entity: tabEntity, + entities, + gridProps, + disableDrop, + disableDrag, + onDrop, + } = this.props; + return (
    @@ -66,54 +69,19 @@ class Tabs extends React.Component { ))} - - {(droppableProvided, droppableSnapshot) => ( -
    - {(tabEntity.children || []).map((id, index) => { - const entity = entities[id] || {}; - const Component = COMPONENT_TYPE_LOOKUP[entity.type]; - return Component && ( - - {draggableProvided => ( -
    -
    -
    - -
    - {draggableProvided.placeholder} -
    - )} - - ); - })} - {droppableProvided.placeholder} -
    - )} - + {(tabEntity.children || []).map((id, index) => ( + + ))}
    ); } diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/components.css b/superset/assets/javascripts/dashboard/v2/components/gridComponents/components.css index a7197f64c16ea..9e3911ea468c8 100644 --- a/superset/assets/javascripts/dashboard/v2/components/gridComponents/components.css +++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/components.css @@ -8,6 +8,11 @@ padding: 16px 0; color: #263238; } + .draggable-row .draggable-row .dashboard-component-header, + .draggable-row .draggable-row .dashboard-component-divider { + padding-left: 16px; + padding-right: 16px; + } .grid-row-container .dashboard-component-header { padding-left: 16px; @@ -51,7 +56,7 @@ } .dashboard-component-tabs .nav-tabs { - border-bottom: 1px solid #CFD8DC; + border-bottom: none; } /* by moving padding from to
  • we can restrict the selected tab indicator to text width */ diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/grid.css b/superset/assets/javascripts/dashboard/v2/components/gridComponents/grid.css index e0bd7b4b0f9f7..5b096303ddb3b 100644 --- a/superset/assets/javascripts/dashboard/v2/components/gridComponents/grid.css +++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/grid.css @@ -17,6 +17,7 @@ flex-wrap: wrap; align-items: flex-start; min-height: 16px; + width: 100%; height: fit-content; background-color: transparent; } @@ -29,6 +30,7 @@ width: 100%; height: 100%; background-color: transparent; + box-shadow: inset 0 0 0 1px #CFD8DC; } /* Editing guides */ diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/index.js b/superset/assets/javascripts/dashboard/v2/components/gridComponents/index.js index 144eae366da0e..e6a58db9690ff 100644 --- a/superset/assets/javascripts/dashboard/v2/components/gridComponents/index.js +++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/index.js @@ -9,7 +9,7 @@ import { ROW_TYPE, SPACER_TYPE, TABS_TYPE, -} from '../../util/constants'; +} from '../../util/componentTypes'; import Chart from './Chart'; import Column from './Column'; diff --git a/superset/assets/javascripts/dashboard/v2/components/resizable/resizable.css b/superset/assets/javascripts/dashboard/v2/components/resizable/resizable.css index a16f434d5c351..3a31107f25238 100644 --- a/superset/assets/javascripts/dashboard/v2/components/resizable/resizable.css +++ b/superset/assets/javascripts/dashboard/v2/components/resizable/resizable.css @@ -14,12 +14,6 @@ box-shadow: inset 0 0 0 2px #44C0FF; } -.grid-container--resizing .grid-spacer, -.dashboard-builder--dragging .grid-spacer, -.grid-spacer:hover { - box-shadow: inset 0 0 0 1px #CFD8DC; -} - .resize-handle { opacity: 0; } diff --git a/superset/assets/javascripts/dashboard/v2/fixtures/testLayout.js b/superset/assets/javascripts/dashboard/v2/fixtures/testLayout.js index 026c1fff2dff7..2d46e2ba13287 100644 --- a/superset/assets/javascripts/dashboard/v2/fixtures/testLayout.js +++ b/superset/assets/javascripts/dashboard/v2/fixtures/testLayout.js @@ -7,183 +7,123 @@ import { TABS_TYPE, CHART_TYPE, DIVIDER_TYPE, - DROPPABLE_ID_DASHBOARD_ROOT, -} from '../util/constants'; +} from '../util/componentTypes'; -export default { - entities: { - [DROPPABLE_ID_DASHBOARD_ROOT]: { - id: DROPPABLE_ID_DASHBOARD_ROOT, - children: [ - 'header0', - 'row1', - 'divider0', - 'row3', - 'tabs0', - // 'row5', - ], - }, - // row0: { - // id: 'row0', - // type: INVISIBLE_ROW_TYPE, - // children: ['header0'], - // }, - row1: { - id: 'row1', - type: INVISIBLE_ROW_TYPE, - children: [ - 'charta', - 'chartb', - 'chartc', - ], - }, - // row2: { - // id: 'row2', - // type: INVISIBLE_ROW_TYPE, - // children: [ - // 'divider0', - // ], - // }, - row3: { - id: 'row3', - type: ROW_TYPE, - children: [ - 'header1', - ], - }, - row4: { - id: 'row4', - type: ROW_TYPE, - children: [ - 'tabs0', - ], - }, - row5: { - id: 'row5', - type: ROW_TYPE, - children: [ - 'column0', - 'spacer0', - 'chart3', - 'spacer1', - 'chart4', - ], - }, - header0: { - id: 'header0', - type: HEADER_TYPE, - meta: { - text: 'Section header', - }, - }, - header1: { - id: 'header1', - type: HEADER_TYPE, - meta: { - text: 'Header in row', - }, - }, - divider0: { - id: 'divider0', - type: DIVIDER_TYPE, - children: [], - }, +import { DASHBOARD_ROOT_ID } from '../util/constants'; - chart0: { - id: 'chart0', - type: CHART_TYPE, - meta: { - height: 6, - }, - }, - chart1: { - id: 'chart1', - type: CHART_TYPE, - meta: { - height: 6, - }, - }, - chart2: { - id: 'chart2', - type: CHART_TYPE, - meta: { - height: 6, - }, - }, - chart3: { - id: 'chart3', - type: CHART_TYPE, - meta: { - width: 3, - height: 20, - }, +export default { + [DASHBOARD_ROOT_ID]: { + 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: [ + 'row2', + ], + meta: { }, - chart4: { - id: 'chart4', - type: CHART_TYPE, - meta: { - width: 3, - height: 20, - }, + }, + header0: { + id: 'header0', + type: HEADER_TYPE, + meta: { + text: 'Header 1', }, - charta: { - id: 'charta', - type: CHART_TYPE, - meta: { - width: 3, - height: 20, - }, + }, + header1: { + id: 'header1', + type: HEADER_TYPE, + meta: { + text: 'Header 2', }, - chartb: { - id: 'chartb', - type: CHART_TYPE, - meta: { - width: 6, - height: 20, - }, + }, + divider0: { + id: 'divider0', + type: DIVIDER_TYPE, + }, + divider1: { + id: 'divider1', + type: DIVIDER_TYPE, + }, + charta: { + id: 'charta', + type: CHART_TYPE, + meta: { + width: 3, + height: 10, }, - chartc: { - id: 'chartc', - type: CHART_TYPE, - meta: { - width: 3, - height: 20, - }, + }, + chartb: { + id: 'chartb', + type: CHART_TYPE, + meta: { + width: 3, + height: 10, }, - column0: { - id: 'column0', - type: COLUMN_TYPE, - children: [ - 'chart0', - 'chart1', - 'chart2', - ], - meta: { - width: 3, - }, + }, + chartc: { + id: 'chartc', + type: CHART_TYPE, + meta: { + width: 3, + height: 10, }, - spacer0: { - id: 'spacer0', - type: SPACER_TYPE, - meta: { - width: 1, - }, + }, + chartd: { + id: 'chartd', + type: CHART_TYPE, + meta: { + width: 6, + height: 10, }, - spacer1: { - id: 'spacer1', - type: SPACER_TYPE, - meta: { - width: 1, - }, + }, + charte: { + id: 'charte', + type: CHART_TYPE, + meta: { + width: 6, + height: 10, }, - tabs0: { - id: 'tabs0', - type: TABS_TYPE, - children: [ - 'row5', - ], - meta: { - }, + }, + spacer0: { + id: 'spacer0', + type: SPACER_TYPE, + meta: { + width: 1, }, }, }; diff --git a/superset/assets/javascripts/dashboard/v2/util/gridUtils.js b/superset/assets/javascripts/dashboard/v2/util/componentIsResizable.js similarity index 69% rename from superset/assets/javascripts/dashboard/v2/util/gridUtils.js rename to superset/assets/javascripts/dashboard/v2/util/componentIsResizable.js index 2c483b4445a74..ab701a73e6e02 100644 --- a/superset/assets/javascripts/dashboard/v2/util/gridUtils.js +++ b/superset/assets/javascripts/dashboard/v2/util/componentIsResizable.js @@ -3,9 +3,9 @@ import { COLUMN_TYPE, CHART_TYPE, MARKDOWN_TYPE, -} from './constants'; +} from './componentTypes'; -export function componentIsResizable(entity) { +export default function componentIsResizable(entity) { return [ SPACER_TYPE, COLUMN_TYPE, diff --git a/superset/assets/javascripts/dashboard/v2/util/componentTypes.js b/superset/assets/javascripts/dashboard/v2/util/componentTypes.js new file mode 100644 index 0000000000000..ea9288abbb49f --- /dev/null +++ b/superset/assets/javascripts/dashboard/v2/util/componentTypes.js @@ -0,0 +1,23 @@ +export const CHART_TYPE = 'DASHBOARD_CHART_TYPE'; +export const COLUMN_TYPE = 'DASHBOARD_COLUMN_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 INVISIBLE_ROW_TYPE = 'DASHBOARD_INVISIBLE_ROW_TYPE'; +export const MARKDOWN_TYPE = 'DASHBOARD_MARKDOWN_TYPE'; +export const ROW_TYPE = 'DASHBOARD_ROW_TYPE'; +export const SPACER_TYPE = 'DASHBOARD_SPACER_TYPE'; +export const TABS_TYPE = 'DASHBOARD_TABS_TYPE'; + +export default { + CHART_TYPE, + COLUMN_TYPE, + DIVIDER_TYPE, + GRID_ROOT_TYPE, + HEADER_TYPE, + INVISIBLE_ROW_TYPE, + MARKDOWN_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 4f185f01fce7b..ddc3030ab639b 100644 --- a/superset/assets/javascripts/dashboard/v2/util/constants.js +++ b/superset/assets/javascripts/dashboard/v2/util/constants.js @@ -1,32 +1,5 @@ -// Component types -export const CHART_TYPE = 'DASHBOARD_CHART_TYPE'; -export const COLUMN_TYPE = 'DASHBOARD_COLUMN_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 INVISIBLE_ROW_TYPE = 'DASHBOARD_INVISIBLE_ROW_TYPE'; -export const MARKDOWN_TYPE = 'DASHBOARD_MARKDOWN_TYPE'; -export const ROW_TYPE = 'DASHBOARD_ROW_TYPE'; -export const SPACER_TYPE = 'DASHBOARD_SPACER_TYPE'; -export const TABS_TYPE = 'DASHBOARD_TABS_TYPE'; - -// Drag and drop constants -export const DROPPABLE_DIRECTION_VERTICAL = 'vertical'; -export const DROPPABLE_DIRECTION_HORIZONTAL = 'horizontal'; - -// Ids for new components -export const DROPPABLE_NEW_COMPONENT = 'DROPPABLE_NEW_COMPONENT'; -export const DROPPABLE_ID_DASHBOARD_ROOT = 'DROPPABLE_ID_DASHBOARD_ROOT'; - -export const DRAGGABLE_NEW_CHART = 'DRAGGABLE_NEW_CHART'; -export const DRAGGABLE_NEW_DIVIDER = 'DRAGGABLE_NEW_DIVIDER'; -export const DRAGGABLE_NEW_HEADER = 'DRAGGABLE_NEW_HEADER'; -export const DRAGGABLE_NEW_ROW = 'DRAGGABLE_NEW_ROW'; -export const DRAGGABLE_NEW_SPACER = 'DRAGGABLE_NEW_SPACER'; - -// Draggable types -export const DRAGGABLE_TYPE_ROW = 'DRAGGABLE_TYPE_ROW'; -export const DRAGGABLE_TYPE_ROW_ITEM = 'DRAGGABLE_TYPE_ROW_ITEM'; +// Ids +export const DASHBOARD_ROOT_ID = 'DASHBOARD_ROOT_ID'; // grid constants export const GRID_BASE_UNIT = 8; diff --git a/superset/assets/javascripts/dashboard/v2/util/isValidChild.js b/superset/assets/javascripts/dashboard/v2/util/isValidChild.js index b3449c6ce205c..fddc9cab42d43 100644 --- a/superset/assets/javascripts/dashboard/v2/util/isValidChild.js +++ b/superset/assets/javascripts/dashboard/v2/util/isValidChild.js @@ -9,13 +9,7 @@ import { ROW_TYPE, SPACER_TYPE, TABS_TYPE, - - DRAGGABLE_NEW_CHART, - DRAGGABLE_NEW_DIVIDER, - DRAGGABLE_NEW_HEADER, - DRAGGABLE_NEW_ROW, - DRAGGABLE_NEW_SPACER, -} from './constants'; +} from './componentTypes'; const typeToValidChildType = { // root @@ -25,10 +19,6 @@ const typeToValidChildType = { [TABS_TYPE]: true, [DIVIDER_TYPE]: true, [HEADER_TYPE]: true, - [DRAGGABLE_NEW_CHART]: true, - [DRAGGABLE_NEW_DIVIDER]: true, - [DRAGGABLE_NEW_HEADER]: true, - [DRAGGABLE_NEW_ROW]: true, }, [ROW_TYPE]: { @@ -37,10 +27,6 @@ const typeToValidChildType = { [COLUMN_TYPE]: true, [SPACER_TYPE]: true, [HEADER_TYPE]: true, - - [DRAGGABLE_NEW_SPACER]: true, - [DRAGGABLE_NEW_CHART]: true, - [DRAGGABLE_NEW_HEADER]: true, }, [INVISIBLE_ROW_TYPE]: { @@ -48,9 +34,6 @@ const typeToValidChildType = { [MARKDOWN_TYPE]: true, [COLUMN_TYPE]: true, [SPACER_TYPE]: true, - [DRAGGABLE_NEW_SPACER]: true, - [DRAGGABLE_NEW_CHART]: true, - [DRAGGABLE_NEW_HEADER]: true, }, [TABS_TYPE]: { @@ -58,11 +41,6 @@ const typeToValidChildType = { [INVISIBLE_ROW_TYPE]: true, [DIVIDER_TYPE]: true, [HEADER_TYPE]: true, - // [CHART_TYPE]: true, - // [MARKDOWN_TYPE]: true, - // [SPACER_TYPE]: true, - // [COLUMN_TYPE]: true, - // [DRAGGABLE_NEW_SPACER]: true, }, [COLUMN_TYPE]: { @@ -71,10 +49,6 @@ const typeToValidChildType = { [HEADER_TYPE]: true, [SPACER_TYPE]: true, [DIVIDER_TYPE]: true, - [DRAGGABLE_NEW_DIVIDER]: true, - [DRAGGABLE_NEW_SPACER]: true, - [DRAGGABLE_NEW_CHART]: true, - [DRAGGABLE_NEW_HEADER]: true, }, // these have no valid children @@ -91,6 +65,5 @@ export default function isValidChild({ parentType, childType }) { typeToValidChildType[parentType][childType], ); - console.log(`${parentType} > ${childType} -> ${isValid}`); return isValid; } diff --git a/superset/assets/javascripts/dashboard/v2/util/newEntitiesFromDrop.js b/superset/assets/javascripts/dashboard/v2/util/newEntitiesFromDrop.js new file mode 100644 index 0000000000000..04e5d51947896 --- /dev/null +++ b/superset/assets/javascripts/dashboard/v2/util/newEntitiesFromDrop.js @@ -0,0 +1,84 @@ +import isValidChild from './isValidChild'; + +import { + CHART_TYPE, + COLUMN_TYPE, + DIVIDER_TYPE, + HEADER_TYPE, + INVISIBLE_ROW_TYPE, + MARKDOWN_TYPE, + ROW_TYPE, + SPACER_TYPE, + TABS_TYPE, +} from './componentTypes'; + +const typeToDefaultMetaData = { + [CHART_TYPE]: { width: 3, height: 10 }, + [COLUMN_TYPE]: { width: 3 }, + [DIVIDER_TYPE]: null, + [HEADER_TYPE]: { text: 'New header' }, + [INVISIBLE_ROW_TYPE]: null, + [MARKDOWN_TYPE]: { width: 3, height: 10 }, + [ROW_TYPE]: null, + [SPACER_TYPE]: { width: 1 }, + [TABS_TYPE]: null, +}; + +// @TODO this should be replaced by a more robust algorithm +function uuid(type) { + return `${type}-${Math.random().toString(16)}`; +} + +function entityFactory(type) { + return { + dashboardVersion: 'v0', + type, + id: uuid(type), + children: [], + meta: { + ...typeToDefaultMetaData[type], + }, + }; +} + +export default function newEntitiesFromDrop({ dropResult, entitiesMap }) { + const { draggableId, destination } = dropResult; + + const dragType = draggableId; // @TODO will need to fix this + const dropEntity = entitiesMap[destination.droppableId]; + + if (!dropEntity) { + console.warn('Drop target entity', destination.droppableId, 'not found'); + return null; + } + + const dropType = dropEntity.type; + let newDropChild = entityFactory(dragType); + const isValidDrop = isValidChild({ parentType: dropType, childType: dragType }); + + const newEntities = { + [newDropChild.id]: newDropChild, + }; + + if (!isValidDrop) { + console.log('wrapping', dragType, 'in invisible row'); + if (!isValidChild({ parentType: dropType, childType: INVISIBLE_ROW_TYPE })) { + console.warn('wrapping in an invalid component'); + } + + const rowWrapper = entityFactory(INVISIBLE_ROW_TYPE); + rowWrapper.children = [newDropChild.id]; + newEntities[rowWrapper.id] = rowWrapper; + newDropChild = rowWrapper; + } + + const nextDropChildren = [...dropEntity.children]; + nextDropChildren.splice(destination.index, 0, newDropChild.id); + + newEntities[destination.droppableId] = { + ...dropEntity, + children: nextDropChildren, + }; + + return newEntities; +} diff --git a/superset/assets/javascripts/dashboard/v2/util/propShapes.jsx b/superset/assets/javascripts/dashboard/v2/util/propShapes.jsx new file mode 100644 index 0000000000000..3c11d2a8b3177 --- /dev/null +++ b/superset/assets/javascripts/dashboard/v2/util/propShapes.jsx @@ -0,0 +1,12 @@ +import PropTypes from 'prop-types'; + +export const entityShape = PropTypes.shape({ + id: PropTypes.string.isRequired, + childIds: PropTypes.arrayOf(PropTypes.string), + parentId: PropTypes.string.isRequired, + type: PropTypes.string.isRequired, // @TODO enumerate + meta: PropTypes.shape({ + width: PropTypes.number, + height: PropTypes.number, + }), +}); diff --git a/superset/assets/package.json b/superset/assets/package.json index 0028b656c8ac0..7d63bb0b25dbb 100644 --- a/superset/assets/package.json +++ b/superset/assets/package.json @@ -78,11 +78,12 @@ "react-addons-css-transition-group": "^15.6.0", "react-addons-shallow-compare": "^15.4.2", "react-alert": "^2.3.0", - "react-beautiful-dnd": "^4.0.0", "react-bootstrap": "^0.31.5", "react-bootstrap-table": "^4.0.2", "react-color": "^2.13.8", "react-datetime": "2.9.0", + "react-dnd": "^2.5.4", + "react-dnd-html5-backend": "^2.5.4", "react-dom": "^15.6.2", "react-gravatar": "^2.6.1", "react-grid-layout": "^0.16.0", From 9f1576291eb4da46549c0b99e003548609d5f554 Mon Sep 17 00:00:00 2001 From: Chris Williams Date: Mon, 12 Feb 2018 11:51:40 -0800 Subject: [PATCH 10/25] [react-dnd] refactor to use composable structure --- .../v2/components/BuilderComponentPane.jsx | 26 +- .../v2/components/DashboardComponent.jsx | 124 +++++++ .../dashboard/v2/components/DashboardGrid.jsx | 100 ++---- .../v2/components/dnd/DragDroppable.jsx | 273 ++++++++++++++ .../v2/components/dnd/DragHandle.jsx | 37 ++ .../dashboard/v2/components/dnd/Draggable.jsx | 62 ---- .../v2/components/dnd/DraggableColumn.jsx | 332 ------------------ .../components/dnd/DraggableNewComponent.jsx | 37 -- .../v2/components/dnd/DraggableRow.jsx | 274 --------------- .../dashboard/v2/components/dnd/dnd.css | 92 +++-- .../v2/components/dnd/dragDroppableConfig.js | 55 +++ .../v2/components/gridComponents/Column.jsx | 121 +++---- .../v2/components/gridComponents/Header.jsx | 13 +- .../v2/components/gridComponents/Row.jsx | 95 ++--- .../v2/components/gridComponents/Tabs.jsx | 64 ++-- .../components/gridComponents/components.css | 69 +++- .../v2/components/gridComponents/grid.css | 35 -- .../v2/components/gridComponents/index.js | 2 +- .../new/DraggableNewComponent.jsx | 31 ++ .../new/NewChart.jsx} | 2 +- .../gridComponents/new/NewColumn.jsx | 21 ++ .../new/NewDivider.jsx} | 2 +- .../new/NewHeader.jsx} | 2 +- .../new/NewRow.jsx} | 2 +- .../gridComponents/new/NewSpacer.jsx | 21 ++ .../components/gridComponents/new/NewTabs.jsx | 21 ++ .../resizable/DimensionProvider.jsx | 80 +++++ .../resizable/ResizableContainer.jsx | 2 +- .../dashboard/v2/fixtures/testLayout.js | 20 +- .../dashboard/v2/util/isValidChild.js | 2 +- .../dashboard/v2/util/propShapes.jsx | 21 +- superset/assets/stylesheets/dashboard-v2.css | 21 -- 32 files changed, 1023 insertions(+), 1036 deletions(-) create mode 100644 superset/assets/javascripts/dashboard/v2/components/DashboardComponent.jsx create mode 100644 superset/assets/javascripts/dashboard/v2/components/dnd/DragDroppable.jsx create mode 100644 superset/assets/javascripts/dashboard/v2/components/dnd/DragHandle.jsx delete mode 100644 superset/assets/javascripts/dashboard/v2/components/dnd/Draggable.jsx delete mode 100644 superset/assets/javascripts/dashboard/v2/components/dnd/DraggableColumn.jsx delete mode 100644 superset/assets/javascripts/dashboard/v2/components/dnd/DraggableNewComponent.jsx delete mode 100644 superset/assets/javascripts/dashboard/v2/components/dnd/DraggableRow.jsx create mode 100644 superset/assets/javascripts/dashboard/v2/components/dnd/dragDroppableConfig.js create mode 100644 superset/assets/javascripts/dashboard/v2/components/gridComponents/new/DraggableNewComponent.jsx rename superset/assets/javascripts/dashboard/v2/components/{dnd/DraggableNewChart.jsx => gridComponents/new/NewChart.jsx} (86%) create mode 100644 superset/assets/javascripts/dashboard/v2/components/gridComponents/new/NewColumn.jsx rename superset/assets/javascripts/dashboard/v2/components/{dnd/DraggableNewDivider.jsx => gridComponents/new/NewDivider.jsx} (86%) rename superset/assets/javascripts/dashboard/v2/components/{dnd/DraggableNewHeader.jsx => gridComponents/new/NewHeader.jsx} (86%) rename superset/assets/javascripts/dashboard/v2/components/{dnd/DraggableNewRow.jsx => gridComponents/new/NewRow.jsx} (85%) create mode 100644 superset/assets/javascripts/dashboard/v2/components/gridComponents/new/NewSpacer.jsx create mode 100644 superset/assets/javascripts/dashboard/v2/components/gridComponents/new/NewTabs.jsx create mode 100644 superset/assets/javascripts/dashboard/v2/components/resizable/DimensionProvider.jsx diff --git a/superset/assets/javascripts/dashboard/v2/components/BuilderComponentPane.jsx b/superset/assets/javascripts/dashboard/v2/components/BuilderComponentPane.jsx index 6f90a38a3c7fe..86f3788bae6ae 100644 --- a/superset/assets/javascripts/dashboard/v2/components/BuilderComponentPane.jsx +++ b/superset/assets/javascripts/dashboard/v2/components/BuilderComponentPane.jsx @@ -1,26 +1,34 @@ import React from 'react'; import PropTypes from 'prop-types'; -import DraggableNewChart from './dnd/DraggableNewChart'; -import DraggableNewDivider from './dnd/DraggableNewDivider'; -import DraggableNewHeader from './dnd/DraggableNewHeader'; -import DraggableNewRow from './dnd/DraggableNewRow'; +import NewChart from './gridComponents/new/NewChart'; +import NewColumn from './gridComponents/new/NewColumn'; +import NewDivider from './gridComponents/new/NewDivider'; +import NewHeader from './gridComponents/new/NewHeader'; +import NewRow from './gridComponents/new/NewRow'; +import NewSpacer from './gridComponents/new/NewSpacer'; +import NewTabs from './gridComponents/new/NewTabs'; const propTypes = { editMode: PropTypes.bool, }; -class BuilderComponentPane extends React.Component { +class BuilderComponentPane extends React.PureComponent { render() { return (
    Insert components
    - - - - + + + + + + + + +
    ); } diff --git a/superset/assets/javascripts/dashboard/v2/components/DashboardComponent.jsx b/superset/assets/javascripts/dashboard/v2/components/DashboardComponent.jsx new file mode 100644 index 0000000000000..2f9936ed0b1e6 --- /dev/null +++ b/superset/assets/javascripts/dashboard/v2/components/DashboardComponent.jsx @@ -0,0 +1,124 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +import DimensionProvider from './resizable/DimensionProvider'; +import DragDroppable from './dnd/DragDroppable'; +import ComponentLookup from './gridComponents'; +import { componentShape } from '../util/propShapes'; + +const propTypes = { + component: componentShape.isRequired, + components: PropTypes.object.isRequired, + depth: PropTypes.number.isRequired, + index: PropTypes.number.isRequired, + parentId: PropTypes.string.isRequired, + + // grid related + availableColumnCount: PropTypes.number.isRequired, + columnWidth: PropTypes.number.isRequired, + rowHeight: PropTypes.number, + onResizeStart: PropTypes.func.isRequired, + onResize: PropTypes.func.isRequired, + onResizeStop: PropTypes.func.isRequired, +}; + +const defaultProps = { + rowHeight: null, +}; + +class DashboardComponent extends React.PureComponent { + constructor(props) { + super(props); + this.state = {}; + this.handleHoverDragDroppable = this.handleHoverDragDroppable.bind(this); + this.handleClick = this.handleClick.bind(this); + } + + componentDidMount() { + document.addEventListener('click', this.handleClick, true); + } + + componentWillUnmount() { + document.removeEventListener('click', this.handleClick, true); + } + + handleClick(event) { + if (!this.dragDroppable) return; + debugger; + if (!this.dragDroppable.contains(event.target)) { + console.log('outside click', this.props.component.type); + } else { + console.log('inside click', event.target === this.dragDroppable, this.props.component.type); + + } + } + + handleHoverDragDroppable(...args) { + if (this.hoverDragDroppable) this.hoverDragDroppable(...args); + } + + render() { + const { + component, + components, + depth, + index, + parentId, + availableColumnCount, + columnWidth, + rowHeight, + onResizeStart, + onResize, + onResizeStop, + onDrop, + bubbleUpHover, + } = this.props; + + const componentType = component.type; + const Component = ComponentLookup[componentType]; + + return ( + { this.dragDroppable = ref; }} + hoverRef={(hover) => { this.hoverDragDroppable = hover; }} + component={component} + components={components} + depth={depth} // @TODO might have to change this to direction: vertical/horizontal + index={index} + parentId={parentId} + onDrop={onDrop} + bubbleUpHover={bubbleUpHover} + > + + + + + ); + } +} + +DashboardComponent.propTypes = propTypes; +DashboardComponent.defaultProps = defaultProps; + +export default DashboardComponent; diff --git a/superset/assets/javascripts/dashboard/v2/components/DashboardGrid.jsx b/superset/assets/javascripts/dashboard/v2/components/DashboardGrid.jsx index 7959226bb7d8f..1af8b9aae3e8e 100644 --- a/superset/assets/javascripts/dashboard/v2/components/DashboardGrid.jsx +++ b/superset/assets/javascripts/dashboard/v2/components/DashboardGrid.jsx @@ -2,7 +2,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import ParentSize from '@vx/responsive/build/components/ParentSize'; import cx from 'classnames'; -import DraggableRow from './dnd/DraggableRow'; +import DashboardComponent from './DashboardComponent'; import { DASHBOARD_ROOT_ID, @@ -26,12 +26,11 @@ class DashboardGrid extends React.PureComponent { constructor(props) { super(props); this.state = { - showGrid: false, + isResizing: false, rowGuideTop: null, - disableDrop: false, - disableDrag: false, - selectedEntityId: null, - dropIndicatorTop: null, + // disableDrop: false, + // disableDrag: false, + // selectedComponentId: null, }; this.handleToggleSelectEntityId = this.handleToggleSelectEntityId.bind(this); @@ -49,112 +48,87 @@ class DashboardGrid extends React.PureComponent { } handleResizeStart({ ref, direction }) { - console.log('resize start'); let rowGuideTop = null; if (direction === 'bottom' || direction === 'bottomRight') { rowGuideTop = this.getRowGuidePosition(ref); } this.setState(() => ({ - showGrid: true, + isResizing: true, rowGuideTop, - disableDrag: true, - disableDrop: true, })); } handleResize({ ref, direction }) { - console.log('resize'); if (direction === 'bottom' || direction === 'bottomRight') { this.setState(() => ({ rowGuideTop: this.getRowGuidePosition(ref) })); } } handleResizeStop({ id, widthMultiple, heightMultiple }) { - console.log('resize stop'); - - const { layout: entities, updateEntity } = this.props; - const entity = entities[id]; - if (entity && (entity.meta.width !== widthMultiple || entity.meta.height !== heightMultiple)) { + const { layout: components, updateEntity } = this.props; + const component = components[id]; + if ( + component && + (component.meta.width !== widthMultiple || component.meta.height !== heightMultiple) + ) { updateEntity({ - ...entity, + ...component, meta: { - ...entity.meta, - width: widthMultiple || entity.meta.width, - height: heightMultiple || entity.meta.height, + ...component.meta, + width: widthMultiple || component.meta.width, + height: heightMultiple || component.meta.height, }, }); } this.setState(() => ({ - showGrid: false, + isResizing: false, rowGuideTop: null, - disableDrag: false, - disableDrop: false, })); } handleToggleSelectEntityId(id) { - // only enable selection if no drag is occurring - if (!this.props.draggingEntity) { - this.setState(({ selectedEntityId }) => { - const nextSelectedEntityId = id === selectedEntityId ? null : id; - const disableDragDrop = Boolean(nextSelectedEntityId); - return { - selectedEntityId: nextSelectedEntityId, - disableDrop: disableDragDrop, - disableDrag: disableDragDrop, - }; - }); - } + this.setState(({ selectedComponentId }) => ({ + selectedComponentId: id === selectedComponentId ? null : id, + })); } render() { - const { layout: entities, onDrop, canDrop } = this.props; - const { - showGrid, - rowGuideTop, - disableDrop, - disableDrag, - // selectedEntityId, - } = this.state; - - const rootEntity = entities[DASHBOARD_ROOT_ID]; + const { layout: components, onDrop, canDrop } = this.props; + const { isResizing, rowGuideTop } = this.state; + const rootComponent = components[DASHBOARD_ROOT_ID]; return (
    { this.grid = ref; }} - className={cx('grid-container', showGrid && 'grid-container--resizing')} + className={cx('grid-container', isResizing && 'grid-container--resizing')} > {({ width }) => { // account for (COLUMN_COUNT - 1) gutters const columnPlusGutterWidth = (width + GRID_GUTTER_SIZE) / GRID_COLUMN_COUNT; const columnWidth = columnPlusGutterWidth - GRID_GUTTER_SIZE; - const gridProps = { - columnWidth, - rowWidth: width, - onResizeStart: this.handleResizeStart, - onResize: this.handleResize, - onResizeStop: this.handleResizeStop, - }; return width < 50 ? null : (
    - {rootEntity.children.map((id, index) => ( - ( + ))} - {showGrid && Array(GRID_COLUMN_COUNT).fill(null).map((_, i) => ( + {isResizing && Array(GRID_COLUMN_COUNT).fill(null).map((_, i) => (
    ))} - {showGrid && rowGuideTop && + {isResizing && rowGuideTop &&
    ({ + dropIndicator: { + top: 0, + right: 8, + height: orientation === 'vertical' ? '100%' : 3, + width: orientation === 'vertical' ? 3 : '100%', + minHeight: orientation === 'vertical' ? 16 : null, + minWidth: orientation === 'vertical' ? null : 16, + margin: 'auto', + backgroundColor: '#44C0FF', + position: 'absolute', + zIndex: 10, + }, + })); + } else if (validSibling) { // indicate drop near parent + console.log('valid sibling', components[parentId].type, draggingItem.type); + const refBoundingRect = this.ref.getBoundingClientRect(); + const clientOffset = monitor.getClientOffset(); + + if (clientOffset) { + let dropOffset; + const orientation = containerOrientation; + if (orientation === 'horizontal') { + const refMiddleY = + refBoundingRect.top + (refBoundingRect.bottom - refBoundingRect.top) / 2; + dropOffset = clientOffset.y < refMiddleY ? 0 : refBoundingRect.height; + } else { + const refMiddleX = + refBoundingRect.left + (refBoundingRect.right - refBoundingRect.left) / 2; + dropOffset = clientOffset.x < refMiddleX ? 0 : refBoundingRect.width; + } + + this.setState(() => ({ + dropIndicator: { + top: orientation === 'vertical' ? 0 : dropOffset, + left: orientation === 'vertical' ? dropOffset : 0, + height: orientation === 'vertical' ? '100%' : 3, + width: orientation === 'vertical' ? 3 : '100%', + backgroundColor: '#44C0FF', + position: 'absolute', + zIndex: 10, + }, + })); + } + } else if (bubbleUpHover) { + // console.log('bubble'); + // debugger; + // bubbleUpHover(monitor); + } + } + + handleDrop(monitor) { + console.log('drop'); + + const { components, component, parentId, index: componentIndex, onDrop, depth } = this.props; + const draggingItem = monitor.getItem(); + + // if dropped self on self, do nothing + if (!draggingItem || draggingItem.draggableId === component.id) { + console.log(draggingItem ? 'drag self' : 'no item'); + return undefined; + } + + // append to self, or parent + const validChild = isValidChild({ + parentType: component.type, + childType: draggingItem.type, + }); + + const validSibling = isValidChild({ + parentType: components[parentId] && components[parentId].type, + childType: draggingItem.type, + }); + + if (!validChild && !validSibling) { + console.log('not valid child or sibling') + return undefined; + } + + const dropResult = { + source: draggingItem.parentId ? { + droppableId: draggingItem.parentId, + index: draggingItem.index, + } : null, + draggableId: draggingItem.draggableId, + }; + + if (validChild) { // append it to component.children + dropResult.destination = { + droppableId: component.id, + index: component.children.length, + }; + } else { // insert as sibling + const containerOrientation = depth % 2 === 0 ? 'horizontal' : 'vertical'; + let nextIndex = componentIndex; + const refBoundingRect = this.ref.getBoundingClientRect(); + const clientOffset = monitor.getClientOffset(); + + if (clientOffset) { + if (containerOrientation === 'horizontal') { + const refMiddleY = + refBoundingRect.top + (refBoundingRect.bottom - refBoundingRect.top) / 2; + nextIndex += clientOffset.y >= refMiddleY ? 1 : 0; + } else { + const refMiddleX = + refBoundingRect.left + (refBoundingRect.right - refBoundingRect.left) / 2; + nextIndex += clientOffset.x >= refMiddleX ? 1 : 0; + } + } + + if (draggingItem.parentId === parentId && draggingItem.index < componentIndex) { + nextIndex = Math.max(0, nextIndex - 1); + } + + dropResult.destination = { + droppableId: parentId, + index: nextIndex, + }; + } + + onDrop(dropResult); + + return dropResult; + } + + renderDropIndicator() { + const { dropIndicator } = this.state; + const { isDraggingOverShallow } = this.props; + return isDraggingOverShallow && dropIndicator && ( +
    + ); + } + + render() { + const { + children, + depth, + droppableRef, + dragSourceRef, + dragPreviewRef, + // dropIndicator, + innerRef, + isDragging, + useChildAsDragHandle, + } = this.props; + + return ( +
    { + this.ref = ref; + dragPreviewRef(ref); + droppableRef(ref); + if (innerRef) innerRef(ref); + }} + className={cx( + 'dragdroppable', + depth % 2 === 0 ? 'dragdroppable-row' : 'dragdroppable-column', + isDragging && 'dragdroppable--dragging', + )} + > + + {useChildAsDragHandle && +
    {children}
    } + + {!useChildAsDragHandle && children} + + {!useChildAsDragHandle && + } + + {this.renderDropIndicator()} +
    + ); + } +} + +DragDroppable.propTypes = propTypes; +DragDroppable.defaultProps = defaultProps; + +// note that the composition order here determines using +// component.method() vs decoratedComponentInstance.method() in the above config +export default DropTarget(...dropConfig)( + DragSource(...dragConfig)(DragDroppable), +); diff --git a/superset/assets/javascripts/dashboard/v2/components/dnd/DragHandle.jsx b/superset/assets/javascripts/dashboard/v2/components/dnd/DragHandle.jsx new file mode 100644 index 0000000000000..8b9d98192d678 --- /dev/null +++ b/superset/assets/javascripts/dashboard/v2/components/dnd/DragHandle.jsx @@ -0,0 +1,37 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import cx from 'classnames'; + +const propTypes = { + position: PropTypes.oneOf(['left', 'top']), + innerRef: PropTypes.func, +}; + +const defaultProps = { + position: 'left', + innerRef: null, +}; + +export default class DragHandle extends React.PureComponent { + render() { + const { innerRef, position } = this.props; + return ( +
    +
    + {Array(8).fill(null).map((_, i) => ( +
    + ))} +
    +
    + ); + } +} + +DragHandle.propTypes = propTypes; +DragHandle.defaultProps = defaultProps; diff --git a/superset/assets/javascripts/dashboard/v2/components/dnd/Draggable.jsx b/superset/assets/javascripts/dashboard/v2/components/dnd/Draggable.jsx deleted file mode 100644 index b15449eb8d716..0000000000000 --- a/superset/assets/javascripts/dashboard/v2/components/dnd/Draggable.jsx +++ /dev/null @@ -1,62 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { DragSource } from 'react-dnd'; - -const propTypes = { - draggableId: PropTypes.string.isRequired, - index: PropTypes.number.isRequired, - children: PropTypes.func.isRequired, - beginDrag: PropTypes.func, - endDrag: PropTypes.func, - canDrag: PropTypes.func, - // from react-dnd - dragSourceRef: PropTypes.func.isRequired, - dragPreviewRef: PropTypes.func.isRequired, - isDragging: PropTypes.bool, -}; - -const defaultProps = { - beginDrag: null, - endDrag: null, - canDrag: null, - isDragging: false, -}; - -class Draggable extends React.Component { - render() { - const { children, dragSourceRef, dragPreviewRef, isDragging } = this.props; - - return children({ - dragSourceRef, - dragPreviewRef, - isDragging, - }); - } -} - -Draggable.propTypes = propTypes; -Draggable.defaultProps = defaultProps; - -export default DragSource( - 'DEFAULT', // @TODO this should be a constant, not a useful hook for us - { - beginDrag(props, monitor, component) { - return props.beginDrag ? props.beginDrag(props, monitor, component) : ({ - draggableId: props.draggableId, - index: props.index, - type: props.type, - }); - }, - endDrag(props, monitor, component) { - return props.endDrag && props.endDrag(props, monitor, component); - }, - canDrag(props, monitor) { return props.canDrag ? props.canDrag(props, monitor) : true; }, - }, - function dndStateToProps(connect, monitor) { - return { - dragSourceRef: connect.dragSource(), // @TODO non-ref if this doesn't work - dragPreviewRef: connect.dragPreview(), - isDragging: monitor.isDragging(), - }; - }, -)(Draggable); diff --git a/superset/assets/javascripts/dashboard/v2/components/dnd/DraggableColumn.jsx b/superset/assets/javascripts/dashboard/v2/components/dnd/DraggableColumn.jsx deleted file mode 100644 index bc25a5e4e672c..0000000000000 --- a/superset/assets/javascripts/dashboard/v2/components/dnd/DraggableColumn.jsx +++ /dev/null @@ -1,332 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { DragSource, DropTarget } from 'react-dnd'; -import cx from 'classnames'; -import throttle from 'lodash.throttle'; - -import ResizableContainer from '../resizable/ResizableContainer'; -import componentIsResizable from '../../util/componentIsResizable'; - -import { - GRID_GUTTER_SIZE, - GRID_ROW_HEIGHT_UNIT, - GRID_COLUMN_COUNT, - GRID_MIN_COLUMN_COUNT, - GRID_MIN_ROW_UNITS, - GRID_MAX_ROW_UNITS, -} from '../../util/constants'; - -import { SPACER_TYPE, COLUMN_TYPE } from '../../util/componentTypes'; -import { COMPONENT_TYPE_LOOKUP } from '../gridComponents'; - -import isValidChild from '../../util/isValidChild'; - -const HOVER_THROTTLE_MS = 200; - -const propTypes = { - // depth: PropTypes.number.isRequired, - entity: PropTypes.object.isRequired, - index: PropTypes.number.isRequired, - parentId: PropTypes.string.isRequired, - disableDrop: PropTypes.bool, - disableDrag: PropTypes.bool, - - // from HOCs - isDragging: PropTypes.bool.isRequired, - droppableRef: PropTypes.func.isRequired, - dragSourceRef: PropTypes.func.isRequired, - dragPreviewRef: PropTypes.func.isRequired, -}; - -const defaultProps = { - disableDrag: false, - disableDrop: false, -}; - -class DraggableColumn extends React.PureComponent { - constructor(props) { - super(props); - this.state = { - horizontalDropIndicator: null, - }; - - this.handleBeginDrag = this.handleBeginDrag.bind(this); - this.handleHover = throttle(this.hover.bind(this), HOVER_THROTTLE_MS).bind(this); - this.handleDrop = this.handleDrop.bind(this); - - this.renderDropIndicator = this.renderDropIndicator.bind(this); - this.renderComponent = this.renderComponent.bind(this); - } - - handleBeginDrag() { - const { entity, index, parentId } = this.props; - return { draggableId: entity.id, index, parentId, type: entity.type }; - } - - hover(props, monitor, component) { - const { entity } = this.props; - const draggingItem = monitor.getItem(); - - if (!draggingItem || draggingItem.draggableId === entity.id) { - return; - } - - const draggingItemIsValidChild = isValidChild({ - parentType: entity.type, - childType: draggingItem.type, - }); - - if (draggingItemIsValidChild) { // vertical indicator, append to container - this.setState(() => ({ - dropIndicator: { - position: 'relative', - width: 'auto', - minWidth: 20, - margin: 'auto', - left: 0, - right: 0, - }, - })); - } else { // vertical indicator, left or right - const colBoundingRect = this.ref.getBoundingClientRect(); - const clientOffset = monitor.getClientOffset(); - - if (clientOffset) { - const colMiddleX = colBoundingRect.left - + (colBoundingRect.right - colBoundingRect.left) / 2; - - let dropIndicatorLeft = 0; - if (clientOffset.x >= colMiddleX) { // place indicator right of col - dropIndicatorLeft = colBoundingRect.width; - } - - this.setState(() => ({ - dropIndicator: { - left: dropIndicatorLeft, - height: '100%', - width: 3, - }, - })); - } - } - } - - handleDrop(props, monitor, component) { - console.log('column drop'); - // check if a nested drop target handled the drop - if (monitor.didDrop()) return; - - const { entity, parentId: columnParentId, index: columnIndex } = this.props; - const draggingItem = monitor.getItem(); - - // if dropped self on self, do nothing - if (draggingItem && draggingItem.draggableId === entity.id) return; - - // append to self, or parent - const acceptChild = isValidChild({ parentType: entity.type, childType: draggingItem.type }); - - // if (!acceptChild) return undefined; - - const dropResult = { - source: draggingItem.parentId ? { - droppableId: draggingItem.parentId, - index: draggingItem.index, - } : null, - draggableId: draggingItem.draggableId, - }; - - if (acceptChild) { // if it's a valid child, append it to entity.children - dropResult.destination = { - droppableId: entity.id, - index: entity.children.length, - }; - } else { - let nextIndex = columnIndex; - const colBoundingRect = this.ref.getBoundingClientRect(); - const clientOffset = monitor.getClientOffset(); - - if (clientOffset) { - const colMiddleX = colBoundingRect.left + - (colBoundingRect.right - colBoundingRect.left) / 2; - - if (clientOffset.x >= colMiddleX) { // right of column - nextIndex += 1; - } - } - - if (draggingItem.parentId === columnParentId && draggingItem.index < columnIndex) { - nextIndex = Math.max(0, nextIndex - 1); - } - - dropResult.destination = { - droppableId: columnParentId, - index: nextIndex, - }; - } - // @TODO this should be a redux action - this.props.onDrop(dropResult); - - return dropResult; - } - - renderResizableContainer(children) { - const { entity, gridProps } = this.props; - const isResizable = componentIsResizable(entity); - const isSpacer = entity.type === SPACER_TYPE; - - if (!isResizable) return children; - - const { - onResizeStart, - onResize, - onResizeStop, - columnWidth, - occupiedColumnCount, - currentRowHeight, - } = gridProps; - - return ( - - {children} - - ); - } - - renderComponent() { - const { entity, entities, gridProps } = this.props; - const Component = COMPONENT_TYPE_LOOKUP[entity.type]; - - return ( - - ); - } - - renderDropIndicator() { - const { dropIndicator } = this.state; - const { isDraggingOverShallow } = this.props; - return dropIndicator && isDraggingOverShallow && ( -
    - ); - } - - render() { - const { - droppableRef, - dragSourceRef, - dragPreviewRef, - isDragging, - disableDrag, - // isDraggingOverShallow - } = this.props; - - return ( -
    { - this.ref = ref; - dragPreviewRef(ref); - droppableRef(ref); - }} - className={cx( - 'draggable', - 'draggable-row-item', // @TODO update to draggable-column - isDragging && 'draggable--dragging', - disableDrag && 'draggable--disabled', - )} - style={{ - display: 'flex', - flexDirection: 'row', - opacity: isDragging && 0.25, - position: 'relative', - backgroundColor: isDragging ? '#ccc' : null, - }} - > - -
    - - {this.renderResizableContainer( - this.renderComponent(), - )} - - {this.renderDropIndicator()} -
    - ); - } -} - -DraggableColumn.propTypes = propTypes; -DraggableColumn.defaultProps = defaultProps; - -const dragConfig = [ - 'DEFAULT', - { - beginDrag(props, monitor, component) { - return component.handleBeginDrag(monitor); - }, - }, - function dragStateToProps(connect, monitor) { - return { - dragSourceRef: connect.dragSource(), // @TODO non-ref if this doesn't work - dragPreviewRef: connect.dragPreview(), - isDragging: monitor.isDragging(), - }; - }, - -]; - -const dropConfig = [ - 'DEFAULT', - { - hover(props, monitor, component) { - if (monitor.isOver({ shallow: true })) { - component.decoratedComponentInstance.handleHover(props, monitor, component); - } - }, - drop(props, monitor, component) { - if (!component) return undefined - return component.decoratedComponentInstance.handleDrop(props, monitor, component); - }, - }, - function dropStateToProps(connect, monitor) { - return { - droppableRef: connect.dropTarget(), - isDraggingOverShallow: monitor.isOver({ shallow: true }), - draggableId: (monitor.getItem() || {}).draggableId || null, - }; - }, -]; - -// note that the composition order here determines using -// component.method() vs decoratedComponentInstance.method() in the above config -export default DropTarget(...dropConfig)( - DragSource(...dragConfig)(DraggableColumn), -); diff --git a/superset/assets/javascripts/dashboard/v2/components/dnd/DraggableNewComponent.jsx b/superset/assets/javascripts/dashboard/v2/components/dnd/DraggableNewComponent.jsx deleted file mode 100644 index 971ba0aa5ca80..0000000000000 --- a/superset/assets/javascripts/dashboard/v2/components/dnd/DraggableNewComponent.jsx +++ /dev/null @@ -1,37 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import cx from 'classnames'; -import Draggable from './Draggable'; - -const propTypes = { - // id: PropTypes.string.isRequired, - type: PropTypes.string.isRequired, - label: PropTypes.string.isRequired, -}; - -export default class DraggableNewComponent extends React.PureComponent { - render() { - const { type, label } = this.props; - return ( - - {({ dragSourceRef, dragPreviewRef, isDragging }) => ( -
    { - dragSourceRef(ref); - dragPreviewRef(ref); - }} - className={cx( - 'new-draggable-component', - isDragging && 'draggable--dragging', - )} - > -
    - {label} -
    - )} - - ); - } -} - -DraggableNewComponent.propTypes = propTypes; diff --git a/superset/assets/javascripts/dashboard/v2/components/dnd/DraggableRow.jsx b/superset/assets/javascripts/dashboard/v2/components/dnd/DraggableRow.jsx deleted file mode 100644 index 8e53c4ffdb1ff..0000000000000 --- a/superset/assets/javascripts/dashboard/v2/components/dnd/DraggableRow.jsx +++ /dev/null @@ -1,274 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { DragSource, DropTarget } from 'react-dnd'; -import cx from 'classnames'; -import throttle from 'lodash.throttle'; - -import isValidChild from '../../util/isValidChild'; -import { COMPONENT_TYPE_LOOKUP } from '../gridComponents'; - -const HOVER_THROTTLE_MS = 200; - -const propTypes = { - // depth: PropTypes.number.isRequired, - entity: PropTypes.object.isRequired, - parentId: PropTypes.string.isRequired, - index: PropTypes.number.isRequired, - disableDrop: PropTypes.bool, - disableDrag: PropTypes.bool, - - // from HOCs - isDragging: PropTypes.bool.isRequired, - droppableRef: PropTypes.func.isRequired, - dragSourceRef: PropTypes.func.isRequired, - dragPreviewRef: PropTypes.func.isRequired, -}; - -const defaultProps = { - disableDrag: false, - disableDrop: false, -}; - -class DraggableRow extends React.PureComponent { - constructor(props) { - super(props); - this.state = { - dropIndicator: null, - }; - - this.handleBeginDrag = this.handleBeginDrag.bind(this); - this.handleHover = throttle(this.hover.bind(this), HOVER_THROTTLE_MS).bind(this); - this.handleDrop = this.handleDrop.bind(this); - this.renderDropIndicator = this.renderDropIndicator.bind(this); - this.renderComponent = this.renderComponent.bind(this); - } - - handleBeginDrag() { - const { entity, index, parentId, type } = this.props; - return { draggableId: entity.id, index, parentId, type }; - } - - hover(props, monitor, component) { - const { entity } = this.props; - const draggingItem = monitor.getItem(); - - if (!draggingItem || draggingItem.draggableId === entity.id) { - return; - } - - const draggingItemIsValidChild = isValidChild({ - parentType: entity.type, - childType: draggingItem.type, - }); - - if (draggingItemIsValidChild) { // vertical indicator, append to container - this.setState(() => ({ - dropIndicator: { - height: '50%', - minHeight: 16, - top: 0, - right: 24, - width: 3, - }, - })); - } else { // horizontal indicator, top or bottom - const rowBoundingRect = this.ref.getBoundingClientRect(); - const clientOffset = monitor.getClientOffset(); - if (clientOffset) { - const rowMiddleY = rowBoundingRect.top + (rowBoundingRect.bottom - rowBoundingRect.top) / 2; - - let dropIndicatorTop = null; - if (clientOffset.y < rowMiddleY) { // place indicator above row - dropIndicatorTop = 0; - } else if (clientOffset.y >= rowMiddleY) { // place indicator below row - dropIndicatorTop = rowBoundingRect.height; - } - - this.setState(() => ({ - dropIndicator: { - top: dropIndicatorTop, - width: '100%', - height: 3, - }, - })); - } - } - } - - handleDrop(props, monitor, component) { - console.log('handle drop'); - // check if a nested drop target handled the drop - if (monitor.didDrop()) return; - - const { entity, entities, parentId: rowParentId, index: rowIndex } = this.props; - const draggingItem = monitor.getItem(); - - // if dropped self on self, do nothing - if (!draggingItem || draggingItem.draggableId === entity.id) return; - - // append to self, or parent - const acceptChild = isValidChild({ parentType: entity.type, childType: draggingItem.type }); - - const dropResult = { - source: draggingItem.parentId ? { - droppableId: draggingItem.parentId, - index: draggingItem.index, - } : null, - draggableId: draggingItem.draggableId, - }; - - if (acceptChild) { // if it's a valid child, append it to entity.children - dropResult.destination = { - droppableId: entity.id, - index: entity.children.length, - }; - } else { - let nextIndex = rowIndex; - const rowBoundingRect = this.ref.getBoundingClientRect(); - const clientOffset = monitor.getClientOffset(); - - if (clientOffset) { - const rowMiddleY = rowBoundingRect.top + (rowBoundingRect.bottom - rowBoundingRect.top) / 2; - - if (clientOffset.y >= rowMiddleY) { // place indicator below row - nextIndex += 1; - } - } - - if (draggingItem.parentId === rowParentId && draggingItem.index < rowIndex) { - nextIndex = Math.max(0, nextIndex - 1); - } - - dropResult.destination = { - droppableId: rowParentId, - index: nextIndex, - }; - } - // @TODO this should be a redux action - this.props.onDrop(dropResult); - - return dropResult; - } - - renderComponent() { - const { - entity, - droppableRef, - dragSourceRef, - dragPreviewRef, - ...restProps - } = this.props; - - const Component = COMPONENT_TYPE_LOOKUP[entity.type]; - - return ( - - ); - } - - renderDropIndicator() { - const { dropIndicator } = this.state; - const { isDraggingOverShallow } = this.props; - return dropIndicator && isDraggingOverShallow && ( -
    - ); - } - - render() { - const { - droppableRef, - dragSourceRef, - dragPreviewRef, - isDragging, - disableDrag, - // isDraggingOver, - // isDraggingOverShallow - } = this.props; - - return ( -
    { - this.ref = ref; - dragPreviewRef(ref); - droppableRef(ref); - }} - className={cx( - 'draggable', - 'draggable-row', - isDragging && 'draggable--dragging', - disableDrag && 'draggable--disabled', - )} - style={{ - opacity: isDragging && 0.25, - position: 'relative', - backgroundColor: isDragging ? '#ccc' : null, - }} - > - -
    - - {this.renderComponent()} - {this.renderDropIndicator()} -
    - ); - } -} - -DraggableRow.propTypes = propTypes; -DraggableRow.defaultProps = defaultProps; - -const dragConfig = [ - 'DEFAULT', - { - beginDrag(props, monitor, component) { - return component.handleBeginDrag(monitor); - }, - }, - function dragStateToProps(connect, monitor) { - return { - dragSourceRef: connect.dragSource(), // @TODO non-ref if this doesn't work - dragPreviewRef: connect.dragPreview(), - isDragging: monitor.isDragging(), - }; - }, - -]; - -const dropConfig = [ - 'DEFAULT', - { - hover(props, monitor, component) { - if (monitor.isOver({ shallow: true })) { - component.decoratedComponentInstance.handleHover(props, monitor, component); - } - }, - drop(props, monitor, component) { - if (!component) return undefined; - return component.decoratedComponentInstance.handleDrop(props, monitor, component); - }, - }, - function dropStateToProps(connect, monitor) { - return { - droppableRef: connect.dropTarget(), - isDraggingOverShallow: monitor.isOver({ shallow: true }), - draggableId: (monitor.getItem() || {}).draggableId || null, - }; - }, -]; - -// note that the composition order here determines using -// component.method() vs decoratedComponentInstance.method() in the above config -export default DropTarget(...dropConfig)( - DragSource(...dragConfig)(DraggableRow), -); diff --git a/superset/assets/javascripts/dashboard/v2/components/dnd/dnd.css b/superset/assets/javascripts/dashboard/v2/components/dnd/dnd.css index 6b0c2b6a1d221..542490f8f25c6 100644 --- a/superset/assets/javascripts/dashboard/v2/components/dnd/dnd.css +++ b/superset/assets/javascripts/dashboard/v2/components/dnd/dnd.css @@ -1,29 +1,38 @@ -.draggable-row { - width: 100%; +.dragdroppable { + position: relative; } -.draggable-row, -.draggable-row-item { - position: relative; +.dragdroppable--dragging { + opacity: 0.25; } -.draggable-row--dragging, -.draggable-row-item--dragging { - box-shadow: inset 0 0 0 1px #CFD8DC; +.dragdroppable-row { + width: 100%; } -/*.draggable-row:hover { - box-shadow: 0 0 5px 2px rgba(0, 0, 0, 0.1); - z-index: 10; -}*/ +.grid-container .dragdroppable-row:after { + border: 1px dashed transparent; + content: ""; + position: absolute; + width: 100%; + height: 100%; + top: 1px; + left: 0; + z-index: 1; + pointer-events: none; +} + + .grid-container .dragdroppable-row:hover:after { + border: 1px dashed #aaa; + } .draggable-row-handle { opacity: 0; position: absolute; - width: 10px; + width: 20px; height: 100%; top: 0; - left: -10px; + left: -20px; cursor: move; z-index: 1; display: flex; @@ -32,46 +41,57 @@ } /* offset nested row handles */ - .draggable-row .draggable-row .draggable-row-handle { - left: 10px; + .dragdroppable-row .dragdroppable-row .draggable-row-handle { + left: -2px; } -.draggable-row-handle:after { +.draggable-row-handle .handle { + padding-left: 7px; + overflow: hidden; + width: 15px +} + +.draggable-row-handle .handle .handle-dot, +.draggable-row-item-handle .handle .handle-dot { + float: left; + height: 2px; + margin: 1px; + width: 2px +} + +.draggable-row-handle .handle .handle-dot:after, +.draggable-row-item-handle .handle .handle-dot:after { + background: #aaa; content: ""; - background: #44C0FF; - display: block; - width: 5px; - height: 20%; - min-height: 20px; - margin: auto -5px auto 0; + float: left; + height: 2px; + margin: -1px; + width: 2px; } + .draggable-row-item-handle { opacity: 0; position: absolute; width: 100%; - height: 10px; + height: 20px; top: 0; left: 0; cursor: move; z-index: 1; } -.draggable-row-item-handle:after { - content: ""; - background: #44C0FF; - display: block; - margin: 5px auto 0 auto; - width: 20%; - min-width: 20px; - height: 5px; +.draggable-row-item-handle .handle { + overflow: hidden; + width: 16px; + margin: 10px auto; } -.draggable-row:hover .draggable-row-handle, +.dragdroppable:hover .draggable-row-handle, .draggable-row-handle:hover, -.draggable-row-item:hover .draggable-row-item-handle, +.dragdroppable:hover .draggable-row-item-handle, .draggable-row-item-handle:hover, -.draggable-column-item:hover .draggable-column-item-handle, -.draggable-column-item-handle:hover { +.dragdroppable-row .dragdroppable-row:hover .draggable-row-handle, +.dragdroppable-row .dragdroppable-row:hover .draggable-row-item-handle { opacity: 1; } diff --git a/superset/assets/javascripts/dashboard/v2/components/dnd/dragDroppableConfig.js b/superset/assets/javascripts/dashboard/v2/components/dnd/dragDroppableConfig.js new file mode 100644 index 0000000000000..993632d0c07b5 --- /dev/null +++ b/superset/assets/javascripts/dashboard/v2/components/dnd/dragDroppableConfig.js @@ -0,0 +1,55 @@ +const TYPE = 'DRAG_DROPPABLE'; // 'type' hook is not useful for us + +export const dragConfig = [ + TYPE, + { + beginDrag(props /* , monitor, component */) { + const { component, index, parentId } = props; + return { draggableId: component.id, index, parentId, type: component.type }; + }, + }, + function dragStateToProps(connect, monitor) { + return { + dragSourceRef: connect.dragSource(), + dragPreviewRef: connect.dragPreview(), + isDragging: monitor.isDragging(), + }; + }, +]; + +export const dropConfig = [ + TYPE, + { + hover(props, monitor, component) { + if ( + monitor.isOver({ shallow: true }) + && component + && component.decoratedComponentInstance + && component.decoratedComponentInstance.handleHover + ) { + component.decoratedComponentInstance.handleHover(monitor); + } + }, + // note: + // it's important that the drop() method return a result in order for the react-dnd + // monitor.didDrop() method to bubble up properly up nested droppables + drop(props, monitor, component) { + if ( + !monitor.didDrop() + && component + && component.decoratedComponentInstance + && component.decoratedComponentInstance.handleDrop + ) { + return component.decoratedComponentInstance.handleDrop(monitor); + } + return undefined; + }, + }, + function dropStateToProps(connect, monitor) { + return { + droppableRef: connect.dropTarget(), + isDraggingOver: monitor.isOver(), + isDraggingOverShallow: monitor.isOver({ shallow: true }), + }; + }, +]; diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Column.jsx b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Column.jsx index 7142733bf8c8e..d1d9b93d6fbe9 100644 --- a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Column.jsx +++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Column.jsx @@ -1,91 +1,78 @@ import React from 'react'; import PropTypes from 'prop-types'; +import cx from 'classnames'; -import ResizableContainer from '../resizable/ResizableContainer'; +import DashboardComponent from '../DashboardComponent'; +import { componentShape } from '../../util/propShapes'; -import { COMPONENT_TYPE_LOOKUP } from './'; -import componentIsResizable from '../../util/componentIsResizable'; - -import { - GRID_GUTTER_SIZE, - GRID_ROW_HEIGHT_UNIT, - GRID_MIN_ROW_UNITS, - GRID_MAX_ROW_UNITS, -} from '../../util/constants'; - -import { SPACER_TYPE } from '../../util/componentTypes'; +import { GRID_GUTTER_SIZE } from '../../util/constants'; const propTypes = { - entity: PropTypes.object, - entities: PropTypes.object, - onResizeStart: PropTypes.func, - onResize: PropTypes.func, - onResizeStop: PropTypes.func, + component: componentShape.isRequired, + components: PropTypes.object.isRequired, + depth: PropTypes.number.isRequired, + parentId: PropTypes.string.isRequired, + + // grid related + availableColumnCount: PropTypes.number.isRequired, + columnWidth: PropTypes.number.isRequired, + onResizeStart: PropTypes.func.isRequired, + onResize: PropTypes.func.isRequired, + onResizeStop: PropTypes.func.isRequired, }; const defaultProps = { - entity: {}, - entities: {}, - onResizeStop: null, - onResize: null, - onResizeStart: null, }; class Column extends React.PureComponent { render() { - const { entity: columnEntity, entities, onResizeStop, onResize, onResizeStart } = this.props; + const { + component: columnComponent, + components, + depth, + availableColumnCount, + columnWidth, + onResizeStart, + onResize, + onResizeStop, + onDrop, + } = this.props; const columnItems = []; - (columnEntity.children || []).forEach((id, index) => { - const entity = entities[id]; - columnItems.push(entity); - if (index < columnEntity.children.length - 1) columnItems.push(`gutter-${index}`); + (columnComponent.children || []).forEach((id, index) => { + const component = components[id]; + columnItems.push(component); + if (index < columnComponent.children.length - 1) columnItems.push(`gutter-${index}`); }); return ( -
    - {columnItems.map((entity) => { - const id = entity.id || entity; - const Component = COMPONENT_TYPE_LOOKUP[entity.type]; - const isResizable = componentIsResizable(entity); - - let ColumnItem = Component ? ( - + {columnItems.map((component, index) => ( + !component.id ? ( +
    + ) : ( + - ) :
    ; - - if (isResizable) { - ColumnItem = ( - - {ColumnItem} - - ); - } - return ColumnItem; - })} - - {(!columnEntity.children || !columnEntity.children.length) - && 'Empty column'} + )))}
    ); } diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Header.jsx b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Header.jsx index 034b20d64bcc2..07eb4aa7c96c4 100644 --- a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Header.jsx +++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Header.jsx @@ -1,17 +1,14 @@ import React from 'react'; import PropTypes from 'prop-types'; +import { componentShape } from '../../util/propShapes'; + const propTypes = { - entity: PropTypes.shape({ - id: PropTypes.string.isRequired, - meta: PropTypes.shape({ - text: PropTypes.string, - }), - }), + component: componentShape.isRequired, }; const defaultProps = { - entity: {}, + component: {}, }; class Header extends React.PureComponent { @@ -21,7 +18,7 @@ class Header extends React.PureComponent { } render() { - const { entity: { id, meta } } = this.props; + const { component: { id, meta } } = this.props; return !meta || !id ? null : (
    {meta.text} diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Row.jsx b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Row.jsx index 76c5f1ce6b1e2..662c987c93637 100644 --- a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Row.jsx +++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Row.jsx @@ -2,84 +2,87 @@ import React from 'react'; import PropTypes from 'prop-types'; import cx from 'classnames'; -import DraggableColumn from '../dnd/DraggableColumn'; +import DashboardComponent from '../DashboardComponent'; +import { componentShape } from '../../util/propShapes'; import { GRID_GUTTER_SIZE } from '../../util/constants'; import { INVISIBLE_ROW_TYPE } from '../../util/componentTypes'; const propTypes = { - entity: PropTypes.object, // @TODO shape - entities: PropTypes.object, - columnWidth: PropTypes.number, - onResizeStart: PropTypes.func, - onResize: PropTypes.func, - onResizeStop: PropTypes.func, + component: componentShape.isRequired, + components: PropTypes.object.isRequired, + depth: PropTypes.number.isRequired, + parentId: PropTypes.string.isRequired, + + // grid related + availableColumnCount: PropTypes.number.isRequired, + columnWidth: PropTypes.number.isRequired, + onResizeStart: PropTypes.func.isRequired, + onResize: PropTypes.func.isRequired, + onResizeStop: PropTypes.func.isRequired, }; const defaultProps = { - entity: {}, - entities: {}, - columnWidth: 0, - onResizeStop: null, - onResize: null, - onResizeStart: null, }; class Row extends React.PureComponent { - // shouldComponentUpdate() { - // // @TODO check for updates to this row only - // } - render() { const { - entities, - entity: rowEntity, - disableDrop, - disableDrag, + component: rowComponent, + components, + depth, + availableColumnCount, + columnWidth, + onResizeStart, + onResize, + onResizeStop, onDrop, - gridProps, + bubbleUpHover, } = this.props; let occupiedColumnCount = 0; - let currentRowHeight = 0; + let rowHeight = 0; const rowItems = []; // this adds a gutter between each child in the row. - (rowEntity.children || []).forEach((id, index) => { - const entity = entities[id]; - occupiedColumnCount += (entity.meta || {}).width || 0; - rowItems.push(entity); - if (index < rowEntity.children.length - 1) rowItems.push(`gutter-${index}`); - if ((entity.meta || {}).height) { - currentRowHeight = Math.max(currentRowHeight, entity.meta.height); + (rowComponent.children || []).forEach((id, index) => { + const component = components[id]; + occupiedColumnCount += (component.meta || {}).width || 0; + rowItems.push(component); + if (index < rowComponent.children.length - 1) rowItems.push(`gutter-${index}`); + if ((component.meta || {}).height) { + rowHeight = Math.max(rowHeight, component.meta.height); } }); - const modifiedGridProps = { ...gridProps, occupiedColumnCount, currentRowHeight }; - return (
    - {rowItems.map((entity, index) => ( - !entity.id ? ( -
    + {rowItems.map((component, index) => ( + !component.id ? ( +
    ) : ( - - ) - ))} + )))}
    ); } diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Tabs.jsx b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Tabs.jsx index 52f8bed54bf56..839dc230f150e 100644 --- a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Tabs.jsx +++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Tabs.jsx @@ -1,21 +1,27 @@ import React from 'react'; import PropTypes from 'prop-types'; -import cx from 'classnames'; import { Tabs as BootstrapTabs, Tab } from 'react-bootstrap'; -import DraggableRow from '../dnd/DraggableRow'; +import DashboardComponent from '../DashboardComponent'; +import { componentShape } from '../../util/propShapes'; const propTypes = { - tabs: PropTypes.arrayOf( + tabs: PropTypes.arrayOf( // @TODO this should be parth of the component definition PropTypes.shape({ label: PropTypes.string, }), ), onChangeTab: PropTypes.func, - entity: PropTypes.shape({ - children: PropTypes.arrayOf(PropTypes.string), - }), - entities: PropTypes.object, // @TODO shape + component: componentShape.isRequired, + components: PropTypes.object, + depth: PropTypes.number.isRequired, + + // grid props + availableColumnCount: PropTypes.number.isRequired, + columnWidth: PropTypes.number.isRequired, + onResizeStart: PropTypes.func.isRequired, + onResize: PropTypes.func.isRequired, + onResizeStop: PropTypes.func.isRequired, }; const defaultProps = { @@ -48,11 +54,14 @@ class Tabs extends React.Component { render() { const { tabs, - entity: tabEntity, - entities, - gridProps, - disableDrop, - disableDrag, + depth, + component: tabEntity, + components, + availableColumnCount, + columnWidth, + onResizeStart, + onResize, + onResizeStop, onDrop, } = this.props; @@ -68,20 +77,25 @@ class Tabs extends React.Component { {tab.label}
    } /> ))} +
    - {(tabEntity.children || []).map((id, index) => ( - - ))} + {(tabEntity.children || []).map((id, index) => ( + + ))} +
    ); } diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/components.css b/superset/assets/javascripts/dashboard/v2/components/gridComponents/components.css index 9e3911ea468c8..bfbd50712601f 100644 --- a/superset/assets/javascripts/dashboard/v2/components/gridComponents/components.css +++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/components.css @@ -8,8 +8,8 @@ padding: 16px 0; color: #263238; } - .draggable-row .draggable-row .dashboard-component-header, - .draggable-row .draggable-row .dashboard-component-divider { + .dragdroppable-row .dragdroppable-row .dashboard-component-header, + .dragdroppable-row .dragdroppable-row .dashboard-component-divider { padding-left: 16px; padding-right: 16px; } @@ -54,6 +54,9 @@ width: 100%; background-color: white; } +.dashboard-component-tabs .dashboard-component-tabs-content { + min-height: 48px; +} .dashboard-component-tabs .nav-tabs { border-bottom: none; @@ -88,3 +91,65 @@ background: inherit; color: #000000; } + +/* New components */ +.new-component { + display: flex; + flex-direction: row; + flex-wrap: nowrap; + align-items: center; + padding: 16px; + background: white; +} + +.new-component-placeholder { + background: #f5f5f5; + width: 40px; + height: 40px; + margin-right: 16px; + box-shadow: 0 0 1px #fff; +} + +/* Spacer */ +.grid-container { + flex-grow: 1; + min-width: 66%; + margin: 24px; + height: 100%; + position: relative; +} + +.grid-column { + width: 100%; + min-height: 48px; +} + +.grid-row { + display: flex; + flex-direction: row; + flex-wrap: wrap; + align-items: flex-start; + min-height: 48px; + width: 100%; + height: fit-content; + background-color: transparent; +} + +.grid-row--empty { + content: "Empty row"; + padding: 16px; +} + +.grid-row-container { + background-color: #fff; +} + +.grid-spacer { + width: 100%; + height: 100%; + background-color: transparent; +} + +.dragdroppable:hover .grid-spacer { + box-shadow: inset 0 0 0 1px #CFD8DC; +} diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/grid.css b/superset/assets/javascripts/dashboard/v2/components/gridComponents/grid.css index 5b096303ddb3b..6119eabefcbea 100644 --- a/superset/assets/javascripts/dashboard/v2/components/gridComponents/grid.css +++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/grid.css @@ -1,38 +1,3 @@ -.grid-container { - flex-grow: 1; - min-width: 66%; - margin: 24px; - height: 100%; - position: relative; -} - -.grid-column { - width: 100%; - min-height: 16px; -} - -.grid-row { - display: flex; - flex-direction: row; - flex-wrap: wrap; - align-items: flex-start; - min-height: 16px; - width: 100%; - height: fit-content; - background-color: transparent; -} - -.grid-row-container { - background-color: #fff; -} - -.grid-spacer { - width: 100%; - height: 100%; - background-color: transparent; - box-shadow: inset 0 0 0 1px #CFD8DC; -} - /* Editing guides */ .grid-column-guide { position: absolute; diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/index.js b/superset/assets/javascripts/dashboard/v2/components/gridComponents/index.js index e6a58db9690ff..1d1e4b3b752ad 100644 --- a/superset/assets/javascripts/dashboard/v2/components/gridComponents/index.js +++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/index.js @@ -27,7 +27,7 @@ export { default as Row } from './Row'; export { default as Spacer } from './Spacer'; export { default as Tabs } from './Tabs'; -export const COMPONENT_TYPE_LOOKUP = { +export default { [CHART_TYPE]: Chart, [COLUMN_TYPE]: Column, [DIVIDER_TYPE]: Divider, diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/new/DraggableNewComponent.jsx b/superset/assets/javascripts/dashboard/v2/components/gridComponents/new/DraggableNewComponent.jsx new file mode 100644 index 0000000000000..a0399c917ff23 --- /dev/null +++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/new/DraggableNewComponent.jsx @@ -0,0 +1,31 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import DragDroppable from '../../dnd/DragDroppable'; + +const propTypes = { + // id: PropTypes.string.isRequired, + type: PropTypes.string.isRequired, + label: PropTypes.string.isRequired, +}; + +export default class DraggableNewComponent extends React.PureComponent { + render() { + const { type, label } = this.props; + return ( + +
    +
    + {label} +
    + + ); + } +} + +DraggableNewComponent.propTypes = propTypes; diff --git a/superset/assets/javascripts/dashboard/v2/components/dnd/DraggableNewChart.jsx b/superset/assets/javascripts/dashboard/v2/components/gridComponents/new/NewChart.jsx similarity index 86% rename from superset/assets/javascripts/dashboard/v2/components/dnd/DraggableNewChart.jsx rename to superset/assets/javascripts/dashboard/v2/components/gridComponents/new/NewChart.jsx index 5c36742c28169..6c9d46541962c 100644 --- a/superset/assets/javascripts/dashboard/v2/components/dnd/DraggableNewChart.jsx +++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/new/NewChart.jsx @@ -1,7 +1,7 @@ import React from 'react'; // import PropTypes from 'prop-types'; -import { CHART_TYPE } from '../../util/componentTypes'; +import { CHART_TYPE } from '../../../util/componentTypes'; import DraggableNewComponent from './DraggableNewComponent'; const propTypes = { diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/new/NewColumn.jsx b/superset/assets/javascripts/dashboard/v2/components/gridComponents/new/NewColumn.jsx new file mode 100644 index 0000000000000..4858077f86fbb --- /dev/null +++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/new/NewColumn.jsx @@ -0,0 +1,21 @@ +import React from 'react'; +// import PropTypes from 'prop-types'; + +import { COLUMN_TYPE } from '../../../util/componentTypes'; +import DraggableNewComponent from './DraggableNewComponent'; + +const propTypes = { +}; + +export default class DraggableNewColumn extends React.PureComponent { + render() { + return ( + + ); + } +} + +DraggableNewColumn.propTypes = propTypes; diff --git a/superset/assets/javascripts/dashboard/v2/components/dnd/DraggableNewDivider.jsx b/superset/assets/javascripts/dashboard/v2/components/gridComponents/new/NewDivider.jsx similarity index 86% rename from superset/assets/javascripts/dashboard/v2/components/dnd/DraggableNewDivider.jsx rename to superset/assets/javascripts/dashboard/v2/components/gridComponents/new/NewDivider.jsx index e2b3f99814b05..90aea4e47a421 100644 --- a/superset/assets/javascripts/dashboard/v2/components/dnd/DraggableNewDivider.jsx +++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/new/NewDivider.jsx @@ -1,7 +1,7 @@ import React from 'react'; // import PropTypes from 'prop-types'; -import { DIVIDER_TYPE } from '../../util/componentTypes'; +import { DIVIDER_TYPE } from '../../../util/componentTypes'; import DraggableNewComponent from './DraggableNewComponent'; const propTypes = { diff --git a/superset/assets/javascripts/dashboard/v2/components/dnd/DraggableNewHeader.jsx b/superset/assets/javascripts/dashboard/v2/components/gridComponents/new/NewHeader.jsx similarity index 86% rename from superset/assets/javascripts/dashboard/v2/components/dnd/DraggableNewHeader.jsx rename to superset/assets/javascripts/dashboard/v2/components/gridComponents/new/NewHeader.jsx index 6d1c76f4300a2..7b5cfd65ffa20 100644 --- a/superset/assets/javascripts/dashboard/v2/components/dnd/DraggableNewHeader.jsx +++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/new/NewHeader.jsx @@ -1,7 +1,7 @@ import React from 'react'; // import PropTypes from 'prop-types'; -import { HEADER_TYPE } from '../../util/componentTypes'; +import { HEADER_TYPE } from '../../../util/componentTypes'; import DraggableNewComponent from './DraggableNewComponent'; const propTypes = { diff --git a/superset/assets/javascripts/dashboard/v2/components/dnd/DraggableNewRow.jsx b/superset/assets/javascripts/dashboard/v2/components/gridComponents/new/NewRow.jsx similarity index 85% rename from superset/assets/javascripts/dashboard/v2/components/dnd/DraggableNewRow.jsx rename to superset/assets/javascripts/dashboard/v2/components/gridComponents/new/NewRow.jsx index a4ecf0d8c9c34..af20f9d488365 100644 --- a/superset/assets/javascripts/dashboard/v2/components/dnd/DraggableNewRow.jsx +++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/new/NewRow.jsx @@ -1,6 +1,6 @@ import React from 'react'; -import { ROW_TYPE } from '../../util/componentTypes'; +import { ROW_TYPE } from '../../../util/componentTypes'; import DraggableNewComponent from './DraggableNewComponent'; const propTypes = { diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/new/NewSpacer.jsx b/superset/assets/javascripts/dashboard/v2/components/gridComponents/new/NewSpacer.jsx new file mode 100644 index 0000000000000..5db1ca759a9cc --- /dev/null +++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/new/NewSpacer.jsx @@ -0,0 +1,21 @@ +import React from 'react'; +// import PropTypes from 'prop-types'; + +import { SPACER_TYPE } from '../../../util/componentTypes'; +import DraggableNewComponent from './DraggableNewComponent'; + +const propTypes = { +}; + +export default class DraggableNewChart extends React.PureComponent { + render() { + return ( + + ); + } +} + +DraggableNewChart.propTypes = propTypes; diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/new/NewTabs.jsx b/superset/assets/javascripts/dashboard/v2/components/gridComponents/new/NewTabs.jsx new file mode 100644 index 0000000000000..adacc8e217cf2 --- /dev/null +++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/new/NewTabs.jsx @@ -0,0 +1,21 @@ +import React from 'react'; +// import PropTypes from 'prop-types'; + +import { TABS_TYPE } from '../../../util/componentTypes'; +import DraggableNewComponent from './DraggableNewComponent'; + +const propTypes = { +}; + +export default class DraggableNewTabs extends React.PureComponent { + render() { + return ( + + ); + } +} + +DraggableNewTabs.propTypes = propTypes; diff --git a/superset/assets/javascripts/dashboard/v2/components/resizable/DimensionProvider.jsx b/superset/assets/javascripts/dashboard/v2/components/resizable/DimensionProvider.jsx new file mode 100644 index 0000000000000..063c27d02bff9 --- /dev/null +++ b/superset/assets/javascripts/dashboard/v2/components/resizable/DimensionProvider.jsx @@ -0,0 +1,80 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +import ResizableContainer from './ResizableContainer'; +import componentIsResizable from '../../util/componentIsResizable'; +import { componentShape } from '../../util/propShapes'; + +import { + GRID_GUTTER_SIZE, + GRID_ROW_HEIGHT_UNIT, + GRID_MIN_COLUMN_COUNT, + GRID_MIN_ROW_UNITS, + GRID_MAX_ROW_UNITS, +} from '../../util/constants'; + +import { SPACER_TYPE, COLUMN_TYPE } from '../../util/componentTypes'; + +const propTypes = { + availableColumnCount: PropTypes.number.isRequired, + children: PropTypes.node.isRequired, + columnWidth: PropTypes.number.isRequired, + component: componentShape.isRequired, + rowHeight: PropTypes.number, + onResizeStart: PropTypes.func.isRequired, + onResize: PropTypes.func.isRequired, + onResizeStop: PropTypes.func.isRequired, +}; + +const defaultProps = { + rowHeight: 0, +}; + +class DimensionProvider extends React.PureComponent { + render() { + const { + availableColumnCount, + children, + columnWidth, + rowHeight, + component, + onResizeStart, + onResize, + onResizeStop, + } = this.props; + + const isResizable = componentIsResizable(component); + const isSpacer = component.type === SPACER_TYPE; + + if (!isResizable) return children; + + return ( + + {children} + + ); + } +} + +DimensionProvider.propTypes = propTypes; +DimensionProvider.defaultProps = defaultProps; + +export default DimensionProvider; diff --git a/superset/assets/javascripts/dashboard/v2/components/resizable/ResizableContainer.jsx b/superset/assets/javascripts/dashboard/v2/components/resizable/ResizableContainer.jsx index 92d93e44ce16a..9037549246edd 100644 --- a/superset/assets/javascripts/dashboard/v2/components/resizable/ResizableContainer.jsx +++ b/superset/assets/javascripts/dashboard/v2/components/resizable/ResizableContainer.jsx @@ -46,7 +46,7 @@ const defaultProps = { }; // 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 columns on resize stop +// we snap to the base unit and then snap to actual column multiples on stop const snapToGrid = [GRID_BASE_UNIT, GRID_BASE_UNIT]; class ResizableContainer extends React.PureComponent { diff --git a/superset/assets/javascripts/dashboard/v2/fixtures/testLayout.js b/superset/assets/javascripts/dashboard/v2/fixtures/testLayout.js index 2d46e2ba13287..853b922284044 100644 --- a/superset/assets/javascripts/dashboard/v2/fixtures/testLayout.js +++ b/superset/assets/javascripts/dashboard/v2/fixtures/testLayout.js @@ -7,29 +7,31 @@ import { 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', + // 'divider0', + // 'row1', + // 'tabs0', + // 'divider1', ], }, row0: { id: 'row0', - type: INVISIBLE_ROW_TYPE, + type: ROW_TYPE, children: [ 'charta', - 'chartb', - 'chartc', + // 'chartb', + // 'chartc', ], }, row1: { @@ -107,7 +109,7 @@ export default { id: 'chartd', type: CHART_TYPE, meta: { - width: 6, + width: 3, height: 10, }, }, @@ -115,7 +117,7 @@ export default { id: 'charte', type: CHART_TYPE, meta: { - width: 6, + width: 3, height: 10, }, }, diff --git a/superset/assets/javascripts/dashboard/v2/util/isValidChild.js b/superset/assets/javascripts/dashboard/v2/util/isValidChild.js index fddc9cab42d43..3319ff601a604 100644 --- a/superset/assets/javascripts/dashboard/v2/util/isValidChild.js +++ b/superset/assets/javascripts/dashboard/v2/util/isValidChild.js @@ -26,7 +26,7 @@ const typeToValidChildType = { [MARKDOWN_TYPE]: true, [COLUMN_TYPE]: true, [SPACER_TYPE]: true, - [HEADER_TYPE]: true, + // [HEADER_TYPE]: true, }, [INVISIBLE_ROW_TYPE]: { diff --git a/superset/assets/javascripts/dashboard/v2/util/propShapes.jsx b/superset/assets/javascripts/dashboard/v2/util/propShapes.jsx index 3c11d2a8b3177..320b692cddb93 100644 --- a/superset/assets/javascripts/dashboard/v2/util/propShapes.jsx +++ b/superset/assets/javascripts/dashboard/v2/util/propShapes.jsx @@ -1,12 +1,27 @@ import PropTypes from 'prop-types'; +import componentTypes from './componentTypes'; -export const entityShape = PropTypes.shape({ +export const componentShape = PropTypes.shape({ id: PropTypes.string.isRequired, + type: PropTypes.oneOf( + Object.values(componentTypes), + ).isRequired, childIds: PropTypes.arrayOf(PropTypes.string), - parentId: PropTypes.string.isRequired, - type: PropTypes.string.isRequired, // @TODO enumerate meta: PropTypes.shape({ + // Dimensions width: PropTypes.number, height: PropTypes.number, + + // Header + text: PropTypes.string, }), }); + +// export const gridPropsShape = PropTypes.shape({ +// availableColumnCount: PropTypes.number.isRequired, +// columnWidth: PropTypes.number.isRequired, +// rowWidth: PropTypes.number.isRequired, +// onResizeStart: PropTypes.func.isRequired, +// onResize: PropTypes.func.isRequired, +// onResizeStop: PropTypes.func.isRequired, +// }); diff --git a/superset/assets/stylesheets/dashboard-v2.css b/superset/assets/stylesheets/dashboard-v2.css index c30f9e12535bf..534a17eafa7f2 100644 --- a/superset/assets/stylesheets/dashboard-v2.css +++ b/superset/assets/stylesheets/dashboard-v2.css @@ -35,27 +35,6 @@ padding: 16px; } -.new-draggable-component { - display: flex; - flex-direction: row; - flex-wrap: nowrap; - align-items: center; - padding: 16px; - background: white; -} - - .new-draggable-component--dragging { - box-shadow: 0 0 1px #aaa; /* @TODO color */ - } - -.new-draggable-placeholder { - background: #f5f5f5; - width: 40px; - height: 40px; - margin-right: 16px; - box-shadow: 0 0 1px #fff; -} - /* @TODO remove upon new theme */ .btn.btn-primary { background: #263238 !important; From 86e9cd6fa5c193492f7b5bb3635b1b14a3f2212f Mon Sep 17 00:00:00 2001 From: Chris Williams Date: Tue, 13 Feb 2018 16:52:59 -0800 Subject: [PATCH 11/25] [dnd] factor out DashboardComponent, let components render dropInidcator and set draggableRef, add draggable tabs --- .../assets/javascripts/dashboard/v2/.eslintrc | 29 +++ .../v2/components/DashboardBuilder.jsx | 3 +- .../v2/components/DashboardComponent.jsx | 124 ---------- .../dashboard/v2/components/DashboardGrid.jsx | 47 ++-- .../v2/components/dnd/DragDroppable.jsx | 232 +++--------------- .../v2/components/dnd/DragHandle.jsx | 8 +- .../dashboard/v2/components/dnd/dnd.css | 2 + .../v2/components/dnd/dragDroppableConfig.js | 29 +-- .../dashboard/v2/components/dnd/handleDrop.js | 87 +++++++ .../v2/components/dnd/handleHover.js | 84 +++++++ .../v2/components/gridComponents/Chart.jsx | 67 ++++- .../v2/components/gridComponents/Column.jsx | 105 +++++--- .../v2/components/gridComponents/Divider.jsx | 46 +++- .../v2/components/gridComponents/Header.jsx | 41 +++- .../v2/components/gridComponents/Row.jsx | 97 +++++--- .../v2/components/gridComponents/Spacer.jsx | 81 +++++- .../v2/components/gridComponents/Tabs.jsx | 188 ++++++++++---- .../components/gridComponents/components.css | 48 +++- .../new/DraggableNewComponent.jsx | 19 +- .../gridComponents/new/NewChart.jsx | 1 + .../gridComponents/new/NewColumn.jsx | 1 + .../gridComponents/new/NewDivider.jsx | 1 + .../gridComponents/new/NewHeader.jsx | 1 + .../components/gridComponents/new/NewRow.jsx | 5 +- .../gridComponents/new/NewSpacer.jsx | 1 + .../components/gridComponents/new/NewTabs.jsx | 1 + .../resizable/DimensionProvider.jsx | 2 +- .../dashboard/v2/fixtures/testLayout.js | 39 ++- .../dashboard/v2/util/componentTypes.js | 2 + .../dashboard/v2/util/isValidChild.js | 6 +- .../dashboard/v2/util/newEntitiesFromDrop.js | 6 + .../dashboard/v2/util/propShapes.jsx | 26 +- 32 files changed, 916 insertions(+), 513 deletions(-) create mode 100644 superset/assets/javascripts/dashboard/v2/.eslintrc delete mode 100644 superset/assets/javascripts/dashboard/v2/components/DashboardComponent.jsx create mode 100644 superset/assets/javascripts/dashboard/v2/components/dnd/handleDrop.js create mode 100644 superset/assets/javascripts/dashboard/v2/components/dnd/handleHover.js diff --git a/superset/assets/javascripts/dashboard/v2/.eslintrc b/superset/assets/javascripts/dashboard/v2/.eslintrc new file mode 100644 index 0000000000000..70efc15a3ab35 --- /dev/null +++ b/superset/assets/javascripts/dashboard/v2/.eslintrc @@ -0,0 +1,29 @@ +{ + "rules": { + "prefer-template": 2, + "new-cap": 2, + "no-restricted-syntax": 2, + "guard-for-in": 2, + "prefer-arrow-callback": 2, + "func-names": 2, + "react/jsx-no-bind": 2, + "no-confusing-arrow": 2, + "jsx-a11y/no-static-element-interactions": 2, + "jsx-a11y/anchor-has-content": 2, + "react/require-default-props": 2, + "no-plusplus": 2, + "no-mixed-operators": 2, + "no-continue": 2, + "no-bitwise": 2, + "no-undef": 2, + "no-multi-assign": 2, + "no-restricted-properties": 2, + "no-prototype-builtins": 2, + "jsx-a11y/href-no-hash": 2, + "class-methods-use-this": 2, + "import/no-named-as-default": 2, + "import/prefer-default-export": 2, + "react/no-unescaped-entities": 2, + "react/no-string-refs": 2, + } +} diff --git a/superset/assets/javascripts/dashboard/v2/components/DashboardBuilder.jsx b/superset/assets/javascripts/dashboard/v2/components/DashboardBuilder.jsx index 33979121e97d0..f4d465eedf6bf 100644 --- a/superset/assets/javascripts/dashboard/v2/components/DashboardBuilder.jsx +++ b/superset/assets/javascripts/dashboard/v2/components/DashboardBuilder.jsx @@ -55,6 +55,7 @@ class DashboardBuilder extends React.Component { handleNewEntity(dropResult) { console.log('new entity'); this.setState(({ layout }) => { + debugger; const newEntities = newEntitiesFromDrop({ dropResult, entitiesMap: layout }); return { layout: { @@ -65,7 +66,7 @@ class DashboardBuilder extends React.Component { }); } - handleMoveEntity({ source, destination, draggableId }) { + handleMoveEntity({ source, destination }) { this.setState(({ layout }) => { const nextEntities = reorderItem({ entitiesMap: layout, diff --git a/superset/assets/javascripts/dashboard/v2/components/DashboardComponent.jsx b/superset/assets/javascripts/dashboard/v2/components/DashboardComponent.jsx deleted file mode 100644 index 2f9936ed0b1e6..0000000000000 --- a/superset/assets/javascripts/dashboard/v2/components/DashboardComponent.jsx +++ /dev/null @@ -1,124 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; - -import DimensionProvider from './resizable/DimensionProvider'; -import DragDroppable from './dnd/DragDroppable'; -import ComponentLookup from './gridComponents'; -import { componentShape } from '../util/propShapes'; - -const propTypes = { - component: componentShape.isRequired, - components: PropTypes.object.isRequired, - depth: PropTypes.number.isRequired, - index: PropTypes.number.isRequired, - parentId: PropTypes.string.isRequired, - - // grid related - availableColumnCount: PropTypes.number.isRequired, - columnWidth: PropTypes.number.isRequired, - rowHeight: PropTypes.number, - onResizeStart: PropTypes.func.isRequired, - onResize: PropTypes.func.isRequired, - onResizeStop: PropTypes.func.isRequired, -}; - -const defaultProps = { - rowHeight: null, -}; - -class DashboardComponent extends React.PureComponent { - constructor(props) { - super(props); - this.state = {}; - this.handleHoverDragDroppable = this.handleHoverDragDroppable.bind(this); - this.handleClick = this.handleClick.bind(this); - } - - componentDidMount() { - document.addEventListener('click', this.handleClick, true); - } - - componentWillUnmount() { - document.removeEventListener('click', this.handleClick, true); - } - - handleClick(event) { - if (!this.dragDroppable) return; - debugger; - if (!this.dragDroppable.contains(event.target)) { - console.log('outside click', this.props.component.type); - } else { - console.log('inside click', event.target === this.dragDroppable, this.props.component.type); - - } - } - - handleHoverDragDroppable(...args) { - if (this.hoverDragDroppable) this.hoverDragDroppable(...args); - } - - render() { - const { - component, - components, - depth, - index, - parentId, - availableColumnCount, - columnWidth, - rowHeight, - onResizeStart, - onResize, - onResizeStop, - onDrop, - bubbleUpHover, - } = this.props; - - const componentType = component.type; - const Component = ComponentLookup[componentType]; - - return ( - { this.dragDroppable = ref; }} - hoverRef={(hover) => { this.hoverDragDroppable = hover; }} - component={component} - components={components} - depth={depth} // @TODO might have to change this to direction: vertical/horizontal - index={index} - parentId={parentId} - onDrop={onDrop} - bubbleUpHover={bubbleUpHover} - > - - - - - ); - } -} - -DashboardComponent.propTypes = propTypes; -DashboardComponent.defaultProps = defaultProps; - -export default DashboardComponent; diff --git a/superset/assets/javascripts/dashboard/v2/components/DashboardGrid.jsx b/superset/assets/javascripts/dashboard/v2/components/DashboardGrid.jsx index 1af8b9aae3e8e..db25d89002c89 100644 --- a/superset/assets/javascripts/dashboard/v2/components/DashboardGrid.jsx +++ b/superset/assets/javascripts/dashboard/v2/components/DashboardGrid.jsx @@ -2,7 +2,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import ParentSize from '@vx/responsive/build/components/ParentSize'; import cx from 'classnames'; -import DashboardComponent from './DashboardComponent'; +import ComponentLookup from './gridComponents'; import { DASHBOARD_ROOT_ID, @@ -15,11 +15,13 @@ import './gridComponents/grid.css'; const propTypes = { layout: PropTypes.object, updateEntity: PropTypes.func, + onDrop: PropTypes.func, }; const defaultProps = { layout: {}, updateEntity() {}, + onDrop() {}, }; class DashboardGrid extends React.PureComponent { @@ -28,9 +30,6 @@ class DashboardGrid extends React.PureComponent { this.state = { isResizing: false, rowGuideTop: null, - // disableDrop: false, - // disableDrag: false, - // selectedComponentId: null, }; this.handleToggleSelectEntityId = this.handleToggleSelectEntityId.bind(this); @@ -94,7 +93,7 @@ class DashboardGrid extends React.PureComponent { } render() { - const { layout: components, onDrop, canDrop } = this.props; + const { layout: components, onDrop } = this.props; const { isResizing, rowGuideTop } = this.state; const rootComponent = components[DASHBOARD_ROOT_ID]; @@ -111,22 +110,28 @@ class DashboardGrid extends React.PureComponent { return width < 50 ? null : (
    - {(rootComponent.children || []).map((id, index) => ( - - ))} + {(rootComponent.children || []).map((id, index) => { + const component = components[id]; + const componentType = component.type; + const Component = ComponentLookup[componentType]; + + return ( + + ); + })} {isResizing && Array(GRID_COLUMN_COUNT).fill(null).map((_, i) => (
    ({ - dropIndicator: { - top: 0, - right: 8, - height: orientation === 'vertical' ? '100%' : 3, - width: orientation === 'vertical' ? 3 : '100%', - minHeight: orientation === 'vertical' ? 16 : null, - minWidth: orientation === 'vertical' ? null : 16, - margin: 'auto', - backgroundColor: '#44C0FF', - position: 'absolute', - zIndex: 10, - }, - })); - } else if (validSibling) { // indicate drop near parent - console.log('valid sibling', components[parentId].type, draggingItem.type); - const refBoundingRect = this.ref.getBoundingClientRect(); - const clientOffset = monitor.getClientOffset(); - - if (clientOffset) { - let dropOffset; - const orientation = containerOrientation; - if (orientation === 'horizontal') { - const refMiddleY = - refBoundingRect.top + (refBoundingRect.bottom - refBoundingRect.top) / 2; - dropOffset = clientOffset.y < refMiddleY ? 0 : refBoundingRect.height; - } else { - const refMiddleX = - refBoundingRect.left + (refBoundingRect.right - refBoundingRect.left) / 2; - dropOffset = clientOffset.x < refMiddleX ? 0 : refBoundingRect.width; - } - - this.setState(() => ({ - dropIndicator: { - top: orientation === 'vertical' ? 0 : dropOffset, - left: orientation === 'vertical' ? dropOffset : 0, - height: orientation === 'vertical' ? '100%' : 3, - width: orientation === 'vertical' ? 3 : '100%', - backgroundColor: '#44C0FF', - position: 'absolute', - zIndex: 10, - }, - })); - } - } else if (bubbleUpHover) { - // console.log('bubble'); - // debugger; - // bubbleUpHover(monitor); - } - } - - handleDrop(monitor) { - console.log('drop'); - - const { components, component, parentId, index: componentIndex, onDrop, depth } = this.props; - const draggingItem = monitor.getItem(); - - // if dropped self on self, do nothing - if (!draggingItem || draggingItem.draggableId === component.id) { - console.log(draggingItem ? 'drag self' : 'no item'); - return undefined; - } - - // append to self, or parent - const validChild = isValidChild({ - parentType: component.type, - childType: draggingItem.type, - }); - - const validSibling = isValidChild({ - parentType: components[parentId] && components[parentId].type, - childType: draggingItem.type, - }); - - if (!validChild && !validSibling) { - console.log('not valid child or sibling') - return undefined; - } - - const dropResult = { - source: draggingItem.parentId ? { - droppableId: draggingItem.parentId, - index: draggingItem.index, - } : null, - draggableId: draggingItem.draggableId, - }; - - if (validChild) { // append it to component.children - dropResult.destination = { - droppableId: component.id, - index: component.children.length, - }; - } else { // insert as sibling - const containerOrientation = depth % 2 === 0 ? 'horizontal' : 'vertical'; - let nextIndex = componentIndex; - const refBoundingRect = this.ref.getBoundingClientRect(); - const clientOffset = monitor.getClientOffset(); - - if (clientOffset) { - if (containerOrientation === 'horizontal') { - const refMiddleY = - refBoundingRect.top + (refBoundingRect.bottom - refBoundingRect.top) / 2; - nextIndex += clientOffset.y >= refMiddleY ? 1 : 0; - } else { - const refMiddleX = - refBoundingRect.left + (refBoundingRect.right - refBoundingRect.left) / 2; - nextIndex += clientOffset.x >= refMiddleX ? 1 : 0; - } - } - - if (draggingItem.parentId === parentId && draggingItem.index < componentIndex) { - nextIndex = Math.max(0, nextIndex - 1); - } - - dropResult.destination = { - droppableId: parentId, - index: nextIndex, - }; - } - - onDrop(dropResult); - - return dropResult; - } - - renderDropIndicator() { - const { dropIndicator } = this.state; - const { isDraggingOverShallow } = this.props; - return isDraggingOverShallow && dropIndicator && ( -
    - ); + hover(props, monitor, Component) { + this.props.handleHover(props, monitor, Component); } render() { const { children, - depth, + orientation, droppableRef, dragSourceRef, dragPreviewRef, - // dropIndicator, - innerRef, isDragging, - useChildAsDragHandle, + isDraggingOver, } = this.props; + const { dropIndicator } = this.state; + return (
    { this.ref = ref; dragPreviewRef(ref); droppableRef(ref); - if (innerRef) innerRef(ref); }} className={cx( 'dragdroppable', - depth % 2 === 0 ? 'dragdroppable-row' : 'dragdroppable-column', + orientation === 'horizontal' && 'dragdroppable-row', + orientation === 'vertical' && 'dragdroppable-column', isDragging && 'dragdroppable--dragging', )} > - - {useChildAsDragHandle && -
    {children}
    } - - {!useChildAsDragHandle && children} - - {!useChildAsDragHandle && - } - - {this.renderDropIndicator()} + {children({ + dragSourceRef, + dropIndicatorProps: isDraggingOver && dropIndicator && { + className: 'drop-indicator', + style: dropIndicator, + }, + })}
    ); } @@ -267,7 +105,7 @@ DragDroppable.propTypes = propTypes; DragDroppable.defaultProps = defaultProps; // note that the composition order here determines using -// component.method() vs decoratedComponentInstance.method() in the above config +// component.method() vs decoratedComponentInstance.method() in the drag/drop config export default DropTarget(...dropConfig)( DragSource(...dragConfig)(DragDroppable), ); diff --git a/superset/assets/javascripts/dashboard/v2/components/dnd/DragHandle.jsx b/superset/assets/javascripts/dashboard/v2/components/dnd/DragHandle.jsx index 8b9d98192d678..7a8c584c459e5 100644 --- a/superset/assets/javascripts/dashboard/v2/components/dnd/DragHandle.jsx +++ b/superset/assets/javascripts/dashboard/v2/components/dnd/DragHandle.jsx @@ -5,26 +5,28 @@ import cx from 'classnames'; const propTypes = { position: PropTypes.oneOf(['left', 'top']), innerRef: PropTypes.func, + dotCount: PropTypes.number, }; const defaultProps = { position: 'left', innerRef: null, + dotCount: 8, }; export default class DragHandle extends React.PureComponent { render() { - const { innerRef, position } = this.props; + const { innerRef, position, dotCount } = this.props; return (
    - {Array(8).fill(null).map((_, i) => ( + {Array(dotCount).fill(null).map((_, i) => (
    ))}
    diff --git a/superset/assets/javascripts/dashboard/v2/components/dnd/dnd.css b/superset/assets/javascripts/dashboard/v2/components/dnd/dnd.css index 542490f8f25c6..bbcfd5172bc60 100644 --- a/superset/assets/javascripts/dashboard/v2/components/dnd/dnd.css +++ b/superset/assets/javascripts/dashboard/v2/components/dnd/dnd.css @@ -95,3 +95,5 @@ .dragdroppable-row .dragdroppable-row:hover .draggable-row-item-handle { opacity: 1; } + +/*top: -20px;*/ diff --git a/superset/assets/javascripts/dashboard/v2/components/dnd/dragDroppableConfig.js b/superset/assets/javascripts/dashboard/v2/components/dnd/dragDroppableConfig.js index 993632d0c07b5..5e2c7d0b1dfff 100644 --- a/superset/assets/javascripts/dashboard/v2/components/dnd/dragDroppableConfig.js +++ b/superset/assets/javascripts/dashboard/v2/components/dnd/dragDroppableConfig.js @@ -1,4 +1,5 @@ -const TYPE = 'DRAG_DROPPABLE'; // 'type' hook is not useful for us +// note: the 'type' hook is not useful for us as dropping is contigent on other properties +const TYPE = 'DRAG_DROPPABLE'; export const dragConfig = [ TYPE, @@ -22,25 +23,25 @@ export const dropConfig = [ { hover(props, monitor, component) { if ( - monitor.isOver({ shallow: true }) - && component + component && component.decoratedComponentInstance && component.decoratedComponentInstance.handleHover - ) { - component.decoratedComponentInstance.handleHover(monitor); + ) { // use the component instance so we can throttle calls + component.decoratedComponentInstance.handleHover( + props, + monitor, + component.decoratedComponentInstance, + ); } }, // note: - // it's important that the drop() method return a result in order for the react-dnd - // monitor.didDrop() method to bubble up properly up nested droppables + // the react-dnd api requires that the drop() method return a result or undefined + // monitor.didDrop() cannot be used because it returns true only for the most-nested target drop(props, monitor, component) { - if ( - !monitor.didDrop() - && component - && component.decoratedComponentInstance - && component.decoratedComponentInstance.handleDrop - ) { - return component.decoratedComponentInstance.handleDrop(monitor); + const Component = component.decoratedComponentInstance; + const dropResult = monitor.getDropResult(); + if ((!dropResult || !dropResult.destination) && Component.props.handleDrop) { + return Component.props.handleDrop(props, monitor, Component); } return undefined; }, diff --git a/superset/assets/javascripts/dashboard/v2/components/dnd/handleDrop.js b/superset/assets/javascripts/dashboard/v2/components/dnd/handleDrop.js new file mode 100644 index 0000000000000..427ce14c04ea7 --- /dev/null +++ b/superset/assets/javascripts/dashboard/v2/components/dnd/handleDrop.js @@ -0,0 +1,87 @@ +export default function handleDrop(props, monitor, Component) { + Component.setState(() => ({ dropIndicator: null })); + const { + components, + component, + parentId, + index: componentIndex, + onDrop, + orientation, + isValidChild, + isValidSibling, + } = Component.props; + + const draggingItem = monitor.getItem(); + + // if dropped self on self, do nothing + if (!draggingItem || draggingItem.draggableId === component.id) { + console.log(draggingItem ? 'drop self' : 'drop no item'); + return undefined; + } + + // append to self, or parent + const validChild = isValidChild({ + parentType: component.type, + childType: draggingItem.type, + }); + + const validSibling = isValidSibling({ + parentType: components[parentId] && components[parentId].type, + siblingType: draggingItem.type, + }); + + if (!validChild && !validSibling) { + console.log('not valid drop child or sibling') + return undefined; + } + + const dropResult = { + source: draggingItem.parentId ? { + droppableId: draggingItem.parentId, + index: draggingItem.index, + } : null, + draggableId: draggingItem.draggableId, + }; + + if (validChild) { // append it to component.children + dropResult.destination = { + droppableId: component.id, + index: component.children.length, + }; + } else { // insert as sibling + let nextIndex = componentIndex; + const refBoundingRect = Component.ref.getBoundingClientRect(); + const clientOffset = monitor.getClientOffset(); + + if (clientOffset) { + if (orientation === 'horizontal') { + const refMiddleY = + refBoundingRect.top + ((refBoundingRect.bottom - refBoundingRect.top) / 2); + nextIndex += clientOffset.y >= refMiddleY ? 1 : 0; + } else { + const refMiddleX = + refBoundingRect.left + ((refBoundingRect.right - refBoundingRect.left) / 2); + nextIndex += clientOffset.x >= refMiddleX ? 1 : 0; + } + } + + // if the item is in the same list with a smaller index, you must account for the + // "missing" index upon movement within the list + if ( + draggingItem.parentId + && draggingItem.parentId === parentId + && draggingItem.index < nextIndex + ) { + nextIndex = Math.max(0, nextIndex - 1); + } + + dropResult.destination = { + droppableId: parentId, + index: nextIndex, + }; + } + + onDrop(dropResult); + + return dropResult; +} diff --git a/superset/assets/javascripts/dashboard/v2/components/dnd/handleHover.js b/superset/assets/javascripts/dashboard/v2/components/dnd/handleHover.js new file mode 100644 index 0000000000000..c83bd1944c56d --- /dev/null +++ b/superset/assets/javascripts/dashboard/v2/components/dnd/handleHover.js @@ -0,0 +1,84 @@ +export default function handleHover(props, monitor, Component) { + const { + component, + components, + parentId, + orientation, + isDraggingOverShallow, + isValidChild, + isValidSibling, + } = Component.props; + + const draggingItem = monitor.getItem(); + + if (!draggingItem || draggingItem.draggableId === component.id) { + Component.setState(() => ({ dropIndicator: null })); + console.log(draggingItem ? 'drag self' : 'no item'); + return; + } + + const validChild = isValidChild({ + parentType: component.type, + childType: draggingItem.type, + }); + + const validSibling = isValidSibling({ + parentType: components[parentId] && components[parentId].type, + siblingType: draggingItem.type, + }); + + if (validChild && !isDraggingOverShallow) { + Component.setState(() => ({ dropIndicator: null })); + return; + } + + if (validChild) { // indicate drop in container + console.log('valid child', component.type, draggingItem.type); + const indicatorOrientation = orientation === 'horizontal' ? 'vertical' : 'horizontal'; + + Component.setState(() => ({ + dropIndicator: { + top: 0, + right: component.children.length ? 8 : null, + left: component.children.length ? null : null, + height: indicatorOrientation === 'vertical' ? '100%' : 3, + width: indicatorOrientation === 'vertical' ? 3 : '100%', + minHeight: indicatorOrientation === 'vertical' ? 16 : null, + minWidth: indicatorOrientation === 'vertical' ? null : 16, + margin: 'auto', + backgroundColor: '#44C0FF', + position: 'absolute', + zIndex: 10, + }, + })); + } else if (validSibling) { // indicate drop near parent + console.log('valid sibling', components[parentId].type, draggingItem.type); + const refBoundingRect = Component.ref.getBoundingClientRect(); + const clientOffset = monitor.getClientOffset(); + + if (clientOffset) { + let dropOffset; + if (orientation === 'horizontal') { + const refMiddleY = + refBoundingRect.top + ((refBoundingRect.bottom - refBoundingRect.top) / 2); + dropOffset = clientOffset.y < refMiddleY ? 0 : refBoundingRect.height; + } else { + const refMiddleX = + refBoundingRect.left + ((refBoundingRect.right - refBoundingRect.left) / 2); + dropOffset = clientOffset.x < refMiddleX ? 0 : refBoundingRect.width; + } + + Component.setState(() => ({ + dropIndicator: { + top: orientation === 'vertical' ? 0 : dropOffset, + left: orientation === 'vertical' ? dropOffset : 0, + height: orientation === 'vertical' ? '100%' : 3, + width: orientation === 'vertical' ? 3 : '100%', + backgroundColor: '#44C0FF', + position: 'absolute', + zIndex: 10, + }, + })); + } + } +} diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Chart.jsx b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Chart.jsx index 8133db8b25c31..99e8b1b8e82cd 100644 --- a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Chart.jsx +++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Chart.jsx @@ -1,10 +1,31 @@ import React from 'react'; -// import PropTypes from 'prop-types'; +import PropTypes from 'prop-types'; + +import DimensionProvider from '../resizable/DimensionProvider'; +import DragDroppable from '../dnd/DragDroppable'; +import DragHandle from '../dnd/DragHandle'; +import { componentShape } from '../../util/propShapes'; const propTypes = { + component: componentShape.isRequired, + components: PropTypes.object.isRequired, + index: PropTypes.number.isRequired, + parentId: PropTypes.string.isRequired, + + // grid related + availableColumnCount: PropTypes.number.isRequired, + columnWidth: PropTypes.number.isRequired, + rowHeight: PropTypes.number, + onResizeStart: PropTypes.func.isRequired, + onResize: PropTypes.func.isRequired, + onResizeStop: PropTypes.func.isRequired, + + // dnd + onDrop: PropTypes.func.isRequired, }; const defaultProps = { + rowHeight: null, }; class Chart extends React.Component { @@ -15,8 +36,50 @@ class Chart extends React.Component { } render() { + const { + component, + components, + index, + parentId, + availableColumnCount, + columnWidth, + rowHeight, + onResizeStart, + onResize, + onResizeStop, + onDrop, + } = this.props; + return ( -
    Chart
    + + {({ dropIndicatorProps, dragSourceRef }) => ( + + +
    + Chart +
    + {dropIndicatorProps &&
    } + + )} + ); } } diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Column.jsx b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Column.jsx index d1d9b93d6fbe9..8915353f2a526 100644 --- a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Column.jsx +++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Column.jsx @@ -2,7 +2,10 @@ import React from 'react'; import PropTypes from 'prop-types'; import cx from 'classnames'; -import DashboardComponent from '../DashboardComponent'; +import DragDroppable from '../dnd/DragDroppable'; +import DragHandle from '../dnd/DragHandle'; +import DimensionProvider from '../resizable/DimensionProvider'; +import ComponentLookup from '../gridComponents'; import { componentShape } from '../../util/propShapes'; import { GRID_GUTTER_SIZE } from '../../util/constants'; @@ -10,8 +13,10 @@ import { GRID_GUTTER_SIZE } from '../../util/constants'; const propTypes = { component: componentShape.isRequired, components: PropTypes.object.isRequired, + index: PropTypes.number.isRequired, depth: PropTypes.number.isRequired, parentId: PropTypes.string.isRequired, + rowHeight: PropTypes.number, // grid related availableColumnCount: PropTypes.number.isRequired, @@ -19,9 +24,13 @@ const propTypes = { onResizeStart: PropTypes.func.isRequired, onResize: PropTypes.func.isRequired, onResizeStop: PropTypes.func.isRequired, + + // dnd + onDrop: PropTypes.func.isRequired, }; const defaultProps = { + rowHeight: null, }; class Column extends React.PureComponent { @@ -29,9 +38,12 @@ class Column extends React.PureComponent { const { component: columnComponent, components, - depth, + index, + parentId, availableColumnCount, columnWidth, + rowHeight, + depth, onResizeStart, onResize, onResizeStop, @@ -40,40 +52,73 @@ class Column extends React.PureComponent { const columnItems = []; - (columnComponent.children || []).forEach((id, index) => { + (columnComponent.children || []).forEach((id, childIndex) => { const component = components[id]; columnItems.push(component); - if (index < columnComponent.children.length - 1) columnItems.push(`gutter-${index}`); + if (index < columnComponent.children.length - 1) { + columnItems.push(`gutter-${childIndex}`); + } }); return ( -
    - {columnItems.map((component, index) => ( - !component.id ? ( -
    - ) : ( - - )))} -
    + {({ dropIndicatorProps, dragSourceRef }) => ( + +
    + + + {columnItems.map((component, itemIndex) => { + if (!component.id) { + return
    ; + } + const { type: componentType } = component; + const Component = ComponentLookup[componentType]; + 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 d6fe42f4bb4d7..b7f84a4162022 100644 --- a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Divider.jsx +++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Divider.jsx @@ -1,13 +1,53 @@ import React from 'react'; +import PropTypes from 'prop-types'; + +import DragDroppable from '../dnd/DragDroppable'; +import DragHandle from '../dnd/DragHandle'; +import { componentShape } from '../../util/propShapes'; + +const propTypes = { + component: componentShape.isRequired, + components: PropTypes.object.isRequired, + index: PropTypes.number.isRequired, + parentId: PropTypes.string.isRequired, + onDrop: PropTypes.func.isRequired, +}; class Divider extends React.PureComponent { render() { + const { + component, + components, + index, + parentId, + onDrop, + } = this.props; + return ( -
    -
    -
    + + {({ dropIndicatorProps, dragSourceRef }) => ( +
    + + +
    +
    +
    + + {dropIndicatorProps &&
    } +
    + )} + ); } } +Divider.propTypes = propTypes; + export default Divider; diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Header.jsx b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Header.jsx index 07eb4aa7c96c4..008a05aa84820 100644 --- a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Header.jsx +++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Header.jsx @@ -1,10 +1,17 @@ import React from 'react'; import PropTypes from 'prop-types'; +import DragDroppable from '../dnd/DragDroppable'; +import DragHandle from '../dnd/DragHandle'; import { componentShape } from '../../util/propShapes'; const propTypes = { component: componentShape.isRequired, + components: PropTypes.object.isRequired, + index: PropTypes.number.isRequired, + parentId: PropTypes.string.isRequired, + // dnd + onDrop: PropTypes.func.isRequired, }; const defaultProps = { @@ -18,11 +25,35 @@ class Header extends React.PureComponent { } render() { - const { component: { id, meta } } = this.props; - return !meta || !id ? null : ( -
    - {meta.text} -
    + const { + component, + components, + index, + parentId, + onDrop, + } = this.props; + + return ( + + {({ dropIndicatorProps, dragSourceRef }) => ( +
    + + +
    + {component.meta.text} +
    + + {dropIndicatorProps &&
    } +
    + )} + ); } } diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Row.jsx b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Row.jsx index 662c987c93637..e02106ae46898 100644 --- a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Row.jsx +++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Row.jsx @@ -2,7 +2,9 @@ import React from 'react'; import PropTypes from 'prop-types'; import cx from 'classnames'; -import DashboardComponent from '../DashboardComponent'; +import DragDroppable from '../dnd/DragDroppable'; +import DragHandle from '../dnd/DragHandle'; +import ComponentLookup from '../gridComponents'; import { componentShape } from '../../util/propShapes'; import { GRID_GUTTER_SIZE } from '../../util/constants'; import { INVISIBLE_ROW_TYPE } from '../../util/componentTypes'; @@ -10,18 +12,24 @@ import { INVISIBLE_ROW_TYPE } from '../../util/componentTypes'; const propTypes = { component: componentShape.isRequired, components: PropTypes.object.isRequired, + index: PropTypes.number.isRequired, depth: PropTypes.number.isRequired, parentId: PropTypes.string.isRequired, // grid related availableColumnCount: PropTypes.number.isRequired, columnWidth: PropTypes.number.isRequired, + rowHeight: PropTypes.number, onResizeStart: PropTypes.func.isRequired, onResize: PropTypes.func.isRequired, onResizeStop: PropTypes.func.isRequired, + + // dnd + onDrop: PropTypes.func.isRequired, }; const defaultProps = { + rowHeight: null, }; class Row extends React.PureComponent { @@ -29,14 +37,15 @@ class Row extends React.PureComponent { const { component: rowComponent, components, - depth, + index, + parentId, availableColumnCount, columnWidth, + depth, onResizeStart, onResize, onResizeStop, onDrop, - bubbleUpHover, } = this.props; let occupiedColumnCount = 0; @@ -44,46 +53,70 @@ class Row extends React.PureComponent { const rowItems = []; // this adds a gutter between each child in the row. - (rowComponent.children || []).forEach((id, index) => { + (rowComponent.children || []).forEach((id, childIndex) => { const component = components[id]; occupiedColumnCount += (component.meta || {}).width || 0; rowItems.push(component); - if (index < rowComponent.children.length - 1) rowItems.push(`gutter-${index}`); + if (childIndex < rowComponent.children.length - 1) { + rowItems.push(`gutter-${childIndex}`); + } if ((component.meta || {}).height) { rowHeight = Math.max(rowHeight, component.meta.height); } }); return ( -
    - {rowItems.map((component, index) => ( - !component.id ? ( -
    - ) : ( - ( +
    + - )))} -
    + + {rowItems.map((component, itemIndex) => { + if (!component.id) { + return
    ; + } + + const { type: componentType } = component; + const Component = ComponentLookup[componentType]; + return ( + + ); + })} + {dropIndicatorProps && +
    } +
    + )} + ); } } diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Spacer.jsx b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Spacer.jsx index 2cd2755ede8e2..10f896571d5b3 100644 --- a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Spacer.jsx +++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Spacer.jsx @@ -1,9 +1,88 @@ import React from 'react'; +import PropTypes from 'prop-types'; + +import DimensionProvider from '../resizable/DimensionProvider'; +import DragDroppable from '../dnd/DragDroppable'; +import DragHandle from '../dnd/DragHandle'; +import { componentShape } from '../../util/propShapes'; + +const propTypes = { + component: componentShape.isRequired, + components: PropTypes.object.isRequired, + index: PropTypes.number.isRequired, + depth: PropTypes.number.isRequired, + parentId: PropTypes.string.isRequired, + + // grid related + availableColumnCount: PropTypes.number.isRequired, + columnWidth: PropTypes.number.isRequired, + rowHeight: PropTypes.number, + onResizeStart: PropTypes.func.isRequired, + onResize: PropTypes.func.isRequired, + onResizeStop: PropTypes.func.isRequired, + + // dnd + onDrop: PropTypes.func.isRequired, +}; + +const defaultProps = { + rowHeight: null, +}; class Spacer extends React.PureComponent { render() { - return
    ; + const { + component, + components, + index, + depth, + parentId, + availableColumnCount, + columnWidth, + rowHeight, + onResizeStart, + onResize, + onResizeStop, + onDrop, + } = this.props; + + return ( + + {({ dropIndicatorProps, dragSourceRef }) => ( + + + +
    + + {dropIndicatorProps &&
    } + + )} + + ); + + return ; } } +Spacer.propTypes = propTypes; +Spacer.defaultProps = defaultProps; + export default Spacer; diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Tabs.jsx b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Tabs.jsx index 839dc230f150e..79d945785623f 100644 --- a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Tabs.jsx +++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Tabs.jsx @@ -2,38 +2,37 @@ import React from 'react'; import PropTypes from 'prop-types'; import { Tabs as BootstrapTabs, Tab } from 'react-bootstrap'; -import DashboardComponent from '../DashboardComponent'; +import DragDroppable from '../dnd/DragDroppable'; +import DragHandle from '../dnd/DragHandle'; +import ComponentLookup from '../gridComponents'; import { componentShape } from '../../util/propShapes'; const propTypes = { - tabs: PropTypes.arrayOf( // @TODO this should be parth of the component definition - PropTypes.shape({ - label: PropTypes.string, - }), - ), - onChangeTab: PropTypes.func, component: componentShape.isRequired, - components: PropTypes.object, + components: PropTypes.object.isRequired, + index: PropTypes.number.isRequired, depth: PropTypes.number.isRequired, + parentId: PropTypes.string.isRequired, - // grid props + // grid related availableColumnCount: PropTypes.number.isRequired, columnWidth: PropTypes.number.isRequired, onResizeStart: PropTypes.func.isRequired, onResize: PropTypes.func.isRequired, onResizeStop: PropTypes.func.isRequired, + + // dnd + onDrop: PropTypes.func.isRequired, + onChangeTab: PropTypes.func, }; const defaultProps = { - tabs: [ - { label: 'Section Tab' }, - { label: 'Section Tab' }, - { label: 'Section Tab' }, - ], onChangeTab: null, children: null, }; +const NEW_TAB_INDEX = -1; + class Tabs extends React.Component { constructor(props) { super(props); @@ -44,19 +43,27 @@ class Tabs extends React.Component { } handleClicKTab(tabIndex) { - const { onChangeTab, tabs } = this.props; - this.setState(() => ({ tabIndex })); - if (onChangeTab) { - onChangeTab({ tabIndex, tab: tabs[tabIndex] }); + const { onChangeTab, component, components } = this.props; + if (tabIndex !== NEW_TAB_INDEX) { + this.setState(() => ({ tabIndex })); + if (onChangeTab) { + onChangeTab({ tabIndex, tab: component.children[tabIndex] }); + } + } else { + const newTabId = `new-tab-${component.children.length}`; + component.children.push(newTabId); + components[newTabId] = { id: newTabId, type: 'DASHBOARD_TAB_TYPE', meta: { text: 'New Tab' } }; + this.setState(() => ({ forceUpdate: Math.random() })); } } render() { const { - tabs, depth, - component: tabEntity, + component: tabsComponent, components, + parentId, + index, availableColumnCount, columnWidth, onResizeStart, @@ -65,38 +72,117 @@ class Tabs extends React.Component { onDrop, } = this.props; + const { tabIndex: selectedTabIndex } = this.state; + const { children: tabIds } = tabsComponent; + return ( -
    - - {tabs.map((tab, i) => ( - {tab.label}
    } /> - ))} - -
    - - {(tabEntity.children || []).map((id, index) => ( - - ))} -
    -
    + + {({ dropIndicatorProps: tabsDropIndicatorProps, dragSourceRef: tabsDragSourceRef }) => ( +
    + + + + {tabIds.map((tabId, tabIndex) => { + const tabComponent = components[tabId]; + return ( + { + onDrop(dropResult); + + // Ensure dropped tab is now visible + const { destination } = dropResult; + if (destination) { + const dropTabIndex = destination.droppableId === tabsComponent.id + ? destination.index // dropped ON tab + : tabIds.indexOf(destination.droppableId); // dropped IN tab + + if (dropTabIndex > -1) { + setTimeout(() => { + this.handleClicKTab(dropTabIndex); + }, 20); + } + } + }} + > + {({ dropIndicatorProps, dragSourceRef }) => ( +
    + {tabComponent.meta.text} + {dropIndicatorProps && +
    } +
    + )} + + } + > + {/* + react-bootstrap renders all children with display:none, so we don't + render potentially-expensive charts (this also enables lazy loading + their content) + */} + {tabIndex === selectedTabIndex && +
    + {tabComponent.children.map((componentId, componentIndex) => { + const component = components[componentId]; + const componentType = component.type; + const Component = ComponentLookup[componentType]; + return ( + + ); + })} +
    } + + ); + })} + + } + /> + + + {tabsDropIndicatorProps + && tabsDropIndicatorProps.style + && tabsDropIndicatorProps.style.width === '100%' + &&
    } + +
    + )} + ); } } diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/components.css b/superset/assets/javascripts/dashboard/v2/components/gridComponents/components.css index bfbd50712601f..b90a428b38f6b 100644 --- a/superset/assets/javascripts/dashboard/v2/components/gridComponents/components.css +++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/components.css @@ -56,6 +56,7 @@ } .dashboard-component-tabs .dashboard-component-tabs-content { min-height: 48px; + margin-top: 1px; } .dashboard-component-tabs .nav-tabs { @@ -92,6 +93,29 @@ color: #000000; } + +.dashboard-component-tabs .nav-tabs > li > a:focus { + outline: none; + background: #fff; +} + +.dashboard-component-tabs .nav-tabs > li .dragdroppable-tab { + cursor: move; +} + +.dashboard-component-tabs .nav-tabs > li .drop-indicator { + height: 40px !important; + top: -10px !important; +} + +.dashboard-component-tabs .fa-plus-square { + background: linear-gradient(to right, #E32464, #2C2261); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + display: initial; + font-size: 16px; +} + /* New components */ .new-component { display: flex; @@ -119,9 +143,10 @@ position: relative; } +/* columns and rows */ .grid-column { width: 100%; - min-height: 48px; + min-height: 56px; } .grid-row { @@ -129,15 +154,30 @@ flex-direction: row; flex-wrap: wrap; align-items: flex-start; - min-height: 48px; + min-height: 56px; width: 100%; height: fit-content; background-color: transparent; } -.grid-row--empty { +.grid-row--empty:after { content: "Empty row"; - padding: 16px; + display: flex; + align-items: center; + justify-content: center; + width: 100%; + height: 100%; + color: #aaa; +} + +.grid-column--empty:after { + content: "Empty column"; + display: flex; + align-items: center; + justify-content: center; + width: 100%; + height: 100%; + color: #aaa; } .grid-row-container { 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 a0399c917ff23..b96f3e792a963 100644 --- a/superset/assets/javascripts/dashboard/v2/components/gridComponents/new/DraggableNewComponent.jsx +++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/new/DraggableNewComponent.jsx @@ -1,28 +1,29 @@ import React from 'react'; import PropTypes from 'prop-types'; + import DragDroppable from '../../dnd/DragDroppable'; const propTypes = { - // id: PropTypes.string.isRequired, + id: PropTypes.string.isRequired, type: PropTypes.string.isRequired, label: PropTypes.string.isRequired, }; export default class DraggableNewComponent extends React.PureComponent { render() { - const { type, label } = this.props; + const { label, id, type } = this.props; return ( -
    -
    - {label} -
    + {({ dragSourceRef }) => ( +
    +
    + {label} +
    + )} ); } diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/new/NewChart.jsx b/superset/assets/javascripts/dashboard/v2/components/gridComponents/new/NewChart.jsx index 6c9d46541962c..3c735bae058fc 100644 --- a/superset/assets/javascripts/dashboard/v2/components/gridComponents/new/NewChart.jsx +++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/new/NewChart.jsx @@ -11,6 +11,7 @@ export default class DraggableNewChart extends React.PureComponent { render() { return ( diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/new/NewColumn.jsx b/superset/assets/javascripts/dashboard/v2/components/gridComponents/new/NewColumn.jsx index 4858077f86fbb..7d56179e0b9b0 100644 --- a/superset/assets/javascripts/dashboard/v2/components/gridComponents/new/NewColumn.jsx +++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/new/NewColumn.jsx @@ -11,6 +11,7 @@ export default class DraggableNewColumn extends React.PureComponent { render() { return ( diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/new/NewDivider.jsx b/superset/assets/javascripts/dashboard/v2/components/gridComponents/new/NewDivider.jsx index 90aea4e47a421..4d859015e88bf 100644 --- a/superset/assets/javascripts/dashboard/v2/components/gridComponents/new/NewDivider.jsx +++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/new/NewDivider.jsx @@ -11,6 +11,7 @@ export default class DraggableNewDivider extends React.PureComponent { render() { return ( diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/new/NewHeader.jsx b/superset/assets/javascripts/dashboard/v2/components/gridComponents/new/NewHeader.jsx index 7b5cfd65ffa20..fb106a2c3ef5b 100644 --- a/superset/assets/javascripts/dashboard/v2/components/gridComponents/new/NewHeader.jsx +++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/new/NewHeader.jsx @@ -11,6 +11,7 @@ export default class DraggableNewHeader extends React.Component { render() { return ( diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/new/NewRow.jsx b/superset/assets/javascripts/dashboard/v2/components/gridComponents/new/NewRow.jsx index af20f9d488365..3b1b6eb027659 100644 --- a/superset/assets/javascripts/dashboard/v2/components/gridComponents/new/NewRow.jsx +++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/new/NewRow.jsx @@ -1,6 +1,6 @@ import React from 'react'; -import { ROW_TYPE } from '../../../util/componentTypes'; +import { INVISIBLE_ROW_TYPE } from '../../../util/componentTypes'; import DraggableNewComponent from './DraggableNewComponent'; const propTypes = { @@ -10,7 +10,8 @@ export default class DraggableNewRow extends React.PureComponent { render() { return ( ); diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/new/NewSpacer.jsx b/superset/assets/javascripts/dashboard/v2/components/gridComponents/new/NewSpacer.jsx index 5db1ca759a9cc..b81f13c729b76 100644 --- a/superset/assets/javascripts/dashboard/v2/components/gridComponents/new/NewSpacer.jsx +++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/new/NewSpacer.jsx @@ -11,6 +11,7 @@ export default class DraggableNewChart extends React.PureComponent { render() { return ( diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/new/NewTabs.jsx b/superset/assets/javascripts/dashboard/v2/components/gridComponents/new/NewTabs.jsx index adacc8e217cf2..5a866052a466f 100644 --- a/superset/assets/javascripts/dashboard/v2/components/gridComponents/new/NewTabs.jsx +++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/new/NewTabs.jsx @@ -11,6 +11,7 @@ export default class DraggableNewTabs extends React.PureComponent { render() { return ( diff --git a/superset/assets/javascripts/dashboard/v2/components/resizable/DimensionProvider.jsx b/superset/assets/javascripts/dashboard/v2/components/resizable/DimensionProvider.jsx index 063c27d02bff9..f5ef280791d1b 100644 --- a/superset/assets/javascripts/dashboard/v2/components/resizable/DimensionProvider.jsx +++ b/superset/assets/javascripts/dashboard/v2/components/resizable/DimensionProvider.jsx @@ -57,7 +57,7 @@ class DimensionProvider extends React.PureComponent { heightStep={GRID_ROW_HEIGHT_UNIT} widthMultiple={component.meta.width || null} heightMultiple={ - component.meta.height || (component.type !== COLUMN_TYPE ? rowHeight : null) + component.meta.height || (component.type === COLUMN_TYPE ? rowHeight : null) } minWidthMultiple={isSpacer ? 1 : GRID_MIN_COLUMN_COUNT} maxWidthMultiple={availableColumnCount + (component.meta.width || 0)} diff --git a/superset/assets/javascripts/dashboard/v2/fixtures/testLayout.js b/superset/assets/javascripts/dashboard/v2/fixtures/testLayout.js index 853b922284044..80370c00ae35a 100644 --- a/superset/assets/javascripts/dashboard/v2/fixtures/testLayout.js +++ b/superset/assets/javascripts/dashboard/v2/fixtures/testLayout.js @@ -4,6 +4,7 @@ import { ROW_TYPE, INVISIBLE_ROW_TYPE, SPACER_TYPE, + TAB_TYPE, TABS_TYPE, CHART_TYPE, DIVIDER_TYPE, @@ -17,7 +18,7 @@ export default { type: GRID_ROOT_TYPE, id: DASHBOARD_ROOT_ID, children: [ - 'header0', + // 'header0', 'row0', // 'divider0', // 'row1', @@ -27,9 +28,9 @@ export default { }, row0: { id: 'row0', - type: ROW_TYPE, + type: INVISIBLE_ROW_TYPE, children: [ - 'charta', + // 'charta', // 'chartb', // 'chartc', ], @@ -54,9 +55,39 @@ export default { id: 'tabs0', type: TABS_TYPE, children: [ - 'row2', + '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: { diff --git a/superset/assets/javascripts/dashboard/v2/util/componentTypes.js b/superset/assets/javascripts/dashboard/v2/util/componentTypes.js index ea9288abbb49f..f86205e604768 100644 --- a/superset/assets/javascripts/dashboard/v2/util/componentTypes.js +++ b/superset/assets/javascripts/dashboard/v2/util/componentTypes.js @@ -8,6 +8,7 @@ export const MARKDOWN_TYPE = 'DASHBOARD_MARKDOWN_TYPE'; export const ROW_TYPE = 'DASHBOARD_ROW_TYPE'; export const SPACER_TYPE = 'DASHBOARD_SPACER_TYPE'; export const TABS_TYPE = 'DASHBOARD_TABS_TYPE'; +export const TAB_TYPE = 'DASHBOARD_TAB_TYPE'; export default { CHART_TYPE, @@ -20,4 +21,5 @@ export default { ROW_TYPE, SPACER_TYPE, TABS_TYPE, + TAB_TYPE, }; diff --git a/superset/assets/javascripts/dashboard/v2/util/isValidChild.js b/superset/assets/javascripts/dashboard/v2/util/isValidChild.js index 3319ff601a604..aeae2e64136e3 100644 --- a/superset/assets/javascripts/dashboard/v2/util/isValidChild.js +++ b/superset/assets/javascripts/dashboard/v2/util/isValidChild.js @@ -9,6 +9,7 @@ import { ROW_TYPE, SPACER_TYPE, TABS_TYPE, + TAB_TYPE, } from './componentTypes'; const typeToValidChildType = { @@ -26,7 +27,6 @@ const typeToValidChildType = { [MARKDOWN_TYPE]: true, [COLUMN_TYPE]: true, [SPACER_TYPE]: true, - // [HEADER_TYPE]: true, }, [INVISIBLE_ROW_TYPE]: { @@ -37,6 +37,10 @@ const typeToValidChildType = { }, [TABS_TYPE]: { + [TAB_TYPE]: true, + }, + + [TAB_TYPE]: { [ROW_TYPE]: true, [INVISIBLE_ROW_TYPE]: true, [DIVIDER_TYPE]: true, diff --git a/superset/assets/javascripts/dashboard/v2/util/newEntitiesFromDrop.js b/superset/assets/javascripts/dashboard/v2/util/newEntitiesFromDrop.js index 04e5d51947896..dc5c773bfc664 100644 --- a/superset/assets/javascripts/dashboard/v2/util/newEntitiesFromDrop.js +++ b/superset/assets/javascripts/dashboard/v2/util/newEntitiesFromDrop.js @@ -10,6 +10,7 @@ import { ROW_TYPE, SPACER_TYPE, TABS_TYPE, + TAB_TYPE, } from './componentTypes'; const typeToDefaultMetaData = { @@ -22,6 +23,7 @@ const typeToDefaultMetaData = { [ROW_TYPE]: null, [SPACER_TYPE]: { width: 1 }, [TABS_TYPE]: null, + [TAB_TYPE]: { text: 'New Tab' }, }; // @TODO this should be replaced by a more robust algorithm @@ -70,6 +72,10 @@ export default function newEntitiesFromDrop({ dropResult, entitiesMap }) { rowWrapper.children = [newDropChild.id]; newEntities[rowWrapper.id] = rowWrapper; newDropChild = rowWrapper; + } else if (dragType === TABS_TYPE) { + const tabChild = entityFactory(TAB_TYPE); + newDropChild.children = [tabChild.id]; + newEntities[tabChild.id] = tabChild; } const nextDropChildren = [...dropEntity.children]; diff --git a/superset/assets/javascripts/dashboard/v2/util/propShapes.jsx b/superset/assets/javascripts/dashboard/v2/util/propShapes.jsx index 320b692cddb93..4354398c0feab 100644 --- a/superset/assets/javascripts/dashboard/v2/util/propShapes.jsx +++ b/superset/assets/javascripts/dashboard/v2/util/propShapes.jsx @@ -17,11 +17,21 @@ export const componentShape = PropTypes.shape({ }), }); -// export const gridPropsShape = PropTypes.shape({ -// availableColumnCount: PropTypes.number.isRequired, -// columnWidth: PropTypes.number.isRequired, -// rowWidth: PropTypes.number.isRequired, -// onResizeStart: PropTypes.func.isRequired, -// onResize: PropTypes.func.isRequired, -// onResizeStop: PropTypes.func.isRequired, -// }); +export const componentProps = { + component: componentShape.isRequired, + components: PropTypes.object.isRequired, + depth: PropTypes.number.isRequired, + index: PropTypes.number.isRequired, + parentId: PropTypes.string.isRequired, + + // grid related + availableColumnCount: PropTypes.number.isRequired, + columnWidth: PropTypes.number.isRequired, + rowHeight: PropTypes.number, + onResizeStart: PropTypes.func.isRequired, + onResize: PropTypes.func.isRequired, + onResizeStop: PropTypes.func.isRequired, + + // dnd + onDrop: PropTypes.func.isRequired, +}; From 0fb9b2a3d36ca69b8a1ccc0b1d7181ce386f4c8f Mon Sep 17 00:00:00 2001 From: Chris Williams Date: Wed, 14 Feb 2018 00:58:59 -0800 Subject: [PATCH 12/25] [dnd] refactor to use redux, add DashboardComponent and DashboardGrid containers --- .../components/DashboardContainer.jsx | 20 +- .../assets/javascripts/dashboard/index.jsx | 21 +- .../javascripts/dashboard/v2/actions/index.js | 66 +++++ .../v2/components/DashboardBuilder.jsx | 87 +----- .../dashboard/v2/components/DashboardGrid.jsx | 90 +++--- .../v2/components/dnd/DragDroppable.jsx | 10 +- .../v2/components/gridComponents/Chart.jsx | 6 +- .../v2/components/gridComponents/Column.jsx | 15 +- .../v2/components/gridComponents/Divider.jsx | 6 +- .../v2/components/gridComponents/Header.jsx | 6 +- .../v2/components/gridComponents/Row.jsx | 18 +- .../v2/components/gridComponents/Spacer.jsx | 10 +- .../v2/components/gridComponents/Tabs.jsx | 82 +++--- .../components/gridComponents/components.css | 12 +- .../resizable/DimensionProvider.jsx | 4 +- .../v2/containers/DashboardComponent.jsx | 53 ++++ .../dashboard/v2/containers/DashboardGrid.jsx | 23 ++ .../dashboard/v2/fixtures/testLayout.js | 268 +++++++++--------- .../dashboard/v2/reducers/dashboard.js | 61 ++++ .../dashboard/v2/reducers/index.js | 6 + .../dashboard/v2/util/dnd-reorder.js | 2 +- .../dashboard/v2/util/isValidChild.js | 1 + .../dashboard/v2/util/newEntitiesFromDrop.js | 11 +- 23 files changed, 520 insertions(+), 358 deletions(-) create mode 100644 superset/assets/javascripts/dashboard/v2/actions/index.js create mode 100644 superset/assets/javascripts/dashboard/v2/containers/DashboardComponent.jsx create mode 100644 superset/assets/javascripts/dashboard/v2/containers/DashboardGrid.jsx create mode 100644 superset/assets/javascripts/dashboard/v2/reducers/dashboard.js create mode 100644 superset/assets/javascripts/dashboard/v2/reducers/index.js diff --git a/superset/assets/javascripts/dashboard/components/DashboardContainer.jsx b/superset/assets/javascripts/dashboard/components/DashboardContainer.jsx index dbec7907c8bd5..6779746e636da 100644 --- a/superset/assets/javascripts/dashboard/components/DashboardContainer.jsx +++ b/superset/assets/javascripts/dashboard/components/DashboardContainer.jsx @@ -7,16 +7,16 @@ import Dashboard from '../v2/components/Dashboard'; function mapStateToProps({ charts, dashboard }) { return { - initMessages: dashboard.common.flash_messages, - timeout: dashboard.common.conf.SUPERSET_WEBSERVER_TIMEOUT, - dashboard: dashboard.dashboard, - slices: charts, - datasources: dashboard.datasources, - filters: dashboard.filters, - refresh: !!dashboard.refresh, - userId: dashboard.userId, - isStarred: !!dashboard.isStarred, - editMode: dashboard.editMode, + // initMessages: dashboard.common.flash_messages, + // timeout: dashboard.common.conf.SUPERSET_WEBSERVER_TIMEOUT, + // dashboard: dashboard.dashboard, + // slices: charts, + // datasources: dashboard.datasources, + // filters: dashboard.filters, + // refresh: !!dashboard.refresh, + // userId: dashboard.userId, + // isStarred: !!dashboard.isStarred, + // editMode: dashboard.editMode, }; } diff --git a/superset/assets/javascripts/dashboard/index.jsx b/superset/assets/javascripts/dashboard/index.jsx index 774e07101f461..c9236bd24feea 100644 --- a/superset/assets/javascripts/dashboard/index.jsx +++ b/superset/assets/javascripts/dashboard/index.jsx @@ -8,17 +8,29 @@ import { initEnhancer } from '../reduxUtils'; import { appSetup } from '../common'; import { initJQueryAjax } from '../modules/utils'; import DashboardContainer from './components/DashboardContainer'; -import rootReducer, { getInitialState } from './reducers'; +// import rootReducer, { getInitialState } from './reducers'; + +import testLayout from './v2/fixtures/testLayout'; +import rootReducer from './v2/reducers/'; appSetup(); initJQueryAjax(); const appContainer = document.getElementById('app'); -const bootstrapData = JSON.parse(appContainer.getAttribute('data-bootstrap')); -const initState = Object.assign({}, getInitialState(bootstrapData)); +// const bootstrapData = JSON.parse(appContainer.getAttribute('data-bootstrap')); +// const initState = Object.assign({}, getInitialState(bootstrapData)); +const initState = { + dashboard: testLayout, +}; const store = createStore( - rootReducer, initState, compose(applyMiddleware(thunk), initEnhancer(false))); + rootReducer, + initState, + compose( + applyMiddleware(thunk), + initEnhancer(false), + ), +); ReactDOM.render( @@ -26,4 +38,3 @@ ReactDOM.render( , appContainer, ); - diff --git a/superset/assets/javascripts/dashboard/v2/actions/index.js b/superset/assets/javascripts/dashboard/v2/actions/index.js new file mode 100644 index 0000000000000..f847e1057ccb2 --- /dev/null +++ b/superset/assets/javascripts/dashboard/v2/actions/index.js @@ -0,0 +1,66 @@ +export const UPDATE_COMPONENTS = 'UPDATE_COMPONENTS'; +export function updateComponents(nextComponents) { + return { + type: UPDATE_COMPONENTS, + payload: { + nextComponents, + }, + }; +} + +export const DELETE_COMPONENT = 'DELETE_COMPONENT'; +export function deleteComponent(id) { + return { + type: DELETE_COMPONENT, + payload: { + id, + }, + }; +} + +export const CREATE_COMPONENT = 'CREATE_COMPONENT'; +export function createComponent(dropResult) { + return { + type: CREATE_COMPONENT, + payload: { + dropResult, + }, + }; +} + + +// Drag and drop -------------------------------------------------------------- +export const MOVE_COMPONENT = 'MOVE_COMPONENT'; +export function moveComponent(dropResult) { + return { + type: MOVE_COMPONENT, + payload: { + dropResult, + }, + }; +} + +export const HANDLE_COMPONENT_DROP = 'HANDLE_COMPONENT_DROP'; +export function handleComponentDrop(dropResult) { + return (dispatch) => { + if ( + dropResult.destination + && dropResult.source + && !( // ensure it has moved + dropResult.destination.droppableId === dropResult.source.droppableId + && dropResult.destination.index === dropResult.source.index + ) + ) { + return dispatch(moveComponent(dropResult)); + } else if (dropResult.destination && !dropResult.source) { + return dispatch(createComponent(dropResult)); + } + return null; + } +} + +// Resize --------------------------------------------------------------------- + +// export function dashboardComponentResizeStart() {} +// export function dashboardComponentResize() {} +// export function dashboardComponentResizeStop() {} diff --git a/superset/assets/javascripts/dashboard/v2/components/DashboardBuilder.jsx b/superset/assets/javascripts/dashboard/v2/components/DashboardBuilder.jsx index f4d465eedf6bf..94069b79dafaa 100644 --- a/superset/assets/javascripts/dashboard/v2/components/DashboardBuilder.jsx +++ b/superset/assets/javascripts/dashboard/v2/components/DashboardBuilder.jsx @@ -5,13 +5,9 @@ import { DragDropContext } from 'react-dnd'; import cx from 'classnames'; import BuilderComponentPane from './BuilderComponentPane'; -import DashboardGrid from './DashboardGrid'; -import newEntitiesFromDrop from '../util/newEntitiesFromDrop'; -import { reorderItem } from '../util/dnd-reorder'; +import DashboardGrid from '../containers/DashboardGrid'; import './dnd/dnd.css'; -import testLayout from '../fixtures/testLayout'; - const propTypes = { editMode: PropTypes.bool, @@ -24,88 +20,15 @@ const defaultProps = { class DashboardBuilder extends React.Component { constructor(props) { super(props); - this.state = { - layout: testLayout, - }; - - // @TODO most of this can probably be moved into redux - this.handleDrop = this.handleDrop.bind(this); - this.handleMoveEntity = this.handleMoveEntity.bind(this); - this.handleNewEntity = this.handleNewEntity.bind(this); - this.handleUpdateEntity = this.handleUpdateEntity.bind(this); - } - - handleDrop(dropResult) { - console.log('builder handleDrop', dropResult); - - if ( - dropResult.destination - && dropResult.source - && !( // ensure it has moved - dropResult.destination.droppableId === dropResult.source.droppableId - && dropResult.destination.index === dropResult.source.index - ) - ) { - this.handleMoveEntity(dropResult); - } else if (dropResult.destination && !dropResult.source) { - this.handleNewEntity(dropResult); - } - } - - handleNewEntity(dropResult) { - console.log('new entity'); - this.setState(({ layout }) => { - debugger; - const newEntities = newEntitiesFromDrop({ dropResult, entitiesMap: layout }); - return { - layout: { - ...layout, - ...newEntities, - }, - }; - }); - } - - handleMoveEntity({ source, destination }) { - this.setState(({ layout }) => { - const nextEntities = reorderItem({ - entitiesMap: layout, - source, - destination, - }); - - return { - layout: { - ...layout, - ...nextEntities, - }, - }; - }); - } - - handleUpdateEntity(nextEntity) { - console.log('update entity', nextEntity); - this.setState(({ layout }) => ({ - layout: { - ...layout, - [nextEntity.id]: nextEntity, - }, - })); + // this component might control the state of the side pane etc. in the future + this.state = {}; } render() { - const { layout } = this.state; - return (
    - - + +
    ); } diff --git a/superset/assets/javascripts/dashboard/v2/components/DashboardGrid.jsx b/superset/assets/javascripts/dashboard/v2/components/DashboardGrid.jsx index db25d89002c89..1567d8cc6ebc9 100644 --- a/superset/assets/javascripts/dashboard/v2/components/DashboardGrid.jsx +++ b/superset/assets/javascripts/dashboard/v2/components/DashboardGrid.jsx @@ -2,7 +2,9 @@ import React from 'react'; import PropTypes from 'prop-types'; import ParentSize from '@vx/responsive/build/components/ParentSize'; import cx from 'classnames'; -import ComponentLookup from './gridComponents'; + +import DragDroppable from './dnd/DragDroppable'; +import DashboardComponent from '../containers/DashboardComponent'; import { DASHBOARD_ROOT_ID, @@ -13,15 +15,12 @@ import { import './gridComponents/grid.css'; const propTypes = { - layout: PropTypes.object, - updateEntity: PropTypes.func, - onDrop: PropTypes.func, + dashboard: PropTypes.object.isRequired, + updateComponents: PropTypes.func.isRequired, + handleComponentDrop: PropTypes.func.isRequired, }; const defaultProps = { - layout: {}, - updateEntity() {}, - onDrop() {}, }; class DashboardGrid extends React.PureComponent { @@ -32,7 +31,6 @@ class DashboardGrid extends React.PureComponent { rowGuideTop: null, }; - this.handleToggleSelectEntityId = this.handleToggleSelectEntityId.bind(this); this.handleResizeStart = this.handleResizeStart.bind(this); this.handleResize = this.handleResize.bind(this); this.handleResizeStop = this.handleResizeStop.bind(this); @@ -65,18 +63,20 @@ class DashboardGrid extends React.PureComponent { } handleResizeStop({ id, widthMultiple, heightMultiple }) { - const { layout: components, updateEntity } = this.props; + const { dashboard: components, updateComponents } = this.props; const component = components[id]; if ( component && (component.meta.width !== widthMultiple || component.meta.height !== heightMultiple) ) { - updateEntity({ - ...component, - meta: { - ...component.meta, - width: widthMultiple || component.meta.width, - height: heightMultiple || component.meta.height, + updateComponents({ + [id]: { + ...component, + meta: { + ...component.meta, + width: widthMultiple || component.meta.width, + height: heightMultiple || component.meta.height, + }, }, }); } @@ -86,14 +86,8 @@ class DashboardGrid extends React.PureComponent { })); } - handleToggleSelectEntityId(id) { - this.setState(({ selectedComponentId }) => ({ - selectedComponentId: id === selectedComponentId ? null : id, - })); - } - render() { - const { layout: components, onDrop } = this.props; + const { dashboard: components, handleComponentDrop } = this.props; const { isResizing, rowGuideTop } = this.state; const rootComponent = components[DASHBOARD_ROOT_ID]; @@ -110,28 +104,36 @@ class DashboardGrid extends React.PureComponent { return width < 50 ? null : (
    - {(rootComponent.children || []).map((id, index) => { - const component = components[id]; - const componentType = component.type; - const Component = ComponentLookup[componentType]; - - return ( - - ); - })} + {(rootComponent.children || []).map((id, index) => ( + + ))} + + {rootComponent.children.length === 0 && + + {({ dropIndicatorProps }) => ( +
    + {dropIndicatorProps &&
    } +
    + )} + } {isResizing && Array(GRID_COLUMN_COUNT).fill(null).map((_, i) => (
    {({ dropIndicatorProps, dragSourceRef }) => ( {({ dropIndicatorProps, dragSourceRef }) => ( ; } - const { type: componentType } = component; - const Component = ComponentLookup[componentType]; + return ( - {({ 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 008a05aa84820..0c91025a6ea2f 100644 --- a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Header.jsx +++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Header.jsx @@ -11,7 +11,7 @@ const propTypes = { index: PropTypes.number.isRequired, parentId: PropTypes.string.isRequired, // dnd - onDrop: PropTypes.func.isRequired, + handleComponentDrop: PropTypes.func.isRequired, }; const defaultProps = { @@ -30,7 +30,7 @@ class Header extends React.PureComponent { components, index, parentId, - onDrop, + handleComponentDrop, } = this.props; return ( @@ -40,7 +40,7 @@ class Header extends React.PureComponent { orientation="horizontal" index={index} parentId={parentId} - onDrop={onDrop} + onDrop={handleComponentDrop} > {({ dropIndicatorProps, dragSourceRef }) => (
    diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Row.jsx b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Row.jsx index e02106ae46898..bab154805a55b 100644 --- a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Row.jsx +++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Row.jsx @@ -4,7 +4,7 @@ import cx from 'classnames'; import DragDroppable from '../dnd/DragDroppable'; import DragHandle from '../dnd/DragHandle'; -import ComponentLookup from '../gridComponents'; +import DashboardComponent from '../../containers/DashboardComponent'; import { componentShape } from '../../util/propShapes'; import { GRID_GUTTER_SIZE } from '../../util/constants'; import { INVISIBLE_ROW_TYPE } from '../../util/componentTypes'; @@ -25,7 +25,7 @@ const propTypes = { onResizeStop: PropTypes.func.isRequired, // dnd - onDrop: PropTypes.func.isRequired, + handleComponentDrop: PropTypes.func.isRequired, }; const defaultProps = { @@ -45,7 +45,7 @@ class Row extends React.PureComponent { onResizeStart, onResize, onResizeStop, - onDrop, + handleComponentDrop, } = this.props; let occupiedColumnCount = 0; @@ -72,7 +72,7 @@ class Row extends React.PureComponent { orientation="horizontal" index={index} parentId={parentId} - onDrop={onDrop} + onDrop={handleComponentDrop} > {({ dropIndicatorProps, dragSourceRef }) => (
    ; } - const { type: componentType } = component; - const Component = ComponentLookup[componentType]; return ( - } +
    }
    )} diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Spacer.jsx b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Spacer.jsx index 10f896571d5b3..845e8cd4fe8b3 100644 --- a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Spacer.jsx +++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Spacer.jsx @@ -22,7 +22,7 @@ const propTypes = { onResizeStop: PropTypes.func.isRequired, // dnd - onDrop: PropTypes.func.isRequired, + handleComponentDrop: PropTypes.func.isRequired, }; const defaultProps = { @@ -43,17 +43,17 @@ class Spacer extends React.PureComponent { onResizeStart, onResize, onResizeStop, - onDrop, + handleComponentDrop, } = this.props; return ( {({ dropIndicatorProps, dragSourceRef }) => (
    diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Tabs.jsx b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Tabs.jsx index 79d945785623f..e9fe979a47d79 100644 --- a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Tabs.jsx +++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Tabs.jsx @@ -4,8 +4,9 @@ import { Tabs as BootstrapTabs, Tab } from 'react-bootstrap'; import DragDroppable from '../dnd/DragDroppable'; import DragHandle from '../dnd/DragHandle'; -import ComponentLookup from '../gridComponents'; +import DashboardComponent from '../../containers/DashboardComponent'; import { componentShape } from '../../util/propShapes'; +import { TAB_TYPE } from '../../util/componentTypes'; const propTypes = { component: componentShape.isRequired, @@ -22,7 +23,8 @@ const propTypes = { onResizeStop: PropTypes.func.isRequired, // dnd - onDrop: PropTypes.func.isRequired, + createComponent: PropTypes.func.isRequired, + handleComponentDrop: PropTypes.func.isRequired, onChangeTab: PropTypes.func, }; @@ -42,18 +44,29 @@ class Tabs extends React.Component { this.handleClicKTab = this.handleClicKTab.bind(this); } + componentWillReceiveProps(nextProps) { + const maxIndex = Math.max(0, nextProps.component.children.length - 1); + if (this.state.tabIndex >= maxIndex) { + this.setState(() => ({ tabIndex: maxIndex })); + } + } + handleClicKTab(tabIndex) { - const { onChangeTab, component, components } = this.props; + const { onChangeTab, component, createComponent } = this.props; + if (tabIndex !== NEW_TAB_INDEX) { this.setState(() => ({ tabIndex })); if (onChangeTab) { onChangeTab({ tabIndex, tab: component.children[tabIndex] }); } } else { - const newTabId = `new-tab-${component.children.length}`; - component.children.push(newTabId); - components[newTabId] = { id: newTabId, type: 'DASHBOARD_TAB_TYPE', meta: { text: 'New Tab' } }; - this.setState(() => ({ forceUpdate: Math.random() })); + createComponent({ + destination: { + droppableId: component.id, + index: component.children.length, + }, + draggableId: TAB_TYPE, + }); } } @@ -69,7 +82,7 @@ class Tabs extends React.Component { onResizeStart, onResize, onResizeStop, - onDrop, + handleComponentDrop, } = this.props; const { tabIndex: selectedTabIndex } = this.state; @@ -82,7 +95,7 @@ class Tabs extends React.Component { orientation="horizontal" index={index} parentId={parentId} - onDrop={onDrop} + onDrop={handleComponentDrop} > {({ dropIndicatorProps: tabsDropIndicatorProps, dragSourceRef: tabsDragSourceRef }) => (
    @@ -108,9 +121,9 @@ class Tabs extends React.Component { index={tabIndex} parentId={tabsComponent.id} onDrop={(dropResult) => { - onDrop(dropResult); + handleComponentDrop(dropResult); - // Ensure dropped tab is now visible + // Ensure dropped tab is visible const { destination } = dropResult; if (destination) { const dropTabIndex = destination.droppableId === tabsComponent.id @@ -142,37 +155,32 @@ class Tabs extends React.Component { */} {tabIndex === selectedTabIndex &&
    - {tabComponent.children.map((componentId, componentIndex) => { - const component = components[componentId]; - const componentType = component.type; - const Component = ComponentLookup[componentType]; - return ( - - ); - })} + {tabComponent.children.map((componentId, componentIndex) => ( + + ))}
    } ); })} - } - /> + {tabIds.length < 5 && + } + />} {tabsDropIndicatorProps diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/components.css b/superset/assets/javascripts/dashboard/v2/components/gridComponents/components.css index b90a428b38f6b..19a4a0a383eaf 100644 --- a/superset/assets/javascripts/dashboard/v2/components/gridComponents/components.css +++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/components.css @@ -109,7 +109,7 @@ } .dashboard-component-tabs .fa-plus-square { - background: linear-gradient(to right, #E32464, #2C2261); + background: linear-gradient(135deg, #E32464, #2C2261); -webkit-background-clip: text; -webkit-text-fill-color: transparent; display: initial; @@ -154,12 +154,16 @@ flex-direction: row; flex-wrap: wrap; align-items: flex-start; - min-height: 56px; width: 100%; height: fit-content; background-color: transparent; } +.grid-row.grid-row--empty { + align-items: center; /* this centers the empty note content */ + min-height: 56px; +} + .grid-row--empty:after { content: "Empty row"; display: flex; @@ -167,7 +171,7 @@ justify-content: center; width: 100%; height: 100%; - color: #aaa; + color: #ccc; } .grid-column--empty:after { @@ -177,7 +181,7 @@ justify-content: center; width: 100%; height: 100%; - color: #aaa; + color: #ccc; } .grid-row-container { diff --git a/superset/assets/javascripts/dashboard/v2/components/resizable/DimensionProvider.jsx b/superset/assets/javascripts/dashboard/v2/components/resizable/DimensionProvider.jsx index f5ef280791d1b..cb9ca101de54e 100644 --- a/superset/assets/javascripts/dashboard/v2/components/resizable/DimensionProvider.jsx +++ b/superset/assets/javascripts/dashboard/v2/components/resizable/DimensionProvider.jsx @@ -52,7 +52,7 @@ class DimensionProvider extends React.PureComponent { : null; + } +} + +DashboardComponent.propTypes = propTypes; + +export default connect(mapStateToProps, mapDispatchToProps)(DashboardComponent); diff --git a/superset/assets/javascripts/dashboard/v2/containers/DashboardGrid.jsx b/superset/assets/javascripts/dashboard/v2/containers/DashboardGrid.jsx new file mode 100644 index 0000000000000..741151b780e82 --- /dev/null +++ b/superset/assets/javascripts/dashboard/v2/containers/DashboardGrid.jsx @@ -0,0 +1,23 @@ +import { bindActionCreators } from 'redux'; +import { connect } from 'react-redux'; +import DashboardGrid from '../components/DashboardGrid'; + +import { + updateComponents, + handleComponentDrop, +} from '../actions'; + +function mapStateToProps({ dashboard = {} }) { + return { + dashboard, + }; +} + +function mapDispatchToProps(dispatch) { + return bindActionCreators({ + updateComponents, + handleComponentDrop, + }, dispatch); +} + +export default connect(mapStateToProps, mapDispatchToProps)(DashboardGrid); diff --git a/superset/assets/javascripts/dashboard/v2/fixtures/testLayout.js b/superset/assets/javascripts/dashboard/v2/fixtures/testLayout.js index 80370c00ae35a..64b46725dea7e 100644 --- a/superset/assets/javascripts/dashboard/v2/fixtures/testLayout.js +++ b/superset/assets/javascripts/dashboard/v2/fixtures/testLayout.js @@ -19,144 +19,144 @@ export default { id: DASHBOARD_ROOT_ID, children: [ // 'header0', - 'row0', + // '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, - }, - }, + // 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 new file mode 100644 index 0000000000000..a34ffa203b2a1 --- /dev/null +++ b/superset/assets/javascripts/dashboard/v2/reducers/dashboard.js @@ -0,0 +1,61 @@ +import newEntitiesFromDrop from '../util/newEntitiesFromDrop'; +import reorderItem from '../util/dnd-reorder'; + +import { + UPDATE_COMPONENTS, + DELETE_COMPONENT, + CREATE_COMPONENT, + MOVE_COMPONENT, +} from '../actions'; + +const actionHandlers = { + [UPDATE_COMPONENTS](state, action) { + const { payload: { nextComponents } } = action; + return { + ...state, + ...nextComponents, + }; + }, + + [DELETE_COMPONENT](state, action) { + // const { payload: id } = action; + // recursively delete children + console.log('@TODO implement', DELETE_COMPONENT); + + return state; + }, + + [CREATE_COMPONENT](state, action) { + const { payload: { dropResult } } = action; + const newEntities = newEntitiesFromDrop({ dropResult, components: state }); + return { + ...state, + ...newEntities, + }; + }, + + [MOVE_COMPONENT](state, action) { + const { payload: { dropResult } } = action; + const { source, destination } = dropResult; + + const nextEntities = reorderItem({ + entitiesMap: state, + source, + destination, + }); + + return { + ...state, + ...nextEntities, + }; + }, +}; + +export default function dashboardReducer(state = {}, action) { + if (action.type in actionHandlers) { + const handler = actionHandlers[action.type]; + return handler(state, action); + } + + return state; +} diff --git a/superset/assets/javascripts/dashboard/v2/reducers/index.js b/superset/assets/javascripts/dashboard/v2/reducers/index.js new file mode 100644 index 0000000000000..103fda0178890 --- /dev/null +++ b/superset/assets/javascripts/dashboard/v2/reducers/index.js @@ -0,0 +1,6 @@ +import { combineReducers } from 'redux'; +import dashboard from './dashboard'; + +export default combineReducers({ + dashboard, +}); diff --git a/superset/assets/javascripts/dashboard/v2/util/dnd-reorder.js b/superset/assets/javascripts/dashboard/v2/util/dnd-reorder.js index 868062dec0104..5ebca8cf92034 100644 --- a/superset/assets/javascripts/dashboard/v2/util/dnd-reorder.js +++ b/superset/assets/javascripts/dashboard/v2/util/dnd-reorder.js @@ -6,7 +6,7 @@ export function reorder(list, startIndex, endIndex) { return result; } -export function reorderItem({ +export default function reorderItem({ entitiesMap, source, destination, diff --git a/superset/assets/javascripts/dashboard/v2/util/isValidChild.js b/superset/assets/javascripts/dashboard/v2/util/isValidChild.js index aeae2e64136e3..e153a811ba065 100644 --- a/superset/assets/javascripts/dashboard/v2/util/isValidChild.js +++ b/superset/assets/javascripts/dashboard/v2/util/isValidChild.js @@ -20,6 +20,7 @@ const typeToValidChildType = { [TABS_TYPE]: true, [DIVIDER_TYPE]: true, [HEADER_TYPE]: true, + [SPACER_TYPE]: true, }, [ROW_TYPE]: { diff --git a/superset/assets/javascripts/dashboard/v2/util/newEntitiesFromDrop.js b/superset/assets/javascripts/dashboard/v2/util/newEntitiesFromDrop.js index dc5c773bfc664..195982df8ac85 100644 --- a/superset/assets/javascripts/dashboard/v2/util/newEntitiesFromDrop.js +++ b/superset/assets/javascripts/dashboard/v2/util/newEntitiesFromDrop.js @@ -14,12 +14,12 @@ import { } from './componentTypes'; const typeToDefaultMetaData = { - [CHART_TYPE]: { width: 3, height: 10 }, + [CHART_TYPE]: { width: 3, height: 15 }, [COLUMN_TYPE]: { width: 3 }, [DIVIDER_TYPE]: null, [HEADER_TYPE]: { text: 'New header' }, [INVISIBLE_ROW_TYPE]: null, - [MARKDOWN_TYPE]: { width: 3, height: 10 }, + [MARKDOWN_TYPE]: { width: 3, height: 15 }, [ROW_TYPE]: null, [SPACER_TYPE]: { width: 1 }, [TABS_TYPE]: null, @@ -43,11 +43,11 @@ function entityFactory(type) { }; } -export default function newEntitiesFromDrop({ dropResult, entitiesMap }) { +export default function newEntitiesFromDrop({ dropResult, components }) { const { draggableId, destination } = dropResult; - const dragType = draggableId; // @TODO will need to fix this - const dropEntity = entitiesMap[destination.droppableId]; + const dragType = draggableId; // @TODO idToType + const dropEntity = components[destination.droppableId]; if (!dropEntity) { console.warn('Drop target entity', destination.droppableId, 'not found'); @@ -72,6 +72,7 @@ export default function newEntitiesFromDrop({ dropResult, entitiesMap }) { rowWrapper.children = [newDropChild.id]; newEntities[rowWrapper.id] = rowWrapper; newDropChild = rowWrapper; + } else if (dragType === TABS_TYPE) { const tabChild = entityFactory(TAB_TYPE); newDropChild.children = [tabChild.id]; From 6db1b62cb83b2fe434d5e7c19c663ea05a7c73e8 Mon Sep 17 00:00:00 2001 From: Chris Williams Date: Wed, 14 Feb 2018 09:52:57 -0800 Subject: [PATCH 13/25] [dragdroppable] rename horizontal/vertical => row/column --- .../dashboard/v2/components/DashboardGrid.jsx | 2 +- .../v2/components/dnd/DragDroppable.jsx | 8 ++++---- .../dashboard/v2/components/dnd/handleDrop.js | 2 +- .../v2/components/dnd/handleHover.js | 20 +++++++++---------- .../v2/components/gridComponents/Chart.jsx | 2 +- .../v2/components/gridComponents/Column.jsx | 2 +- .../v2/components/gridComponents/Divider.jsx | 2 +- .../v2/components/gridComponents/Header.jsx | 2 +- .../v2/components/gridComponents/Row.jsx | 2 +- .../v2/components/gridComponents/Spacer.jsx | 4 +--- .../v2/components/gridComponents/Tabs.jsx | 11 +++++----- .../components/gridComponents/components.css | 4 ++++ 12 files changed, 32 insertions(+), 29 deletions(-) diff --git a/superset/assets/javascripts/dashboard/v2/components/DashboardGrid.jsx b/superset/assets/javascripts/dashboard/v2/components/DashboardGrid.jsx index 1567d8cc6ebc9..884fc899bf71c 100644 --- a/superset/assets/javascripts/dashboard/v2/components/DashboardGrid.jsx +++ b/superset/assets/javascripts/dashboard/v2/components/DashboardGrid.jsx @@ -123,7 +123,7 @@ class DashboardGrid extends React.PureComponent { diff --git a/superset/assets/javascripts/dashboard/v2/components/dnd/handleDrop.js b/superset/assets/javascripts/dashboard/v2/components/dnd/handleDrop.js index 427ce14c04ea7..96e16bde28750 100644 --- a/superset/assets/javascripts/dashboard/v2/components/dnd/handleDrop.js +++ b/superset/assets/javascripts/dashboard/v2/components/dnd/handleDrop.js @@ -54,7 +54,7 @@ export default function handleDrop(props, monitor, Component) { const clientOffset = monitor.getClientOffset(); if (clientOffset) { - if (orientation === 'horizontal') { + if (orientation === 'row') { const refMiddleY = refBoundingRect.top + ((refBoundingRect.bottom - refBoundingRect.top) / 2); nextIndex += clientOffset.y >= refMiddleY ? 1 : 0; diff --git a/superset/assets/javascripts/dashboard/v2/components/dnd/handleHover.js b/superset/assets/javascripts/dashboard/v2/components/dnd/handleHover.js index c83bd1944c56d..02f2377bd7130 100644 --- a/superset/assets/javascripts/dashboard/v2/components/dnd/handleHover.js +++ b/superset/assets/javascripts/dashboard/v2/components/dnd/handleHover.js @@ -34,17 +34,17 @@ export default function handleHover(props, monitor, Component) { if (validChild) { // indicate drop in container console.log('valid child', component.type, draggingItem.type); - const indicatorOrientation = orientation === 'horizontal' ? 'vertical' : 'horizontal'; + const indicatorOrientation = orientation === 'row' ? 'column' : 'row'; Component.setState(() => ({ dropIndicator: { top: 0, right: component.children.length ? 8 : null, left: component.children.length ? null : null, - height: indicatorOrientation === 'vertical' ? '100%' : 3, - width: indicatorOrientation === 'vertical' ? 3 : '100%', - minHeight: indicatorOrientation === 'vertical' ? 16 : null, - minWidth: indicatorOrientation === 'vertical' ? null : 16, + height: indicatorOrientation === 'column' ? '100%' : 3, + width: indicatorOrientation === 'column' ? 3 : '100%', + minHeight: indicatorOrientation === 'column' ? 16 : null, + minWidth: indicatorOrientation === 'column' ? null : 16, margin: 'auto', backgroundColor: '#44C0FF', position: 'absolute', @@ -58,7 +58,7 @@ export default function handleHover(props, monitor, Component) { if (clientOffset) { let dropOffset; - if (orientation === 'horizontal') { + if (orientation === 'row') { const refMiddleY = refBoundingRect.top + ((refBoundingRect.bottom - refBoundingRect.top) / 2); dropOffset = clientOffset.y < refMiddleY ? 0 : refBoundingRect.height; @@ -70,10 +70,10 @@ export default function handleHover(props, monitor, Component) { Component.setState(() => ({ dropIndicator: { - top: orientation === 'vertical' ? 0 : dropOffset, - left: orientation === 'vertical' ? dropOffset : 0, - height: orientation === 'vertical' ? '100%' : 3, - width: orientation === 'vertical' ? 3 : '100%', + top: orientation === 'column' ? 0 : dropOffset, + left: orientation === 'column' ? dropOffset : 0, + height: orientation === 'column' ? '100%' : 3, + width: orientation === 'column' ? 3 : '100%', backgroundColor: '#44C0FF', position: 'absolute', zIndex: 10, diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Chart.jsx b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Chart.jsx index 45ed7f69460d4..b973dd492af35 100644 --- a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Chart.jsx +++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Chart.jsx @@ -54,7 +54,7 @@ class Chart extends React.Component { ); - - return ; } } diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Tabs.jsx b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Tabs.jsx index e9fe979a47d79..31f6d40757e36 100644 --- a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Tabs.jsx +++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Tabs.jsx @@ -8,6 +8,9 @@ import DashboardComponent from '../../containers/DashboardComponent'; import { componentShape } from '../../util/propShapes'; import { TAB_TYPE } from '../../util/componentTypes'; +const NEW_TAB_INDEX = -1; +const MAX_TAB_COUNT = 5; + const propTypes = { component: componentShape.isRequired, components: PropTypes.object.isRequired, @@ -33,8 +36,6 @@ const defaultProps = { children: null, }; -const NEW_TAB_INDEX = -1; - class Tabs extends React.Component { constructor(props) { super(props); @@ -92,7 +93,7 @@ class Tabs extends React.Component { { @@ -175,7 +176,7 @@ class Tabs extends React.Component { ); })} - {tabIds.length < 5 && + {tabIds.length < MAX_TAB_COUNT && Date: Wed, 14 Feb 2018 12:24:42 -0800 Subject: [PATCH 14/25] [builder] refactor into HoverMenu, add WithPopoverMenu --- .../javascripts/dashboard/v2/actions/index.js | 3 +- .../v2/components/DeleteComponentButton.jsx | 27 ++++++ .../v2/components/dnd/DragHandle.jsx | 13 ++- .../dashboard/v2/components/dnd/dnd.css | 65 +++---------- .../v2/components/gridComponents/Chart.jsx | 8 +- .../v2/components/gridComponents/Column.jsx | 8 +- .../v2/components/gridComponents/Divider.jsx | 18 +++- .../v2/components/gridComponents/Header.jsx | 24 ++++- .../v2/components/gridComponents/Row.jsx | 8 +- .../v2/components/gridComponents/Spacer.jsx | 9 +- .../v2/components/gridComponents/Tabs.jsx | 5 +- .../components/gridComponents/components.css | 96 ++++++++++++++++++- .../v2/components/menu/HoverMenu.jsx | 36 +++++++ .../v2/components/menu/WithPopoverMenu.jsx | 85 ++++++++++++++++ .../v2/components/resizable/resizable.css | 2 +- .../dashboard/v2/reducers/dashboard.js | 32 ++++++- 16 files changed, 347 insertions(+), 92 deletions(-) create mode 100644 superset/assets/javascripts/dashboard/v2/components/DeleteComponentButton.jsx create mode 100644 superset/assets/javascripts/dashboard/v2/components/menu/HoverMenu.jsx create mode 100644 superset/assets/javascripts/dashboard/v2/components/menu/WithPopoverMenu.jsx diff --git a/superset/assets/javascripts/dashboard/v2/actions/index.js b/superset/assets/javascripts/dashboard/v2/actions/index.js index f847e1057ccb2..16fde6b365c62 100644 --- a/superset/assets/javascripts/dashboard/v2/actions/index.js +++ b/superset/assets/javascripts/dashboard/v2/actions/index.js @@ -9,11 +9,12 @@ export function updateComponents(nextComponents) { } export const DELETE_COMPONENT = 'DELETE_COMPONENT'; -export function deleteComponent(id) { +export function deleteComponent(id, parentId) { return { type: DELETE_COMPONENT, payload: { id, + parentId, }, }; } diff --git a/superset/assets/javascripts/dashboard/v2/components/DeleteComponentButton.jsx b/superset/assets/javascripts/dashboard/v2/components/DeleteComponentButton.jsx new file mode 100644 index 0000000000000..c9d99799e3fb6 --- /dev/null +++ b/superset/assets/javascripts/dashboard/v2/components/DeleteComponentButton.jsx @@ -0,0 +1,27 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import cx from 'classnames'; + +const propTypes = { + onDelete: PropTypes.func.isRequired, +}; + +const defaultProps = { +}; + +export default class DeleteComponentButton extends React.PureComponent { + render() { + const { onDelete } = this.props; + return ( +
    + ); + } +} + +DeleteComponentButton.propTypes = propTypes; +DeleteComponentButton.defaultProps = defaultProps; diff --git a/superset/assets/javascripts/dashboard/v2/components/dnd/DragHandle.jsx b/superset/assets/javascripts/dashboard/v2/components/dnd/DragHandle.jsx index 7a8c584c459e5..36d1e6beff521 100644 --- a/superset/assets/javascripts/dashboard/v2/components/dnd/DragHandle.jsx +++ b/superset/assets/javascripts/dashboard/v2/components/dnd/DragHandle.jsx @@ -21,15 +21,14 @@ export default class DragHandle extends React.PureComponent {
    -
    - {Array(dotCount).fill(null).map((_, i) => ( -
    - ))} -
    + {Array(dotCount).fill(null).map((_, i) => ( +
    + ))}
    ); } diff --git a/superset/assets/javascripts/dashboard/v2/components/dnd/dnd.css b/superset/assets/javascripts/dashboard/v2/components/dnd/dnd.css index bbcfd5172bc60..cd25a66363eee 100644 --- a/superset/assets/javascripts/dashboard/v2/components/dnd/dnd.css +++ b/superset/assets/javascripts/dashboard/v2/components/dnd/dnd.css @@ -26,74 +26,33 @@ border: 1px dashed #aaa; } -.draggable-row-handle { - opacity: 0; - position: absolute; - width: 20px; - height: 100%; - top: 0; - left: -20px; +/* Drag handle */ +.drag-handle { + overflow: hidden; + width: 16px; cursor: move; - z-index: 1; - display: flex; - flex-direction: column; - justify-content: space-around; } - /* offset nested row handles */ - .dragdroppable-row .dragdroppable-row .draggable-row-handle { - left: -2px; - } - -.draggable-row-handle .handle { +.drag-handle--left { padding-left: 7px; - overflow: hidden; - width: 15px } -.draggable-row-handle .handle .handle-dot, -.draggable-row-item-handle .handle .handle-dot { +.drag-handle--top { + margin: 10px auto; +} + +.drag-handle-dot { float: left; height: 2px; margin: 1px; width: 2px } -.draggable-row-handle .handle .handle-dot:after, -.draggable-row-item-handle .handle .handle-dot:after { - background: #aaa; +.drag-handle-dot:after { content: ""; + background: #aaa; float: left; height: 2px; margin: -1px; width: 2px; } - - -.draggable-row-item-handle { - opacity: 0; - position: absolute; - width: 100%; - height: 20px; - top: 0; - left: 0; - cursor: move; - z-index: 1; -} - -.draggable-row-item-handle .handle { - overflow: hidden; - width: 16px; - margin: 10px auto; -} - -.dragdroppable:hover .draggable-row-handle, -.draggable-row-handle:hover, -.dragdroppable:hover .draggable-row-item-handle, -.draggable-row-item-handle:hover, -.dragdroppable-row .dragdroppable-row:hover .draggable-row-handle, -.dragdroppable-row .dragdroppable-row:hover .draggable-row-item-handle { - opacity: 1; -} - -/*top: -20px;*/ diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Chart.jsx b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Chart.jsx index b973dd492af35..17d2d0e622b1f 100644 --- a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Chart.jsx +++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Chart.jsx @@ -4,6 +4,7 @@ import PropTypes from 'prop-types'; import DimensionProvider from '../resizable/DimensionProvider'; import DragDroppable from '../dnd/DragDroppable'; import DragHandle from '../dnd/DragHandle'; +import HoverMenu from '../menu/HoverMenu'; import { componentShape } from '../../util/propShapes'; const propTypes = { @@ -69,10 +70,9 @@ class Chart extends React.Component { onResize={onResize} onResizeStop={onResizeStop} > - + + +
    Chart
    diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Column.jsx b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Column.jsx index bb9e25a98b387..90a8da89f7c89 100644 --- a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Column.jsx +++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Column.jsx @@ -6,6 +6,7 @@ import DragDroppable from '../dnd/DragDroppable'; import DragHandle from '../dnd/DragHandle'; import DimensionProvider from '../resizable/DimensionProvider'; import DashboardComponent from '../../containers/DashboardComponent'; +import HoverMenu from '../menu/HoverMenu'; import { componentShape } from '../../util/propShapes'; import { GRID_GUTTER_SIZE } from '../../util/constants'; @@ -85,10 +86,9 @@ class Column extends React.PureComponent { columnItems.length === 0 && 'grid-column--empty', )} > - + + + {columnItems.map((component, itemIndex) => { if (!component.id) { diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Divider.jsx b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Divider.jsx index df25a3c557e70..79f33a364998c 100644 --- a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Divider.jsx +++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Divider.jsx @@ -2,7 +2,8 @@ import React from 'react'; import PropTypes from 'prop-types'; import DragDroppable from '../dnd/DragDroppable'; -import DragHandle from '../dnd/DragHandle'; +import HoverMenu from '../menu/HoverMenu'; +import DeleteComponentButton from '../DeleteComponentButton'; import { componentShape } from '../../util/propShapes'; const propTypes = { @@ -11,9 +12,20 @@ const propTypes = { index: PropTypes.number.isRequired, parentId: PropTypes.string.isRequired, handleComponentDrop: PropTypes.func.isRequired, + deleteComponent: PropTypes.func.isRequired, }; class Divider extends React.PureComponent { + constructor(props) { + super(props); + this.handleDeleteComponent = this.handleDeleteComponent.bind(this); + } + + handleDeleteComponent() { + const { deleteComponent, component, parentId } = this.props; + deleteComponent(component.id, parentId); + } + render() { const { component, @@ -34,7 +46,9 @@ class Divider extends React.PureComponent { > {({ 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 16a493e7b3230..0bbf94399660b 100644 --- a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Header.jsx +++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Header.jsx @@ -3,6 +3,8 @@ import PropTypes from 'prop-types'; import DragDroppable from '../dnd/DragDroppable'; import DragHandle from '../dnd/DragHandle'; +import HoverMenu from '../menu/HoverMenu'; +import WithPopoverMenu from '../menu/WithPopoverMenu'; import { componentShape } from '../../util/propShapes'; const propTypes = { @@ -10,8 +12,8 @@ const propTypes = { components: PropTypes.object.isRequired, index: PropTypes.number.isRequired, parentId: PropTypes.string.isRequired, - // dnd handleComponentDrop: PropTypes.func.isRequired, + deleteComponent: PropTypes.func.isRequired, }; const defaultProps = { @@ -22,6 +24,12 @@ class Header extends React.PureComponent { constructor(props) { super(props); this.state = {}; + this.handleDeleteComponent = this.handleDeleteComponent.bind(this); + } + + handleDeleteComponent() { + const { deleteComponent, component, parentId } = this.props; + deleteComponent(component.id, parentId); } render() { @@ -44,11 +52,17 @@ class Header extends React.PureComponent { > {({ dropIndicatorProps, dragSourceRef }) => (
    - + + + -
    - {component.meta.text} -
    + +
    + {component.meta.text} +
    +
    {dropIndicatorProps &&
    }
    diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Row.jsx b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Row.jsx index a9f5c9d0a5fca..dfe2f87f33d47 100644 --- a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Row.jsx +++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Row.jsx @@ -5,6 +5,7 @@ import cx from 'classnames'; import DragDroppable from '../dnd/DragDroppable'; import DragHandle from '../dnd/DragHandle'; import DashboardComponent from '../../containers/DashboardComponent'; +import HoverMenu from '../menu/HoverMenu'; import { componentShape } from '../../util/propShapes'; import { GRID_GUTTER_SIZE } from '../../util/constants'; import { INVISIBLE_ROW_TYPE } from '../../util/componentTypes'; @@ -82,10 +83,9 @@ class Row extends React.PureComponent { rowComponent.type !== INVISIBLE_ROW_TYPE && 'grid-row-container', )} > - + + + {rowItems.map((component, itemIndex) => { if (!component.id) { diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Spacer.jsx b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Spacer.jsx index 8f430aa113cc2..4857a18f26839 100644 --- a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Spacer.jsx +++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Spacer.jsx @@ -4,6 +4,7 @@ import PropTypes from 'prop-types'; import DimensionProvider from '../resizable/DimensionProvider'; import DragDroppable from '../dnd/DragDroppable'; import DragHandle from '../dnd/DragHandle'; +import HoverMenu from '../menu/HoverMenu'; import { componentShape } from '../../util/propShapes'; const propTypes = { @@ -46,6 +47,7 @@ class Spacer extends React.PureComponent { handleComponentDrop, } = this.props; + const hoverMenuPosition = depth % 2 !== 0 ? 'left' : 'top'; return ( - + + +
    diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Tabs.jsx b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Tabs.jsx index 31f6d40757e36..a942dc8347939 100644 --- a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Tabs.jsx +++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Tabs.jsx @@ -5,6 +5,7 @@ import { Tabs as BootstrapTabs, Tab } from 'react-bootstrap'; import DragDroppable from '../dnd/DragDroppable'; import DragHandle from '../dnd/DragHandle'; import DashboardComponent from '../../containers/DashboardComponent'; +import HoverMenu from '../menu/HoverMenu'; import { componentShape } from '../../util/propShapes'; import { TAB_TYPE } from '../../util/componentTypes'; @@ -100,7 +101,9 @@ class Tabs extends React.Component { > {({ dropIndicatorProps: tabsDropIndicatorProps, dragSourceRef: tabsDragSourceRef }) => (
    - + + + + {children} +
    + ); + } +} + +HoverMenu.propTypes = propTypes; +HoverMenu.defaultProps = defaultProps; diff --git a/superset/assets/javascripts/dashboard/v2/components/menu/WithPopoverMenu.jsx b/superset/assets/javascripts/dashboard/v2/components/menu/WithPopoverMenu.jsx new file mode 100644 index 0000000000000..06e79031ec9f5 --- /dev/null +++ b/superset/assets/javascripts/dashboard/v2/components/menu/WithPopoverMenu.jsx @@ -0,0 +1,85 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import cx from 'classnames'; + +import DeleteComponentButton from '../DeleteComponentButton'; + +const propTypes = { + children: PropTypes.node, + onPressDelete: PropTypes.func, +}; + +const defaultProps = { + children: null, + onPressDelete() {}, +}; + +class WithPopoverMenu extends React.Component { + constructor(props) { + super(props); + this.state = { + isFocused: false, + }; + this.setRef = this.setRef.bind(this); + this.handleClick = this.handleClick.bind(this); + } + + componentWillUnmount() { + document.removeEventListener('click', this.handleClick, true); + } + + setRef(ref) { + this.container = ref; + } + + handleClick(event) { + if (!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); + this.setState(() => ({ isFocused: true })); + } else if (!this.container.contains(event.target)) { + console.log('outside click'); + document.removeEventListener('click', this.handleClick, true); + this.setState(() => ({ isFocused: false })); + } else { + console.log('inside click'); + } + } + + renderMenu() { + const { onPressDelete } = this.props; + return ( +
    + Menu +
    + +
    + ); + } + + render() { + const { children } = this.props; + const { isFocused } = this.state; + return ( +
    + {children} + {isFocused && this.renderMenu()} +
    + ); + } +} + +WithPopoverMenu.propTypes = propTypes; +WithPopoverMenu.defaultProps = defaultProps; + +export default WithPopoverMenu; diff --git a/superset/assets/javascripts/dashboard/v2/components/resizable/resizable.css b/superset/assets/javascripts/dashboard/v2/components/resizable/resizable.css index 3a31107f25238..2d1828604da41 100644 --- a/superset/assets/javascripts/dashboard/v2/components/resizable/resizable.css +++ b/superset/assets/javascripts/dashboard/v2/components/resizable/resizable.css @@ -4,7 +4,7 @@ } /* after ensures border visibility on top of any children */ -.grid-resizable-container--resizing::after { +.grid-resizable-container--resizing:after { content: ""; position: absolute; top: 0; diff --git a/superset/assets/javascripts/dashboard/v2/reducers/dashboard.js b/superset/assets/javascripts/dashboard/v2/reducers/dashboard.js index a34ffa203b2a1..34c673b4b30c0 100644 --- a/superset/assets/javascripts/dashboard/v2/reducers/dashboard.js +++ b/superset/assets/javascripts/dashboard/v2/reducers/dashboard.js @@ -18,11 +18,35 @@ const actionHandlers = { }, [DELETE_COMPONENT](state, action) { - // const { payload: id } = action; - // recursively delete children - console.log('@TODO implement', DELETE_COMPONENT); + const { payload: { id, parentId } } = action; - return state; + if (!parentId || !id || !state[id] || !state[parentId]) return state; + + const nextComponents = { ...state }; + + // recursively find children to remove + let deleteCount = 0; + function recursivelyDeleteChildren(componentId, componentParentId) { + const component = nextComponents[componentId]; + const parent = nextComponents[componentParentId]; + + if (parent && component) { + const componentIndex = (parent.children || []).indexOf(componentId); + if (componentIndex > -1) { + parent.children.splice(componentIndex, 1); + delete nextComponents[componentId]; + deleteCount += 1; + + const { children = [] } = component; + children.forEach((childId) => { recursivelyDeleteChildren(childId, componentId); }); + } + } + } + + recursivelyDeleteChildren(id, parentId); + console.log('Deleted', deleteCount, 'total components'); + + return nextComponents; }, [CREATE_COMPONENT](state, action) { From 8baebb037da3585ba92cb8fc8dc9d5669cef9fde Mon Sep 17 00:00:00 2001 From: Chris Williams Date: Wed, 14 Feb 2018 17:57:37 -0800 Subject: [PATCH 15/25] [builder] add editable header and disableDragDrop prop for Dragdroppable's --- .../javascripts/components/EditableTitle.jsx | 5 +- .../v2/components/dnd/DragDroppable.jsx | 2 + .../v2/components/dnd/dragDroppableConfig.js | 3 + .../v2/components/gridComponents/Header.jsx | 78 +++++++++++++++++-- .../components/gridComponents/components.css | 51 ++++++++++-- .../components/menu/HeaderStyleDropdown.jsx | 58 ++++++++++++++ .../v2/components/menu/WithPopoverMenu.jsx | 45 ++++++----- .../dashboard/v2/util/constants.js | 10 +++ .../dashboard/v2/util/newEntitiesFromDrop.js | 6 +- superset/assets/stylesheets/superset.less | 15 ++-- 10 files changed, 230 insertions(+), 43 deletions(-) create mode 100644 superset/assets/javascripts/dashboard/v2/components/menu/HeaderStyleDropdown.jsx diff --git a/superset/assets/javascripts/components/EditableTitle.jsx b/superset/assets/javascripts/components/EditableTitle.jsx index b773340846622..eaed843d6e6a4 100644 --- a/superset/assets/javascripts/components/EditableTitle.jsx +++ b/superset/assets/javascripts/components/EditableTitle.jsx @@ -1,5 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; +import cx from 'classnames'; import TooltipWrapper from './TooltipWrapper'; import { t } from '../locales'; @@ -110,7 +111,9 @@ class EditableTitle extends React.PureComponent { ); } return ( - {input} + + {input} + ); } } diff --git a/superset/assets/javascripts/dashboard/v2/components/dnd/DragDroppable.jsx b/superset/assets/javascripts/dashboard/v2/components/dnd/DragDroppable.jsx index c62532188449e..ba87e5709ea19 100644 --- a/superset/assets/javascripts/dashboard/v2/components/dnd/DragDroppable.jsx +++ b/superset/assets/javascripts/dashboard/v2/components/dnd/DragDroppable.jsx @@ -16,6 +16,7 @@ const propTypes = { children: PropTypes.func, component: componentShape.isRequired, components: PropTypes.object.isRequired, + disableDragDrop: PropTypes.bool, orientation: PropTypes.oneOf(['row', 'column']), index: PropTypes.number.isRequired, parentId: PropTypes.string, @@ -37,6 +38,7 @@ const propTypes = { }; const defaultProps = { + disableDragDrop: false, children() {}, onDrop() {}, parentId: null, diff --git a/superset/assets/javascripts/dashboard/v2/components/dnd/dragDroppableConfig.js b/superset/assets/javascripts/dashboard/v2/components/dnd/dragDroppableConfig.js index 5e2c7d0b1dfff..6eb37e90f393e 100644 --- a/superset/assets/javascripts/dashboard/v2/components/dnd/dragDroppableConfig.js +++ b/superset/assets/javascripts/dashboard/v2/components/dnd/dragDroppableConfig.js @@ -4,6 +4,9 @@ const TYPE = 'DRAG_DROPPABLE'; export const dragConfig = [ TYPE, { + canDrag(props) { + return !props.disableDragDrop; + }, beginDrag(props /* , monitor, component */) { const { component, index, parentId } = props; return { draggableId: component.id, index, parentId, type: component.type }; diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Header.jsx b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Header.jsx index 0bbf94399660b..64c3535b9a347 100644 --- a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Header.jsx +++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Header.jsx @@ -1,11 +1,16 @@ import React from 'react'; import PropTypes from 'prop-types'; +import cx from 'classnames'; import DragDroppable from '../dnd/DragDroppable'; import DragHandle from '../dnd/DragHandle'; +import EditableTitle from '../../../../components/EditableTitle'; import HoverMenu from '../menu/HoverMenu'; import WithPopoverMenu from '../menu/WithPopoverMenu'; +import DeleteComponentButton from '../DeleteComponentButton'; +import HeaderStyleDropdown, { headerStyleOptions } from '../menu/HeaderStyleDropdown'; import { componentShape } from '../../util/propShapes'; +import { SMALL_HEADER } from '../../util/constants'; const propTypes = { component: componentShape.isRequired, @@ -14,17 +19,54 @@ const propTypes = { parentId: PropTypes.string.isRequired, handleComponentDrop: PropTypes.func.isRequired, deleteComponent: PropTypes.func.isRequired, + updateComponents: PropTypes.func.isRequired, }; const defaultProps = { - component: {}, }; class Header extends React.PureComponent { constructor(props) { super(props); - this.state = {}; + this.state = { + isFocused: false, + }; this.handleDeleteComponent = this.handleDeleteComponent.bind(this); + this.handleChangeFocus = this.handleChangeFocus.bind(this); + this.handleChangeStyle = this.handleChangeStyle.bind(this); + this.handleChangeText = this.handleChangeText.bind(this); + } + + handleChangeFocus(nextFocus) { + this.setState(() => ({ isFocused: nextFocus })); + } + + handleChangeStyle(nextStyle) { + const { updateComponents, component } = this.props; + updateComponents({ + [component.id]: { + ...component, + meta: { + ...component.meta, + style: nextStyle.value, + }, + }, + }); + } + + handleChangeText(nextText) { + const { updateComponents, component } = this.props; + if (nextText && nextText !== component.meta.text) { + updateComponents({ + [component.id]: { + ...component, + meta: { + ...component.meta, + text: nextText, + }, + }, + }); + } } handleDeleteComponent() { @@ -33,6 +75,8 @@ class Header extends React.PureComponent { } render() { + const { isFocused } = this.state; + const { component, components, @@ -41,6 +85,10 @@ class Header extends React.PureComponent { handleComponentDrop, } = this.props; + const headerStyle = headerStyleOptions.find( + opt => opt.value === (component.meta.style || SMALL_HEADER), + ); + return ( {({ dropIndicatorProps, dragSourceRef }) => (
    @@ -57,10 +106,29 @@ class Header extends React.PureComponent { , + , + ]} > -
    - {component.meta.text} +
    +
    diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/components.css b/superset/assets/javascripts/dashboard/v2/components/gridComponents/components.css index 52803402384af..84a76f966d474 100644 --- a/superset/assets/javascripts/dashboard/v2/components/gridComponents/components.css +++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/components.css @@ -1,13 +1,25 @@ /* Header */ .dashboard-component-header { width: 100%; - font-size: 24px; /* @TODO will need different sizes */ - line-height: 24px; + line-height: 1em; font-weight: 700; background-color: inherit; padding: 16px 0; color: #263238; } + +.header-small { + font-size: 16px; +} + +.header-medium { + font-size: 22px; +} + +.header-large { + font-size: 32px; +} + .dragdroppable-row .dragdroppable-row .dashboard-component-header, .dragdroppable-row .dragdroppable-row .dashboard-component-divider { padding-left: 16px; @@ -232,10 +244,11 @@ content: ""; position: absolute; top: 1; - left: 0; + left: -1; width: 100%; height: 100%; box-shadow: inset 0 0 0 2px #44C0FF; + pointer-events: none; } .popover-menu { @@ -244,23 +257,49 @@ flex-direction: row; align-items: center; flex-wrap: nowrap; - left: 0; + left: 1px; top: -42px; height: 40px; padding: 0 16px; background: #fff; - box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.2); + box-shadow: 0 1px 2px 1px rgba(0, 0, 0, 0.2); font-size: 14px; cursor: default; } -.popover-menu-vertical-separator { +.popover-menu .menu-item { + display: flex; + flex-direction: row; + align-items: center; +} + +/* vertical spacer after each menu item */ +.popover-menu .menu-item:nth-child(odd):after { + content: ""; width: 1; height: 100%; background: #CFD8DC; margin: 0 16px; } +.popover-menu .popover-dropdown.btn { + border: none; + padding: 0; + font-size: inherit; +} + +.popover-menu .popover-dropdown.btn:hover, +.popover-menu .popover-dropdown.btn:active, +.popover-menu .popover-dropdown.btn:focus { + background: initial; + box-shadow: none; +} + +.popover-menu li.dropdown-item:hover a, +.popover-menu li.dropdown-item.active a { + background: #CFD8DC; +} + /* hover menu */ .hover-menu { opacity: 0; diff --git a/superset/assets/javascripts/dashboard/v2/components/menu/HeaderStyleDropdown.jsx b/superset/assets/javascripts/dashboard/v2/components/menu/HeaderStyleDropdown.jsx new file mode 100644 index 0000000000000..cadc19025b483 --- /dev/null +++ b/superset/assets/javascripts/dashboard/v2/components/menu/HeaderStyleDropdown.jsx @@ -0,0 +1,58 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { DropdownButton, MenuItem } from 'react-bootstrap'; +import { t } from '../../../../locales'; +import { SMALL_HEADER, MEDIUM_HEADER, LARGE_HEADER } from '../../util/constants'; + +export const headerStyleOptions = [ + { value: SMALL_HEADER, label: t('Small'), className: 'header-small' }, + { value: MEDIUM_HEADER, label: t('Medium'), className: 'header-medium' }, + { value: LARGE_HEADER, label: t('Large'), className: 'header-large' }, +]; + +const propTypes = { + id: PropTypes.string.isRequired, + onChange: PropTypes.func.isRequired, + value: PropTypes.oneOf(headerStyleOptions.map(opt => opt.value)).isRequired, +}; + +class HeaderStyleDropdown extends React.PureComponent { + constructor(props) { + super(props); + this.handleSelect = this.handleSelect.bind(this); + } + + handleSelect(nextStyle) { + console.log(nextStyle); + this.props.onChange(nextStyle); + } + + render() { + const { id, value } = this.props; + const selected = headerStyleOptions.find(opt => opt.value === value); + return ( + + {headerStyleOptions.map(option => ( + +
    {option.label}
    +
    + ))} +
    + ); + } +} + +HeaderStyleDropdown.propTypes = propTypes; + +export default HeaderStyleDropdown; diff --git a/superset/assets/javascripts/dashboard/v2/components/menu/WithPopoverMenu.jsx b/superset/assets/javascripts/dashboard/v2/components/menu/WithPopoverMenu.jsx index 06e79031ec9f5..fd219638ffba6 100644 --- a/superset/assets/javascripts/dashboard/v2/components/menu/WithPopoverMenu.jsx +++ b/superset/assets/javascripts/dashboard/v2/components/menu/WithPopoverMenu.jsx @@ -2,16 +2,17 @@ import React from 'react'; import PropTypes from 'prop-types'; import cx from 'classnames'; -import DeleteComponentButton from '../DeleteComponentButton'; - const propTypes = { children: PropTypes.node, - onPressDelete: PropTypes.func, + menuItems: PropTypes.arrayOf(PropTypes.node), + onChangeFocus: PropTypes.func, }; const defaultProps = { children: null, + onChangeFocus: null, onPressDelete() {}, + menuItems: [], }; class WithPopoverMenu extends React.Component { @@ -25,7 +26,7 @@ class WithPopoverMenu extends React.Component { } componentWillUnmount() { - document.removeEventListener('click', this.handleClick, true); + document.removeEventListener('mousedown', this.handleClick, true); } setRef(ref) { @@ -33,39 +34,32 @@ class WithPopoverMenu extends React.Component { } handleClick(event) { + const { onChangeFocus } = this.props; if (!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); + document.addEventListener('mousedown', this.handleClick, true); this.setState(() => ({ isFocused: true })); + if (onChangeFocus) { + onChangeFocus(true); + } } else if (!this.container.contains(event.target)) { - console.log('outside click'); - document.removeEventListener('click', this.handleClick, true); + document.removeEventListener('mousedown', this.handleClick, true); this.setState(() => ({ isFocused: false })); - } else { - console.log('inside click'); + if (onChangeFocus) { + onChangeFocus(false); + } } } - renderMenu() { - const { onPressDelete } = this.props; - return ( -
    - Menu -
    - -
    - ); - } - render() { - const { children } = this.props; + const { children, menuItems } = this.props; const { isFocused } = this.state; return (
    {children} - {isFocused && this.renderMenu()} + {isFocused && menuItems.length && +
    + {menuItems.map((node, i) => ( +
    {node}
    + ))} +
    }
    ); } diff --git a/superset/assets/javascripts/dashboard/v2/util/constants.js b/superset/assets/javascripts/dashboard/v2/util/constants.js index ddc3030ab639b..6c85f542863a5 100644 --- a/superset/assets/javascripts/dashboard/v2/util/constants.js +++ b/superset/assets/javascripts/dashboard/v2/util/constants.js @@ -1,5 +1,6 @@ // Ids export const DASHBOARD_ROOT_ID = 'DASHBOARD_ROOT_ID'; +// @TODO new component ids // grid constants export const GRID_BASE_UNIT = 8; @@ -10,3 +11,12 @@ export const GRID_MIN_COLUMN_COUNT = 3; export const GRID_MIN_ROW_UNITS = 5; export const GRID_MAX_ROW_UNITS = 100; export const GRID_MIN_ROW_HEIGHT = GRID_GUTTER_SIZE; + +// Header types +export const SMALL_HEADER = 'SMALL_HEADER'; +export const MEDIUM_HEADER = 'MEDIUM_HEADER'; +export const LARGE_HEADER = 'LARGE_HEADER'; + +// Row types +export const WHITE_ROW = 'WHITE_ROW'; +export const TRANSPARENT_ROW = 'TRANSPARENT_ROW'; diff --git a/superset/assets/javascripts/dashboard/v2/util/newEntitiesFromDrop.js b/superset/assets/javascripts/dashboard/v2/util/newEntitiesFromDrop.js index 195982df8ac85..bf209e3be1467 100644 --- a/superset/assets/javascripts/dashboard/v2/util/newEntitiesFromDrop.js +++ b/superset/assets/javascripts/dashboard/v2/util/newEntitiesFromDrop.js @@ -13,11 +13,15 @@ import { TAB_TYPE, } from './componentTypes'; +import { + MEDIUM_HEADER, +} from './constants'; + const typeToDefaultMetaData = { [CHART_TYPE]: { width: 3, height: 15 }, [COLUMN_TYPE]: { width: 3 }, [DIVIDER_TYPE]: null, - [HEADER_TYPE]: { text: 'New header' }, + [HEADER_TYPE]: { text: 'New header', style: MEDIUM_HEADER }, [INVISIBLE_ROW_TYPE]: null, [MARKDOWN_TYPE]: { width: 3, height: 15 }, [ROW_TYPE]: null, diff --git a/superset/assets/stylesheets/superset.less b/superset/assets/stylesheets/superset.less index 72a657252df66..5cf448852ace1 100644 --- a/superset/assets/stylesheets/superset.less +++ b/superset/assets/stylesheets/superset.less @@ -234,23 +234,24 @@ table.table-no-hover tr:hover { } .editable-title input { - padding: 2px 0px 3px 0px; + outline: none; + background: transparent; + border: none; + box-shadow: none; + padding-left: 0; } .editable-title input[type="button"] { - border-color: transparent; - background: inherit; + font-size: inherit; + line-height: inherit; white-space: normal; text-align: left; } -.editable-title input[type="button"]:hover { +.editable-title--editable input[type="button"]:hover { cursor: text; } -.editable-title input[type="button"]:focus { - outline: none; -} .m-r-5 { margin-right: 5px; } From c1902e9e0f77f60d8f3a5da4decb0b509bc12316 Mon Sep 17 00:00:00 2001 From: Chris Williams Date: Thu, 15 Feb 2018 22:58:32 -0800 Subject: [PATCH 16/25] [builder] make tabs editable --- .../javascripts/components/EditableTitle.jsx | 33 ++++- .../dashboard/v2/components/DashboardGrid.jsx | 2 +- .../v2/components/gridComponents/Tabs.jsx | 118 ++++++++++++++---- .../components/gridComponents/components.css | 9 +- .../v2/components/gridComponents/index.js | 4 + .../v2/components/menu/WithPopoverMenu.jsx | 6 +- 6 files changed, 141 insertions(+), 31 deletions(-) diff --git a/superset/assets/javascripts/components/EditableTitle.jsx b/superset/assets/javascripts/components/EditableTitle.jsx index eaed843d6e6a4..20370d9ddd442 100644 --- a/superset/assets/javascripts/components/EditableTitle.jsx +++ b/superset/assets/javascripts/components/EditableTitle.jsx @@ -28,9 +28,12 @@ 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.handleKeyPress = this.handleKeyPress.bind(this); } + componentWillReceiveProps(nextProps) { + console.log('new props') if (nextProps.title !== this.state.title) { this.setState({ lastTitle: this.state.title, @@ -38,8 +41,9 @@ class EditableTitle extends React.PureComponent { }); } } + handleClick() { - if (!this.props.canEdit) { + if (!this.props.canEdit || this.state.isEditing) { return; } @@ -47,7 +51,9 @@ class EditableTitle extends React.PureComponent { isEditing: true, }); } + handleBlur() { + console.log('blur') if (!this.props.canEdit) { return; } @@ -68,9 +74,31 @@ class EditableTitle extends React.PureComponent { this.setState({ lastTitle: this.state.title, }); + } + + if (this.props.title !== this.state.title) { this.props.onSaveTitle(this.state.title); } } + + handleKeyDown(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 + // + // tl;dr when a Tab EditableTitle is being edited, typically the Tab it's within has been + // clicked and is focused/active. for accessibility, when focused the Tab intercepts + // the ' ' key (among others, including all arrows) and onChange() doesn't fire. somehow + // keydown is still called so we can detect this and manually add a ' ' to the current title + if (ev.key === ' ') { + let title = ev.target.value; + const titleLength = (title || '').length; + if (title && title[titleLength - 1] !== ' ') { + title = `${title} `; + this.setState(() => ({ title })); + } + } + } + handleChange(ev) { if (!this.props.canEdit) { return; @@ -80,6 +108,7 @@ class EditableTitle extends React.PureComponent { title: ev.target.value, }); } + handleKeyPress(ev) { if (ev.key === 'Enter') { ev.preventDefault(); @@ -87,12 +116,14 @@ class EditableTitle extends React.PureComponent { this.handleBlur(); } } + render() { let input = ( {(rootComponent.children || []).map((id, index) => ( diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Tabs.jsx b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Tabs.jsx index a942dc8347939..b801533799da7 100644 --- a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Tabs.jsx +++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Tabs.jsx @@ -5,7 +5,10 @@ import { Tabs as BootstrapTabs, Tab } from 'react-bootstrap'; import DragDroppable from '../dnd/DragDroppable'; import DragHandle from '../dnd/DragHandle'; import DashboardComponent from '../../containers/DashboardComponent'; +import EditableTitle from '../../../../components/EditableTitle'; +import DeleteComponentButton from '../DeleteComponentButton'; import HoverMenu from '../menu/HoverMenu'; +import WithPopoverMenu from '../menu/WithPopoverMenu'; import { componentShape } from '../../util/propShapes'; import { TAB_TYPE } from '../../util/componentTypes'; @@ -30,6 +33,8 @@ const propTypes = { createComponent: PropTypes.func.isRequired, handleComponentDrop: PropTypes.func.isRequired, onChangeTab: PropTypes.func, + deleteComponent: PropTypes.func.isRequired, + updateComponents: PropTypes.func.isRequired, }; const defaultProps = { @@ -37,31 +42,37 @@ const defaultProps = { children: null, }; -class Tabs extends React.Component { +class Tabs extends React.PureComponent { constructor(props) { super(props); this.state = { tabIndex: 0, + focusedId: null, }; this.handleClicKTab = this.handleClicKTab.bind(this); + this.handleDeleteComponent = this.handleDeleteComponent.bind(this); + this.handleDropOnTab = this.handleDropOnTab.bind(this); + this.handleChangeFocus = this.handleChangeFocus.bind(this); + this.handleChangeText = this.handleChangeText.bind(this); } componentWillReceiveProps(nextProps) { const maxIndex = Math.max(0, nextProps.component.children.length - 1); - if (this.state.tabIndex >= maxIndex) { + if (this.state.tabIndex > maxIndex) { this.setState(() => ({ tabIndex: maxIndex })); } } handleClicKTab(tabIndex) { const { onChangeTab, component, createComponent } = this.props; + const { focusedId } = this.state; - if (tabIndex !== NEW_TAB_INDEX) { + if (!focusedId && tabIndex !== NEW_TAB_INDEX && tabIndex !== this.state.tabIndex) { this.setState(() => ({ tabIndex })); if (onChangeTab) { onChangeTab({ tabIndex, tab: component.children[tabIndex] }); } - } else { + } else if (!focusedId && tabIndex === NEW_TAB_INDEX) { createComponent({ destination: { droppableId: component.id, @@ -72,6 +83,53 @@ class Tabs extends React.Component { } } + handleChangeFocus(nextFocus) { + if (this.state.focusedId !== nextFocus) { + this.setState(() => ({ focusedId: nextFocus })); + } + } + + handleChangeText({ id, nextTabText }) { + const { updateComponents, components } = this.props; + const tab = components[id]; + if (nextTabText && tab && nextTabText !== tab.meta.text) { + updateComponents({ + [tab.id]: { + ...tab, + meta: { + ...tab.meta, + text: nextTabText, + }, + }, + }); + } + } + + handleDeleteComponent(id) { + const { deleteComponent, component, parentId } = this.props; + const isTabsComponent = id === component.id; + deleteComponent(id, isTabsComponent ? parentId : component.id); + } + + handleDropOnTab(dropResult) { + const { component, handleComponentDrop } = this.props; + handleComponentDrop(dropResult); + + // Ensure dropped tab is visible + const { destination } = dropResult; + if (destination) { + const dropTabIndex = destination.droppableId === component.id + ? destination.index // dropped ON tab + : component.children.indexOf(destination.droppableId); // dropped IN tab + + if (dropTabIndex > -1) { + setTimeout(() => { + this.handleClicKTab(dropTabIndex); + }, 30); + } + } + } + render() { const { depth, @@ -87,7 +145,7 @@ class Tabs extends React.Component { handleComponentDrop, } = this.props; - const { tabIndex: selectedTabIndex } = this.state; + const { tabIndex: selectedTabIndex, focusedId } = this.state; const { children: tabIds } = tabsComponent; return ( @@ -98,11 +156,17 @@ class Tabs extends React.Component { index={index} parentId={parentId} onDrop={handleComponentDrop} + disableDragDrop={Boolean(focusedId)} > {({ dropIndicatorProps: tabsDropIndicatorProps, dragSourceRef: tabsDragSourceRef }) => (
    + { + this.handleDeleteComponent(tabsComponent.id); + }} + /> {tabIds.map((tabId, tabIndex) => { const tabComponent = components[tabId]; - return ( + return ( // Bootstrap doesn't render a Tab if we move this to its own Tab.jsx { - handleComponentDrop(dropResult); - - // Ensure dropped tab is visible - const { destination } = dropResult; - if (destination) { - const dropTabIndex = destination.droppableId === tabsComponent.id - ? destination.index // dropped ON tab - : tabIds.indexOf(destination.droppableId); // dropped IN tab - - if (dropTabIndex > -1) { - setTimeout(() => { - this.handleClicKTab(dropTabIndex); - }, 20); - } - } - }} + onDrop={this.handleDropOnTab} + disableDragDrop={Boolean(focusedId)} > {({ dropIndicatorProps, dragSourceRef }) => (
    - {tabComponent.meta.text} + { + this.handleChangeFocus(nextFocus && tabId); + }} + menuItems={[ + { + this.handleDeleteComponent(tabId); + }} + />, + ]} + > + { + this.handleChangeText({ id: tabId, nextTabText }); + }} + showTooltip={false} + /> + + {dropIndicatorProps &&
    }
    diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/components.css b/superset/assets/javascripts/dashboard/v2/components/gridComponents/components.css index 84a76f966d474..2d6a7f3377207 100644 --- a/superset/assets/javascripts/dashboard/v2/components/gridComponents/components.css +++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/components.css @@ -265,6 +265,7 @@ box-shadow: 0 1px 2px 1px rgba(0, 0, 0, 0.2); font-size: 14px; cursor: default; + z-index: 10; } .popover-menu .menu-item { @@ -274,7 +275,7 @@ } /* vertical spacer after each menu item */ -.popover-menu .menu-item:nth-child(odd):after { +.popover-menu .menu-item:nth-child(even):before { content: ""; width: 1; height: 100%; @@ -314,7 +315,11 @@ left: -20px; display: flex; flex-direction: column; - justify-content: space-around; + justify-content: center; +} + +.hover-menu--left > div:nth-child(odd):not(:only-child) { + padding-bottom: 8px; } .dragdroppable-row .dragdroppable-row .hover-menu--left { diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/index.js b/superset/assets/javascripts/dashboard/v2/components/gridComponents/index.js index 1d1e4b3b752ad..7f0e840244e02 100644 --- a/superset/assets/javascripts/dashboard/v2/components/gridComponents/index.js +++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/index.js @@ -9,6 +9,7 @@ import { ROW_TYPE, SPACER_TYPE, TABS_TYPE, + TAB_TYPE, } from '../../util/componentTypes'; import Chart from './Chart'; @@ -18,6 +19,7 @@ import Header from './Header'; import Row from './Row'; import Spacer from './Spacer'; import Tabs from './Tabs'; +import Tab from './Tab'; export { default as Chart } from './Chart'; export { default as Column } from './Column'; @@ -26,6 +28,7 @@ export { default as Header } from './Header'; export { default as Row } from './Row'; export { default as Spacer } from './Spacer'; export { default as Tabs } from './Tabs'; +export { default as Tab } from './Tab'; export default { [CHART_TYPE]: Chart, @@ -36,4 +39,5 @@ export default { [ROW_TYPE]: Row, [SPACER_TYPE]: Spacer, [TABS_TYPE]: Tabs, + [TAB_TYPE]: Tab, }; diff --git a/superset/assets/javascripts/dashboard/v2/components/menu/WithPopoverMenu.jsx b/superset/assets/javascripts/dashboard/v2/components/menu/WithPopoverMenu.jsx index fd219638ffba6..97cda2ee93d01 100644 --- a/superset/assets/javascripts/dashboard/v2/components/menu/WithPopoverMenu.jsx +++ b/superset/assets/javascripts/dashboard/v2/components/menu/WithPopoverMenu.jsx @@ -26,7 +26,7 @@ class WithPopoverMenu extends React.Component { } componentWillUnmount() { - document.removeEventListener('mousedown', this.handleClick, true); + document.removeEventListener('click', this.handleClick, true); } setRef(ref) { @@ -38,13 +38,13 @@ class WithPopoverMenu extends React.Component { if (!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('mousedown', this.handleClick, true); + document.addEventListener('click', this.handleClick, true); this.setState(() => ({ isFocused: true })); if (onChangeFocus) { onChangeFocus(true); } } else if (!this.container.contains(event.target)) { - document.removeEventListener('mousedown', this.handleClick, true); + document.removeEventListener('click', this.handleClick, true); this.setState(() => ({ isFocused: false })); if (onChangeFocus) { onChangeFocus(false); From 6cbd3cd54cd500f7dbb4d73a3c7d58492719d8b3 Mon Sep 17 00:00:00 2001 From: Chris Williams Date: Fri, 16 Feb 2018 00:37:36 -0800 Subject: [PATCH 17/25] [builder] add generic popover dropdown and header row style editability --- .../dashboard/v2/components/dnd/dnd.css | 2 +- .../v2/components/gridComponents/Header.jsx | 62 +++++++++++------- .../v2/components/gridComponents/Row.jsx | 17 ++++- .../v2/components/gridComponents/Tabs.jsx | 2 +- .../components/gridComponents/components.css | 53 +++++++++++++-- .../components/menu/HeaderStyleDropdown.jsx | 58 ----------------- .../v2/components/menu/PopoverDropdown.jsx | 64 +++++++++++++++++++ .../v2/components/menu/WithPopoverMenu.jsx | 3 + .../v2/components/menu/headerStyleOptions.js | 8 +++ .../v2/components/menu/rowStyleOptions.js | 7 ++ .../dashboard/v2/util/constants.js | 4 +- .../dashboard/v2/util/newEntitiesFromDrop.js | 3 +- 12 files changed, 187 insertions(+), 96 deletions(-) delete mode 100644 superset/assets/javascripts/dashboard/v2/components/menu/HeaderStyleDropdown.jsx create mode 100644 superset/assets/javascripts/dashboard/v2/components/menu/PopoverDropdown.jsx create mode 100644 superset/assets/javascripts/dashboard/v2/components/menu/headerStyleOptions.js create mode 100644 superset/assets/javascripts/dashboard/v2/components/menu/rowStyleOptions.js diff --git a/superset/assets/javascripts/dashboard/v2/components/dnd/dnd.css b/superset/assets/javascripts/dashboard/v2/components/dnd/dnd.css index cd25a66363eee..7810d8f06d2f7 100644 --- a/superset/assets/javascripts/dashboard/v2/components/dnd/dnd.css +++ b/superset/assets/javascripts/dashboard/v2/components/dnd/dnd.css @@ -34,7 +34,7 @@ } .drag-handle--left { - padding-left: 7px; + width: 8px; } .drag-handle--top { diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Header.jsx b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Header.jsx index 64c3535b9a347..c0c0aabee98aa 100644 --- a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Header.jsx +++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Header.jsx @@ -8,7 +8,9 @@ import EditableTitle from '../../../../components/EditableTitle'; import HoverMenu from '../menu/HoverMenu'; import WithPopoverMenu from '../menu/WithPopoverMenu'; import DeleteComponentButton from '../DeleteComponentButton'; -import HeaderStyleDropdown, { headerStyleOptions } from '../menu/HeaderStyleDropdown'; +import PopoverDropdown from '../menu/PopoverDropdown'; +import headerStyleOptions from '../menu/headerStyleOptions'; +import rowStyleOptions from '../menu/rowStyleOptions'; import { componentShape } from '../../util/propShapes'; import { SMALL_HEADER } from '../../util/constants'; @@ -33,36 +35,25 @@ class Header extends React.PureComponent { }; this.handleDeleteComponent = this.handleDeleteComponent.bind(this); this.handleChangeFocus = this.handleChangeFocus.bind(this); - this.handleChangeStyle = this.handleChangeStyle.bind(this); - this.handleChangeText = this.handleChangeText.bind(this); + this.handleUpdateMeta = this.handleUpdateMeta.bind(this); + this.handleChangeSize = this.handleUpdateMeta.bind(this, 'size'); + this.handleChangeRowStyle = this.handleUpdateMeta.bind(this, 'rowStyle'); + this.handleChangeText = this.handleUpdateMeta.bind(this, 'text'); } handleChangeFocus(nextFocus) { this.setState(() => ({ isFocused: nextFocus })); } - handleChangeStyle(nextStyle) { + handleUpdateMeta(metaKey, nextValue) { const { updateComponents, component } = this.props; - updateComponents({ - [component.id]: { - ...component, - meta: { - ...component.meta, - style: nextStyle.value, - }, - }, - }); - } - - handleChangeText(nextText) { - const { updateComponents, component } = this.props; - if (nextText && nextText !== component.meta.text) { + if (nextValue && component.meta[metaKey] !== nextValue) { updateComponents({ [component.id]: { ...component, meta: { ...component.meta, - text: nextText, + [metaKey]: nextValue, }, }, }); @@ -86,7 +77,11 @@ class Header extends React.PureComponent { } = this.props; const headerStyle = headerStyleOptions.find( - opt => opt.value === (component.meta.style || SMALL_HEADER), + opt => opt.value === (component.meta.size || SMALL_HEADER), + ); + + const rowStyle = rowStyleOptions.find( + opt => opt.value === (component.meta.rowStyle || SMALL_HEADER), ); return ( @@ -108,10 +103,28 @@ class Header extends React.PureComponent { `${option.label} header`} + />, + ( +
    + {`${option.label} background`} +
    + )} + renderOption={option => ( +
    + {option.label} +
    + )} />, , ]} @@ -121,6 +134,7 @@ class Header extends React.PureComponent { 'dashboard-component', 'dashboard-component-header', headerStyle.className, + rowStyle.className, )} > + {rowItems.map((component, itemIndex) => { @@ -108,8 +121,8 @@ class Row extends React.PureComponent { /> ); })} - {dropIndicatorProps && -
    } + + {dropIndicatorProps &&
    }
    )} diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Tabs.jsx b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Tabs.jsx index b801533799da7..e53fa8bfb8a14 100644 --- a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Tabs.jsx +++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Tabs.jsx @@ -189,7 +189,7 @@ class Tabs extends React.PureComponent { index={tabIndex} parentId={tabsComponent.id} onDrop={this.handleDropOnTab} - disableDragDrop={Boolean(focusedId)} + disableDragDrop={tabId === focusedId} > {({ dropIndicatorProps, dragSourceRef }) => (
    diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/components.css b/superset/assets/javascripts/dashboard/v2/components/gridComponents/components.css index 2d6a7f3377207..a1c6e03d59dfe 100644 --- a/superset/assets/javascripts/dashboard/v2/components/gridComponents/components.css +++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/components.css @@ -122,6 +122,7 @@ .dashboard-component-tabs .nav-tabs > li .drop-indicator { height: 40px !important; top: -10px !important; + opacity: 0.5; } .dashboard-component-tabs .fa-plus-square { @@ -175,6 +176,15 @@ background-color: transparent; } +.grid-row--transparent { + background-color: transparent; +} + +.grid-row--white { + background-color: #fff; + padding-left: 16px; +} + .grid-row.grid-row--empty { align-items: center; /* this centers the empty note content */ min-height: 56px; @@ -200,10 +210,6 @@ color: #CFD8DC; } -.grid-row-container { - background-color: #fff; -} - .grid-spacer { width: 100%; height: 100%; @@ -275,7 +281,7 @@ } /* vertical spacer after each menu item */ -.popover-menu .menu-item:nth-child(even):before { +.popover-menu .menu-item:nth-child(n+2):before { content: ""; width: 1; height: 100%; @@ -287,6 +293,7 @@ border: none; padding: 0; font-size: inherit; + color: #000; } .popover-menu .popover-dropdown.btn:hover, @@ -296,8 +303,40 @@ box-shadow: none; } -.popover-menu li.dropdown-item:hover a, +.popover-menu li.dropdown-item:hover a { + background: #CFD8DC; +} + .popover-menu li.dropdown-item.active a { + background: #fff; + font-weight: bold; + color: #000; +} + +.row-style-option { + display: inline-block; +} + +.row-style-option:before { + content: ""; + width: 1em; + height: 1em; + margin-right: 8px; + display: inline-block; + vertical-align: middle; +} + +.row-style-option.grid-row--white { + padding-left: 0; + background: transparent; +} + +.row-style-option.grid-row--white:before { + background: #fff; + border: 1px solid #CFD8DC; +} + +.row-style-option.grid-row--transparent:before { background: #CFD8DC; } @@ -318,7 +357,7 @@ justify-content: center; } -.hover-menu--left > div:nth-child(odd):not(:only-child) { +.hover-menu--left > div:nth-child(n):not(:only-child) { padding-bottom: 8px; } diff --git a/superset/assets/javascripts/dashboard/v2/components/menu/HeaderStyleDropdown.jsx b/superset/assets/javascripts/dashboard/v2/components/menu/HeaderStyleDropdown.jsx deleted file mode 100644 index cadc19025b483..0000000000000 --- a/superset/assets/javascripts/dashboard/v2/components/menu/HeaderStyleDropdown.jsx +++ /dev/null @@ -1,58 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { DropdownButton, MenuItem } from 'react-bootstrap'; -import { t } from '../../../../locales'; -import { SMALL_HEADER, MEDIUM_HEADER, LARGE_HEADER } from '../../util/constants'; - -export const headerStyleOptions = [ - { value: SMALL_HEADER, label: t('Small'), className: 'header-small' }, - { value: MEDIUM_HEADER, label: t('Medium'), className: 'header-medium' }, - { value: LARGE_HEADER, label: t('Large'), className: 'header-large' }, -]; - -const propTypes = { - id: PropTypes.string.isRequired, - onChange: PropTypes.func.isRequired, - value: PropTypes.oneOf(headerStyleOptions.map(opt => opt.value)).isRequired, -}; - -class HeaderStyleDropdown extends React.PureComponent { - constructor(props) { - super(props); - this.handleSelect = this.handleSelect.bind(this); - } - - handleSelect(nextStyle) { - console.log(nextStyle); - this.props.onChange(nextStyle); - } - - render() { - const { id, value } = this.props; - const selected = headerStyleOptions.find(opt => opt.value === value); - return ( - - {headerStyleOptions.map(option => ( - -
    {option.label}
    -
    - ))} -
    - ); - } -} - -HeaderStyleDropdown.propTypes = propTypes; - -export default HeaderStyleDropdown; diff --git a/superset/assets/javascripts/dashboard/v2/components/menu/PopoverDropdown.jsx b/superset/assets/javascripts/dashboard/v2/components/menu/PopoverDropdown.jsx new file mode 100644 index 0000000000000..6a56eab239dd3 --- /dev/null +++ b/superset/assets/javascripts/dashboard/v2/components/menu/PopoverDropdown.jsx @@ -0,0 +1,64 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { DropdownButton, MenuItem } from 'react-bootstrap'; + +const propTypes = { + id: PropTypes.string.isRequired, + options: PropTypes.arrayOf( + PropTypes.shape({ + value: PropTypes.string.isRequired, + label: PropTypes.string.isRequired, + className: PropTypes.string, + }), + ).isRequired, + onChange: PropTypes.func.isRequired, + value: PropTypes.string.isRequired, + renderButton: PropTypes.func, + renderOption: PropTypes.func, +}; + +const defaultProps = { + renderButton: option => option.label, + renderOption: option =>
    {option.label}
    , +}; + +class PopoverDropdown extends React.PureComponent { + constructor(props) { + super(props); + this.handleSelect = this.handleSelect.bind(this); + } + + handleSelect(nextValue) { + this.props.onChange(nextValue); + } + + render() { + const { id, value, options, renderButton, renderOption } = this.props; + const selected = options.find(opt => opt.value === value); + return ( + + {options.map(option => ( + + {renderOption(option)} + + ))} + + ); + } +} + +PopoverDropdown.propTypes = propTypes; +PopoverDropdown.defaultProps = defaultProps; + +export default PopoverDropdown; diff --git a/superset/assets/javascripts/dashboard/v2/components/menu/WithPopoverMenu.jsx b/superset/assets/javascripts/dashboard/v2/components/menu/WithPopoverMenu.jsx index 97cda2ee93d01..93efd4821dd80 100644 --- a/superset/assets/javascripts/dashboard/v2/components/menu/WithPopoverMenu.jsx +++ b/superset/assets/javascripts/dashboard/v2/components/menu/WithPopoverMenu.jsx @@ -27,6 +27,7 @@ class WithPopoverMenu extends React.Component { componentWillUnmount() { document.removeEventListener('click', this.handleClick, true); + document.removeEventListener('drag', this.handleClick, true); } setRef(ref) { @@ -39,12 +40,14 @@ class WithPopoverMenu extends React.Component { // 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); + document.addEventListener('drag', this.handleClick, true); this.setState(() => ({ isFocused: true })); if (onChangeFocus) { onChangeFocus(true); } } else if (!this.container.contains(event.target)) { document.removeEventListener('click', this.handleClick, true); + document.removeEventListener('drag', this.handleClick, true); this.setState(() => ({ isFocused: false })); if (onChangeFocus) { onChangeFocus(false); diff --git a/superset/assets/javascripts/dashboard/v2/components/menu/headerStyleOptions.js b/superset/assets/javascripts/dashboard/v2/components/menu/headerStyleOptions.js new file mode 100644 index 0000000000000..f16f90e0d4757 --- /dev/null +++ b/superset/assets/javascripts/dashboard/v2/components/menu/headerStyleOptions.js @@ -0,0 +1,8 @@ +import { t } from '../../../../locales'; +import { SMALL_HEADER, MEDIUM_HEADER, LARGE_HEADER } from '../../util/constants'; + +export default [ + { value: SMALL_HEADER, label: t('Small'), className: 'header-small' }, + { value: MEDIUM_HEADER, label: t('Medium'), className: 'header-medium' }, + { value: LARGE_HEADER, label: t('Large'), className: 'header-large' }, +]; diff --git a/superset/assets/javascripts/dashboard/v2/components/menu/rowStyleOptions.js b/superset/assets/javascripts/dashboard/v2/components/menu/rowStyleOptions.js new file mode 100644 index 0000000000000..2797b64632b76 --- /dev/null +++ b/superset/assets/javascripts/dashboard/v2/components/menu/rowStyleOptions.js @@ -0,0 +1,7 @@ +import { t } from '../../../../locales'; +import { ROW_TRANSPARENT, ROW_WHITE } from '../../util/constants'; + +export default [ + { value: ROW_TRANSPARENT, label: t('Regular'), className: 'grid-row--transparent' }, + { value: ROW_WHITE, label: t('White'), className: 'grid-row--white' }, +]; diff --git a/superset/assets/javascripts/dashboard/v2/util/constants.js b/superset/assets/javascripts/dashboard/v2/util/constants.js index 6c85f542863a5..12435bb784595 100644 --- a/superset/assets/javascripts/dashboard/v2/util/constants.js +++ b/superset/assets/javascripts/dashboard/v2/util/constants.js @@ -18,5 +18,5 @@ export const MEDIUM_HEADER = 'MEDIUM_HEADER'; export const LARGE_HEADER = 'LARGE_HEADER'; // Row types -export const WHITE_ROW = 'WHITE_ROW'; -export const TRANSPARENT_ROW = 'TRANSPARENT_ROW'; +export const ROW_WHITE = 'ROW_WHITE'; +export const ROW_TRANSPARENT = 'ROW_TRANSPARENT'; diff --git a/superset/assets/javascripts/dashboard/v2/util/newEntitiesFromDrop.js b/superset/assets/javascripts/dashboard/v2/util/newEntitiesFromDrop.js index bf209e3be1467..85179a1b98b4f 100644 --- a/superset/assets/javascripts/dashboard/v2/util/newEntitiesFromDrop.js +++ b/superset/assets/javascripts/dashboard/v2/util/newEntitiesFromDrop.js @@ -15,13 +15,14 @@ import { import { MEDIUM_HEADER, + ROW_TRANSPARENT, } from './constants'; const typeToDefaultMetaData = { [CHART_TYPE]: { width: 3, height: 15 }, [COLUMN_TYPE]: { width: 3 }, [DIVIDER_TYPE]: null, - [HEADER_TYPE]: { text: 'New header', style: MEDIUM_HEADER }, + [HEADER_TYPE]: { text: 'New header', size: MEDIUM_HEADER, rowStyle: ROW_TRANSPARENT }, [INVISIBLE_ROW_TYPE]: null, [MARKDOWN_TYPE]: { width: 3, height: 15 }, [ROW_TYPE]: null, From 5a68f02a707d8d9891d057ad35a3aefb7db39f42 Mon Sep 17 00:00:00 2001 From: Chris Williams Date: Fri, 16 Feb 2018 14:26:43 -0800 Subject: [PATCH 18/25] [builder] add hover rowStyle dropdown, make row styles editable --- .../v2/components/DeleteComponentButton.jsx | 2 +- .../v2/components/gridComponents/Header.jsx | 4 +- .../v2/components/gridComponents/Row.jsx | 35 ++++++++- .../components/gridComponents/components.css | 75 +++++++++++-------- .../components/gridComponents/new/NewRow.jsx | 6 +- .../components/menu/RowStyleHoverDropdown.jsx | 59 +++++++++++++++ .../components/menu/RowStyleHoverTrigger.jsx | 36 +++++++++ .../dashboard/v2/util/newEntitiesFromDrop.js | 13 ++-- 8 files changed, 181 insertions(+), 49 deletions(-) create mode 100644 superset/assets/javascripts/dashboard/v2/components/menu/RowStyleHoverDropdown.jsx create mode 100644 superset/assets/javascripts/dashboard/v2/components/menu/RowStyleHoverTrigger.jsx diff --git a/superset/assets/javascripts/dashboard/v2/components/DeleteComponentButton.jsx b/superset/assets/javascripts/dashboard/v2/components/DeleteComponentButton.jsx index c9d99799e3fb6..d955d214edfed 100644 --- a/superset/assets/javascripts/dashboard/v2/components/DeleteComponentButton.jsx +++ b/superset/assets/javascripts/dashboard/v2/components/DeleteComponentButton.jsx @@ -15,7 +15,7 @@ export default class DeleteComponentButton extends React.PureComponent { return (
    diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Header.jsx b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Header.jsx index c0c0aabee98aa..4a8836d3ba255 100644 --- a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Header.jsx +++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Header.jsx @@ -12,7 +12,7 @@ import PopoverDropdown from '../menu/PopoverDropdown'; import headerStyleOptions from '../menu/headerStyleOptions'; import rowStyleOptions from '../menu/rowStyleOptions'; import { componentShape } from '../../util/propShapes'; -import { SMALL_HEADER } from '../../util/constants'; +import { SMALL_HEADER, ROW_TRANSPARENT } from '../../util/constants'; const propTypes = { component: componentShape.isRequired, @@ -81,7 +81,7 @@ class Header extends React.PureComponent { ); const rowStyle = rowStyleOptions.find( - opt => opt.value === (component.meta.rowStyle || SMALL_HEADER), + opt => opt.value === (component.meta.rowStyle || ROW_TRANSPARENT), ); return ( diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Row.jsx b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Row.jsx index 7caee28c8edf6..ab348861b9a3c 100644 --- a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Row.jsx +++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Row.jsx @@ -7,9 +7,11 @@ import DragHandle from '../dnd/DragHandle'; import DashboardComponent from '../../containers/DashboardComponent'; import DeleteComponentButton from '../DeleteComponentButton'; import HoverMenu from '../menu/HoverMenu'; +import RowStyleHoverDropdown from '../menu/RowStyleHoverDropdown'; +// import PopoverDropdown from '../menu/PopoverDropdown'; +import rowStyleOptions from '../menu/rowStyleOptions'; import { componentShape } from '../../util/propShapes'; -import { GRID_GUTTER_SIZE } from '../../util/constants'; -import { INVISIBLE_ROW_TYPE } from '../../util/componentTypes'; +import { GRID_GUTTER_SIZE, ROW_TRANSPARENT } from '../../util/constants'; const propTypes = { component: componentShape.isRequired, @@ -29,6 +31,7 @@ const propTypes = { // dnd handleComponentDrop: PropTypes.func.isRequired, deleteComponent: PropTypes.func.isRequired, + updateComponents: PropTypes.func.isRequired, }; const defaultProps = { @@ -39,6 +42,23 @@ class Row extends React.PureComponent { constructor(props) { super(props); this.handleDeleteComponent = this.handleDeleteComponent.bind(this); + this.handleUpdateMeta = this.handleUpdateMeta.bind(this); + this.handleChangeRowStyle = this.handleUpdateMeta.bind(this, 'rowStyle'); + } + + handleUpdateMeta(metaKey, nextValue) { + const { updateComponents, component } = this.props; + if (nextValue && component.meta[metaKey] !== nextValue) { + updateComponents({ + [component.id]: { + ...component, + meta: { + ...component.meta, + [metaKey]: nextValue, + }, + }, + }); + } } handleDeleteComponent() { @@ -78,6 +98,10 @@ class Row extends React.PureComponent { } }); + const rowStyle = rowStyleOptions.find( + opt => opt.value === (rowComponent.meta.rowStyle || ROW_TRANSPARENT), + ); + return ( + diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/components.css b/superset/assets/javascripts/dashboard/v2/components/gridComponents/components.css index a1c6e03d59dfe..fba19e3698202 100644 --- a/superset/assets/javascripts/dashboard/v2/components/gridComponents/components.css +++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/components.css @@ -26,10 +26,6 @@ padding-right: 16px; } -.grid-row-container .dashboard-component-header { - padding-left: 16px; -} - /* Chart */ .dashboard-component-chart { width: 100%; @@ -155,7 +151,7 @@ .grid-container { flex-grow: 1; min-width: 66%; - margin: 24px; + margin: 24px 32px; height: 100%; position: relative; } @@ -182,12 +178,15 @@ .grid-row--white { background-color: #fff; +} + +.dashboard-component-header.grid-row--white { padding-left: 16px; } .grid-row.grid-row--empty { align-items: center; /* this centers the empty note content */ - min-height: 56px; + min-height: 72px; } .grid-row--empty:after { @@ -220,26 +219,6 @@ box-shadow: inset 0 0 0 1px #CFD8DC; } -/* Delete component button */ -.delete-component-button { - color: #879399; - font-size: 16px; - display: flex; - flex-direction: row; - align-items: center; - justify-content: center; -} - -.delete-component-button:hover, -.delete-component-button:active { - color: #484848; -} - -.delete-component-button:focus { - outline: none; -} - - /* popover menu */ .with-popover-menu { position: relative; @@ -255,6 +234,7 @@ height: 100%; box-shadow: inset 0 0 0 2px #44C0FF; pointer-events: none; + z-index: 9; } .popover-menu { @@ -298,21 +278,27 @@ .popover-menu .popover-dropdown.btn:hover, .popover-menu .popover-dropdown.btn:active, -.popover-menu .popover-dropdown.btn:focus { +.popover-menu .popover-dropdown.btn:focus, +.hover-dropdown .btn:hover, +.hover-dropdown .btn:active, +.hover-dropdown .btn:focus { background: initial; box-shadow: none; } +.hover-dropdown li.dropdown-item:hover a, .popover-menu li.dropdown-item:hover a { background: #CFD8DC; } +.hover-dropdown li.dropdown-item.active a, .popover-menu li.dropdown-item.active a { background: #fff; font-weight: bold; color: #000; } +/* row style menu */ .row-style-option { display: inline-block; } @@ -344,7 +330,7 @@ .hover-menu { opacity: 0; position: absolute; - z-index: 1; + z-index: 2; } .hover-menu--left { @@ -355,15 +341,16 @@ display: flex; flex-direction: column; justify-content: center; + align-items: center; } -.hover-menu--left > div:nth-child(n):not(:only-child) { +.hover-menu--left > div:nth-child(n):not(:only-child):not(:last-child) { padding-bottom: 8px; } - .dragdroppable-row .dragdroppable-row .hover-menu--left { - left: -2px; - } +.dragdroppable-row .dragdroppable-row .hover-menu--left { + left: 1px; +} .hover-menu--top { width: 100%; @@ -376,3 +363,27 @@ .dragdroppable .hover-menu:hover { opacity: 1; } + + +/* Menu fa buttons */ +.hover-menu .fa, +.popover-menu .fa { + color: #879399; + font-size: 1em; + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; +} + +.hover-menu .fa:hover, +.hover-menu .fa:active, +.popover-menu .fa:hover, +.popover-menu .fa:active { + color: #484848; +} + +.hover-menu .fa:focus, +.popover-menu .fa:focus { + outline: none; +} diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/new/NewRow.jsx b/superset/assets/javascripts/dashboard/v2/components/gridComponents/new/NewRow.jsx index 3b1b6eb027659..7bb41d5cb36b8 100644 --- a/superset/assets/javascripts/dashboard/v2/components/gridComponents/new/NewRow.jsx +++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/new/NewRow.jsx @@ -1,6 +1,6 @@ import React from 'react'; -import { INVISIBLE_ROW_TYPE } from '../../../util/componentTypes'; +import { ROW_TYPE } from '../../../util/componentTypes'; import DraggableNewComponent from './DraggableNewComponent'; const propTypes = { @@ -10,8 +10,8 @@ export default class DraggableNewRow extends React.PureComponent { render() { return ( ); diff --git a/superset/assets/javascripts/dashboard/v2/components/menu/RowStyleHoverDropdown.jsx b/superset/assets/javascripts/dashboard/v2/components/menu/RowStyleHoverDropdown.jsx new file mode 100644 index 0000000000000..e45efb148877f --- /dev/null +++ b/superset/assets/javascripts/dashboard/v2/components/menu/RowStyleHoverDropdown.jsx @@ -0,0 +1,59 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import cx from 'classnames'; +import { MenuItem, Dropdown } from 'react-bootstrap'; + +import RowStyleHoverTrigger from './RowStyleHoverTrigger'; +import rowStyleOptions from './rowStyleOptions'; + +const propTypes = { + id: PropTypes.string.isRequired, + onChange: PropTypes.func.isRequired, + value: PropTypes.string.isRequired, +}; + +const defaultProps = { +}; + +export default class RowStyleHoverDropdown extends React.PureComponent { + constructor(props) { + super(props); + this.setRef = this.setRef.bind(this); + } + + componentWillUnmount() { + document.removeEventListener('click', this.handleClick, true); + document.removeEventListener('drag', this.handleClick, true); + } + + setRef(ref) { + this.container = ref; + } + + render() { + const { id, onChange, value = rowStyleOptions[0].value } = this.props; + return ( + + + + {rowStyleOptions.map(option => ( + +
    + {option.label} background +
    +
    + ))} +
    +
    + ); + } +} + +RowStyleHoverDropdown.propTypes = propTypes; +RowStyleHoverDropdown.defaultProps = defaultProps; diff --git a/superset/assets/javascripts/dashboard/v2/components/menu/RowStyleHoverTrigger.jsx b/superset/assets/javascripts/dashboard/v2/components/menu/RowStyleHoverTrigger.jsx new file mode 100644 index 0000000000000..e8643f2f90514 --- /dev/null +++ b/superset/assets/javascripts/dashboard/v2/components/menu/RowStyleHoverTrigger.jsx @@ -0,0 +1,36 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import cx from 'classnames'; + +const propTypes = { + onClick: PropTypes.func, +}; + +const defaultProps = { + onClick() {}, +}; + +// Note: this has to be a separate component because react-bootstrap injects the onClick prop +export default class RowStyleHoverTrigger extends React.PureComponent { + constructor(props) { + super(props); + this.handleClick = this.handleClick.bind(this); + } + + handleClick(event) { + const { onClick } = this.props; + event.preventDefault(); + onClick(event); + } + + render() { + return ( +
    +
    + + ); + } +} + +RowStyleHoverTrigger.propTypes = propTypes; +RowStyleHoverTrigger.defaultProps = defaultProps; diff --git a/superset/assets/javascripts/dashboard/v2/util/newEntitiesFromDrop.js b/superset/assets/javascripts/dashboard/v2/util/newEntitiesFromDrop.js index 85179a1b98b4f..34ddd720f50d9 100644 --- a/superset/assets/javascripts/dashboard/v2/util/newEntitiesFromDrop.js +++ b/superset/assets/javascripts/dashboard/v2/util/newEntitiesFromDrop.js @@ -5,7 +5,6 @@ import { COLUMN_TYPE, DIVIDER_TYPE, HEADER_TYPE, - INVISIBLE_ROW_TYPE, MARKDOWN_TYPE, ROW_TYPE, SPACER_TYPE, @@ -23,9 +22,8 @@ const typeToDefaultMetaData = { [COLUMN_TYPE]: { width: 3 }, [DIVIDER_TYPE]: null, [HEADER_TYPE]: { text: 'New header', size: MEDIUM_HEADER, rowStyle: ROW_TRANSPARENT }, - [INVISIBLE_ROW_TYPE]: null, [MARKDOWN_TYPE]: { width: 3, height: 15 }, - [ROW_TYPE]: null, + [ROW_TYPE]: { rowStyle: ROW_TRANSPARENT }, [SPACER_TYPE]: { width: 1 }, [TABS_TYPE]: null, [TAB_TYPE]: { text: 'New Tab' }, @@ -51,7 +49,7 @@ function entityFactory(type) { export default function newEntitiesFromDrop({ dropResult, components }) { const { draggableId, destination } = dropResult; - const dragType = draggableId; // @TODO idToType + const dragType = draggableId; // @TODO newComponentIdToType lookup const dropEntity = components[destination.droppableId]; if (!dropEntity) { @@ -68,16 +66,15 @@ export default function newEntitiesFromDrop({ dropResult, components }) { }; if (!isValidDrop) { - console.log('wrapping', dragType, 'in invisible row'); - if (!isValidChild({ parentType: dropType, childType: INVISIBLE_ROW_TYPE })) { + console.log('wrapping', dragType, 'in row'); + if (!isValidChild({ parentType: dropType, childType: ROW_TYPE })) { console.warn('wrapping in an invalid component'); } - const rowWrapper = entityFactory(INVISIBLE_ROW_TYPE); + const rowWrapper = entityFactory(ROW_TYPE); rowWrapper.children = [newDropChild.id]; newEntities[rowWrapper.id] = rowWrapper; newDropChild = rowWrapper; - } else if (dragType === TABS_TYPE) { const tabChild = entityFactory(TAB_TYPE); newDropChild.children = [tabChild.id]; From dfa7e3c5c3c92d4ad3b815c24d6d9863f0e2cfd8 Mon Sep 17 00:00:00 2001 From: Chris Williams Date: Fri, 16 Feb 2018 18:10:44 -0800 Subject: [PATCH 19/25] [builder] add some new component icons, add popover with delete to charts --- .../v2/components/gridComponents/Chart.jsx | 36 ++++++++++++++++--- .../components/gridComponents/components.css | 12 ++++++- .../v2/components/gridComponents/index.js | 4 --- .../new/DraggableNewComponent.jsx | 5 +-- .../gridComponents/new/NewChart.jsx | 1 + .../gridComponents/new/NewColumn.jsx | 1 + .../gridComponents/new/NewHeader.jsx | 1 + .../components/gridComponents/new/NewRow.jsx | 1 + 8 files changed, 50 insertions(+), 11 deletions(-) diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Chart.jsx b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Chart.jsx index 17d2d0e622b1f..c7c703b390331 100644 --- a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Chart.jsx +++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Chart.jsx @@ -1,10 +1,12 @@ import React from 'react'; import PropTypes from 'prop-types'; +import DeleteComponentButton from '../DeleteComponentButton'; import DimensionProvider from '../resizable/DimensionProvider'; import DragDroppable from '../dnd/DragDroppable'; import DragHandle from '../dnd/DragHandle'; import HoverMenu from '../menu/HoverMenu'; +import WithPopoverMenu from '../menu/WithPopoverMenu'; import { componentShape } from '../../util/propShapes'; const propTypes = { @@ -22,6 +24,7 @@ const propTypes = { onResizeStop: PropTypes.func.isRequired, // dnd + deleteComponent: PropTypes.func.isRequired, handleComponentDrop: PropTypes.func.isRequired, }; @@ -33,10 +36,25 @@ class Chart extends React.Component { constructor(props) { super(props); this.state = { + isFocused: false, }; + + this.handleChangeFocus = this.handleChangeFocus.bind(this); + this.handleDeleteComponent = this.handleDeleteComponent.bind(this); + } + + handleChangeFocus(nextFocus) { + this.setState(() => ({ isFocused: nextFocus })); + } + + handleDeleteComponent() { + const { deleteComponent, component, parentId } = this.props; + deleteComponent(component.id, parentId); } render() { + const { isFocused } = this.state; + const { component, components, @@ -59,6 +77,7 @@ class Chart extends React.Component { index={index} parentId={parentId} onDrop={handleComponentDrop} + disableDragDrop={isFocused} > {({ dropIndicatorProps, dragSourceRef }) => ( -
    - Chart -
    - {dropIndicatorProps &&
    } + + , + ]} + > +
    +
    +
    + + {dropIndicatorProps &&
    } + )} diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/components.css b/superset/assets/javascripts/dashboard/v2/components/gridComponents/components.css index fba19e3698202..ab0504c3c6bc5 100644 --- a/superset/assets/javascripts/dashboard/v2/components/gridComponents/components.css +++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/components.css @@ -38,6 +38,11 @@ justify-content: center; } +.dashboard-component-chart .fa { + font-size: 100px; + opacity: 0.3; +} + .grid-container--resizing .dashboard-component-chart, .dashboard-builder--dragging .dashboard-component-chart, .dashboard-component-chart:hover { @@ -145,6 +150,11 @@ height: 40px; margin-right: 16px; box-shadow: 0 0 1px #fff; + display: flex; + align-items: center; + justify-content: center; + color: #aaa; + font-size: 1.5em; } /* Spacer */ @@ -196,7 +206,7 @@ justify-content: center; width: 100%; height: 100%; - color: #CFD8DC; + color: #aaa; } .grid-column--empty:after { diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/index.js b/superset/assets/javascripts/dashboard/v2/components/gridComponents/index.js index 7f0e840244e02..1d1e4b3b752ad 100644 --- a/superset/assets/javascripts/dashboard/v2/components/gridComponents/index.js +++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/index.js @@ -9,7 +9,6 @@ import { ROW_TYPE, SPACER_TYPE, TABS_TYPE, - TAB_TYPE, } from '../../util/componentTypes'; import Chart from './Chart'; @@ -19,7 +18,6 @@ import Header from './Header'; import Row from './Row'; import Spacer from './Spacer'; import Tabs from './Tabs'; -import Tab from './Tab'; export { default as Chart } from './Chart'; export { default as Column } from './Column'; @@ -28,7 +26,6 @@ export { default as Header } from './Header'; export { default as Row } from './Row'; export { default as Spacer } from './Spacer'; export { default as Tabs } from './Tabs'; -export { default as Tab } from './Tab'; export default { [CHART_TYPE]: Chart, @@ -39,5 +36,4 @@ export default { [ROW_TYPE]: Row, [SPACER_TYPE]: Spacer, [TABS_TYPE]: Tabs, - [TAB_TYPE]: Tab, }; 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 b96f3e792a963..f99150c3a3165 100644 --- a/superset/assets/javascripts/dashboard/v2/components/gridComponents/new/DraggableNewComponent.jsx +++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/new/DraggableNewComponent.jsx @@ -1,5 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; +import cx from 'classnames'; import DragDroppable from '../../dnd/DragDroppable'; @@ -11,7 +12,7 @@ const propTypes = { export default class DraggableNewComponent extends React.PureComponent { render() { - const { label, id, type } = this.props; + const { label, id, type, className } = this.props; return ( {({ dragSourceRef }) => (
    -
    +
    {label}
    )} diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/new/NewChart.jsx b/superset/assets/javascripts/dashboard/v2/components/gridComponents/new/NewChart.jsx index 3c735bae058fc..80ad814e0bd42 100644 --- a/superset/assets/javascripts/dashboard/v2/components/gridComponents/new/NewChart.jsx +++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/new/NewChart.jsx @@ -14,6 +14,7 @@ export default class DraggableNewChart extends React.PureComponent { id={CHART_TYPE} type={CHART_TYPE} label="Chart" + className="fa fa-area-chart" /> ); } diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/new/NewColumn.jsx b/superset/assets/javascripts/dashboard/v2/components/gridComponents/new/NewColumn.jsx index 7d56179e0b9b0..8697302279144 100644 --- a/superset/assets/javascripts/dashboard/v2/components/gridComponents/new/NewColumn.jsx +++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/new/NewColumn.jsx @@ -14,6 +14,7 @@ export default class DraggableNewColumn extends React.PureComponent { id={COLUMN_TYPE} type={COLUMN_TYPE} label="Column" + className="fa fa-long-arrow-down" /> ); } diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/new/NewHeader.jsx b/superset/assets/javascripts/dashboard/v2/components/gridComponents/new/NewHeader.jsx index fb106a2c3ef5b..571442bef2698 100644 --- a/superset/assets/javascripts/dashboard/v2/components/gridComponents/new/NewHeader.jsx +++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/new/NewHeader.jsx @@ -14,6 +14,7 @@ export default class DraggableNewHeader extends React.Component { id={HEADER_TYPE} type={HEADER_TYPE} label="Header" + className="fa fa-header" /> ); } diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/new/NewRow.jsx b/superset/assets/javascripts/dashboard/v2/components/gridComponents/new/NewRow.jsx index 7bb41d5cb36b8..62375018a0172 100644 --- a/superset/assets/javascripts/dashboard/v2/components/gridComponents/new/NewRow.jsx +++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/new/NewRow.jsx @@ -13,6 +13,7 @@ export default class DraggableNewRow extends React.PureComponent { id={ROW_TYPE} type={ROW_TYPE} label="Row" + className="fa fa-long-arrow-right" /> ); } From 5a38d6f071ca7d8c2c3ec649129bcaabc98c381a Mon Sep 17 00:00:00 2001 From: Chris Williams Date: Sun, 18 Feb 2018 22:34:55 -0800 Subject: [PATCH 20/25] [builder] add preview icons, add popover menu to rows. --- .../javascripts/components/EditableTitle.jsx | 2 - .../v2/components/DeleteComponentButton.jsx | 10 +- .../dashboard/v2/components/dnd/dnd.css | 2 + .../v2/components/gridComponents/Chart.jsx | 7 +- .../v2/components/gridComponents/Divider.jsx | 4 +- .../v2/components/gridComponents/Header.jsx | 15 +-- .../v2/components/gridComponents/Row.jsx | 103 +++++++++++------- .../components/gridComponents/components.css | 71 +++++++++--- .../gridComponents/new/NewDivider.jsx | 1 + .../gridComponents/new/NewSpacer.jsx | 1 + .../components/gridComponents/new/NewTabs.jsx | 1 + .../v2/components/menu/WithPopoverMenu.jsx | 26 ++++- .../v2/components/menu/rowStyleOptions.js | 2 +- .../resizable/ResizableContainer.jsx | 12 +- superset/assets/package.json | 3 +- 15 files changed, 166 insertions(+), 94 deletions(-) diff --git a/superset/assets/javascripts/components/EditableTitle.jsx b/superset/assets/javascripts/components/EditableTitle.jsx index 20370d9ddd442..14976766975ac 100644 --- a/superset/assets/javascripts/components/EditableTitle.jsx +++ b/superset/assets/javascripts/components/EditableTitle.jsx @@ -33,7 +33,6 @@ class EditableTitle extends React.PureComponent { } componentWillReceiveProps(nextProps) { - console.log('new props') if (nextProps.title !== this.state.title) { this.setState({ lastTitle: this.state.title, @@ -53,7 +52,6 @@ class EditableTitle extends React.PureComponent { } handleBlur() { - console.log('blur') if (!this.props.canEdit) { return; } diff --git a/superset/assets/javascripts/dashboard/v2/components/DeleteComponentButton.jsx b/superset/assets/javascripts/dashboard/v2/components/DeleteComponentButton.jsx index d955d214edfed..18efff43ac4f9 100644 --- a/superset/assets/javascripts/dashboard/v2/components/DeleteComponentButton.jsx +++ b/superset/assets/javascripts/dashboard/v2/components/DeleteComponentButton.jsx @@ -1,6 +1,7 @@ import React from 'react'; import PropTypes from 'prop-types'; -import cx from 'classnames'; + +import IconButton from './IconButton'; const propTypes = { onDelete: PropTypes.func.isRequired, @@ -13,12 +14,7 @@ export default class DeleteComponentButton extends React.PureComponent { render() { const { onDelete } = this.props; return ( -
    + ); } } diff --git a/superset/assets/javascripts/dashboard/v2/components/dnd/dnd.css b/superset/assets/javascripts/dashboard/v2/components/dnd/dnd.css index 7810d8f06d2f7..d084f1f9a1918 100644 --- a/superset/assets/javascripts/dashboard/v2/components/dnd/dnd.css +++ b/superset/assets/javascripts/dashboard/v2/components/dnd/dnd.css @@ -1,5 +1,7 @@ .dragdroppable { position: relative; + /*height: fit-content; + min-height: 2px;*/ } .dragdroppable--dragging { diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Chart.jsx b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Chart.jsx index c7c703b390331..179cef0a11b42 100644 --- a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Chart.jsx +++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Chart.jsx @@ -13,6 +13,7 @@ const propTypes = { component: componentShape.isRequired, components: PropTypes.object.isRequired, index: PropTypes.number.isRequired, + depth: PropTypes.number.isRequired, parentId: PropTypes.string.isRequired, // grid related @@ -59,6 +60,7 @@ class Chart extends React.Component { component, components, index, + depth, parentId, availableColumnCount, columnWidth, @@ -68,12 +70,12 @@ class Chart extends React.Component { onResizeStop, handleComponentDrop, } = this.props; - + console.log('chart depth', depth) return ( ( -
    -
    -
    +
    {dropIndicatorProps &&
    }
    diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Header.jsx b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Header.jsx index 4a8836d3ba255..4d697304f2a13 100644 --- a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Header.jsx +++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Header.jsx @@ -7,7 +7,9 @@ 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 DeleteComponentButton from '../DeleteComponentButton'; +import IconButton from '../IconButton'; import PopoverDropdown from '../menu/PopoverDropdown'; import headerStyleOptions from '../menu/headerStyleOptions'; import rowStyleOptions from '../menu/rowStyleOptions'; @@ -110,21 +112,10 @@ class Header extends React.PureComponent { onChange={this.handleChangeSize} renderTitle={option => `${option.label} header`} />, - ( -
    - {`${option.label} background`} -
    - )} - renderOption={option => ( -
    - {option.label} -
    - )} />, , ]} diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Row.jsx b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Row.jsx index ab348861b9a3c..2012f061a88e6 100644 --- a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Row.jsx +++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Row.jsx @@ -7,10 +7,12 @@ import DragHandle from '../dnd/DragHandle'; import DashboardComponent from '../../containers/DashboardComponent'; import DeleteComponentButton from '../DeleteComponentButton'; import HoverMenu from '../menu/HoverMenu'; -import RowStyleHoverDropdown from '../menu/RowStyleHoverDropdown'; -// import PopoverDropdown from '../menu/PopoverDropdown'; -import rowStyleOptions from '../menu/rowStyleOptions'; +import IconButton from '../IconButton'; +import RowStyleDropdown from '../menu/RowStyleDropdown'; +import WithPopoverMenu from '../menu/WithPopoverMenu'; + import { componentShape } from '../../util/propShapes'; +import rowStyleOptions from '../menu/rowStyleOptions'; import { GRID_GUTTER_SIZE, ROW_TRANSPARENT } from '../../util/constants'; const propTypes = { @@ -41,9 +43,17 @@ const defaultProps = { class Row extends React.PureComponent { constructor(props) { super(props); + this.state = { + isFocused: false, + }; this.handleDeleteComponent = this.handleDeleteComponent.bind(this); this.handleUpdateMeta = this.handleUpdateMeta.bind(this); this.handleChangeRowStyle = this.handleUpdateMeta.bind(this, 'rowStyle'); + this.handleChangeFocus = this.handleChangeFocus.bind(this); + } + + handleChangeFocus(nextFocus) { + this.setState(() => ({ isFocused: Boolean(nextFocus) })); } handleUpdateMeta(metaKey, nextValue) { @@ -112,47 +122,60 @@ class Row extends React.PureComponent { onDrop={handleComponentDrop} > {({ dropIndicatorProps, dragSourceRef }) => ( -
    - - - - - - - {rowItems.map((component, itemIndex) => { - if (!component.id) { - return
    ; - } - - return ( - - ); - })} + />, + ]} + > - {dropIndicatorProps &&
    } -
    +
    + + + + + + + {rowItems.map((component, itemIndex) => { + if (!component.id) { + return
    ; + } + + return ( + + ); + })} + + {dropIndicatorProps &&
    } +
    + )} ); diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/components.css b/superset/assets/javascripts/dashboard/v2/components/gridComponents/components.css index ab0504c3c6bc5..154973c560af9 100644 --- a/superset/assets/javascripts/dashboard/v2/components/gridComponents/components.css +++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/components.css @@ -56,10 +56,19 @@ background-color: transparent; } -.dashboard-component-divider > div { +.dashboard-component-divider:after { + content: ""; height: 1px; width: 100%; background-color: #CFD8DC; + display: block; +} + +.new-component-placeholder.divider-placeholder:after { + content: ""; + height: 2px; + width: 100%; + background-color: #CFD8DC; } .dragdroppable .dashboard-component-divider { @@ -145,6 +154,7 @@ } .new-component-placeholder { + position: relative; background: #f5f5f5; width: 40px; height: 40px; @@ -166,12 +176,28 @@ position: relative; } +.new-component-placeholder.spacer-placeholder { + font-size: 1em; +} + +.new-component-placeholder.spacer-placeholder:after { + content: ""; + position: absolute; + height: 60%; + width: 60%; + border: 1px dashed #aaa; +} + /* columns and rows */ .grid-column { width: 100%; min-height: 56px; } +.grid-column .hover-menu--top { + top: -20px; +} + .grid-row { display: flex; flex-direction: row; @@ -196,10 +222,13 @@ .grid-row.grid-row--empty { align-items: center; /* this centers the empty note content */ - min-height: 72px; + height: 80px; } .grid-row--empty:after { + position: absolute; + top: 0; + left: 0; content: "Empty row"; display: flex; align-items: center; @@ -211,11 +240,14 @@ .grid-column--empty:after { content: "Empty column"; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; display: flex; align-items: center; justify-content: center; - width: 100%; - height: 100%; color: #CFD8DC; } @@ -235,6 +267,11 @@ outline: none; } +.grid-row.grid-row--empty .with-popover-menu { /* indicator doesn't show up without this */ + width: 100%; + height: 100%; +} + .with-popover-menu--focused:after { content: ""; position: absolute; @@ -271,7 +308,7 @@ } /* vertical spacer after each menu item */ -.popover-menu .menu-item:nth-child(n+2):before { +.popover-menu .menu-item:not(:only-child):not(:last-child):after { content: ""; width: 1; height: 100%; @@ -301,6 +338,12 @@ background: #CFD8DC; } +.popover-dropdown .caret { /* without this the caret doesn't take up full width / is clipped */ + width: auto; + border-top-color: transparent; +} + + .hover-dropdown li.dropdown-item.active a, .popover-menu li.dropdown-item.active a { background: #fff; @@ -354,7 +397,7 @@ align-items: center; } -.hover-menu--left > div:nth-child(n):not(:only-child):not(:last-child) { +.hover-menu--left > :nth-child(n):not(:only-child):not(:last-child) { padding-bottom: 8px; } @@ -376,24 +419,20 @@ /* Menu fa buttons */ -.hover-menu .fa, -.popover-menu .fa { +.icon-button { color: #879399; font-size: 1em; display: flex; flex-direction: row; align-items: center; justify-content: center; + outline: none; } -.hover-menu .fa:hover, -.hover-menu .fa:active, -.popover-menu .fa:hover, -.popover-menu .fa:active { +.icon-button:hover, +.icon-button:active, +.icon-button:focus { color: #484848; -} - -.hover-menu .fa:focus, -.popover-menu .fa:focus { outline: none; + text-decoration: none; } diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/new/NewDivider.jsx b/superset/assets/javascripts/dashboard/v2/components/gridComponents/new/NewDivider.jsx index 4d859015e88bf..d41bcb6ab81bb 100644 --- a/superset/assets/javascripts/dashboard/v2/components/gridComponents/new/NewDivider.jsx +++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/new/NewDivider.jsx @@ -14,6 +14,7 @@ export default class DraggableNewDivider extends React.PureComponent { id={DIVIDER_TYPE} type={DIVIDER_TYPE} label="Divider" + className="divider-placeholder" /> ); } diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/new/NewSpacer.jsx b/superset/assets/javascripts/dashboard/v2/components/gridComponents/new/NewSpacer.jsx index b81f13c729b76..5b4e507c4a647 100644 --- a/superset/assets/javascripts/dashboard/v2/components/gridComponents/new/NewSpacer.jsx +++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/new/NewSpacer.jsx @@ -14,6 +14,7 @@ export default class DraggableNewChart extends React.PureComponent { id={SPACER_TYPE} type={SPACER_TYPE} label="Spacer" + className="spacer-placeholder fa fa-arrows" /> ); } diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/new/NewTabs.jsx b/superset/assets/javascripts/dashboard/v2/components/gridComponents/new/NewTabs.jsx index 5a866052a466f..e8356c62cc669 100644 --- a/superset/assets/javascripts/dashboard/v2/components/gridComponents/new/NewTabs.jsx +++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/new/NewTabs.jsx @@ -14,6 +14,7 @@ export default class DraggableNewTabs extends React.PureComponent { id={TABS_TYPE} type={TABS_TYPE} label="Tabs" + className="fa fa-window-restore" /> ); } diff --git a/superset/assets/javascripts/dashboard/v2/components/menu/WithPopoverMenu.jsx b/superset/assets/javascripts/dashboard/v2/components/menu/WithPopoverMenu.jsx index 93efd4821dd80..2e449c921b96c 100644 --- a/superset/assets/javascripts/dashboard/v2/components/menu/WithPopoverMenu.jsx +++ b/superset/assets/javascripts/dashboard/v2/components/menu/WithPopoverMenu.jsx @@ -4,27 +4,40 @@ import cx from 'classnames'; const propTypes = { children: PropTypes.node, + disableClick: PropTypes.bool, menuItems: PropTypes.arrayOf(PropTypes.node), onChangeFocus: PropTypes.func, + isFocused: PropTypes.bool, }; const defaultProps = { children: null, + disableClick: false, onChangeFocus: null, onPressDelete() {}, menuItems: [], + isFocused: false, }; -class WithPopoverMenu extends React.Component { +class WithPopoverMenu extends React.PureComponent { constructor(props) { super(props); this.state = { - isFocused: false, + isFocused: props.isFocused, }; this.setRef = this.setRef.bind(this); + // this.setPopoverRef = this.setPopoverRef.bind(this); this.handleClick = this.handleClick.bind(this); } + componentWillReceiveProps(nextProps) { + if (nextProps.isFocused && !this.state.isFocused) { + document.addEventListener('click', this.handleClick, true); + document.addEventListener('drag', this.handleClick, true); + this.setState({ isFocused: true }); + } + } + componentWillUnmount() { document.removeEventListener('click', this.handleClick, true); document.removeEventListener('drag', this.handleClick, true); @@ -33,6 +46,10 @@ class WithPopoverMenu extends React.Component { setRef(ref) { this.container = ref; } + // + // setPopoverRef(ref) { + // this.popover = ref; + // } handleClick(event) { const { onChangeFocus } = this.props; @@ -56,12 +73,13 @@ class WithPopoverMenu extends React.Component { } render() { - const { children, menuItems } = this.props; + const { children, menuItems, disableClick } = this.props; const { isFocused } = this.state; + console.log('render popover') return (
    Date: Sun, 18 Feb 2018 22:35:54 -0800 Subject: [PATCH 21/25] [builder] add IconButton and RowStyleDropdown --- .../dashboard/v2/components/IconButton.jsx | 40 ++++++++++++++++ .../v2/components/menu/RowStyleDropdown.jsx | 46 +++++++++++++++++++ 2 files changed, 86 insertions(+) create mode 100644 superset/assets/javascripts/dashboard/v2/components/IconButton.jsx create mode 100644 superset/assets/javascripts/dashboard/v2/components/menu/RowStyleDropdown.jsx diff --git a/superset/assets/javascripts/dashboard/v2/components/IconButton.jsx b/superset/assets/javascripts/dashboard/v2/components/IconButton.jsx new file mode 100644 index 0000000000000..98044c92029be --- /dev/null +++ b/superset/assets/javascripts/dashboard/v2/components/IconButton.jsx @@ -0,0 +1,40 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import cx from 'classnames'; + +const propTypes = { + onClick: PropTypes.func.isRequired, + className: PropTypes.string, +}; + +const defaultProps = { + className: null, +}; + +export default class IconButton extends React.PureComponent { + constructor(props) { + super(props); + this.handleClick = this.handleClick.bind(this); + } + + handleClick(event) { + event.preventDefault(); + const { onClick } = this.props; + onClick(event); + } + + render() { + const { className } = this.props; + return ( +
    + ); + } +} + +IconButton.propTypes = propTypes; +IconButton.defaultProps = defaultProps; diff --git a/superset/assets/javascripts/dashboard/v2/components/menu/RowStyleDropdown.jsx b/superset/assets/javascripts/dashboard/v2/components/menu/RowStyleDropdown.jsx new file mode 100644 index 0000000000000..50d8f461b65d5 --- /dev/null +++ b/superset/assets/javascripts/dashboard/v2/components/menu/RowStyleDropdown.jsx @@ -0,0 +1,46 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import cx from 'classnames'; + +import rowStyleOptions from './rowStyleOptions'; +import PopoverDropdown from './PopoverDropdown'; + +const propTypes = { + id: PropTypes.string.isRequired, + value: PropTypes.string.isRequired, + onChange: PropTypes.func.isRequired, +}; + +function renderButton(option) { + return ( +
    + {`${option.label} background`} +
    + ); +} + +function renderOption(option) { + return ( +
    + {option.label} +
    + ); +} + +export default class RowStyleDropdown extends React.PureComponent { + render() { + const { id, value, onChange } = this.props; + return ( + + ); + } +} + +RowStyleDropdown.propTypes = propTypes; From a474f044cbe0f7226948fc1772b022d14cf5d56f Mon Sep 17 00:00:00 2001 From: Chris Williams Date: Tue, 20 Feb 2018 11:35:54 -0800 Subject: [PATCH 22/25] [resizable] use ResizableContainer instead of DimensionProvider, fix resize and delete bugs --- .../dashboard/v2/components/dnd/dnd.css | 10 +-- .../v2/components/gridComponents/Chart.jsx | 25 ++++-- .../v2/components/gridComponents/Column.jsx | 37 ++++++--- .../v2/components/gridComponents/Spacer.jsx | 43 +++++++--- .../components/gridComponents/components.css | 18 ++++- .../resizable/DimensionProvider.jsx | 80 ------------------- .../resizable/ResizableContainer.jsx | 40 +++++++--- .../v2/components/resizable/resizable.css | 8 ++ .../dashboard/v2/reducers/dashboard.js | 16 ++-- superset/assets/package.json | 2 +- 10 files changed, 144 insertions(+), 135 deletions(-) delete mode 100644 superset/assets/javascripts/dashboard/v2/components/resizable/DimensionProvider.jsx diff --git a/superset/assets/javascripts/dashboard/v2/components/dnd/dnd.css b/superset/assets/javascripts/dashboard/v2/components/dnd/dnd.css index d084f1f9a1918..fb010e08a82bc 100644 --- a/superset/assets/javascripts/dashboard/v2/components/dnd/dnd.css +++ b/superset/assets/javascripts/dashboard/v2/components/dnd/dnd.css @@ -1,7 +1,5 @@ .dragdroppable { position: relative; - /*height: fit-content; - min-height: 2px;*/ } .dragdroppable--dragging { @@ -12,7 +10,8 @@ width: 100%; } -.grid-container .dragdroppable-row:after { +.grid-container .dragdroppable-row:after, +.grid-container .dragdroppable-column:after { border: 1px dashed transparent; content: ""; position: absolute; @@ -24,7 +23,8 @@ pointer-events: none; } - .grid-container .dragdroppable-row:hover:after { + .grid-container .dragdroppable-row:hover:after, + .grid-container .dragdroppable-column:hover:after { border: 1px dashed #aaa; } @@ -40,7 +40,7 @@ } .drag-handle--top { - margin: 10px auto; + /*margin: 10px auto;*/ } .drag-handle-dot { diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Chart.jsx b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Chart.jsx index 179cef0a11b42..a8a90f65fa1bf 100644 --- a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Chart.jsx +++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Chart.jsx @@ -2,13 +2,18 @@ import React from 'react'; import PropTypes from 'prop-types'; import DeleteComponentButton from '../DeleteComponentButton'; -import DimensionProvider from '../resizable/DimensionProvider'; import DragDroppable from '../dnd/DragDroppable'; import DragHandle from '../dnd/DragHandle'; import HoverMenu from '../menu/HoverMenu'; +import ResizableContainer from '../resizable/ResizableContainer'; import WithPopoverMenu from '../menu/WithPopoverMenu'; import { componentShape } from '../../util/propShapes'; +import { + GRID_MIN_COLUMN_COUNT, + GRID_MIN_ROW_UNITS, +} from '../../util/constants'; + const propTypes = { component: componentShape.isRequired, components: PropTypes.object.isRequired, @@ -82,12 +87,16 @@ class Chart extends React.Component { disableDragDrop={isFocused} > {({ dropIndicatorProps, dragSourceRef }) => ( - } - + )} ); diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Column.jsx b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Column.jsx index 90a8da89f7c89..c38a4618a891b 100644 --- a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Column.jsx +++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Column.jsx @@ -2,14 +2,15 @@ import React from 'react'; import PropTypes from 'prop-types'; import cx from 'classnames'; +import DashboardComponent from '../../containers/DashboardComponent'; +import DeleteComponentButton from '../DeleteComponentButton'; import DragDroppable from '../dnd/DragDroppable'; import DragHandle from '../dnd/DragHandle'; -import DimensionProvider from '../resizable/DimensionProvider'; -import DashboardComponent from '../../containers/DashboardComponent'; import HoverMenu from '../menu/HoverMenu'; +import ResizableContainer from '../resizable/ResizableContainer'; import { componentShape } from '../../util/propShapes'; -import { GRID_GUTTER_SIZE } from '../../util/constants'; +import { GRID_GUTTER_SIZE, GRID_MIN_COLUMN_COUNT } from '../../util/constants'; const propTypes = { component: componentShape.isRequired, @@ -27,6 +28,7 @@ const propTypes = { onResizeStop: PropTypes.func.isRequired, // dnd + deleteComponent: PropTypes.func.isRequired, handleComponentDrop: PropTypes.func.isRequired, }; @@ -35,6 +37,16 @@ const defaultProps = { }; class Column extends React.PureComponent { + constructor(props) { + super(props); + this.handleDeleteComponent = this.handleDeleteComponent.bind(this); + } + + handleDeleteComponent() { + const { deleteComponent, component, parentId } = this.props; + deleteComponent(component.id, parentId); + } + render() { const { component: columnComponent, @@ -56,7 +68,7 @@ class Column extends React.PureComponent { (columnComponent.children || []).forEach((id, childIndex) => { const component = components[id]; columnItems.push(component); - if (index < columnComponent.children.length - 1) { + if (childIndex < columnComponent.children.length - 1) { columnItems.push(`gutter-${childIndex}`); } }); @@ -71,11 +83,15 @@ class Column extends React.PureComponent { onDrop={handleComponentDrop} > {({ dropIndicatorProps, dragSourceRef }) => ( - + {columnItems.map((component, itemIndex) => { @@ -114,7 +131,7 @@ class Column extends React.PureComponent { })} {dropIndicatorProps &&
    }
    -
    + )} diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Spacer.jsx b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Spacer.jsx index 4857a18f26839..65271e45d8753 100644 --- a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Spacer.jsx +++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Spacer.jsx @@ -1,12 +1,18 @@ import React from 'react'; import PropTypes from 'prop-types'; -import DimensionProvider from '../resizable/DimensionProvider'; +import DeleteComponentButton from '../DeleteComponentButton'; import DragDroppable from '../dnd/DragDroppable'; import DragHandle from '../dnd/DragHandle'; import HoverMenu from '../menu/HoverMenu'; +import ResizableContainer from '../resizable/ResizableContainer'; import { componentShape } from '../../util/propShapes'; +// import { +// GRID_MIN_COLUMN_COUNT, +// GRID_MIN_ROW_UNITS, +// } from '../../util/constants'; + const propTypes = { component: componentShape.isRequired, components: PropTypes.object.isRequired, @@ -23,6 +29,7 @@ const propTypes = { onResizeStop: PropTypes.func.isRequired, // dnd + deleteComponent: PropTypes.func.isRequired, handleComponentDrop: PropTypes.func.isRequired, }; @@ -31,6 +38,16 @@ const defaultProps = { }; class Spacer extends React.PureComponent { + constructor(props) { + super(props); + this.handleDeleteComponent = this.handleDeleteComponent.bind(this); + } + + handleDeleteComponent() { + const { deleteComponent, component, parentId } = this.props; + deleteComponent(component.id, parentId); + } + render() { const { component, @@ -47,34 +64,42 @@ class Spacer extends React.PureComponent { handleComponentDrop, } = this.props; - const hoverMenuPosition = depth % 2 !== 0 ? 'left' : 'top'; + const orientation = depth % 2 === 0 ? 'row' : 'column'; + const hoverMenuPosition = depth % 2 === 0 ? 'left' : 'top'; + return ( {({ dropIndicatorProps, dragSourceRef }) => ( - +
    {dropIndicatorProps &&
    } - + )} ); diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/components.css b/superset/assets/javascripts/dashboard/v2/components/gridComponents/components.css index 154973c560af9..b4412ee26a914 100644 --- a/superset/assets/javascripts/dashboard/v2/components/gridComponents/components.css +++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/components.css @@ -180,6 +180,10 @@ font-size: 1em; } +.new-component-placeholder.fa-window-restore { + font-size: 1em; +} + .new-component-placeholder.spacer-placeholder:after { content: ""; position: absolute; @@ -194,7 +198,7 @@ min-height: 56px; } -.grid-column .hover-menu--top { +.grid-column > .hover-menu--top { top: -20px; } @@ -267,7 +271,7 @@ outline: none; } -.grid-row.grid-row--empty .with-popover-menu { /* indicator doesn't show up without this */ +.grid-row.grid-row--empty .with-popover-menu { /* drop indicator doesn't show up without this */ width: 100%; height: 100%; } @@ -398,7 +402,7 @@ } .hover-menu--left > :nth-child(n):not(:only-child):not(:last-child) { - padding-bottom: 8px; + margin-bottom: 8px; } .dragdroppable-row .dragdroppable-row .hover-menu--left { @@ -410,6 +414,14 @@ height: 20px; top: 0; left: 0; + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; +} + +.hover-menu--top > :nth-child(n):not(:only-child):not(:last-child) { + margin-right: 8px; } .dragdroppable:hover .hover-menu, diff --git a/superset/assets/javascripts/dashboard/v2/components/resizable/DimensionProvider.jsx b/superset/assets/javascripts/dashboard/v2/components/resizable/DimensionProvider.jsx deleted file mode 100644 index cb9ca101de54e..0000000000000 --- a/superset/assets/javascripts/dashboard/v2/components/resizable/DimensionProvider.jsx +++ /dev/null @@ -1,80 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; - -import ResizableContainer from './ResizableContainer'; -import componentIsResizable from '../../util/componentIsResizable'; -import { componentShape } from '../../util/propShapes'; - -import { - GRID_GUTTER_SIZE, - GRID_ROW_HEIGHT_UNIT, - GRID_MIN_COLUMN_COUNT, - GRID_MIN_ROW_UNITS, - GRID_MAX_ROW_UNITS, -} from '../../util/constants'; - -import { SPACER_TYPE, COLUMN_TYPE } from '../../util/componentTypes'; - -const propTypes = { - availableColumnCount: PropTypes.number.isRequired, - children: PropTypes.node.isRequired, - columnWidth: PropTypes.number.isRequired, - component: componentShape.isRequired, - rowHeight: PropTypes.number, - onResizeStart: PropTypes.func.isRequired, - onResize: PropTypes.func.isRequired, - onResizeStop: PropTypes.func.isRequired, -}; - -const defaultProps = { - rowHeight: 0, -}; - -class DimensionProvider extends React.PureComponent { - render() { - const { - availableColumnCount, - children, - columnWidth, - rowHeight, - component, - onResizeStart, - onResize, - onResizeStop, - } = this.props; - - const isResizable = componentIsResizable(component); - const isSpacer = component.type === SPACER_TYPE; - - if (!isResizable) return children; - - return ( - - {children} - - ); - } -} - -DimensionProvider.propTypes = propTypes; -DimensionProvider.defaultProps = defaultProps; - -export default DimensionProvider; diff --git a/superset/assets/javascripts/dashboard/v2/components/resizable/ResizableContainer.jsx b/superset/assets/javascripts/dashboard/v2/components/resizable/ResizableContainer.jsx index 9166adde46636..fe1da1ffe4457 100644 --- a/superset/assets/javascripts/dashboard/v2/components/resizable/ResizableContainer.jsx +++ b/superset/assets/javascripts/dashboard/v2/components/resizable/ResizableContainer.jsx @@ -5,7 +5,12 @@ import cx from 'classnames'; import ResizableHandle from './ResizableHandle'; import resizableConfig from '../../util/resizableConfig'; -import { GRID_BASE_UNIT } from '../../util/constants'; +import { + GRID_BASE_UNIT, + GRID_ROW_HEIGHT_UNIT, + GRID_GUTTER_SIZE, +} from '../../util/constants'; + import './resizable.css'; const propTypes = { @@ -31,9 +36,9 @@ const defaultProps = { children: null, adjustableWidth: true, adjustableHeight: true, - gutterWidth: 0, + gutterWidth: GRID_GUTTER_SIZE, widthStep: GRID_BASE_UNIT, - heightStep: GRID_BASE_UNIT, + heightStep: GRID_ROW_HEIGHT_UNIT, widthMultiple: 1, heightMultiple: 1, minWidthMultiple: 1, @@ -89,11 +94,14 @@ class ResizableContainer extends React.PureComponent { heightMultiple, adjustableHeight, adjustableWidth, + gutterWidth, } = this.props; if (onResizeStop) { - const nextWidthMultiple = Math.round(widthMultiple + (delta.width / widthStep)); - const nextHeightMultiple = Math.round(heightMultiple + (delta.height / heightStep)); + const nextWidthMultiple = + Math.round(widthMultiple + (delta.width / (widthStep + gutterWidth))); + const nextHeightMultiple = + Math.round(heightMultiple + (delta.height / heightStep)); onResizeStop({ id, @@ -122,8 +130,10 @@ class ResizableContainer extends React.PureComponent { } = this.props; const size = { - width: adjustableWidth ? (widthStep * widthMultiple) - gutterWidth : null, - height: (adjustableHeight || heightMultiple) ? heightStep * heightMultiple : null, + width: adjustableWidth + ? ((widthStep + gutterWidth) * widthMultiple) - gutterWidth : undefined, + height: adjustableHeight + ? heightStep * heightMultiple : undefined, }; let enableConfig = resizableConfig.widthAndHeight; @@ -136,10 +146,18 @@ class ResizableContainer extends React.PureComponent { span .resize-handle { border-color: #44C0FF; } + +/* 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 { + width: auto !important; + height: auto !important; +} diff --git a/superset/assets/javascripts/dashboard/v2/reducers/dashboard.js b/superset/assets/javascripts/dashboard/v2/reducers/dashboard.js index 34c673b4b30c0..45180114dc1be 100644 --- a/superset/assets/javascripts/dashboard/v2/reducers/dashboard.js +++ b/superset/assets/javascripts/dashboard/v2/reducers/dashboard.js @@ -27,24 +27,24 @@ const actionHandlers = { // recursively find children to remove let deleteCount = 0; function recursivelyDeleteChildren(componentId, componentParentId) { + // delete child and it's children const component = nextComponents[componentId]; - const parent = nextComponents[componentParentId]; + delete nextComponents[componentId]; + deleteCount += 1; + const { children = [] } = component; + children.forEach((childId) => { recursivelyDeleteChildren(childId, componentId); }); - if (parent && component) { + const parent = nextComponents[componentParentId]; + if (parent) { // may have been deleted in another recursion const componentIndex = (parent.children || []).indexOf(componentId); if (componentIndex > -1) { parent.children.splice(componentIndex, 1); - delete nextComponents[componentId]; - deleteCount += 1; - - const { children = [] } = component; - children.forEach((childId) => { recursivelyDeleteChildren(childId, componentId); }); } } } recursivelyDeleteChildren(id, parentId); - console.log('Deleted', deleteCount, 'total components'); + console.log('Deleted', deleteCount, 'total components', nextComponents); return nextComponents; }, diff --git a/superset/assets/package.json b/superset/assets/package.json index 54278215770c3..797a6f745f9b5 100644 --- a/superset/assets/package.json +++ b/superset/assets/package.json @@ -72,7 +72,7 @@ "nvd3": "1.8.6", "po2json": "^0.4.5", "prop-types": "^15.6.0", - "re-resizable": "^4.3.0", + "re-resizable": "^4.3.1", "react": "^15.6.2", "react-ace": "^5.0.1", "react-addons-css-transition-group": "^15.6.0", From ab6e63d8cddfbbde86993bed80b48a724eca7af2 Mon Sep 17 00:00:00 2001 From: Chris Williams Date: Tue, 20 Feb 2018 12:48:50 -0800 Subject: [PATCH 23/25] [builder] fix bug with spacer --- .../v2/components/gridComponents/Row.jsx | 2 +- .../v2/components/gridComponents/Spacer.jsx | 22 ++++++++++--------- .../components/gridComponents/components.css | 5 +++++ .../resizable/ResizableContainer.jsx | 10 ++++++--- .../dashboard/v2/util/newEntitiesFromDrop.js | 2 +- 5 files changed, 26 insertions(+), 15 deletions(-) diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Row.jsx b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Row.jsx index 2012f061a88e6..620e88ee53a8b 100644 --- a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Row.jsx +++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Row.jsx @@ -92,7 +92,7 @@ class Row extends React.PureComponent { } = this.props; let occupiedColumnCount = 0; - let rowHeight = 0; + let rowHeight = 0; // row items without height require this const rowItems = []; // this adds a gutter between each child in the row. diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Spacer.jsx b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Spacer.jsx index 65271e45d8753..80d85e622b9be 100644 --- a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Spacer.jsx +++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Spacer.jsx @@ -8,10 +8,10 @@ import HoverMenu from '../menu/HoverMenu'; import ResizableContainer from '../resizable/ResizableContainer'; import { componentShape } from '../../util/propShapes'; -// import { +import { // GRID_MIN_COLUMN_COUNT, -// GRID_MIN_ROW_UNITS, -// } from '../../util/constants'; + GRID_MIN_ROW_UNITS, +} from '../../util/constants'; const propTypes = { component: componentShape.isRequired, @@ -65,7 +65,9 @@ class Spacer extends React.PureComponent { } = this.props; const orientation = depth % 2 === 0 ? 'row' : 'column'; - const hoverMenuPosition = depth % 2 === 0 ? 'left' : 'top'; + const hoverMenuPosition = orientation === 'row' ? 'left' : 'top'; + const adjustableWidth = orientation === 'column'; + const adjustableHeight = orientation === 'row'; return ( ( - - + -
    +
    {dropIndicatorProps &&
    } diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/components.css b/superset/assets/javascripts/dashboard/v2/components/gridComponents/components.css index b4412ee26a914..a88ea0991abbe 100644 --- a/superset/assets/javascripts/dashboard/v2/components/gridComponents/components.css +++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/components.css @@ -255,12 +255,17 @@ color: #CFD8DC; } +/* spacer */ .grid-spacer { width: 100%; height: 100%; background-color: transparent; } +.dragdroppable .grid-spacer { + cursor: move; +} + .dragdroppable:hover .grid-spacer { box-shadow: inset 0 0 0 1px #CFD8DC; } diff --git a/superset/assets/javascripts/dashboard/v2/components/resizable/ResizableContainer.jsx b/superset/assets/javascripts/dashboard/v2/components/resizable/ResizableContainer.jsx index fe1da1ffe4457..bd590ae2c82c3 100644 --- a/superset/assets/javascripts/dashboard/v2/components/resizable/ResizableContainer.jsx +++ b/superset/assets/javascripts/dashboard/v2/components/resizable/ResizableContainer.jsx @@ -27,6 +27,7 @@ const propTypes = { maxWidthMultiple: PropTypes.number, minHeightMultiple: PropTypes.number, maxHeightMultiple: PropTypes.number, + staticHeightMultiple: PropTypes.number, onResizeStop: PropTypes.func, onResize: PropTypes.func, onResizeStart: PropTypes.func, @@ -39,12 +40,13 @@ const defaultProps = { gutterWidth: GRID_GUTTER_SIZE, widthStep: GRID_BASE_UNIT, heightStep: GRID_ROW_HEIGHT_UNIT, - widthMultiple: 1, - heightMultiple: 1, + widthMultiple: null, + heightMultiple: null, minWidthMultiple: 1, maxWidthMultiple: Infinity, minHeightMultiple: 1, maxHeightMultiple: Infinity, + staticHeightMultiple: null, onResizeStop: null, onResize: null, onResizeStart: null, @@ -120,6 +122,7 @@ class ResizableContainer extends React.PureComponent { adjustableHeight, widthStep, heightStep, + staticHeightMultiple, widthMultiple, heightMultiple, minWidthMultiple, @@ -133,7 +136,8 @@ class ResizableContainer extends React.PureComponent { width: adjustableWidth ? ((widthStep + gutterWidth) * widthMultiple) - gutterWidth : undefined, height: adjustableHeight - ? heightStep * heightMultiple : undefined, + ? heightStep * heightMultiple + : (staticHeightMultiple && staticHeightMultiple * heightStep) || undefined, }; let enableConfig = resizableConfig.widthAndHeight; diff --git a/superset/assets/javascripts/dashboard/v2/util/newEntitiesFromDrop.js b/superset/assets/javascripts/dashboard/v2/util/newEntitiesFromDrop.js index 34ddd720f50d9..b31120fc7d066 100644 --- a/superset/assets/javascripts/dashboard/v2/util/newEntitiesFromDrop.js +++ b/superset/assets/javascripts/dashboard/v2/util/newEntitiesFromDrop.js @@ -24,7 +24,7 @@ const typeToDefaultMetaData = { [HEADER_TYPE]: { text: 'New header', size: MEDIUM_HEADER, rowStyle: ROW_TRANSPARENT }, [MARKDOWN_TYPE]: { width: 3, height: 15 }, [ROW_TYPE]: { rowStyle: ROW_TRANSPARENT }, - [SPACER_TYPE]: { width: 1 }, + [SPACER_TYPE]: {}, [TABS_TYPE]: null, [TAB_TYPE]: { text: 'New Tab' }, }; From 397e6d2516ce500746cda800807ae3bd33f1a922 Mon Sep 17 00:00:00 2001 From: Chris Williams Date: Tue, 20 Feb 2018 16:27:37 -0800 Subject: [PATCH 24/25] [builder] clean up, header.size => header.headerSize --- .../dashboard/v2/components/DashboardGrid.jsx | 2 +- .../v2/components/gridComponents/Header.jsx | 11 ++-- .../v2/components/gridComponents/Row.jsx | 2 +- .../v2/components/menu/RowStyleDropdown.jsx | 2 +- .../components/menu/RowStyleHoverDropdown.jsx | 59 ------------------- .../components/menu/RowStyleHoverTrigger.jsx | 36 ----------- .../menu => util}/headerStyleOptions.js | 4 +- .../dashboard/v2/util/newEntitiesFromDrop.js | 2 +- .../dashboard/v2/util/propShapes.jsx | 29 +++------ .../menu => util}/rowStyleOptions.js | 4 +- 10 files changed, 21 insertions(+), 130 deletions(-) delete mode 100644 superset/assets/javascripts/dashboard/v2/components/menu/RowStyleHoverDropdown.jsx delete mode 100644 superset/assets/javascripts/dashboard/v2/components/menu/RowStyleHoverTrigger.jsx rename superset/assets/javascripts/dashboard/v2/{components/menu => util}/headerStyleOptions.js (66%) rename superset/assets/javascripts/dashboard/v2/{components/menu => util}/rowStyleOptions.js (63%) diff --git a/superset/assets/javascripts/dashboard/v2/components/DashboardGrid.jsx b/superset/assets/javascripts/dashboard/v2/components/DashboardGrid.jsx index 61bae752d4580..884fc899bf71c 100644 --- a/superset/assets/javascripts/dashboard/v2/components/DashboardGrid.jsx +++ b/superset/assets/javascripts/dashboard/v2/components/DashboardGrid.jsx @@ -101,7 +101,7 @@ class DashboardGrid extends React.PureComponent { // account for (COLUMN_COUNT - 1) gutters const columnPlusGutterWidth = (width + GRID_GUTTER_SIZE) / GRID_COLUMN_COUNT; const columnWidth = columnPlusGutterWidth - GRID_GUTTER_SIZE; - console.log('render grid parentsize') + return width < 50 ? null : (
    {(rootComponent.children || []).map((id, index) => ( diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Header.jsx b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Header.jsx index 4d697304f2a13..4826688d3848d 100644 --- a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Header.jsx +++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Header.jsx @@ -9,10 +9,9 @@ import HoverMenu from '../menu/HoverMenu'; import WithPopoverMenu from '../menu/WithPopoverMenu'; import RowStyleDropdown from '../menu/RowStyleDropdown'; import DeleteComponentButton from '../DeleteComponentButton'; -import IconButton from '../IconButton'; import PopoverDropdown from '../menu/PopoverDropdown'; -import headerStyleOptions from '../menu/headerStyleOptions'; -import rowStyleOptions from '../menu/rowStyleOptions'; +import headerStyleOptions from '../../util/headerStyleOptions'; +import rowStyleOptions from '../../util/rowStyleOptions'; import { componentShape } from '../../util/propShapes'; import { SMALL_HEADER, ROW_TRANSPARENT } from '../../util/constants'; @@ -38,7 +37,7 @@ class Header extends React.PureComponent { this.handleDeleteComponent = this.handleDeleteComponent.bind(this); this.handleChangeFocus = this.handleChangeFocus.bind(this); this.handleUpdateMeta = this.handleUpdateMeta.bind(this); - this.handleChangeSize = this.handleUpdateMeta.bind(this, 'size'); + this.handleChangeSize = this.handleUpdateMeta.bind(this, 'headerSize'); this.handleChangeRowStyle = this.handleUpdateMeta.bind(this, 'rowStyle'); this.handleChangeText = this.handleUpdateMeta.bind(this, 'text'); } @@ -79,7 +78,7 @@ class Header extends React.PureComponent { } = this.props; const headerStyle = headerStyleOptions.find( - opt => opt.value === (component.meta.size || SMALL_HEADER), + opt => opt.value === (component.meta.headerSize || SMALL_HEADER), ); const rowStyle = rowStyleOptions.find( @@ -108,7 +107,7 @@ class Header extends React.PureComponent { `${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 620e88ee53a8b..3743534d9b24e 100644 --- a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Row.jsx +++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Row.jsx @@ -12,7 +12,7 @@ import RowStyleDropdown from '../menu/RowStyleDropdown'; import WithPopoverMenu from '../menu/WithPopoverMenu'; import { componentShape } from '../../util/propShapes'; -import rowStyleOptions from '../menu/rowStyleOptions'; +import rowStyleOptions from '../../util/rowStyleOptions'; import { GRID_GUTTER_SIZE, ROW_TRANSPARENT } from '../../util/constants'; const propTypes = { diff --git a/superset/assets/javascripts/dashboard/v2/components/menu/RowStyleDropdown.jsx b/superset/assets/javascripts/dashboard/v2/components/menu/RowStyleDropdown.jsx index 50d8f461b65d5..d3c7eff965774 100644 --- a/superset/assets/javascripts/dashboard/v2/components/menu/RowStyleDropdown.jsx +++ b/superset/assets/javascripts/dashboard/v2/components/menu/RowStyleDropdown.jsx @@ -2,7 +2,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import cx from 'classnames'; -import rowStyleOptions from './rowStyleOptions'; +import rowStyleOptions from '../../util/rowStyleOptions'; import PopoverDropdown from './PopoverDropdown'; const propTypes = { diff --git a/superset/assets/javascripts/dashboard/v2/components/menu/RowStyleHoverDropdown.jsx b/superset/assets/javascripts/dashboard/v2/components/menu/RowStyleHoverDropdown.jsx deleted file mode 100644 index e45efb148877f..0000000000000 --- a/superset/assets/javascripts/dashboard/v2/components/menu/RowStyleHoverDropdown.jsx +++ /dev/null @@ -1,59 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import cx from 'classnames'; -import { MenuItem, Dropdown } from 'react-bootstrap'; - -import RowStyleHoverTrigger from './RowStyleHoverTrigger'; -import rowStyleOptions from './rowStyleOptions'; - -const propTypes = { - id: PropTypes.string.isRequired, - onChange: PropTypes.func.isRequired, - value: PropTypes.string.isRequired, -}; - -const defaultProps = { -}; - -export default class RowStyleHoverDropdown extends React.PureComponent { - constructor(props) { - super(props); - this.setRef = this.setRef.bind(this); - } - - componentWillUnmount() { - document.removeEventListener('click', this.handleClick, true); - document.removeEventListener('drag', this.handleClick, true); - } - - setRef(ref) { - this.container = ref; - } - - render() { - const { id, onChange, value = rowStyleOptions[0].value } = this.props; - return ( - - - - {rowStyleOptions.map(option => ( - -
    - {option.label} background -
    -
    - ))} -
    -
    - ); - } -} - -RowStyleHoverDropdown.propTypes = propTypes; -RowStyleHoverDropdown.defaultProps = defaultProps; diff --git a/superset/assets/javascripts/dashboard/v2/components/menu/RowStyleHoverTrigger.jsx b/superset/assets/javascripts/dashboard/v2/components/menu/RowStyleHoverTrigger.jsx deleted file mode 100644 index e8643f2f90514..0000000000000 --- a/superset/assets/javascripts/dashboard/v2/components/menu/RowStyleHoverTrigger.jsx +++ /dev/null @@ -1,36 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import cx from 'classnames'; - -const propTypes = { - onClick: PropTypes.func, -}; - -const defaultProps = { - onClick() {}, -}; - -// Note: this has to be a separate component because react-bootstrap injects the onClick prop -export default class RowStyleHoverTrigger extends React.PureComponent { - constructor(props) { - super(props); - this.handleClick = this.handleClick.bind(this); - } - - handleClick(event) { - const { onClick } = this.props; - event.preventDefault(); - onClick(event); - } - - render() { - return ( - -
    - - ); - } -} - -RowStyleHoverTrigger.propTypes = propTypes; -RowStyleHoverTrigger.defaultProps = defaultProps; diff --git a/superset/assets/javascripts/dashboard/v2/components/menu/headerStyleOptions.js b/superset/assets/javascripts/dashboard/v2/util/headerStyleOptions.js similarity index 66% rename from superset/assets/javascripts/dashboard/v2/components/menu/headerStyleOptions.js rename to superset/assets/javascripts/dashboard/v2/util/headerStyleOptions.js index f16f90e0d4757..309d482ab6aed 100644 --- a/superset/assets/javascripts/dashboard/v2/components/menu/headerStyleOptions.js +++ b/superset/assets/javascripts/dashboard/v2/util/headerStyleOptions.js @@ -1,5 +1,5 @@ -import { t } from '../../../../locales'; -import { SMALL_HEADER, MEDIUM_HEADER, LARGE_HEADER } from '../../util/constants'; +import { t } from '../../../locales'; +import { SMALL_HEADER, MEDIUM_HEADER, LARGE_HEADER } from './constants'; export default [ { value: SMALL_HEADER, label: t('Small'), className: 'header-small' }, diff --git a/superset/assets/javascripts/dashboard/v2/util/newEntitiesFromDrop.js b/superset/assets/javascripts/dashboard/v2/util/newEntitiesFromDrop.js index b31120fc7d066..605412b9e2061 100644 --- a/superset/assets/javascripts/dashboard/v2/util/newEntitiesFromDrop.js +++ b/superset/assets/javascripts/dashboard/v2/util/newEntitiesFromDrop.js @@ -21,7 +21,7 @@ const typeToDefaultMetaData = { [CHART_TYPE]: { width: 3, height: 15 }, [COLUMN_TYPE]: { width: 3 }, [DIVIDER_TYPE]: null, - [HEADER_TYPE]: { text: 'New header', size: MEDIUM_HEADER, rowStyle: ROW_TRANSPARENT }, + [HEADER_TYPE]: { text: 'New header', headerSize: MEDIUM_HEADER, rowStyle: ROW_TRANSPARENT }, [MARKDOWN_TYPE]: { width: 3, height: 15 }, [ROW_TYPE]: { rowStyle: ROW_TRANSPARENT }, [SPACER_TYPE]: {}, diff --git a/superset/assets/javascripts/dashboard/v2/util/propShapes.jsx b/superset/assets/javascripts/dashboard/v2/util/propShapes.jsx index 4354398c0feab..7c0d3ef46b4f5 100644 --- a/superset/assets/javascripts/dashboard/v2/util/propShapes.jsx +++ b/superset/assets/javascripts/dashboard/v2/util/propShapes.jsx @@ -1,12 +1,14 @@ import PropTypes from 'prop-types'; import componentTypes from './componentTypes'; +import rowStyleOptions from './rowStyleOptions'; +import headerStyleOptions from './headerStyleOptions'; -export const componentShape = PropTypes.shape({ +export const componentShape = PropTypes.shape({ // eslint-disable-line id: PropTypes.string.isRequired, type: PropTypes.oneOf( Object.values(componentTypes), ).isRequired, - childIds: PropTypes.arrayOf(PropTypes.string), + children: PropTypes.arrayOf(PropTypes.string).isRequired, meta: PropTypes.shape({ // Dimensions width: PropTypes.number, @@ -14,24 +16,9 @@ export const componentShape = PropTypes.shape({ // Header text: PropTypes.string, + headerSize: PropTypes.oneOf(headerStyleOptions.map(opt => opt.value)), + + // Row + rowStyle: PropTypes.oneOf(rowStyleOptions.map(opt => opt.value)), }), }); - -export const componentProps = { - component: componentShape.isRequired, - components: PropTypes.object.isRequired, - depth: PropTypes.number.isRequired, - index: PropTypes.number.isRequired, - parentId: PropTypes.string.isRequired, - - // grid related - availableColumnCount: PropTypes.number.isRequired, - columnWidth: PropTypes.number.isRequired, - rowHeight: PropTypes.number, - onResizeStart: PropTypes.func.isRequired, - onResize: PropTypes.func.isRequired, - onResizeStop: PropTypes.func.isRequired, - - // dnd - onDrop: PropTypes.func.isRequired, -}; diff --git a/superset/assets/javascripts/dashboard/v2/components/menu/rowStyleOptions.js b/superset/assets/javascripts/dashboard/v2/util/rowStyleOptions.js similarity index 63% rename from superset/assets/javascripts/dashboard/v2/components/menu/rowStyleOptions.js rename to superset/assets/javascripts/dashboard/v2/util/rowStyleOptions.js index 96c3417a51b50..ad42492296cfb 100644 --- a/superset/assets/javascripts/dashboard/v2/components/menu/rowStyleOptions.js +++ b/superset/assets/javascripts/dashboard/v2/util/rowStyleOptions.js @@ -1,5 +1,5 @@ -import { t } from '../../../../locales'; -import { ROW_TRANSPARENT, ROW_WHITE } from '../../util/constants'; +import { t } from '../../../locales'; +import { ROW_TRANSPARENT, ROW_WHITE } from './constants'; export default [ { value: ROW_TRANSPARENT, label: t('Transparent'), className: 'grid-row--transparent' }, From 8b5e08ffe47696e437b6b57ecf12a3082156eabe Mon Sep 17 00:00:00 2001 From: Chris Williams Date: Wed, 21 Feb 2018 01:45:20 -0800 Subject: [PATCH 25/25] [builder] support more drag/drop combinations by wrapping some components in rows upon drop. fix within list drop index. refactor some utils. --- .../javascripts/dashboard/v2/actions/index.js | 4 +- .../dashboard/v2/components/dnd/handleDrop.js | 18 ++--- .../v2/components/dnd/handleHover.js | 2 - .../v2/components/gridComponents/Tabs.jsx | 10 ++- .../new/DraggableNewComponent.jsx | 6 ++ .../gridComponents/new/NewChart.jsx | 3 +- .../gridComponents/new/NewColumn.jsx | 3 +- .../gridComponents/new/NewDivider.jsx | 3 +- .../gridComponents/new/NewHeader.jsx | 3 +- .../components/gridComponents/new/NewRow.jsx | 3 +- .../gridComponents/new/NewSpacer.jsx | 3 +- .../components/gridComponents/new/NewTabs.jsx | 3 +- .../v2/components/menu/WithPopoverMenu.jsx | 2 +- .../dashboard/v2/reducers/dashboard.js | 26 ++++++- .../dashboard/v2/util/componentTypes.js | 2 - .../dashboard/v2/util/constants.js | 10 ++- .../dashboard/v2/util/isValidChild.js | 25 +++---- .../dashboard/v2/util/newComponentFactory.js | 45 ++++++++++++ .../dashboard/v2/util/newComponentIdToType.js | 35 ++++++++++ .../dashboard/v2/util/newEntitiesFromDrop.js | 68 ++++--------------- .../dashboard/v2/util/propShapes.jsx | 2 +- .../dashboard/v2/util/shouldWrapChildInRow.js | 30 ++++++++ 22 files changed, 208 insertions(+), 98 deletions(-) create mode 100644 superset/assets/javascripts/dashboard/v2/util/newComponentFactory.js create mode 100644 superset/assets/javascripts/dashboard/v2/util/newComponentIdToType.js create mode 100644 superset/assets/javascripts/dashboard/v2/util/shouldWrapChildInRow.js diff --git a/superset/assets/javascripts/dashboard/v2/actions/index.js b/superset/assets/javascripts/dashboard/v2/actions/index.js index 16fde6b365c62..005a77e5dccd4 100644 --- a/superset/assets/javascripts/dashboard/v2/actions/index.js +++ b/superset/assets/javascripts/dashboard/v2/actions/index.js @@ -53,11 +53,13 @@ export function handleComponentDrop(dropResult) { ) ) { return dispatch(moveComponent(dropResult)); + + // new components don't have a source } else if (dropResult.destination && !dropResult.source) { return dispatch(createComponent(dropResult)); } return null; - } + }; } // Resize --------------------------------------------------------------------- diff --git a/superset/assets/javascripts/dashboard/v2/components/dnd/handleDrop.js b/superset/assets/javascripts/dashboard/v2/components/dnd/handleDrop.js index 96e16bde28750..de7ea854abc72 100644 --- a/superset/assets/javascripts/dashboard/v2/components/dnd/handleDrop.js +++ b/superset/assets/javascripts/dashboard/v2/components/dnd/handleDrop.js @@ -49,10 +49,16 @@ export default function handleDrop(props, monitor, Component) { index: component.children.length, }; } else { // insert as sibling - let nextIndex = componentIndex; + // if the item is in the same list with a smaller index, you must account for the + // "missing" index upon movement within the list + const sameList = draggingItem.parentId && draggingItem.parentId === parentId; + const sameListLowerIndex = sameList && draggingItem.index < componentIndex; + + let nextIndex = sameListLowerIndex ? componentIndex - 1 : componentIndex; const refBoundingRect = Component.ref.getBoundingClientRect(); const clientOffset = monitor.getClientOffset(); + if (clientOffset) { if (orientation === 'row') { const refMiddleY = @@ -65,16 +71,6 @@ export default function handleDrop(props, monitor, Component) { } } - // if the item is in the same list with a smaller index, you must account for the - // "missing" index upon movement within the list - if ( - draggingItem.parentId - && draggingItem.parentId === parentId - && draggingItem.index < nextIndex - ) { - nextIndex = Math.max(0, nextIndex - 1); - } - dropResult.destination = { droppableId: parentId, 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 02f2377bd7130..80d0ef9a6a14b 100644 --- a/superset/assets/javascripts/dashboard/v2/components/dnd/handleHover.js +++ b/superset/assets/javascripts/dashboard/v2/components/dnd/handleHover.js @@ -13,7 +13,6 @@ export default function handleHover(props, monitor, Component) { if (!draggingItem || draggingItem.draggableId === component.id) { Component.setState(() => ({ dropIndicator: null })); - console.log(draggingItem ? 'drag self' : 'no item'); return; } @@ -40,7 +39,6 @@ export default function handleHover(props, monitor, Component) { dropIndicator: { top: 0, right: component.children.length ? 8 : null, - left: component.children.length ? null : null, height: indicatorOrientation === 'column' ? '100%' : 3, width: indicatorOrientation === 'column' ? 3 : '100%', minHeight: indicatorOrientation === 'column' ? 16 : null, diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Tabs.jsx b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Tabs.jsx index e53fa8bfb8a14..d2fbd946be6ed 100644 --- a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Tabs.jsx +++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Tabs.jsx @@ -108,7 +108,9 @@ class Tabs extends React.PureComponent { handleDeleteComponent(id) { const { deleteComponent, component, parentId } = this.props; const isTabsComponent = id === component.id; - deleteComponent(id, isTabsComponent ? parentId : component.id); + if (isTabsComponent || component.children.length > 1) { + deleteComponent(id, isTabsComponent ? parentId : component.id); + } } handleDropOnTab(dropResult) { @@ -177,11 +179,15 @@ class Tabs extends React.PureComponent { > {tabIds.map((tabId, tabIndex) => { const tabComponent = components[tabId]; - return ( // Bootstrap doesn't render a Tab if we move this to its own Tab.jsx + return ( + // react-bootstrap doesn't render a Tab if we move this to its own Tab.jsx + // so we set the title as the Tab.jsx component. This also enables not needing + // the entire dashboard component lookup to render Tabs.jsx type in the future + [NEW_COLUMN_ID]: COLUMN_TYPE, + [NEW_DIVIDER_ID]: DIVIDER_TYPE, + [NEW_HEADER_ID]: HEADER_TYPE, + [NEW_MARKDOWN_ID]: MARKDOWN_TYPE, + [NEW_ROW_ID]: ROW_TYPE, + [NEW_SPACER_ID]: SPACER_TYPE, + [NEW_TABS_ID]: TABS_TYPE, + [NEW_TAB_ID]: TAB_TYPE, +}; diff --git a/superset/assets/javascripts/dashboard/v2/util/newEntitiesFromDrop.js b/superset/assets/javascripts/dashboard/v2/util/newEntitiesFromDrop.js index 605412b9e2061..a0d92fa7449c7 100644 --- a/superset/assets/javascripts/dashboard/v2/util/newEntitiesFromDrop.js +++ b/superset/assets/javascripts/dashboard/v2/util/newEntitiesFromDrop.js @@ -1,55 +1,17 @@ -import isValidChild from './isValidChild'; +import newComponentIdToType from './newComponentIdToType'; +import shouldWrapChildInRow from './shouldWrapChildInRow'; +import newComponentFactory from './newComponentFactory'; import { - CHART_TYPE, - COLUMN_TYPE, - DIVIDER_TYPE, - HEADER_TYPE, - MARKDOWN_TYPE, ROW_TYPE, - SPACER_TYPE, TABS_TYPE, TAB_TYPE, } from './componentTypes'; -import { - MEDIUM_HEADER, - ROW_TRANSPARENT, -} from './constants'; - -const typeToDefaultMetaData = { - [CHART_TYPE]: { width: 3, height: 15 }, - [COLUMN_TYPE]: { width: 3 }, - [DIVIDER_TYPE]: null, - [HEADER_TYPE]: { text: 'New header', headerSize: MEDIUM_HEADER, rowStyle: ROW_TRANSPARENT }, - [MARKDOWN_TYPE]: { width: 3, height: 15 }, - [ROW_TYPE]: { rowStyle: ROW_TRANSPARENT }, - [SPACER_TYPE]: {}, - [TABS_TYPE]: null, - [TAB_TYPE]: { text: 'New Tab' }, -}; - -// @TODO this should be replaced by a more robust algorithm -function uuid(type) { - return `${type}-${Math.random().toString(16)}`; -} - -function entityFactory(type) { - return { - dashboardVersion: 'v0', - type, - id: uuid(type), - children: [], - meta: { - ...typeToDefaultMetaData[type], - }, - }; -} - export default function newEntitiesFromDrop({ dropResult, components }) { const { draggableId, destination } = dropResult; - const dragType = draggableId; // @TODO newComponentIdToType lookup + const dragType = newComponentIdToType[draggableId]; const dropEntity = components[destination.droppableId]; if (!dropEntity) { @@ -57,26 +19,26 @@ export default function newEntitiesFromDrop({ dropResult, components }) { return null; } + if (!dragType) { + console.warn('Drag type not found for id', draggableId); + return null; + } + const dropType = dropEntity.type; - let newDropChild = entityFactory(dragType); - const isValidDrop = isValidChild({ parentType: dropType, childType: dragType }); + let newDropChild = newComponentFactory(dragType); + const wrapChildInRow = shouldWrapChildInRow({ parentType: dropType, childType: dragType }); const newEntities = { [newDropChild.id]: newDropChild, }; - if (!isValidDrop) { - console.log('wrapping', dragType, 'in row'); - if (!isValidChild({ parentType: dropType, childType: ROW_TYPE })) { - console.warn('wrapping in an invalid component'); - } - - const rowWrapper = entityFactory(ROW_TYPE); + if (wrapChildInRow) { + const rowWrapper = newComponentFactory(ROW_TYPE); rowWrapper.children = [newDropChild.id]; newEntities[rowWrapper.id] = rowWrapper; newDropChild = rowWrapper; - } else if (dragType === TABS_TYPE) { - const tabChild = entityFactory(TAB_TYPE); + } else if (dragType === TABS_TYPE) { // create a new tab component + const tabChild = newComponentFactory(TAB_TYPE); newDropChild.children = [tabChild.id]; newEntities[tabChild.id] = tabChild; } diff --git a/superset/assets/javascripts/dashboard/v2/util/propShapes.jsx b/superset/assets/javascripts/dashboard/v2/util/propShapes.jsx index 7c0d3ef46b4f5..be84965957163 100644 --- a/superset/assets/javascripts/dashboard/v2/util/propShapes.jsx +++ b/superset/assets/javascripts/dashboard/v2/util/propShapes.jsx @@ -8,7 +8,7 @@ export const componentShape = PropTypes.shape({ // eslint-disable-line type: PropTypes.oneOf( Object.values(componentTypes), ).isRequired, - children: PropTypes.arrayOf(PropTypes.string).isRequired, + children: PropTypes.arrayOf(PropTypes.string), meta: PropTypes.shape({ // Dimensions width: PropTypes.number, diff --git a/superset/assets/javascripts/dashboard/v2/util/shouldWrapChildInRow.js b/superset/assets/javascripts/dashboard/v2/util/shouldWrapChildInRow.js new file mode 100644 index 0000000000000..487e247808e19 --- /dev/null +++ b/superset/assets/javascripts/dashboard/v2/util/shouldWrapChildInRow.js @@ -0,0 +1,30 @@ +import { + GRID_ROOT_TYPE, + CHART_TYPE, + COLUMN_TYPE, + MARKDOWN_TYPE, + TAB_TYPE, +} from './componentTypes'; + +const typeToWrapChildLookup = { + [GRID_ROOT_TYPE]: { + [CHART_TYPE]: true, + [COLUMN_TYPE]: true, + [MARKDOWN_TYPE]: true, + }, + + [TAB_TYPE]: { + [CHART_TYPE]: true, + [COLUMN_TYPE]: true, + [MARKDOWN_TYPE]: true, + }, +}; + +export default function shouldWrapChildInRow({ parentType, childType }) { + if (!parentType || !childType) return false; + + const wrapChildLookup = typeToWrapChildLookup[parentType]; + if (!wrapChildLookup) return false; + + return Boolean(wrapChildLookup[childType]); +}