diff --git a/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardBuilder.tsx b/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardBuilder.tsx index a17b16837..51de15c7a 100644 --- a/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardBuilder.tsx +++ b/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardBuilder.tsx @@ -86,72 +86,10 @@ import { import { getRootLevelTabsComponent, shouldFocusTabs } from './utils'; import DashboardContainer from './DashboardContainer'; import { useNativeFilters } from './state'; +import DashboardWrapper from './DashboardWrapper'; type DashboardBuilderProps = {}; -const StyledDiv = styled.div` - ${({ theme }) => css` - display: grid; - grid-template-columns: auto 1fr; - grid-template-rows: auto 1fr; - flex: 1; - /* Special cases */ - - /* A row within a column has inset hover menu */ - .dragdroppable-column .dragdroppable-row .hover-menu--left { - left: ${theme.gridUnit * -3}px; - background: ${theme.colors.grayscale.light5}; - border: 1px solid ${theme.colors.grayscale.light2}; - } - - .dashboard-component-tabs { - position: relative; - } - - /* A column within a column or tabs has inset hover menu */ - .dragdroppable-column .dragdroppable-column .hover-menu--top, - .dashboard-component-tabs .dragdroppable-column .hover-menu--top { - top: ${theme.gridUnit * -3}px; - background: ${theme.colors.grayscale.light5}; - border: 1px solid ${theme.colors.grayscale.light2}; - } - - /* move Tabs hover menu to top near actual Tabs */ - .dashboard-component-tabs > .hover-menu-container > .hover-menu--left { - top: 0; - transform: unset; - background: transparent; - } - - /* push Chart actions to upper right */ - .dragdroppable-column .dashboard-component-chart-holder .hover-menu--top, - .dragdroppable .dashboard-component-header .hover-menu--top { - right: ${theme.gridUnit * 2}px; - top: ${theme.gridUnit * 2}px; - background: transparent; - border: none; - transform: unset; - left: unset; - } - div:hover > .hover-menu-container .hover-menu, - .hover-menu-container .hover-menu:hover { - opacity: 1; - } - - p { - margin: 0 0 ${theme.gridUnit * 2}px 0; - } - - i.danger { - color: ${theme.colors.error.base}; - } - - i.warning { - color: ${theme.colors.alert.base}; - } - `} -`; - // @z-index-above-dashboard-charts + 1 = 11 const FiltersPanel = styled.div<{ width: number; hidden: boolean }>` grid-column: 1; @@ -317,7 +255,7 @@ const DashboardContentWrapper = styled.div` width: 100%; } - & > .empty-droptarget:first-child { + & > .empty-droptarget:first-child:not(.empty-droptarget--full) { height: ${theme.gridUnit * 4}px; top: -2px; z-index: 10; @@ -640,7 +578,7 @@ const DashboardBuilder: FC = () => { : theme.gridUnit * 8; return ( - + {showFilterBar && filterBarOrientation === FilterBarOrientation.VERTICAL && ( <> = () => { `} /> )} - + ); }; diff --git a/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardWrapper.test.tsx b/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardWrapper.test.tsx new file mode 100644 index 000000000..fb913b462 --- /dev/null +++ b/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardWrapper.test.tsx @@ -0,0 +1,75 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import React from 'react'; +import { fireEvent, render } from 'spec/helpers/testing-library'; +import { OptionControlLabel } from 'src/explore/components/controls/OptionControls'; + +import DashboardWrapper from './DashboardWrapper'; + +test('should render children', () => { + const { getByTestId } = render( + +
+ , + { useRedux: true, useDnd: true }, + ); + expect(getByTestId('mock-children')).toBeInTheDocument(); +}); + +test('should update the style on dragging state', () => { + const defaultProps = { + label: Test label, + tooltipTitle: 'This is a tooltip title', + onRemove: jest.fn(), + onMoveLabel: jest.fn(), + onDropLabel: jest.fn(), + type: 'test', + index: 0, + }; + const { container, getByText } = render( + + Label 1} + /> + Label 2} + /> + , + { + useRedux: true, + useDnd: true, + initialState: { + dashboardState: { + editMode: true, + }, + }, + }, + ); + expect( + container.getElementsByClassName('dragdroppable--dragging'), + ).toHaveLength(0); + fireEvent.dragStart(getByText('Label 1')); + expect( + container.getElementsByClassName('dragdroppable--dragging'), + ).toHaveLength(1); +}); diff --git a/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardWrapper.tsx b/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardWrapper.tsx new file mode 100644 index 000000000..f39c7ed63 --- /dev/null +++ b/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardWrapper.tsx @@ -0,0 +1,128 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import React, { useEffect } from 'react'; +import { css, styled } from '@superset-ui/core'; +import { RootState } from 'src/dashboard/types'; +import { useSelector } from 'react-redux'; +import { useDragDropManager } from 'react-dnd'; +import classNames from 'classnames'; + +const StyledDiv = styled.div` + ${({ theme }) => css` + display: grid; + grid-template-columns: auto 1fr; + grid-template-rows: auto 1fr; + flex: 1; + /* Special cases */ + + &.dragdroppable--dragging + .dashboard-component-tabs-content + > .empty-droptarget.empty-droptarget--full { + height: 100%; + } + + /* A row within a column has inset hover menu */ + .dragdroppable-column .dragdroppable-row .hover-menu--left { + left: ${theme.gridUnit * -3}px; + background: ${theme.colors.grayscale.light5}; + border: 1px solid ${theme.colors.grayscale.light2}; + } + + .dashboard-component-tabs { + position: relative; + } + + /* A column within a column or tabs has inset hover menu */ + .dragdroppable-column .dragdroppable-column .hover-menu--top, + .dashboard-component-tabs .dragdroppable-column .hover-menu--top { + top: ${theme.gridUnit * -3}px; + background: ${theme.colors.grayscale.light5}; + border: 1px solid ${theme.colors.grayscale.light2}; + } + + /* move Tabs hover menu to top near actual Tabs */ + .dashboard-component-tabs > .hover-menu-container > .hover-menu--left { + top: 0; + transform: unset; + background: transparent; + } + + /* push Chart actions to upper right */ + .dragdroppable-column .dashboard-component-chart-holder .hover-menu--top, + .dragdroppable .dashboard-component-header .hover-menu--top { + right: ${theme.gridUnit * 2}px; + top: ${theme.gridUnit * 2}px; + background: transparent; + border: none; + transform: unset; + left: unset; + } + div:hover > .hover-menu-container .hover-menu, + .hover-menu-container .hover-menu:hover { + opacity: 1; + } + + p { + margin: 0 0 ${theme.gridUnit * 2}px 0; + } + + i.danger { + color: ${theme.colors.error.base}; + } + + i.warning { + color: ${theme.colors.alert.base}; + } + `} +`; + +type Props = {}; + +const DashboardWrapper: React.FC = ({ children }) => { + const editMode = useSelector( + state => state.dashboardState.editMode, + ); + const dragDropManager = useDragDropManager(); + const [isDragged, setIsDragged] = React.useState( + dragDropManager.getMonitor().isDragging(), + ); + + useEffect(() => { + const monitor = dragDropManager.getMonitor(); + const unsub = monitor.subscribeToStateChange(() => { + setIsDragged(monitor.isDragging()); + }); + + return () => { + unsub(); + }; + }, [dragDropManager]); + + return ( + + {children} + + ); +}; + +export default DashboardWrapper; diff --git a/superset-frontend/src/dashboard/components/DashboardGrid.jsx b/superset-frontend/src/dashboard/components/DashboardGrid.jsx index 601dbac4a..70cf65218 100644 --- a/superset-frontend/src/dashboard/components/DashboardGrid.jsx +++ b/superset-frontend/src/dashboard/components/DashboardGrid.jsx @@ -18,6 +18,7 @@ */ import React from 'react'; import PropTypes from 'prop-types'; +import classNames from 'classnames'; import { addAlpha, css, styled, t } from '@superset-ui/core'; import { EmptyStateBig } from 'src/components/EmptyState'; import { componentShape } from '../util/propShapes'; @@ -76,10 +77,14 @@ const GridContent = styled.div` & > .empty-droptarget:first-child { height: ${theme.gridUnit * 12}px; margin-top: ${theme.gridUnit * -6}px; - margin-bottom: ${theme.gridUnit * -6}px; } - & > .empty-droptarget:only-child { + & > .empty-droptarget:last-child { + height: ${theme.gridUnit * 12}px; + margin-top: ${theme.gridUnit * -6}px; + } + + & > .empty-droptarget.empty-droptarget--full:only-child { height: 80vh; } `} @@ -270,10 +275,14 @@ class DashboardGrid extends React.PureComponent { index={0} orientation="column" onDrop={this.handleTopDropTargetDrop} - className="empty-droptarget" + className={classNames({ + 'empty-droptarget': true, + 'empty-droptarget--full': + gridComponent?.children?.length === 0, + })} editMode > - {renderDraggableContentBottom} + {renderDraggableContentTop} )} {gridComponent?.children?.map((id, index) => ( @@ -304,7 +313,7 @@ class DashboardGrid extends React.PureComponent { className="empty-droptarget" editMode > - {renderDraggableContentTop} + {renderDraggableContentBottom} )} {isResizing && diff --git a/superset-frontend/src/dashboard/components/dnd/DragDroppable.jsx b/superset-frontend/src/dashboard/components/dnd/DragDroppable.jsx index 3bc9f4d29..6a49f9887 100644 --- a/superset-frontend/src/dashboard/components/dnd/DragDroppable.jsx +++ b/superset-frontend/src/dashboard/components/dnd/DragDroppable.jsx @@ -90,6 +90,11 @@ const DragDroppableStyles = styled.div` z-index: 10; } + &.empty-droptarget--full > .drop-indicator--top { + height: 100%; + opacity: 0.3; + } + & { .drop-indicator { display: block; @@ -99,7 +104,7 @@ const DragDroppableStyles = styled.div` } .drop-indicator--top { - top: 0; + top: ${-theme.gridUnit - 2}px; left: 0; height: ${theme.gridUnit}px; width: 100%; @@ -107,7 +112,7 @@ const DragDroppableStyles = styled.div` } .drop-indicator--bottom { - top: 100%; + bottom: ${-theme.gridUnit - 2}px; left: 0; height: ${theme.gridUnit}px; width: 100%; @@ -116,7 +121,7 @@ const DragDroppableStyles = styled.div` .drop-indicator--right { top: 0; - left: 100%; + left: calc(100% - ${theme.gridUnit}px); height: 100%; width: ${theme.gridUnit}px; min-height: ${theme.gridUnit * 4}px; diff --git a/superset-frontend/src/dashboard/components/gridComponents/Tab.jsx b/superset-frontend/src/dashboard/components/gridComponents/Tab.jsx index 32ac77936..d1d08176b 100644 --- a/superset-frontend/src/dashboard/components/gridComponents/Tab.jsx +++ b/superset-frontend/src/dashboard/components/gridComponents/Tab.jsx @@ -18,6 +18,7 @@ */ import React from 'react'; import PropTypes from 'prop-types'; +import classNames from 'classnames'; import { bindActionCreators } from 'redux'; import { connect } from 'react-redux'; import { styled, t } from '@superset-ui/core'; @@ -173,7 +174,10 @@ class Tab extends React.PureComponent { depth={depth} onDrop={this.handleTopDropTargetDrop} editMode - className="empty-droptarget" + className={classNames({ + 'empty-droptarget': true, + 'empty-droptarget--full': tabComponent.children.length === 0, + })} > {renderDraggableContentTop} @@ -234,7 +238,7 @@ class Tab extends React.PureComponent { /> ))} {/* Make bottom of tab droppable */} - {editMode && ( + {editMode && tabComponent.children.length > 0 && (