diff --git a/superset/assets/images/favicon.png b/superset/assets/images/favicon.png index 55316fa7c5e58..f03cd5c7325e2 100644 Binary files a/superset/assets/images/favicon.png and b/superset/assets/images/favicon.png differ diff --git a/superset/assets/images/superset-logo@2x.png b/superset/assets/images/superset-logo@2x.png deleted file mode 100644 index 839f61798d5ee..0000000000000 Binary files a/superset/assets/images/superset-logo@2x.png and /dev/null differ diff --git a/superset/assets/javascripts/components/EditableTitle.jsx b/superset/assets/javascripts/components/EditableTitle.jsx index b773340846622..14976766975ac 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'; @@ -27,8 +28,10 @@ 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) { if (nextProps.title !== this.state.title) { this.setState({ @@ -37,8 +40,9 @@ class EditableTitle extends React.PureComponent { }); } } + handleClick() { - if (!this.props.canEdit) { + if (!this.props.canEdit || this.state.isEditing) { return; } @@ -46,6 +50,7 @@ class EditableTitle extends React.PureComponent { isEditing: true, }); } + handleBlur() { if (!this.props.canEdit) { return; @@ -67,9 +72,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; @@ -79,6 +106,7 @@ class EditableTitle extends React.PureComponent { title: ev.target.value, }); } + handleKeyPress(ev) { if (ev.key === 'Enter') { ev.preventDefault(); @@ -86,12 +114,14 @@ class EditableTitle extends React.PureComponent { this.handleBlur(); } } + render() { let input = ( {input} + + {input} + ); } } 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/.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/actions/index.js b/superset/assets/javascripts/dashboard/v2/actions/index.js new file mode 100644 index 0000000000000..005a77e5dccd4 --- /dev/null +++ b/superset/assets/javascripts/dashboard/v2/actions/index.js @@ -0,0 +1,69 @@ +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, parentId) { + return { + type: DELETE_COMPONENT, + payload: { + id, + parentId, + }, + }; +} + +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)); + + // new components don't have a source + } 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/BuilderComponentPane.jsx b/superset/assets/javascripts/dashboard/v2/components/BuilderComponentPane.jsx index 3938a51a5fec3..86f3788bae6ae 100644 --- a/superset/assets/javascripts/dashboard/v2/components/BuilderComponentPane.jsx +++ b/superset/assets/javascripts/dashboard/v2/components/BuilderComponentPane.jsx @@ -1,46 +1,34 @@ 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'; +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
- - {provided => ( -
- - - - - {provided.placeholder} -
- )} -
+ + + + + + + + +
); } 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..94069b79dafaa 100644 --- a/superset/assets/javascripts/dashboard/v2/components/DashboardBuilder.jsx +++ b/superset/assets/javascripts/dashboard/v2/components/DashboardBuilder.jsx @@ -1,109 +1,40 @@ import React from 'react'; import PropTypes from 'prop-types'; +import HTML5Backend from 'react-dnd-html5-backend'; +import { DragDropContext } from 'react-dnd'; +import cx from 'classnames'; -import { DragDropContext, Droppable, Draggable } from 'react-beautiful-dnd'; import BuilderComponentPane from './BuilderComponentPane'; -import { reorder, reorderRows } from '../util/dnd-reorder'; +import DashboardGrid from '../containers/DashboardGrid'; -import { DROPPABLE_DASHBOARD_ROOT, DRAGGABLE_ROW_TYPE } from '../util/constants'; +import './dnd/dnd.css'; const propTypes = { editMode: PropTypes.bool, }; +const defaultProps = { + editMode: true, +}; + class DashboardBuilder extends React.Component { constructor(props) { super(props); - this.state = { - rows: [], - entities: {}, - }; - - this.handleDragEnd = this.handleDragEnd.bind(this); - this.handleDragStart = this.handleDragStart.bind(this); - this.handleReorder = this.handleReorder.bind(this); - this.handleNewEntity = this.handleNewEntity.bind(this); - } - - handleDragEnd(dropResult) { - console.log('drag end', dropResult); - if (dropResult.destination) { - // if (isNewEntity(dropResult.draggableId)) { - // this.handleNewEntity(dropResult); - // } else { - // this.handleReorder(dropResult); - // } - } - } - - handleDragStart(obj) { - console.log('drag start', obj); - } - - handleNewEntity() { - - } - - 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 }; - // }); + // this component might control the state of the side pane etc. in the future + this.state = {}; } render() { return ( - -
-
- - {(provided, snapshot) => ( -
- {this.state.rows.map(id => this.renderRow(id))} - {provided.placeholder} -
- )} -
-
- -
-
+
+ + +
); } } 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 new file mode 100644 index 0000000000000..884fc899bf71c --- /dev/null +++ b/superset/assets/javascripts/dashboard/v2/components/DashboardGrid.jsx @@ -0,0 +1,169 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import ParentSize from '@vx/responsive/build/components/ParentSize'; +import cx from 'classnames'; + +import DragDroppable from './dnd/DragDroppable'; +import DashboardComponent from '../containers/DashboardComponent'; + +import { + DASHBOARD_ROOT_ID, + GRID_GUTTER_SIZE, + GRID_COLUMN_COUNT, +} from '../util/constants'; + +import './gridComponents/grid.css'; + +const propTypes = { + dashboard: PropTypes.object.isRequired, + updateComponents: PropTypes.func.isRequired, + handleComponentDrop: PropTypes.func.isRequired, +}; + +const defaultProps = { +}; + +class DashboardGrid extends React.PureComponent { + constructor(props) { + super(props); + this.state = { + isResizing: false, + rowGuideTop: 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); + } + + getRowGuidePosition(resizeRef) { + if (resizeRef && this.grid) { + return resizeRef.getBoundingClientRect().bottom - this.grid.getBoundingClientRect().top - 1; + } + return null; + } + + handleResizeStart({ ref, direction }) { + let rowGuideTop = null; + if (direction === 'bottom' || direction === 'bottomRight') { + rowGuideTop = this.getRowGuidePosition(ref); + } + + this.setState(() => ({ + isResizing: true, + rowGuideTop, + })); + } + + handleResize({ ref, direction }) { + if (direction === 'bottom' || direction === 'bottomRight') { + this.setState(() => ({ rowGuideTop: this.getRowGuidePosition(ref) })); + } + } + + handleResizeStop({ id, widthMultiple, heightMultiple }) { + const { dashboard: components, updateComponents } = this.props; + const component = components[id]; + if ( + component && + (component.meta.width !== widthMultiple || component.meta.height !== heightMultiple) + ) { + updateComponents({ + [id]: { + ...component, + meta: { + ...component.meta, + width: widthMultiple || component.meta.width, + height: heightMultiple || component.meta.height, + }, + }, + }); + } + this.setState(() => ({ + isResizing: false, + rowGuideTop: null, + })); + } + + render() { + const { dashboard: components, handleComponentDrop } = this.props; + const { isResizing, rowGuideTop } = this.state; + const rootComponent = components[DASHBOARD_ROOT_ID]; + + return ( +
{ this.grid = ref; }} + className={cx('grid-container', isResizing && 'grid-container--resizing')} + > + + {({ 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 : ( +
+ {(rootComponent.children || []).map((id, index) => ( + + ))} + + {rootComponent.children.length === 0 && + + {({ dropIndicatorProps }) => ( +
+ {dropIndicatorProps &&
} +
+ )} + } + + {isResizing && Array(GRID_COLUMN_COUNT).fill(null).map((_, i) => ( +
+ ))} + + {isResizing && rowGuideTop && +
} +
+ ); + }} + +
+ ); + } +} + +DashboardGrid.propTypes = propTypes; +DashboardGrid.defaultProps = defaultProps; + +export default DashboardGrid; diff --git a/superset/assets/javascripts/dashboard/v2/components/Header.jsx b/superset/assets/javascripts/dashboard/v2/components/DashboardHeader.jsx similarity index 100% rename from superset/assets/javascripts/dashboard/v2/components/Header.jsx rename to superset/assets/javascripts/dashboard/v2/components/DashboardHeader.jsx 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..18efff43ac4f9 --- /dev/null +++ b/superset/assets/javascripts/dashboard/v2/components/DeleteComponentButton.jsx @@ -0,0 +1,23 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +import IconButton from './IconButton'; + +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/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/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/DragDroppable.jsx b/superset/assets/javascripts/dashboard/v2/components/dnd/DragDroppable.jsx new file mode 100644 index 0000000000000..ba87e5709ea19 --- /dev/null +++ b/superset/assets/javascripts/dashboard/v2/components/dnd/DragDroppable.jsx @@ -0,0 +1,121 @@ +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 handleHover from './handleHover'; +import handleDrop from './handleDrop'; +import { dragConfig, dropConfig } from './dragDroppableConfig'; +import { componentShape } from '../../util/propShapes'; + +const HOVER_THROTTLE_MS = 200; + +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, + + // from react-dnd + isDragging: PropTypes.bool.isRequired, + isDraggingOver: PropTypes.bool.isRequired, + isDraggingOverShallow: PropTypes.bool.isRequired, + droppableRef: PropTypes.func.isRequired, + dragSourceRef: PropTypes.func.isRequired, + dragPreviewRef: PropTypes.func.isRequired, + + // from redux (@TODO) + handleHover: PropTypes.func, + handleDrop: PropTypes.func, + onDrop: PropTypes.func, // @TODO rename broadcastDrop? + isValidChild: PropTypes.func, + isValidSibling: PropTypes.func, +}; + +const defaultProps = { + disableDragDrop: false, + children() {}, + onDrop() {}, + parentId: null, + orientation: 'row', + handleDrop, + handleHover, + isValidChild, + isValidSibling({ parentType, siblingType: childType }) { + return isValidChild({ parentType, childType }); + }, +}; + +class DragDroppable extends React.Component { + constructor(props) { + super(props); + this.state = { + dropIndicator: null, + }; + this.handleHover = throttle(this.hover.bind(this), HOVER_THROTTLE_MS).bind(this); + } + + componentDidMount() { + this.mounted = true; + } + + componentWillUnmount() { + this.mounted = false; + } + + hover(props, monitor, Component) { + if (this.mounted) this.props.handleHover(props, monitor, Component); + } + + render() { + const { + children, + orientation, + droppableRef, + dragSourceRef, + dragPreviewRef, + isDragging, + isDraggingOver, + } = this.props; + + const { dropIndicator } = this.state; + + return ( +
{ + this.ref = ref; + dragPreviewRef(ref); + droppableRef(ref); + }} + className={cx( + 'dragdroppable', + orientation === 'row' && 'dragdroppable-row', + orientation === 'column' && 'dragdroppable-column', + isDragging && 'dragdroppable--dragging', + )} + > + {children({ + dragSourceRef, + dropIndicatorProps: isDraggingOver && dropIndicator && { + className: 'drop-indicator', + style: dropIndicator, + }, + })} +
+ ); + } +} + +DragDroppable.propTypes = propTypes; +DragDroppable.defaultProps = defaultProps; + +// note that the composition order here determines using +// 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 new file mode 100644 index 0000000000000..36d1e6beff521 --- /dev/null +++ b/superset/assets/javascripts/dashboard/v2/components/dnd/DragHandle.jsx @@ -0,0 +1,38 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +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, dotCount } = this.props; + return ( +
+ {Array(dotCount).fill(null).map((_, i) => ( +
+ ))} +
+ ); + } +} + +DragHandle.propTypes = propTypes; +DragHandle.defaultProps = defaultProps; diff --git a/superset/assets/javascripts/dashboard/v2/components/dnd/DraggableNewChart.jsx b/superset/assets/javascripts/dashboard/v2/components/dnd/DraggableNewChart.jsx deleted file mode 100644 index 63171557ce5f3..0000000000000 --- a/superset/assets/javascripts/dashboard/v2/components/dnd/DraggableNewChart.jsx +++ /dev/null @@ -1,18 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; - -import { DRAGGABLE_NEW_CHART } from '../../util/constants'; -import DraggableNewComponent from './DraggableNewComponent'; - -const propTypes = { - index: PropTypes.number.isRequired, -}; - -export default class DraggableNewChart extends React.Component { - render() { - const { index } = this.props; - return ; - } -} - -DraggableNewChart.propTypes = propTypes; 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 a7a4b1caddb34..0000000000000 --- a/superset/assets/javascripts/dashboard/v2/components/dnd/DraggableNewComponent.jsx +++ /dev/null @@ -1,50 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import cx from 'classnames'; -import { Draggable } from 'react-beautiful-dnd'; - -const propTypes = { - id: PropTypes.string.isRequired, - label: PropTypes.string.isRequired, - index: PropTypes.number.isRequired, - draggableType: 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}`); - - return ( - - {(provided, snapshot) => ( -
-
-
-
- {label} -
-
- {provided.placeholder} -
- )} - - ); - } -} - -DraggableNewComponent.propTypes = propTypes; diff --git a/superset/assets/javascripts/dashboard/v2/components/dnd/DraggableNewDivider.jsx b/superset/assets/javascripts/dashboard/v2/components/dnd/DraggableNewDivider.jsx deleted file mode 100644 index 1aaf31bd98708..0000000000000 --- a/superset/assets/javascripts/dashboard/v2/components/dnd/DraggableNewDivider.jsx +++ /dev/null @@ -1,20 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; - -import { DRAGGABLE_NEW_DIVIDER } from '../../util/constants'; -import DraggableNewComponent from './DraggableNewComponent'; - -const propTypes = { - index: PropTypes.number.isRequired, -}; - -export default class DraggableNewDivider extends React.Component { - render() { - const { index } = this.props; - return ( - - ); - } -} - -DraggableNewDivider.propTypes = propTypes; diff --git a/superset/assets/javascripts/dashboard/v2/components/dnd/DraggableNewHeader.jsx b/superset/assets/javascripts/dashboard/v2/components/dnd/DraggableNewHeader.jsx deleted file mode 100644 index 004693ecc8eb5..0000000000000 --- a/superset/assets/javascripts/dashboard/v2/components/dnd/DraggableNewHeader.jsx +++ /dev/null @@ -1,20 +0,0 @@ -import React from 'react'; -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 ( - - ); - } -} - -DraggableNewHeader.propTypes = propTypes; diff --git a/superset/assets/javascripts/dashboard/v2/components/dnd/DraggableNewRow.jsx b/superset/assets/javascripts/dashboard/v2/components/dnd/DraggableNewRow.jsx deleted file mode 100644 index dda588ec810d0..0000000000000 --- a/superset/assets/javascripts/dashboard/v2/components/dnd/DraggableNewRow.jsx +++ /dev/null @@ -1,25 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; - -import { DRAGGABLE_NEW_ROW, DRAGGABLE_ROW_TYPE } from '../../util/constants'; -import DraggableNewComponent from './DraggableNewComponent'; - -const propTypes = { - index: PropTypes.number.isRequired, -}; - -export default class DraggableNewRow extends React.Component { - render() { - const { index } = this.props; - return ( - - ); - } -} - -DraggableNewRow.propTypes = propTypes; 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..fb010e08a82bc --- /dev/null +++ b/superset/assets/javascripts/dashboard/v2/components/dnd/dnd.css @@ -0,0 +1,60 @@ +.dragdroppable { + position: relative; +} + +.dragdroppable--dragging { + opacity: 0.25; +} + +.dragdroppable-row { + width: 100%; +} + +.grid-container .dragdroppable-row:after, +.grid-container .dragdroppable-column:after { + border: 1px dashed transparent; + content: ""; + position: absolute; + width: 100%; + height: 100%; + top: 1px; + left: 0; + z-index: 1; + pointer-events: none; +} + + .grid-container .dragdroppable-row:hover:after, + .grid-container .dragdroppable-column:hover:after { + border: 1px dashed #aaa; + } + +/* Drag handle */ +.drag-handle { + overflow: hidden; + width: 16px; + cursor: move; +} + +.drag-handle--left { + width: 8px; +} + +.drag-handle--top { + /*margin: 10px auto;*/ +} + +.drag-handle-dot { + float: left; + height: 2px; + margin: 1px; + width: 2px +} + +.drag-handle-dot:after { + content: ""; + background: #aaa; + float: left; + height: 2px; + margin: -1px; + width: 2px; +} 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..6eb37e90f393e --- /dev/null +++ b/superset/assets/javascripts/dashboard/v2/components/dnd/dragDroppableConfig.js @@ -0,0 +1,59 @@ +// note: the 'type' hook is not useful for us as dropping is contigent on other properties +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 }; + }, + }, + function dragStateToProps(connect, monitor) { + return { + dragSourceRef: connect.dragSource(), + dragPreviewRef: connect.dragPreview(), + isDragging: monitor.isDragging(), + }; + }, +]; + +export const dropConfig = [ + TYPE, + { + hover(props, monitor, component) { + if ( + component + && component.decoratedComponentInstance + && component.decoratedComponentInstance.handleHover + ) { // use the component instance so we can throttle calls + component.decoratedComponentInstance.handleHover( + props, + monitor, + component.decoratedComponentInstance, + ); + } + }, + // note: + // 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) { + const Component = component.decoratedComponentInstance; + const dropResult = monitor.getDropResult(); + if ((!dropResult || !dropResult.destination) && Component.props.handleDrop) { + return Component.props.handleDrop(props, monitor, Component); + } + 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/dnd/handleDrop.js b/superset/assets/javascripts/dashboard/v2/components/dnd/handleDrop.js new file mode 100644 index 0000000000000..de7ea854abc72 --- /dev/null +++ b/superset/assets/javascripts/dashboard/v2/components/dnd/handleDrop.js @@ -0,0 +1,83 @@ +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 + // 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 = + 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; + } + } + + 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..80d0ef9a6a14b --- /dev/null +++ b/superset/assets/javascripts/dashboard/v2/components/dnd/handleHover.js @@ -0,0 +1,82 @@ +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 })); + 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 === 'row' ? 'column' : 'row'; + + Component.setState(() => ({ + dropIndicator: { + top: 0, + right: component.children.length ? 8 : null, + 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', + 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 === 'row') { + 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 === '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 new file mode 100644 index 0000000000000..a8a90f65fa1bf --- /dev/null +++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Chart.jsx @@ -0,0 +1,130 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +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 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, + 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 + deleteComponent: PropTypes.func.isRequired, + handleComponentDrop: PropTypes.func.isRequired, +}; + +const defaultProps = { + rowHeight: null, +}; + +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, + index, + depth, + parentId, + availableColumnCount, + columnWidth, + rowHeight, + onResizeStart, + onResize, + onResizeStop, + handleComponentDrop, + } = this.props; + console.log('chart depth', depth) + return ( + + {({ dropIndicatorProps, dragSourceRef }) => ( + + + + + + , + ]} + > +
+
+
+ + {dropIndicatorProps &&
} + + + )} + + ); + } +} + +Chart.propTypes = propTypes; +Chart.defaultProps = defaultProps; + +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..c38a4618a891b --- /dev/null +++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Column.jsx @@ -0,0 +1,145 @@ +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 HoverMenu from '../menu/HoverMenu'; +import ResizableContainer from '../resizable/ResizableContainer'; +import { componentShape } from '../../util/propShapes'; + +import { GRID_GUTTER_SIZE, GRID_MIN_COLUMN_COUNT } 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, + columnWidth: PropTypes.number.isRequired, + onResizeStart: PropTypes.func.isRequired, + onResize: PropTypes.func.isRequired, + onResizeStop: PropTypes.func.isRequired, + + // dnd + deleteComponent: PropTypes.func.isRequired, + handleComponentDrop: PropTypes.func.isRequired, +}; + +const defaultProps = { + rowHeight: null, +}; + +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, + components, + index, + parentId, + availableColumnCount, + columnWidth, + rowHeight, + depth, + onResizeStart, + onResize, + onResizeStop, + handleComponentDrop, + } = this.props; + + const columnItems = []; + + (columnComponent.children || []).forEach((id, childIndex) => { + const component = components[id]; + columnItems.push(component); + if (childIndex < columnComponent.children.length - 1) { + columnItems.push(`gutter-${childIndex}`); + } + }); + + return ( + + {({ dropIndicatorProps, dragSourceRef }) => ( + +
+ + + + + + {columnItems.map((component, itemIndex) => { + if (!component.id) { + return
; + } + + return ( + + ); + })} + {dropIndicatorProps &&
} +
+ + )} + + + ); + } +} + +Column.propTypes = propTypes; +Column.defaultProps = defaultProps; + +export default Column; diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Divider.jsx b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Divider.jsx new file mode 100644 index 0000000000000..4aeee058cc7e4 --- /dev/null +++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Divider.jsx @@ -0,0 +1,65 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +import DragDroppable from '../dnd/DragDroppable'; +import HoverMenu from '../menu/HoverMenu'; +import DeleteComponentButton from '../DeleteComponentButton'; +import { componentShape } from '../../util/propShapes'; + +const propTypes = { + component: componentShape.isRequired, + components: PropTypes.object.isRequired, + 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, + components, + index, + parentId, + handleComponentDrop, + } = 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 new file mode 100644 index 0000000000000..4826688d3848d --- /dev/null +++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Header.jsx @@ -0,0 +1,150 @@ +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 RowStyleDropdown from '../menu/RowStyleDropdown'; +import DeleteComponentButton from '../DeleteComponentButton'; +import PopoverDropdown from '../menu/PopoverDropdown'; +import headerStyleOptions from '../../util/headerStyleOptions'; +import rowStyleOptions from '../../util/rowStyleOptions'; +import { componentShape } from '../../util/propShapes'; +import { SMALL_HEADER, ROW_TRANSPARENT } from '../../util/constants'; + +const propTypes = { + component: componentShape.isRequired, + components: PropTypes.object.isRequired, + index: PropTypes.number.isRequired, + parentId: PropTypes.string.isRequired, + handleComponentDrop: PropTypes.func.isRequired, + deleteComponent: PropTypes.func.isRequired, + updateComponents: PropTypes.func.isRequired, +}; + +const defaultProps = { +}; + +class Header extends React.PureComponent { + constructor(props) { + super(props); + this.state = { + isFocused: false, + }; + this.handleDeleteComponent = this.handleDeleteComponent.bind(this); + this.handleChangeFocus = this.handleChangeFocus.bind(this); + this.handleUpdateMeta = this.handleUpdateMeta.bind(this); + this.handleChangeSize = this.handleUpdateMeta.bind(this, 'headerSize'); + this.handleChangeRowStyle = this.handleUpdateMeta.bind(this, 'rowStyle'); + this.handleChangeText = this.handleUpdateMeta.bind(this, 'text'); + } + + handleChangeFocus(nextFocus) { + this.setState(() => ({ isFocused: nextFocus })); + } + + handleUpdateMeta(metaKey, nextValue) { + const { updateComponents, component } = this.props; + if (nextValue && component.meta[metaKey] !== nextValue) { + updateComponents({ + [component.id]: { + ...component, + meta: { + ...component.meta, + [metaKey]: nextValue, + }, + }, + }); + } + } + + handleDeleteComponent() { + const { deleteComponent, component, parentId } = this.props; + deleteComponent(component.id, parentId); + } + + render() { + const { isFocused } = this.state; + + const { + component, + components, + index, + parentId, + handleComponentDrop, + } = this.props; + + const headerStyle = headerStyleOptions.find( + opt => opt.value === (component.meta.headerSize || SMALL_HEADER), + ); + + const rowStyle = rowStyleOptions.find( + opt => opt.value === (component.meta.rowStyle || ROW_TRANSPARENT), + ); + + return ( + + {({ dropIndicatorProps, dragSourceRef }) => ( +
+ + + + + `${option.label} header`} + />, + , + , + ]} + > +
+ +
+
+ + {dropIndicatorProps &&
} +
+ )} + + ); + } +} + +Header.propTypes = propTypes; +Header.defaultProps = defaultProps; + +export default Header; diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Row.jsx b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Row.jsx new file mode 100644 index 0000000000000..3743534d9b24e --- /dev/null +++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Row.jsx @@ -0,0 +1,188 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import cx from 'classnames'; + +import DragDroppable from '../dnd/DragDroppable'; +import DragHandle from '../dnd/DragHandle'; +import DashboardComponent from '../../containers/DashboardComponent'; +import DeleteComponentButton from '../DeleteComponentButton'; +import HoverMenu from '../menu/HoverMenu'; +import IconButton from '../IconButton'; +import RowStyleDropdown from '../menu/RowStyleDropdown'; +import WithPopoverMenu from '../menu/WithPopoverMenu'; + +import { componentShape } from '../../util/propShapes'; +import rowStyleOptions from '../../util/rowStyleOptions'; +import { GRID_GUTTER_SIZE, ROW_TRANSPARENT } from '../../util/constants'; + +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 + handleComponentDrop: PropTypes.func.isRequired, + deleteComponent: PropTypes.func.isRequired, + updateComponents: PropTypes.func.isRequired, +}; + +const defaultProps = { + rowHeight: null, +}; + +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) { + const { updateComponents, component } = this.props; + if (nextValue && component.meta[metaKey] !== nextValue) { + updateComponents({ + [component.id]: { + ...component, + meta: { + ...component.meta, + [metaKey]: nextValue, + }, + }, + }); + } + } + + handleDeleteComponent() { + const { deleteComponent, component, parentId } = this.props; + deleteComponent(component.id, parentId); + } + + render() { + const { + component: rowComponent, + components, + index, + parentId, + availableColumnCount, + columnWidth, + depth, + onResizeStart, + onResize, + onResizeStop, + handleComponentDrop, + } = this.props; + + let occupiedColumnCount = 0; + let rowHeight = 0; // row items without height require this + const rowItems = []; + + // this adds a gutter between each child in the row. + (rowComponent.children || []).forEach((id, childIndex) => { + const component = components[id]; + occupiedColumnCount += (component.meta || {}).width || 0; + rowItems.push(component); + if (childIndex < rowComponent.children.length - 1) { + rowItems.push(`gutter-${childIndex}`); + } + if ((component.meta || {}).height) { + rowHeight = Math.max(rowHeight, component.meta.height); + } + }); + + const rowStyle = rowStyleOptions.find( + opt => opt.value === (rowComponent.meta.rowStyle || ROW_TRANSPARENT), + ); + + return ( + + {({ dropIndicatorProps, dragSourceRef }) => ( + , + ]} + > + +
+ + + + + + + {rowItems.map((component, itemIndex) => { + if (!component.id) { + return
; + } + + return ( + + ); + })} + + {dropIndicatorProps &&
} +
+ + )} + + ); + } +} + +Row.propTypes = propTypes; +Row.defaultProps = defaultProps; + +export default 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..80d85e622b9be --- /dev/null +++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Spacer.jsx @@ -0,0 +1,114 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +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, + 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 + deleteComponent: PropTypes.func.isRequired, + handleComponentDrop: PropTypes.func.isRequired, +}; + +const defaultProps = { + rowHeight: null, +}; + +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, + components, + index, + depth, + parentId, + availableColumnCount, + columnWidth, + rowHeight, + onResizeStart, + onResize, + onResizeStop, + handleComponentDrop, + } = this.props; + + const orientation = depth % 2 === 0 ? 'row' : 'column'; + const hoverMenuPosition = orientation === 'row' ? 'left' : 'top'; + const adjustableWidth = orientation === 'column'; + const adjustableHeight = orientation === 'row'; + + return ( + + {({ dropIndicatorProps, dragSourceRef }) => ( + + + + + +
+ + {dropIndicatorProps &&
} + + )} + + ); + } +} + +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..d2fbd946be6ed --- /dev/null +++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Tabs.jsx @@ -0,0 +1,281 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +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'; + +const NEW_TAB_INDEX = -1; +const MAX_TAB_COUNT = 5; + +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, + onResizeStart: PropTypes.func.isRequired, + onResize: PropTypes.func.isRequired, + onResizeStop: PropTypes.func.isRequired, + + // dnd + createComponent: PropTypes.func.isRequired, + handleComponentDrop: PropTypes.func.isRequired, + onChangeTab: PropTypes.func, + deleteComponent: PropTypes.func.isRequired, + updateComponents: PropTypes.func.isRequired, +}; + +const defaultProps = { + onChangeTab: null, + children: null, +}; + +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) { + this.setState(() => ({ tabIndex: maxIndex })); + } + } + + handleClicKTab(tabIndex) { + const { onChangeTab, component, createComponent } = this.props; + const { focusedId } = this.state; + + if (!focusedId && tabIndex !== NEW_TAB_INDEX && tabIndex !== this.state.tabIndex) { + this.setState(() => ({ tabIndex })); + if (onChangeTab) { + onChangeTab({ tabIndex, tab: component.children[tabIndex] }); + } + } else if (!focusedId && tabIndex === NEW_TAB_INDEX) { + createComponent({ + destination: { + droppableId: component.id, + index: component.children.length, + }, + draggableId: TAB_TYPE, + }); + } + } + + 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; + if (isTabsComponent || component.children.length > 1) { + 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, + component: tabsComponent, + components, + parentId, + index, + availableColumnCount, + columnWidth, + onResizeStart, + onResize, + onResizeStop, + handleComponentDrop, + } = this.props; + + const { tabIndex: selectedTabIndex, focusedId } = this.state; + const { children: tabIds } = tabsComponent; + + return ( + + {({ dropIndicatorProps: tabsDropIndicatorProps, dragSourceRef: tabsDragSourceRef }) => ( +
+ + + { + this.handleDeleteComponent(tabsComponent.id); + }} + /> + + + + {tabIds.map((tabId, tabIndex) => { + const tabComponent = components[tabId]; + 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 + + {({ dropIndicatorProps, dragSourceRef }) => ( +
+ { + this.handleChangeFocus(nextFocus && tabId); + }} + menuItems={[ + { + this.handleDeleteComponent(tabId); + }} + />, + ]} + > + { + this.handleChangeText({ id: tabId, nextTabText }); + }} + showTooltip={false} + /> + + + {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) => ( + + ))} +
} + + ); + })} + + {tabIds.length < MAX_TAB_COUNT && + } + />} + + + {tabsDropIndicatorProps + && tabsDropIndicatorProps.style + && tabsDropIndicatorProps.style.width === '100%' + &&
} + +
+ )} + + ); + } +} + +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..a88ea0991abbe --- /dev/null +++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/components.css @@ -0,0 +1,455 @@ +/* Header */ +.dashboard-component-header { + width: 100%; + 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; + padding-right: 16px; + } + +/* Chart */ +.dashboard-component-chart { + width: 100%; + height: 100%; + color: #879399; + background-color: #fff; + padding: 16px; + display: flex; + align-items: center; + 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 { + 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: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 { + cursor: move; +} + +/* Tabs -- this overwrites Superset bootstrap theme tab styling */ +.dashboard-component-tabs { + width: 100%; + background-color: white; +} +.dashboard-component-tabs .dashboard-component-tabs-content { + min-height: 48px; + margin-top: 1px; +} + +.dashboard-component-tabs .nav-tabs { + border-bottom: none; +} + +/* by moving padding from to
  • we can restrict the selected tab indicator to text width */ +.dashboard-component-tabs .nav-tabs > li { + padding: 0 16px; +} + +.dashboard-component-tabs .nav-tabs > li > a { + color: #263238; + border: none; + padding: 12px 0 14px 0; +} + +.dashboard-component-tabs .nav-tabs > li.active > a { + border: none; +} + +.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); +} + +.dashboard-component-tabs .nav-tabs > li > a:hover { + border: none; + background: inherit; + 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; + opacity: 0.5; +} + +.dashboard-component-tabs .fa-plus-square { + background: linear-gradient(135deg, #E32464, #2C2261); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + display: initial; + font-size: 16px; +} + +/* New components */ +.new-component { + display: flex; + flex-direction: row; + flex-wrap: nowrap; + align-items: center; + padding: 16px; + background: white; +} + +.new-component-placeholder { + position: relative; + background: #f5f5f5; + width: 40px; + 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 */ +.grid-container { + flex-grow: 1; + min-width: 66%; + margin: 24px 32px; + height: 100%; + position: relative; +} + +.new-component-placeholder.spacer-placeholder { + font-size: 1em; +} + +.new-component-placeholder.fa-window-restore { + 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; + flex-wrap: wrap; + align-items: flex-start; + width: 100%; + height: fit-content; + background-color: transparent; +} + +.grid-row--transparent { + background-color: transparent; +} + +.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 */ + height: 80px; +} + +.grid-row--empty:after { + position: absolute; + top: 0; + left: 0; + content: "Empty row"; + display: flex; + align-items: center; + justify-content: center; + width: 100%; + height: 100%; + color: #aaa; +} + +.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; + 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; +} + +/* popover menu */ +.with-popover-menu { + position: relative; + outline: none; +} + +.grid-row.grid-row--empty .with-popover-menu { /* drop indicator doesn't show up without this */ + width: 100%; + height: 100%; +} + +.with-popover-menu--focused:after { + content: ""; + position: absolute; + top: 1; + left: -1; + width: 100%; + height: 100%; + box-shadow: inset 0 0 0 2px #44C0FF; + pointer-events: none; + z-index: 9; +} + +.popover-menu { + position: absolute; + display: flex; + flex-direction: row; + align-items: center; + flex-wrap: nowrap; + left: 1px; + top: -42px; + height: 40px; + padding: 0 16px; + background: #fff; + box-shadow: 0 1px 2px 1px rgba(0, 0, 0, 0.2); + font-size: 14px; + cursor: default; + z-index: 10; +} + +.popover-menu .menu-item { + display: flex; + flex-direction: row; + align-items: center; +} + +/* vertical spacer after each menu item */ +.popover-menu .menu-item:not(:only-child):not(:last-child):after { + content: ""; + width: 1; + height: 100%; + background: #CFD8DC; + margin: 0 16px; +} + +.popover-menu .popover-dropdown.btn { + border: none; + padding: 0; + font-size: inherit; + color: #000; +} + +.popover-menu .popover-dropdown.btn:hover, +.popover-menu .popover-dropdown.btn:active, +.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; +} + +.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; + font-weight: bold; + color: #000; +} + +/* row style menu */ +.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; +} + +/* hover menu */ +.hover-menu { + opacity: 0; + position: absolute; + z-index: 2; +} + +.hover-menu--left { + width: 20px; + height: 100%; + top: 0; + left: -20px; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; +} + +.hover-menu--left > :nth-child(n):not(:only-child):not(:last-child) { + margin-bottom: 8px; +} + +.dragdroppable-row .dragdroppable-row .hover-menu--left { + left: 1px; +} + +.hover-menu--top { + width: 100%; + 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, +.dragdroppable .hover-menu:hover { + opacity: 1; +} + + +/* Menu fa buttons */ +.icon-button { + color: #879399; + font-size: 1em; + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; + outline: none; +} + +.icon-button:hover, +.icon-button:active, +.icon-button:focus { + color: #484848; + outline: none; + text-decoration: none; +} 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..6119eabefcbea --- /dev/null +++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/grid.css @@ -0,0 +1,17 @@ +/* Editing guides */ +.grid-column-guide { + position: absolute; + top: 0; + height: 100%; + 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 { + 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 new file mode 100644 index 0000000000000..1d1e4b3b752ad --- /dev/null +++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/index.js @@ -0,0 +1,39 @@ +import './components.css'; + +import { + CHART_TYPE, + COLUMN_TYPE, + DIVIDER_TYPE, + HEADER_TYPE, + INVISIBLE_ROW_TYPE, + ROW_TYPE, + SPACER_TYPE, + TABS_TYPE, +} from '../../util/componentTypes'; + +import Chart from './Chart'; +import Column from './Column'; +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'; +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 default { + [CHART_TYPE]: Chart, + [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/gridComponents/new/DraggableNewComponent.jsx b/superset/assets/javascripts/dashboard/v2/components/gridComponents/new/DraggableNewComponent.jsx new file mode 100644 index 0000000000000..10a157fc2886c --- /dev/null +++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/new/DraggableNewComponent.jsx @@ -0,0 +1,39 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import cx from 'classnames'; + +import DragDroppable from '../../dnd/DragDroppable'; + +const propTypes = { + id: PropTypes.string.isRequired, + type: PropTypes.string.isRequired, + label: PropTypes.string.isRequired, + className: PropTypes.string, +}; + +const defaultProps = { + className: null, +}; + +export default class DraggableNewComponent extends React.PureComponent { + render() { + const { label, id, type, className } = this.props; + return ( + + {({ dragSourceRef }) => ( +
    +
    + {label} +
    + )} + + ); + } +} + +DraggableNewComponent.propTypes = propTypes; +DraggableNewComponent.defaultProps = defaultProps; diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/new/NewChart.jsx b/superset/assets/javascripts/dashboard/v2/components/gridComponents/new/NewChart.jsx new file mode 100644 index 0000000000000..0255755a888e4 --- /dev/null +++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/new/NewChart.jsx @@ -0,0 +1,24 @@ +import React from 'react'; +// import PropTypes from 'prop-types'; + +import { CHART_TYPE } from '../../../util/componentTypes'; +import { NEW_CHART_ID } from '../../../util/constants'; +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/NewColumn.jsx b/superset/assets/javascripts/dashboard/v2/components/gridComponents/new/NewColumn.jsx new file mode 100644 index 0000000000000..654c60bd45a21 --- /dev/null +++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/new/NewColumn.jsx @@ -0,0 +1,24 @@ +import React from 'react'; +// import PropTypes from 'prop-types'; + +import { COLUMN_TYPE } from '../../../util/componentTypes'; +import { NEW_COLUMN_ID } from '../../../util/constants'; +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/gridComponents/new/NewDivider.jsx b/superset/assets/javascripts/dashboard/v2/components/gridComponents/new/NewDivider.jsx new file mode 100644 index 0000000000000..5d70041a5d4da --- /dev/null +++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/new/NewDivider.jsx @@ -0,0 +1,24 @@ +import React from 'react'; +// import PropTypes from 'prop-types'; + +import { DIVIDER_TYPE } from '../../../util/componentTypes'; +import { NEW_DIVIDER_ID } from '../../../util/constants'; +import DraggableNewComponent from './DraggableNewComponent'; + +const propTypes = { +}; + +export default class DraggableNewDivider extends React.PureComponent { + render() { + return ( + + ); + } +} + +DraggableNewDivider.propTypes = propTypes; diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/new/NewHeader.jsx b/superset/assets/javascripts/dashboard/v2/components/gridComponents/new/NewHeader.jsx new file mode 100644 index 0000000000000..d207a9c9c8fc0 --- /dev/null +++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/new/NewHeader.jsx @@ -0,0 +1,24 @@ +import React from 'react'; +// import PropTypes from 'prop-types'; + +import { HEADER_TYPE } from '../../../util/componentTypes'; +import { NEW_HEADER_ID } from '../../../util/constants'; +import DraggableNewComponent from './DraggableNewComponent'; + +const propTypes = { +}; + +export default class DraggableNewHeader extends React.Component { + render() { + return ( + + ); + } +} + +DraggableNewHeader.propTypes = propTypes; diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/new/NewRow.jsx b/superset/assets/javascripts/dashboard/v2/components/gridComponents/new/NewRow.jsx new file mode 100644 index 0000000000000..1d9ab103a98ec --- /dev/null +++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/new/NewRow.jsx @@ -0,0 +1,23 @@ +import React from 'react'; + +import { ROW_TYPE } from '../../../util/componentTypes'; +import { NEW_ROW_ID } from '../../../util/constants'; +import DraggableNewComponent from './DraggableNewComponent'; + +const propTypes = { +}; + +export default class DraggableNewRow extends React.PureComponent { + render() { + return ( + + ); + } +} + +DraggableNewRow.propTypes = 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..7287770b275e2 --- /dev/null +++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/new/NewSpacer.jsx @@ -0,0 +1,24 @@ +import React from 'react'; +// import PropTypes from 'prop-types'; + +import { SPACER_TYPE } from '../../../util/componentTypes'; +import { NEW_SPACER_ID } from '../../../util/constants'; +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..a473281984ceb --- /dev/null +++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/new/NewTabs.jsx @@ -0,0 +1,24 @@ +import React from 'react'; +// import PropTypes from 'prop-types'; + +import { TABS_TYPE } from '../../../util/componentTypes'; +import { NEW_TABS_ID } from '../../../util/constants'; +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/menu/HoverMenu.jsx b/superset/assets/javascripts/dashboard/v2/components/menu/HoverMenu.jsx new file mode 100644 index 0000000000000..c238d023d8dd1 --- /dev/null +++ b/superset/assets/javascripts/dashboard/v2/components/menu/HoverMenu.jsx @@ -0,0 +1,36 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import cx from 'classnames'; + +const propTypes = { + position: PropTypes.oneOf(['left', 'top']), + innerRef: PropTypes.func, + children: PropTypes.node, +}; + +const defaultProps = { + position: 'left', + innerRef: null, + children: null, +}; + +export default class HoverMenu extends React.PureComponent { + render() { + const { innerRef, position, children } = this.props; + return ( +
    + {children} +
    + ); + } +} + +HoverMenu.propTypes = propTypes; +HoverMenu.defaultProps = defaultProps; 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/RowStyleDropdown.jsx b/superset/assets/javascripts/dashboard/v2/components/menu/RowStyleDropdown.jsx new file mode 100644 index 0000000000000..d3c7eff965774 --- /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 '../../util/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; 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..1cd8458669e00 --- /dev/null +++ b/superset/assets/javascripts/dashboard/v2/components/menu/WithPopoverMenu.jsx @@ -0,0 +1,105 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +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.PureComponent { + constructor(props) { + super(props); + this.state = { + 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); + } + + setRef(ref) { + this.container = ref; + } + // + // setPopoverRef(ref) { + // this.popover = ref; + // } + + 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('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); + } + } + } + + render() { + const { children, menuItems, disableClick } = this.props; + const { isFocused } = this.state; + + return ( +
    + {children} + {isFocused && menuItems.length && +
    + {menuItems.map((node, i) => ( +
    {node}
    + ))} +
    } +
    + ); + } +} + +WithPopoverMenu.propTypes = propTypes; +WithPopoverMenu.defaultProps = defaultProps; + +export default WithPopoverMenu; diff --git a/superset/assets/javascripts/dashboard/v2/components/resizable/ResizableContainer.jsx b/superset/assets/javascripts/dashboard/v2/components/resizable/ResizableContainer.jsx new file mode 100644 index 0000000000000..bd590ae2c82c3 --- /dev/null +++ b/superset/assets/javascripts/dashboard/v2/components/resizable/ResizableContainer.jsx @@ -0,0 +1,184 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import Resizable from 're-resizable'; +import cx from 'classnames'; + +import ResizableHandle from './ResizableHandle'; +import resizableConfig from '../../util/resizableConfig'; +import { + GRID_BASE_UNIT, + GRID_ROW_HEIGHT_UNIT, + GRID_GUTTER_SIZE, +} 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, + heightMultiple: PropTypes.number, + minWidthMultiple: PropTypes.number, + maxWidthMultiple: PropTypes.number, + minHeightMultiple: PropTypes.number, + maxHeightMultiple: PropTypes.number, + staticHeightMultiple: PropTypes.number, + onResizeStop: PropTypes.func, + onResize: PropTypes.func, + onResizeStart: PropTypes.func, +}; + +const defaultProps = { + children: null, + adjustableWidth: true, + adjustableHeight: true, + gutterWidth: GRID_GUTTER_SIZE, + widthStep: GRID_BASE_UNIT, + heightStep: GRID_ROW_HEIGHT_UNIT, + widthMultiple: null, + heightMultiple: null, + minWidthMultiple: 1, + maxWidthMultiple: Infinity, + minHeightMultiple: 1, + maxHeightMultiple: Infinity, + staticHeightMultiple: null, + onResizeStop: null, + onResize: null, + onResizeStart: null, +}; + +// because columns are not actually multiples of a single variable (width = n*cols + (n-1)*gutters) +// we snap to the base unit and then snap to actual column multiples on stop +const snapToGrid = [GRID_BASE_UNIT, GRID_BASE_UNIT]; + +class ResizableContainer extends React.PureComponent { + constructor(props) { + super(props); + + this.state = { + isResizing: false, + }; + + this.handleResizeStart = this.handleResizeStart.bind(this); + this.handleResize = this.handleResize.bind(this); + this.handleResizeStop = this.handleResizeStop.bind(this); + } + + handleResizeStart(event, direction, ref) { + const { id, onResizeStart } = this.props; + + 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, + onResizeStop, + widthStep, + heightStep, + widthMultiple, + heightMultiple, + adjustableHeight, + adjustableWidth, + gutterWidth, + } = this.props; + + if (onResizeStop) { + const nextWidthMultiple = + Math.round(widthMultiple + (delta.width / (widthStep + gutterWidth))); + const nextHeightMultiple = + Math.round(heightMultiple + (delta.height / heightStep)); + + onResizeStop({ + id, + widthMultiple: adjustableWidth ? nextWidthMultiple : null, + heightMultiple: adjustableHeight ? nextHeightMultiple : null, + }); + + this.setState(() => ({ isResizing: false })); + } + } + + render() { + const { + children, + adjustableWidth, + adjustableHeight, + widthStep, + heightStep, + staticHeightMultiple, + widthMultiple, + heightMultiple, + minWidthMultiple, + maxWidthMultiple, + minHeightMultiple, + maxHeightMultiple, + gutterWidth, + } = this.props; + + const size = { + width: adjustableWidth + ? ((widthStep + gutterWidth) * widthMultiple) - gutterWidth : undefined, + height: adjustableHeight + ? heightStep * heightMultiple + : (staticHeightMultiple && staticHeightMultiple * heightStep) || undefined, + }; + + let enableConfig = resizableConfig.widthAndHeight; + if (!adjustableHeight) enableConfig = resizableConfig.widthOnly; + else if (!adjustableWidth) enableConfig = resizableConfig.heightOnly; + + const { isResizing } = this.state; + + return ( + + {children} + + ); + } +} + +ResizableContainer.propTypes = propTypes; +ResizableContainer.defaultProps = defaultProps; + +export default ResizableContainer; 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..1d5de7271b5b6 --- /dev/null +++ b/superset/assets/javascripts/dashboard/v2/components/resizable/resizable.css @@ -0,0 +1,72 @@ +.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: #879399; + border-bottom-color: #879399; + right: 16; + bottom: 16; + width: 8px; + height: 8px; +} + +.resize-handle--right { + width: 2px; + height: 20px; + right: -2px; + top: 47%; + position: absolute; + border-left: 1px solid #879399; + border-right: 1px solid #879399; +} + + .grid-spacer + span .resize-handle--right { + right: 3px; + } + +.resize-handle--bottom { + height: 2px; + width: 20px; + bottom: 10px; + left: 47%; + position: absolute; + border-top: 1px solid #879399; + border-bottom: 1px solid #879399; +} + +.grid-resizable-container--resizing > 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/containers/DashboardComponent.jsx b/superset/assets/javascripts/dashboard/v2/containers/DashboardComponent.jsx new file mode 100644 index 0000000000000..10313f185313b --- /dev/null +++ b/superset/assets/javascripts/dashboard/v2/containers/DashboardComponent.jsx @@ -0,0 +1,53 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +import { bindActionCreators } from 'redux'; +import { connect } from 'react-redux'; + +import ComponentLookup from '../components/gridComponents'; +import { componentShape } from '../util/propShapes'; + +import { + createComponent, + deleteComponent, + updateComponents, + handleComponentDrop, +} from '../actions'; + +const propTypes = { + component: componentShape.isRequired, + components: PropTypes.object.isRequired, + createComponent: PropTypes.func.isRequired, + deleteComponent: PropTypes.func.isRequired, + updateComponents: PropTypes.func.isRequired, + handleComponentDrop: PropTypes.func.isRequired, +}; + +function mapStateToProps({ dashboard = {} }, ownProps) { + const { id } = ownProps; + return { + component: dashboard[id], + components: dashboard, + }; +} + +function mapDispatchToProps(dispatch) { + return bindActionCreators({ + createComponent, + deleteComponent, + updateComponents, + handleComponentDrop, + }, dispatch); +} + +class DashboardComponent extends React.PureComponent { + render() { + const { component } = this.props; + const Component = ComponentLookup[component.type]; + return Component ? : 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 new file mode 100644 index 0000000000000..64b46725dea7e --- /dev/null +++ b/superset/assets/javascripts/dashboard/v2/fixtures/testLayout.js @@ -0,0 +1,162 @@ +import { + COLUMN_TYPE, + HEADER_TYPE, + ROW_TYPE, + INVISIBLE_ROW_TYPE, + SPACER_TYPE, + TAB_TYPE, + TABS_TYPE, + CHART_TYPE, + DIVIDER_TYPE, + GRID_ROOT_TYPE, +} from '../util/componentTypes'; + +import { DASHBOARD_ROOT_ID } from '../util/constants'; + +export default { + [DASHBOARD_ROOT_ID]: { + type: GRID_ROOT_TYPE, + id: DASHBOARD_ROOT_ID, + children: [ + // 'header0', + // 'row0', + // 'divider0', + // 'row1', + // 'tabs0', + // 'divider1', + ], + }, + // row0: { + // id: 'row0', + // type: INVISIBLE_ROW_TYPE, + // children: [ + // // 'charta', + // // 'chartb', + // // 'chartc', + // ], + // }, + // row1: { + // id: 'row1', + // type: ROW_TYPE, + // children: [ + // 'header1', + // ], + // }, + // row2: { + // id: 'row2', + // type: ROW_TYPE, + // children: [ + // 'chartd', + // 'spacer0', + // 'charte', + // ], + // }, + // tabs0: { + // id: 'tabs0', + // type: TABS_TYPE, + // children: [ + // 'tab0', + // 'tab1', + // 'tab3', + // ], + // meta: { + // }, + // }, + // tab0: { + // id: 'tab0', + // type: TAB_TYPE, + // children: [ + // // 'row2', + // ], + // meta: { + // text: 'Tab A', + // }, + // }, + // tab1: { + // id: 'tab1', + // type: TAB_TYPE, + // children: [ + // ], + // meta: { + // text: 'Tab B', + // }, + // }, + // tab3: { + // id: 'tab3', + // type: TAB_TYPE, + // children: [ + // ], + // meta: { + // text: 'Tab C', + // }, + // }, + // header0: { + // id: 'header0', + // type: HEADER_TYPE, + // meta: { + // text: 'Header 1', + // }, + // }, + // header1: { + // id: 'header1', + // type: HEADER_TYPE, + // meta: { + // text: 'Header 2', + // }, + // }, + // divider0: { + // id: 'divider0', + // type: DIVIDER_TYPE, + // }, + // divider1: { + // id: 'divider1', + // type: DIVIDER_TYPE, + // }, + // charta: { + // id: 'charta', + // type: CHART_TYPE, + // meta: { + // width: 3, + // height: 10, + // }, + // }, + // chartb: { + // id: 'chartb', + // type: CHART_TYPE, + // meta: { + // width: 3, + // height: 10, + // }, + // }, + // chartc: { + // id: 'chartc', + // type: CHART_TYPE, + // meta: { + // width: 3, + // height: 10, + // }, + // }, + // chartd: { + // id: 'chartd', + // type: CHART_TYPE, + // meta: { + // width: 3, + // height: 10, + // }, + // }, + // charte: { + // id: 'charte', + // type: CHART_TYPE, + // meta: { + // width: 3, + // height: 10, + // }, + // }, + // spacer0: { + // id: 'spacer0', + // type: SPACER_TYPE, + // meta: { + // width: 1, + // }, + // }, +}; diff --git a/superset/assets/javascripts/dashboard/v2/reducers/dashboard.js b/superset/assets/javascripts/dashboard/v2/reducers/dashboard.js new file mode 100644 index 0000000000000..ea110e6b27d3f --- /dev/null +++ b/superset/assets/javascripts/dashboard/v2/reducers/dashboard.js @@ -0,0 +1,107 @@ +import newComponentFactory from '../util/newComponentFactory'; +import newEntitiesFromDrop from '../util/newEntitiesFromDrop'; +import reorderItem from '../util/dnd-reorder'; +import shouldWrapChildInRow from '../util/shouldWrapChildInRow'; +import { ROW_TYPE } from '../util/componentTypes'; + +import { + 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, parentId } } = action; + + if (!parentId || !id || !state[id] || !state[parentId]) return state; + + const nextComponents = { ...state }; + + // recursively find children to remove + let deleteCount = 0; + function recursivelyDeleteChildren(componentId, componentParentId) { + // delete child and it's children + const component = nextComponents[componentId]; + delete nextComponents[componentId]; + deleteCount += 1; + const { children = [] } = component; + children.forEach((childId) => { recursivelyDeleteChildren(childId, componentId); }); + + 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); + } + } + } + + recursivelyDeleteChildren(id, parentId); + console.log('deleted', deleteCount, 'total components', nextComponents); + + return nextComponents; + }, + + [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, draggableId } = dropResult; + + if (!source || !destination || !draggableId) return state; + + const nextEntities = reorderItem({ + entitiesMap: state, + source, + destination, + }); + + // wrap the dragged component in a row depening on destination type + const destinationType = (state[destination.droppableId] || {}).type; + const draggableType = (state[draggableId] || {}).type; + const wrapInRow = shouldWrapChildInRow({ + parentType: destinationType, + childType: draggableType, + }); + + if (wrapInRow) { + const destinationEntity = nextEntities[destination.droppableId]; + const destinationChildren = destinationEntity.children; + const newRow = newComponentFactory(ROW_TYPE); + newRow.children = [destinationChildren[destination.index]]; + destinationChildren[destination.index] = newRow.id; + nextEntities[newRow.id] = newRow; + } + + 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/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 {}; diff --git a/superset/assets/javascripts/dashboard/v2/util/componentIsResizable.js b/superset/assets/javascripts/dashboard/v2/util/componentIsResizable.js new file mode 100644 index 0000000000000..ab701a73e6e02 --- /dev/null +++ b/superset/assets/javascripts/dashboard/v2/util/componentIsResizable.js @@ -0,0 +1,15 @@ +import { + SPACER_TYPE, + COLUMN_TYPE, + CHART_TYPE, + MARKDOWN_TYPE, +} from './componentTypes'; + +export default 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/componentTypes.js b/superset/assets/javascripts/dashboard/v2/util/componentTypes.js new file mode 100644 index 0000000000000..fd5d2940a7afa --- /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 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, + COLUMN_TYPE, + DIVIDER_TYPE, + GRID_ROOT_TYPE, + HEADER_TYPE, + MARKDOWN_TYPE, + ROW_TYPE, + SPACER_TYPE, + TABS_TYPE, + TAB_TYPE, +}; diff --git a/superset/assets/javascripts/dashboard/v2/util/constants.js b/superset/assets/javascripts/dashboard/v2/util/constants.js index a46585786b7fd..44a0f0e823b87 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'; +// Ids +export const DASHBOARD_ROOT_ID = 'DASHBOARD_ROOT_ID'; +export const NEW_CHART_ID = 'NEW_CHART_ID'; +export const NEW_COLUMN_ID = 'NEW_COLUMN_ID'; +export const NEW_DIVIDER_ID = 'NEW_DIVIDER_ID'; +export const NEW_HEADER_ID = 'NEW_HEADER_ID'; +export const NEW_MARKDOWN_ID = 'NEW_MARKDOWN_ID'; +export const NEW_ROW_ID = 'NEW_ROW_ID'; +export const NEW_SPACER_ID = 'NEW_SPACER_ID'; +export const NEW_TAB_ID = 'NEW_TAB_ID'; +export const NEW_TABS_ID = 'NEW_TABS_ID'; -export const DROPPABLE_NEW_COMPONENT = 'DROPPABLE_NEW_COMPONENT'; -export const DROPPABLE_DASHBOARD_ROOT = 'DASHBOARD_ROOT_DROPPABLE'; +// 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_GUTTER_SIZE; -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'; +// Header types +export const SMALL_HEADER = 'SMALL_HEADER'; +export const MEDIUM_HEADER = 'MEDIUM_HEADER'; +export const LARGE_HEADER = 'LARGE_HEADER'; -export const DRAGGABLE_ROW_TYPE = 'DRAGGABLE_ROW_TYPE'; - -export const VERTICAL_DIRECTION = 'vertical'; -export const HORIZONTAL_DIRECTION = 'horizontal'; +// Row types +export const ROW_WHITE = 'ROW_WHITE'; +export const ROW_TRANSPARENT = 'ROW_TRANSPARENT'; diff --git a/superset/assets/javascripts/dashboard/v2/util/dnd-reorder.js b/superset/assets/javascripts/dashboard/v2/util/dnd-reorder.js index 81d479ae1e65e..5ebca8cf92034 100644 --- a/superset/assets/javascripts/dashboard/v2/util/dnd-reorder.js +++ b/superset/assets/javascripts/dashboard/v2/util/dnd-reorder.js @@ -1,16 +1,16 @@ -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); return result; -}; +} -export const reorderRows = ({ +export default function reorderItem({ entitiesMap, source, destination, -}) => { +}) { const current = [...entitiesMap[source.droppableId].children]; const next = [...entitiesMap[destination.droppableId].children]; const target = current[source.index]; @@ -51,4 +51,4 @@ export const reorderRows = ({ }; return result; -}; +} diff --git a/superset/assets/javascripts/dashboard/v2/util/headerStyleOptions.js b/superset/assets/javascripts/dashboard/v2/util/headerStyleOptions.js new file mode 100644 index 0000000000000..309d482ab6aed --- /dev/null +++ b/superset/assets/javascripts/dashboard/v2/util/headerStyleOptions.js @@ -0,0 +1,8 @@ +import { t } from '../../../locales'; +import { SMALL_HEADER, MEDIUM_HEADER, LARGE_HEADER } from './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/util/isValidChild.js b/superset/assets/javascripts/dashboard/v2/util/isValidChild.js new file mode 100644 index 0000000000000..c8921ec9e91e6 --- /dev/null +++ b/superset/assets/javascripts/dashboard/v2/util/isValidChild.js @@ -0,0 +1,69 @@ +import { + CHART_TYPE, + COLUMN_TYPE, + DIVIDER_TYPE, + HEADER_TYPE, + GRID_ROOT_TYPE, + MARKDOWN_TYPE, + ROW_TYPE, + SPACER_TYPE, + TABS_TYPE, + TAB_TYPE, +} from './componentTypes'; + +const typeToValidChildType = { + // while some components are wrapped in Rows, most types are valid root children + [GRID_ROOT_TYPE]: { + [CHART_TYPE]: true, + [COLUMN_TYPE]: true, + [DIVIDER_TYPE]: true, + [HEADER_TYPE]: true, + [ROW_TYPE]: true, + [SPACER_TYPE]: true, + [TABS_TYPE]: true, + }, + + [ROW_TYPE]: { + [CHART_TYPE]: true, + [MARKDOWN_TYPE]: true, + [COLUMN_TYPE]: true, + [SPACER_TYPE]: true, + }, + + [TABS_TYPE]: { + [TAB_TYPE]: true, + }, + + [TAB_TYPE]: { + [CHART_TYPE]: true, + [COLUMN_TYPE]: true, + [DIVIDER_TYPE]: true, + [HEADER_TYPE]: true, + [ROW_TYPE]: true, + [SPACER_TYPE]: true, + }, + + [COLUMN_TYPE]: { + [CHART_TYPE]: true, + [MARKDOWN_TYPE]: true, + [HEADER_TYPE]: true, + [SPACER_TYPE]: true, + }, + + // 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; + + const isValid = Boolean( + typeToValidChildType[parentType][childType], + ); + + return isValid; +} diff --git a/superset/assets/javascripts/dashboard/v2/util/newComponentFactory.js b/superset/assets/javascripts/dashboard/v2/util/newComponentFactory.js new file mode 100644 index 0000000000000..c1ed03e0aa548 --- /dev/null +++ b/superset/assets/javascripts/dashboard/v2/util/newComponentFactory.js @@ -0,0 +1,45 @@ +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)}`; +} + +export default function entityFactory(type) { + return { + version: 'v0', + type, + id: uuid(type), + children: [], + meta: { + ...typeToDefaultMetaData[type], + }, + }; +} diff --git a/superset/assets/javascripts/dashboard/v2/util/newComponentIdToType.js b/superset/assets/javascripts/dashboard/v2/util/newComponentIdToType.js new file mode 100644 index 0000000000000..38d1c7ca3702f --- /dev/null +++ b/superset/assets/javascripts/dashboard/v2/util/newComponentIdToType.js @@ -0,0 +1,35 @@ +import { + CHART_TYPE, + COLUMN_TYPE, + DIVIDER_TYPE, + HEADER_TYPE, + MARKDOWN_TYPE, + ROW_TYPE, + SPACER_TYPE, + TABS_TYPE, + TAB_TYPE, +} from './componentTypes'; + +import { + NEW_CHART_ID, + NEW_COLUMN_ID, + NEW_DIVIDER_ID, + NEW_HEADER_ID, + NEW_MARKDOWN_ID, + NEW_ROW_ID, + NEW_SPACER_ID, + NEW_TABS_ID, + NEW_TAB_ID, +} from './constants'; + +export default { + [NEW_CHART_ID]: CHART_TYPE, // @TODO we will have to encode real chart ids => 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 new file mode 100644 index 0000000000000..a0d92fa7449c7 --- /dev/null +++ b/superset/assets/javascripts/dashboard/v2/util/newEntitiesFromDrop.js @@ -0,0 +1,55 @@ +import newComponentIdToType from './newComponentIdToType'; +import shouldWrapChildInRow from './shouldWrapChildInRow'; +import newComponentFactory from './newComponentFactory'; + +import { + ROW_TYPE, + TABS_TYPE, + TAB_TYPE, +} from './componentTypes'; + +export default function newEntitiesFromDrop({ dropResult, components }) { + const { draggableId, destination } = dropResult; + + const dragType = newComponentIdToType[draggableId]; + const dropEntity = components[destination.droppableId]; + + if (!dropEntity) { + console.warn('Drop target entity', destination.droppableId, 'not found'); + return null; + } + + if (!dragType) { + console.warn('Drag type not found for id', draggableId); + return null; + } + + const dropType = dropEntity.type; + let newDropChild = newComponentFactory(dragType); + const wrapChildInRow = shouldWrapChildInRow({ parentType: dropType, childType: dragType }); + + const newEntities = { + [newDropChild.id]: newDropChild, + }; + + if (wrapChildInRow) { + const rowWrapper = newComponentFactory(ROW_TYPE); + rowWrapper.children = [newDropChild.id]; + newEntities[rowWrapper.id] = rowWrapper; + newDropChild = rowWrapper; + } else if (dragType === TABS_TYPE) { // create a new tab component + const tabChild = newComponentFactory(TAB_TYPE); + newDropChild.children = [tabChild.id]; + newEntities[tabChild.id] = tabChild; + } + + 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..be84965957163 --- /dev/null +++ b/superset/assets/javascripts/dashboard/v2/util/propShapes.jsx @@ -0,0 +1,24 @@ +import PropTypes from 'prop-types'; +import componentTypes from './componentTypes'; +import rowStyleOptions from './rowStyleOptions'; +import headerStyleOptions from './headerStyleOptions'; + +export const componentShape = PropTypes.shape({ // eslint-disable-line + id: PropTypes.string.isRequired, + type: PropTypes.oneOf( + Object.values(componentTypes), + ).isRequired, + children: PropTypes.arrayOf(PropTypes.string), + meta: PropTypes.shape({ + // Dimensions + width: PropTypes.number, + height: PropTypes.number, + + // Header + text: PropTypes.string, + headerSize: PropTypes.oneOf(headerStyleOptions.map(opt => opt.value)), + + // Row + rowStyle: PropTypes.oneOf(rowStyleOptions.map(opt => opt.value)), + }), +}); 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..40e9af68bbb0f --- /dev/null +++ b/superset/assets/javascripts/dashboard/v2/util/resizableConfig.js @@ -0,0 +1,30 @@ +// config for a ResizableContainer + +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/javascripts/dashboard/v2/util/rowStyleOptions.js b/superset/assets/javascripts/dashboard/v2/util/rowStyleOptions.js new file mode 100644 index 0000000000000..ad42492296cfb --- /dev/null +++ b/superset/assets/javascripts/dashboard/v2/util/rowStyleOptions.js @@ -0,0 +1,7 @@ +import { t } from '../../../locales'; +import { ROW_TRANSPARENT, ROW_WHITE } from './constants'; + +export default [ + { value: ROW_TRANSPARENT, label: t('Transparent'), className: 'grid-row--transparent' }, + { value: ROW_WHITE, label: t('White'), className: 'grid-row--white' }, +]; diff --git a/superset/assets/javascripts/dashboard/v2/util/shouldWrapChildInRow.js b/superset/assets/javascripts/dashboard/v2/util/shouldWrapChildInRow.js 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]); +} diff --git a/superset/assets/package.json b/superset/assets/package.json index d6af355764934..797a6f745f9b5 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,22 +72,23 @@ "nvd3": "1.8.6", "po2json": "^0.4.5", "prop-types": "^15.6.0", + "re-resizable": "^4.3.1", "react": "^15.6.2", "react-ace": "^5.0.1", "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": "^0.32.0", "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", "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", @@ -98,6 +100,7 @@ "redux": "^3.5.2", "redux-localstorage": "^0.4.1", "redux-thunk": "^2.1.0", + "reselect": "^3.0.1", "shortid": "^2.2.6", "sprintf-js": "^1.1.1", "srcdoc-polyfill": "^1.0.0", diff --git a/superset/assets/stylesheets/dashboard-v2.css b/superset/assets/stylesheets/dashboard-v2.css index 0ad56d5d166cb..534a17eafa7f2 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,39 +10,33 @@ 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; } -.dashboard-builder-sidepane { - background: white; - box-shadow: 0 2px 6px #aaa; /* @TODO color */ -} - -.new-draggable-component { +.dashboard-builder { display: flex; flex-direction: row; flex-wrap: nowrap; - align-items: center; - padding: 16; - background: white; + height: auto; } - .new-draggable-component--dragging { - box-shadow: 0 0 1px #aaa; /* @TODO color */ - } +.dashboard-builder-sidepane { + background: white; + flex: 0 0 376px; + box-shadow: 0 0 0 1px #ccc; /* @TODO color */ +} -.new-draggable-placeholder { - background: #f5f5f5; - width: 40px; - height: 40px; - margin-right: 16px; - box-shadow: 0 0 1px #fff; +.dashboard-builder-sidepane-header { + font-size: 16; + font-weight: 700; + border-bottom: 1px solid #ccc; + padding: 16px; } /* @TODO remove upon new theme */ .btn.btn-primary { - background: #484848 !important; + background: #263238 !important; color: white !important; } diff --git a/superset/assets/stylesheets/superset.less b/superset/assets/stylesheets/superset.less index a89c0e09ac10b..5cf448852ace1 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; @@ -223,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; } 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
    -