diff --git a/superset-frontend/src/explore/components/ExploreContainer/ExploreContainer.test.tsx b/superset-frontend/src/explore/components/ExploreContainer/ExploreContainer.test.tsx new file mode 100644 index 0000000000..50922256ea --- /dev/null +++ b/superset-frontend/src/explore/components/ExploreContainer/ExploreContainer.test.tsx @@ -0,0 +1,85 @@ +/** + * 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 ExploreContainer, { DraggingContext } from '.'; +import OptionWrapper from '../controls/DndColumnSelectControl/OptionWrapper'; + +const MockChildren = () => { + const dragging = React.useContext(DraggingContext); + return ( +
+ {dragging ? 'dragging' : 'not dragging'} +
+ ); +}; + +test('should render children', () => { + const { getByTestId, getByText } = render( + + + , + { useRedux: true, useDnd: true }, + ); + expect(getByTestId('mock-children')).toBeInTheDocument(); + expect(getByText('not dragging')).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} + /> + {}} + onShiftOptions={() => {}} + /> + + , + { + useRedux: true, + useDnd: true, + }, + ); + expect(container.getElementsByClassName('dragging')).toHaveLength(0); + fireEvent.dragStart(getByText('Label 1')); + expect(container.getElementsByClassName('dragging')).toHaveLength(1); + fireEvent.dragEnd(getByText('Label 1')); + expect(container.getElementsByClassName('dragging')).toHaveLength(0); + // don't show dragging state for the sorting item + fireEvent.dragStart(getByText('Label 2')); + expect(container.getElementsByClassName('dragging')).toHaveLength(0); +}); diff --git a/superset-frontend/src/explore/components/ExploreContainer/index.tsx b/superset-frontend/src/explore/components/ExploreContainer/index.tsx new file mode 100644 index 0000000000..3c64373948 --- /dev/null +++ b/superset-frontend/src/explore/components/ExploreContainer/index.tsx @@ -0,0 +1,60 @@ +/** + * 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 { styled } from '@superset-ui/core'; +import { useDragDropManager } from 'react-dnd'; + +export const DraggingContext = React.createContext(false); +const StyledDiv = styled.div` + display: flex; + flex-direction: column; + height: 100%; + min-height: 0; +`; +const ExploreContainer: React.FC<{}> = ({ children }) => { + const dragDropManager = useDragDropManager(); + const [dragging, setDragging] = React.useState( + dragDropManager.getMonitor().isDragging(), + ); + + useEffect(() => { + const monitor = dragDropManager.getMonitor(); + const unsub = monitor.subscribeToStateChange(() => { + const item = monitor.getItem() || {}; + // don't show dragging state for the sorting item + if ('dragIndex' in item) { + return; + } + const isDragging = monitor.isDragging(); + setDragging(isDragging); + }); + + return () => { + unsub(); + }; + }, [dragDropManager]); + + return ( + + {children} + + ); +}; + +export default ExploreContainer; diff --git a/superset-frontend/src/explore/components/ExploreViewContainer/index.jsx b/superset-frontend/src/explore/components/ExploreViewContainer/index.jsx index d3a32a2bd6..0da43ebdc0 100644 --- a/superset-frontend/src/explore/components/ExploreViewContainer/index.jsx +++ b/superset-frontend/src/explore/components/ExploreViewContainer/index.jsx @@ -68,6 +68,7 @@ import ConnectedControlPanelsContainer from '../ControlPanelsContainer'; import SaveModal from '../SaveModal'; import DataSourcePanel from '../DatasourcePanel'; import ConnectedExploreChartHeader from '../ExploreChartHeader'; +import ExploreContainer from '../ExploreContainer'; const propTypes = { ...ExploreChartPanel.propTypes, @@ -90,13 +91,6 @@ const propTypes = { isSaveModalVisible: PropTypes.bool, }; -const ExploreContainer = styled.div` - display: flex; - flex-direction: column; - height: 100%; - min-height: 0; -`; - const ExplorePanelContainer = styled.div` ${({ theme }) => css` background: ${theme.colors.grayscale.light5}; diff --git a/superset-frontend/src/explore/components/controls/DndColumnSelectControl/DndSelectLabel.tsx b/superset-frontend/src/explore/components/controls/DndColumnSelectControl/DndSelectLabel.tsx index 397d6f54e0..f4da2d1729 100644 --- a/superset-frontend/src/explore/components/controls/DndColumnSelectControl/DndSelectLabel.tsx +++ b/superset-frontend/src/explore/components/controls/DndColumnSelectControl/DndSelectLabel.tsx @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import React, { ReactNode, useMemo } from 'react'; +import React, { ReactNode, useContext, useMemo } from 'react'; import { useDrop } from 'react-dnd'; import { t, useTheme } from '@superset-ui/core'; import ControlHeader from 'src/explore/components/ControlHeader'; @@ -31,6 +31,7 @@ import { } from 'src/explore/components/DatasourcePanel/types'; import Icons from 'src/components/Icons'; import { DndItemType } from '../../DndItemType'; +import { DraggingContext } from '../../ExploreContainer'; export type DndSelectLabelProps = { name: string; @@ -43,18 +44,20 @@ export type DndSelectLabelProps = { valuesRenderer: () => ReactNode; displayGhostButton?: boolean; onClickGhostButton: () => void; + isLoading?: boolean; }; export default function DndSelectLabel({ displayGhostButton = true, accept, valuesRenderer, + isLoading, ...props }: DndSelectLabelProps) { const theme = useTheme(); const [{ isOver, canDrop }, datasourcePanelDrop] = useDrop({ - accept, + accept: isLoading ? [] : accept, drop: (item: DatasourcePanelDndItem) => { props.onDrop(item); @@ -70,6 +73,7 @@ export default function DndSelectLabel({ type: monitor.getItemType(), }), }); + const isDragging = useContext(DraggingContext); const values = useMemo(() => valuesRenderer(), [valuesRenderer]); @@ -94,6 +98,8 @@ export default function DndSelectLabel({ data-test="dnd-labels-container" canDrop={canDrop} isOver={isOver} + isDragging={isDragging} + isLoading={isLoading} > {values} {displayGhostButton && renderGhostButton()} diff --git a/superset-frontend/src/explore/components/controls/OptionControls/index.tsx b/superset-frontend/src/explore/components/controls/OptionControls/index.tsx index 90ae73c637..2e2bf1f5d7 100644 --- a/superset-frontend/src/explore/components/controls/OptionControls/index.tsx +++ b/superset-frontend/src/explore/components/controls/OptionControls/index.tsx @@ -18,7 +18,7 @@ */ import React, { useRef } from 'react'; import { useDrag, useDrop, DropTargetMonitor } from 'react-dnd'; -import { styled, t, useTheme } from '@superset-ui/core'; +import { styled, t, useTheme, keyframes, css } from '@superset-ui/core'; import { InfoTooltipWithTrigger } from '@superset-ui/chart-controls'; import { Tooltip } from 'src/components/Tooltip'; import Icons from 'src/components/Icons'; @@ -103,21 +103,89 @@ export const LabelsContainer = styled.div` border-radius: ${({ theme }) => theme.gridUnit}px; `; +const borderPulse = keyframes` + 0% { + right: 100%; + } + 50% { + left: 4px; + } + 90% { + right: 4px; + } + 100% { + left: 100%; + } +`; + export const DndLabelsContainer = styled.div<{ canDrop?: boolean; isOver?: boolean; + isDragging?: boolean; + isLoading?: boolean; }>` - padding: ${({ theme }) => theme.gridUnit}px; - border: ${({ canDrop, isOver, theme }) => { - if (canDrop) { - return `dashed 1px ${theme.colors.info.dark1}`; - } - if (isOver && !canDrop) { - return `dashed 1px ${theme.colors.error.dark1}`; - } - return `solid 1px ${theme.colors.grayscale.light2}`; - }}; - border-radius: ${({ theme }) => theme.gridUnit}px; + ${({ theme, isLoading, canDrop, isDragging, isOver }) => ` + position: relative; + padding: ${theme.gridUnit}px; + border: ${ + !isLoading && isDragging + ? `dashed 1px ${ + canDrop ? theme.colors.info.dark1 : theme.colors.error.dark1 + }` + : `solid 1px ${ + isLoading && isDragging + ? theme.colors.warning.light1 + : theme.colors.grayscale.light2 + }` + }; + border-radius: ${theme.gridUnit}px; + &:before, + &:after { + content: ' '; + position: absolute; + border-radius: ${theme.gridUnit}px; + } + &:before { + display: ${isDragging || isLoading ? 'block' : 'none'}; + background-color: ${ + canDrop ? theme.colors.primary.base : theme.colors.error.light1 + }; + z-index: ${theme.zIndex.aboveDashboardCharts}; + opacity: ${theme.opacity.light}; + top: 1px; + right: 1px; + bottom: 1px; + left: 1px; + } + &:after { + display: ${isLoading || (canDrop && isOver) ? 'block' : 'none'}; + background-color: ${ + isLoading ? theme.colors.grayscale.light3 : theme.colors.primary.base + }; + z-index: ${theme.zIndex.dropdown}; + opacity: ${theme.opacity.mediumLight}; + top: ${-theme.gridUnit}px; + right: ${-theme.gridUnit}px; + bottom: ${-theme.gridUnit}px; + left: ${-theme.gridUnit}px; + cursor: ${isLoading ? 'wait' : 'auto'}; + } + `} + + &:before { + ${({ theme, isLoading }) => + isLoading && + css` + animation: ${borderPulse} 2s ease-in infinite; + background: linear-gradient(currentColor 0 0) 0 100%/0% 3px no-repeat; + background-size: 100% ${theme.gridUnit / 2}px; + top: auto; + right: ${theme.gridUnit}px; + left: ${theme.gridUnit}px; + bottom: -${theme.gridUnit / 2}px; + height: ${theme.gridUnit / 2}px; + `}; + } `; export const AddControlLabel = styled.div<{ diff --git a/superset-frontend/src/pages/Chart/Chart.test.tsx b/superset-frontend/src/pages/Chart/Chart.test.tsx index 674a33d44d..f8fb9fdcbb 100644 --- a/superset-frontend/src/pages/Chart/Chart.test.tsx +++ b/superset-frontend/src/pages/Chart/Chart.test.tsx @@ -65,6 +65,7 @@ describe('ChartPage', () => { const { getByTestId } = render(, { useRouter: true, useRedux: true, + useDnd: true, }); await waitFor(() => expect(fetchMock.calls(exploreApiRoute).length).toBe(1), @@ -110,6 +111,7 @@ describe('ChartPage', () => { const { getByTestId } = render(, { useRouter: true, useRedux: true, + useDnd: true, }); await waitFor(() => expect(fetchMock.calls(exploreApiRoute).length).toBe(1), @@ -156,6 +158,7 @@ describe('ChartPage', () => { { useRouter: true, useRedux: true, + useDnd: true, }, ); await waitFor(() =>