diff --git a/superset-frontend/src/dashboard/components/gridComponents/Row/Row.test.jsx b/superset-frontend/src/dashboard/components/gridComponents/Row/Row.test.tsx similarity index 69% rename from superset-frontend/src/dashboard/components/gridComponents/Row/Row.test.jsx rename to superset-frontend/src/dashboard/components/gridComponents/Row/Row.test.tsx index 253a41d0a10..0d1cf099f15 100644 --- a/superset-frontend/src/dashboard/components/gridComponents/Row/Row.test.jsx +++ b/superset-frontend/src/dashboard/components/gridComponents/Row/Row.test.tsx @@ -16,17 +16,24 @@ * specific language governing permissions and limitations * under the License. */ -import { fireEvent, render } from 'spec/helpers/testing-library'; +import React from 'react'; +import { + fireEvent, + render, + RenderResult, + screen, +} from 'spec/helpers/testing-library'; -import BackgroundStyleDropdown from 'src/dashboard/components/menu/BackgroundStyleDropdown'; -import IconButton from 'src/dashboard/components/IconButton'; import { DASHBOARD_GRID_ID } from 'src/dashboard/util/constants'; - import { getMockStore } from 'spec/fixtures/mockStore'; import { dashboardLayout as mockLayout } from 'spec/fixtures/mockDashboardLayout'; import { initialState } from 'src/SqlLab/fixtures'; import Row from './Row'; +interface MockIntersectionObserverEntry { + isIntersecting: boolean; +} + jest.mock('@superset-ui/core', () => ({ ...jest.requireActual('@superset-ui/core'), isFeatureEnabled: jest.fn(() => true), @@ -44,47 +51,84 @@ jest.mock('src/dashboard/util/isEmbedded', () => ({ })); jest.mock('src/dashboard/components/dnd/DragDroppable', () => ({ - Draggable: ({ children }) => ( -
{children({})}
- ), - Droppable: ({ children, depth }) => ( -
+ Draggable: ({ + children, + }: { + children: (args: object) => React.ReactNode; + }) =>
{children({})}
, + + Droppable: ({ + children, + depth, + }: { + children: (args: object) => React.ReactNode; + depth: number; + }) => ( +
{children({})}
), })); -jest.mock( - 'src/dashboard/containers/DashboardComponent', - () => - ({ availableColumnCount, depth }) => ( -
- {availableColumnCount} -
- ), -); -jest.mock( - 'src/dashboard/components/menu/WithPopoverMenu', - () => - ({ children }) =>
{children}
, -); +jest.mock('src/dashboard/containers/DashboardComponent', () => { + return ({ + availableColumnCount, + depth, + }: { + availableColumnCount: number; + depth: number; + }) => ( +
+ {availableColumnCount} +
+ ); +}); -jest.mock( - 'src/dashboard/components/DeleteComponentButton', - () => - ({ onDelete }) => ( - - ), -); +jest.mock('src/dashboard/components/menu/WithPopoverMenu', () => { + return ({ children }: { children: React.ReactNode }) => ( +
{children}
+ ); +}); -const rowWithoutChildren = { ...mockLayout.present.ROW_ID, children: [] }; -const props = { +jest.mock('src/dashboard/components/DeleteComponentButton', () => { + return ({ onDelete }: { onDelete: () => void }) => ( + + ); +}); + +const rowWithoutChildren = { + ...mockLayout.present.ROW_ID, + children: [], +}; +interface RowTestProps { + id: string; + parentId: string; + component: typeof mockLayout.present.ROW_ID; + parentComponent: (typeof mockLayout.present)[typeof DASHBOARD_GRID_ID]; + index: number; + depth: number; + editMode: boolean; + availableColumnCount: number; + columnWidth: number; + occupiedColumnCount: number; + onResizeStart: () => void; + onResize: () => void; + onResizeStop: () => void; + handleComponentDrop: () => void; + deleteComponent: () => void; + updateComponents: () => void; + isComponentVisible: boolean; + maxChildrenHeight: number; + onChangeTab: () => void; +} + +const props: RowTestProps = { id: 'ROW_ID', parentId: DASHBOARD_GRID_ID, component: mockLayout.present.ROW_ID, @@ -95,15 +139,18 @@ const props = { availableColumnCount: 12, columnWidth: 50, occupiedColumnCount: 6, - onResizeStart() {}, - onResize() {}, - onResizeStop() {}, - handleComponentDrop() {}, - deleteComponent() {}, - updateComponents() {}, + onResizeStart: () => {}, + onResize: () => {}, + onResizeStop: () => {}, + handleComponentDrop: () => {}, + deleteComponent: () => {}, + updateComponents: () => {}, + isComponentVisible: true, + maxChildrenHeight: 0, + onChangeTab: () => {}, }; -function setup(overrideProps) { +function setup(overrideProps: Partial = {}): RenderResult { // We have to wrap provide DragDropContext for the underlying DragDroppable // otherwise we cannot assert on DragDroppable children const mockStore = getMockStore({ @@ -153,7 +200,7 @@ test('should render a HoverMenu in editMode', () => { // pass the same depth of its droppable area expect(getByTestId('mock-droppable')).toHaveAttribute( - 'depth', + 'data-depth', `${props.depth}`, ); }); @@ -167,17 +214,16 @@ test('should render a DeleteComponentButton in editMode', () => { }); test.skip('should render a BackgroundStyleDropdown when focused', () => { - let wrapper = setup({ component: rowWithoutChildren }); - expect(wrapper.find(BackgroundStyleDropdown)).toBeFalsy(); + let { rerender } = setup({ component: rowWithoutChildren }); + expect(screen.queryByTestId('background-style-dropdown')).toBeFalsy(); // we cannot set props on the Row because of the WithDragDropContext wrapper - wrapper = setup({ component: rowWithoutChildren, editMode: true }); - wrapper - .find(IconButton) - .at(1) // first one is delete button - .simulate('click'); + rerender(); + const buttons = screen.getAllByRole('button'); + const settingsButton = buttons[1]; + fireEvent.click(settingsButton); - expect(wrapper.find(BackgroundStyleDropdown)).toBeTruthy(); + expect(screen.queryByTestId('background-style-dropdown')).toBeTruthy(); }); test('should call deleteComponent when deleted', () => { @@ -190,14 +236,14 @@ test('should call deleteComponent when deleted', () => { test('should pass appropriate availableColumnCount to children', () => { const { getByTestId } = setup(); expect(getByTestId('mock-dashboard-component')).toHaveTextContent( - props.availableColumnCount - props.occupiedColumnCount, + `${props.availableColumnCount - props.occupiedColumnCount}`, ); }); test('should increment the depth of its children', () => { const { getByTestId } = setup(); expect(getByTestId('mock-dashboard-component')).toHaveAttribute( - 'depth', + 'data-depth', `${props.depth + 1}`, ); }); @@ -222,7 +268,7 @@ describe('visibility handling for intersection observers', () => { }); afterAll(() => { - delete window.IntersectionObserver; + delete (window as any).IntersectionObserver; }); test('should handle visibility prop changes without crashing', () => { @@ -261,7 +307,7 @@ describe('visibility handling for intersection observers', () => { }); test('intersection observer callbacks handle entries without errors', () => { - const callback = ([entry]) => { + const callback = ([entry]: [MockIntersectionObserverEntry]) => { if (entry.isIntersecting) return true; return false; diff --git a/superset-frontend/src/dashboard/components/gridComponents/Row/Row.jsx b/superset-frontend/src/dashboard/components/gridComponents/Row/Row.tsx similarity index 80% rename from superset-frontend/src/dashboard/components/gridComponents/Row/Row.jsx rename to superset-frontend/src/dashboard/components/gridComponents/Row/Row.tsx index ce361c29827..e4a6915ba11 100644 --- a/superset-frontend/src/dashboard/components/gridComponents/Row/Row.jsx +++ b/superset-frontend/src/dashboard/components/gridComponents/Row/Row.tsx @@ -24,13 +24,17 @@ import { useEffect, useMemo, memo, + RefObject, } from 'react'; -import PropTypes from 'prop-types'; import cx from 'classnames'; -import { FeatureFlag, isFeatureEnabled, t } from '@superset-ui/core'; -import { css, styled } from '@apache-superset/core/ui'; +import { + FeatureFlag, + isFeatureEnabled, + t, + JsonObject, +} from '@superset-ui/core'; +import { css, styled, SupersetTheme } from '@apache-superset/core/ui'; import { Icons, Constants } from '@superset-ui/core/components'; - import { Draggable, Droppable, @@ -42,7 +46,6 @@ import HoverMenu from 'src/dashboard/components/menu/HoverMenu'; import IconButton from 'src/dashboard/components/IconButton'; import BackgroundStyleDropdown from 'src/dashboard/components/menu/BackgroundStyleDropdown'; import WithPopoverMenu from 'src/dashboard/components/menu/WithPopoverMenu'; -import { componentShape } from 'src/dashboard/util/propShapes'; import backgroundStyleOptions from 'src/dashboard/util/backgroundStyleOptions'; import { BACKGROUND_TRANSPARENT } from 'src/dashboard/util/constants'; import { isEmbedded } from 'src/dashboard/util/isEmbedded'; @@ -50,31 +53,36 @@ import { EMPTY_CONTAINER_Z_INDEX } from 'src/dashboard/constants'; import { isCurrentUserBot } from 'src/utils/isBot'; import { useDebouncedEffect } from '../../../../explore/exploreUtils'; -const propTypes = { - id: PropTypes.string.isRequired, - parentId: PropTypes.string.isRequired, - component: componentShape.isRequired, - parentComponent: componentShape.isRequired, - index: PropTypes.number.isRequired, - depth: PropTypes.number.isRequired, - editMode: PropTypes.bool.isRequired, +export type RowProps = { + id: string; + parentId: string; + component: JsonObject; + parentComponent: JsonObject; + index: number; + depth: number; + editMode: boolean; // grid related - availableColumnCount: PropTypes.number.isRequired, - columnWidth: PropTypes.number.isRequired, - occupiedColumnCount: PropTypes.number.isRequired, - onResizeStart: PropTypes.func.isRequired, - onResize: PropTypes.func.isRequired, - onResizeStop: PropTypes.func.isRequired, - maxChildrenHeight: PropTypes.number.isRequired, + availableColumnCount: number; + columnWidth: number; + occupiedColumnCount: number; + maxChildrenHeight: number; + + onResizeStart: (e: unknown, direction: unknown) => void; + onResize: (e: unknown, direction: unknown, ref: HTMLElement) => void; + onResizeStop: (e: unknown, direction: unknown, ref: HTMLElement) => void; // dnd - handleComponentDrop: PropTypes.func.isRequired, - deleteComponent: PropTypes.func.isRequired, - updateComponents: PropTypes.func.isRequired, + handleComponentDrop: (dropResult: unknown) => void; + deleteComponent: (id: string, parentId: string) => void; + updateComponents: (updates: Record) => void; + + // visibility + isComponentVisible: boolean; + onChangeTab: (tabId: string) => void; }; -const GridRow = styled.div` +const GridRow = styled.div<{ editMode: boolean }>` ${({ theme, editMode }) => css` position: relative; display: flex; @@ -119,7 +127,7 @@ const GridRow = styled.div` `} `; -const emptyRowContentStyles = theme => css` +const emptyRowContentStyles = (theme: SupersetTheme) => css` position: absolute; width: 100%; height: 100%; @@ -129,7 +137,7 @@ const emptyRowContentStyles = theme => css` color: ${theme.colorTextLabel}; `; -const Row = props => { +const Row = memo((props: RowProps) => { const { component: rowComponent, parentComponent, @@ -153,8 +161,8 @@ const Row = props => { const [isFocused, setIsFocused] = useState(false); const [isInView, setIsInView] = useState(false); const [hoverMenuHovered, setHoverMenuHovered] = useState(false); - const [containerHeight, setContainerHeight] = useState(null); - const containerRef = useRef(); + const [containerHeight, setContainerHeight] = useState(null); + const containerRef = useRef(null); const isComponentVisibleRef = useRef(isComponentVisible); useEffect(() => { @@ -164,8 +172,8 @@ const Row = props => { // if chart not rendered - render it if it's less than 1 view height away from current viewport // if chart rendered - remove it if it's more than 4 view heights away from current viewport useEffect(() => { - let observerEnabler; - let observerDisabler; + let observerEnabler: IntersectionObserver | undefined; + let observerDisabler: IntersectionObserver | undefined; if ( isFeatureEnabled(FeatureFlag.DashboardVirtualization) && @@ -219,23 +227,23 @@ const Row = props => { containerRef.current && updatedHeight !== containerHeight ) { - setContainerHeight(updatedHeight); + setContainerHeight(updatedHeight ?? null); } }, Constants.FAST_DEBOUNCE, [editMode, containerHeight], ); - const handleChangeFocus = useCallback(nextFocus => { + const handleChangeFocus = useCallback((nextFocus: boolean) => { setIsFocused(Boolean(nextFocus)); }, []); const handleChangeBackground = useCallback( - nextValue => { + (nextValue: string) => { const metaKey = 'background'; - if (nextValue && rowComponent.meta[metaKey] !== nextValue) { + if (nextValue && rowComponent.meta?.[metaKey] !== nextValue) { updateComponents({ - [rowComponent.id]: { + [rowComponent.id as string]: { ...rowComponent, meta: { ...rowComponent.meta, @@ -249,26 +257,30 @@ const Row = props => { ); const handleDeleteComponent = useCallback(() => { - deleteComponent(rowComponent.id, parentId); + deleteComponent(rowComponent.id as string, parentId); }, [deleteComponent, rowComponent, parentId]); - const handleMenuHover = useCallback(hovered => { - const { isHovered } = hovered; - setHoverMenuHovered(isHovered); + const handleMenuHover = useCallback((hover: { isHovered: boolean }) => { + setHoverMenuHovered(hover.isHovered); }, []); - const rowItems = useMemo( - () => rowComponent.children || [], + const rowItems: string[] = useMemo( + () => + Array.isArray(rowComponent.children) + ? (rowComponent.children as string[]) + : [], [rowComponent.children], ); - const backgroundStyle = backgroundStyleOptions.find( - opt => - opt.value === (rowComponent.meta.background || BACKGROUND_TRANSPARENT), - ); + const backgroundStyle = + backgroundStyleOptions.find( + opt => + opt.value === (rowComponent.meta?.background ?? BACKGROUND_TRANSPARENT), + ) ?? backgroundStyleOptions[0]; + const remainColumnCount = availableColumnCount - occupiedColumnCount; const renderChild = useCallback( - ({ dragSourceRef }) => ( + ({ dragSourceRef }: { dragSourceRef: RefObject }) => ( { handleChangeFocus(true)} icon={} /> @@ -334,13 +346,13 @@ const Row = props => { ...(rowItems.length > 0 && { width: 16 }), }} > - {({ dropIndicatorProps }) => + {({ dropIndicatorProps }: { dropIndicatorProps: JsonObject }) => dropIndicatorProps &&
} )} {rowItems.length === 0 && ( -
{t('Empty row')}
+
{t('Empty row')}
)} {rowItems.length > 0 && rowItems.map((componentId, itemIndex) => ( @@ -348,7 +360,7 @@ const Row = props => { { itemIndex === rowItems.length - 1 && { width: 16 }), }} > - {({ dropIndicatorProps }) => - dropIndicatorProps &&
- } + {({ + dropIndicatorProps, + }: { + dropIndicatorProps: JsonObject; + }) => dropIndicatorProps &&
} )} @@ -431,8 +445,6 @@ const Row = props => { {renderChild} ); -}; +}); -Row.propTypes = propTypes; - -export default memo(Row); +export default Row; diff --git a/superset-frontend/src/dashboard/components/gridComponents/Row/index.js b/superset-frontend/src/dashboard/components/gridComponents/Row/index.ts similarity index 94% rename from superset-frontend/src/dashboard/components/gridComponents/Row/index.js rename to superset-frontend/src/dashboard/components/gridComponents/Row/index.ts index 2b78be10dc7..c432b945742 100644 --- a/superset-frontend/src/dashboard/components/gridComponents/Row/index.js +++ b/superset-frontend/src/dashboard/components/gridComponents/Row/index.ts @@ -16,6 +16,4 @@ * specific language governing permissions and limitations * under the License. */ -import Row from './Row'; - -export default Row; +export { default } from './Row'; diff --git a/superset-frontend/src/dashboard/components/gridComponents/new/NewRow.test.jsx b/superset-frontend/src/dashboard/components/gridComponents/new/NewRow.test.tsx similarity index 91% rename from superset-frontend/src/dashboard/components/gridComponents/new/NewRow.test.jsx rename to superset-frontend/src/dashboard/components/gridComponents/new/NewRow.test.tsx index 9e222ed3f07..45f31c8c442 100644 --- a/superset-frontend/src/dashboard/components/gridComponents/new/NewRow.test.jsx +++ b/superset-frontend/src/dashboard/components/gridComponents/new/NewRow.test.tsx @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import { render } from 'spec/helpers/testing-library'; +import { render, RenderResult } from 'spec/helpers/testing-library'; import NewRow from 'src/dashboard/components/gridComponents/new/NewRow'; @@ -26,12 +26,12 @@ import { ROW_TYPE } from 'src/dashboard/util/componentTypes'; jest.mock( 'src/dashboard/components/gridComponents/new/DraggableNewComponent', () => - ({ type, id }) => ( + ({ type, id }: { type: string; id: string }) => (
{`${type}:${id}`}
), ); -function setup() { +function setup(): RenderResult { return render(); } diff --git a/superset-frontend/src/dashboard/components/gridComponents/new/NewRow.jsx b/superset-frontend/src/dashboard/components/gridComponents/new/NewRow.tsx similarity index 78% rename from superset-frontend/src/dashboard/components/gridComponents/new/NewRow.jsx rename to superset-frontend/src/dashboard/components/gridComponents/new/NewRow.tsx index 8f735cadbf9..ce18f7a50ac 100644 --- a/superset-frontend/src/dashboard/components/gridComponents/new/NewRow.jsx +++ b/superset-frontend/src/dashboard/components/gridComponents/new/NewRow.tsx @@ -22,14 +22,17 @@ import { Icons } from '@superset-ui/core/components'; import { ROW_TYPE } from '../../../util/componentTypes'; import { NEW_ROW_ID } from '../../../util/constants'; import DraggableNewComponent from './DraggableNewComponent'; +import { FC } from 'react'; -export default function DraggableNewRow() { - return ( - - ); -} +type DraggableNewRowProps = {}; + +const DraggableNewRow: FC = () => ( + +); + +export default DraggableNewRow; diff --git a/superset-frontend/src/dashboard/containers/DashboardComponent.jsx b/superset-frontend/src/dashboard/containers/DashboardComponent.jsx index 2bbc51915d0..2e35ffbc0d4 100644 --- a/superset-frontend/src/dashboard/containers/DashboardComponent.jsx +++ b/superset-frontend/src/dashboard/containers/DashboardComponent.jsx @@ -49,6 +49,12 @@ const propTypes = { directPathToChild: PropTypes.arrayOf(PropTypes.string), directPathLastUpdated: PropTypes.number, isComponentVisible: PropTypes.bool, + availableColumnCount: PropTypes.number, + columnWidth: PropTypes.number, + onResizeStart: PropTypes.func, + onResize: PropTypes.func, + onResizeStop: PropTypes.func, + isInView: PropTypes.bool, }; const DashboardComponent = props => {