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 }) => (
-
+ 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 => {