diff --git a/superset-frontend/src/dashboard/components/gridComponents/Column.jsx b/superset-frontend/src/dashboard/components/gridComponents/Column.jsx
index d11937ab176ab..71c1892f3b887 100644
--- a/superset-frontend/src/dashboard/components/gridComponents/Column.jsx
+++ b/superset-frontend/src/dashboard/components/gridComponents/Column.jsx
@@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
-import { PureComponent, Fragment } from 'react';
+import { Fragment, useCallback, useState, useMemo, memo } from 'react';
import PropTypes from 'prop-types';
import cx from 'classnames';
import { css, styled, t } from '@superset-ui/core';
@@ -119,203 +119,219 @@ const emptyColumnContentStyles = theme => css`
color: ${theme.colors.text.label};
`;
-class Column extends PureComponent {
- constructor(props) {
- super(props);
- this.state = {
- isFocused: false,
- };
- this.handleChangeBackground = this.handleUpdateMeta.bind(
- this,
- 'background',
- );
- this.handleChangeFocus = this.handleChangeFocus.bind(this);
- this.handleDeleteComponent = this.handleDeleteComponent.bind(this);
- }
+const Column = props => {
+ const {
+ component: columnComponent,
+ parentComponent,
+ index,
+ availableColumnCount,
+ columnWidth,
+ minColumnWidth,
+ depth,
+ onResizeStart,
+ onResize,
+ onResizeStop,
+ handleComponentDrop,
+ editMode,
+ onChangeTab,
+ isComponentVisible,
+ deleteComponent,
+ id,
+ parentId,
+ updateComponents,
+ } = props;
- handleDeleteComponent() {
- const { deleteComponent, id, parentId } = this.props;
+ const [isFocused, setIsFocused] = useState(false);
+
+ const handleDeleteComponent = useCallback(() => {
deleteComponent(id, parentId);
- }
+ }, [deleteComponent, id, parentId]);
- handleChangeFocus(nextFocus) {
- this.setState(() => ({ isFocused: Boolean(nextFocus) }));
- }
+ const handleChangeFocus = useCallback(nextFocus => {
+ setIsFocused(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,
+ const handleChangeBackground = useCallback(
+ nextValue => {
+ const metaKey = 'background';
+ if (nextValue && columnComponent.meta[metaKey] !== nextValue) {
+ updateComponents({
+ [columnComponent.id]: {
+ ...columnComponent,
+ meta: {
+ ...columnComponent.meta,
+ [metaKey]: nextValue,
+ },
},
- },
- });
- }
- }
+ });
+ }
+ },
+ [columnComponent, updateComponents],
+ );
- render() {
- const {
- component: columnComponent,
- parentComponent,
- index,
- availableColumnCount,
- columnWidth,
- minColumnWidth,
- depth,
- onResizeStart,
- onResize,
- onResizeStop,
- handleComponentDrop,
- editMode,
- onChangeTab,
- isComponentVisible,
- } = this.props;
+ const columnItems = useMemo(
+ () => columnComponent.children || [],
+ [columnComponent.children],
+ );
- const columnItems = columnComponent.children || [];
- const backgroundStyle = backgroundStyleOptions.find(
- opt =>
- opt.value ===
- (columnComponent.meta.background || BACKGROUND_TRANSPARENT),
- );
+ const backgroundStyle = backgroundStyleOptions.find(
+ opt =>
+ opt.value === (columnComponent.meta.background || BACKGROUND_TRANSPARENT),
+ );
- return (
- (
+
- {({ dragSourceRef }) => (
- ,
+ ]}
+ editMode={editMode}
+ >
+ {editMode && (
+
+
+
+ }
+ />
+
+ )}
+
- ,
- ]}
- editMode={editMode}
- >
- {editMode && (
-
-
-
- }
- />
-
- )}
-
- {editMode && (
- 0 && 'droptarget-edge',
- )}
- editMode
- >
- {({ dropIndicatorProps }) =>
- dropIndicatorProps &&
+ {editMode && (
+
+ : {
+ component: columnItems[0],
+ })}
+ depth={depth}
+ index={0}
+ orientation="column"
+ onDrop={handleComponentDrop}
+ className={cx(
+ 'empty-droptarget',
+ columnItems.length > 0 && 'droptarget-edge',
)}
- {columnItems.length === 0 ? (
- {t('Empty column')}
- ) : (
- columnItems.map((componentId, itemIndex) => (
-
-
- {editMode && (
-
- {({ dropIndicatorProps }) =>
- dropIndicatorProps && (
-
- )
- }
-
+ editMode
+ >
+ {({ dropIndicatorProps }) =>
+ dropIndicatorProps &&
+ }
+
+ )}
+ {columnItems.length === 0 ? (
+ {t('Empty column')}
+ ) : (
+ columnItems.map((componentId, itemIndex) => (
+
+
+ {editMode && (
+
- ))
- )}
-
-
-
- )}
-
- );
- }
-}
+ editMode
+ >
+ {({ dropIndicatorProps }) =>
+ dropIndicatorProps &&
+ }
+
+ )}
+
+ ))
+ )}
+
+
+
+ ),
+ [
+ availableColumnCount,
+ backgroundStyle.className,
+ columnComponent,
+ columnItems,
+ columnWidth,
+ depth,
+ editMode,
+ handleChangeBackground,
+ handleChangeFocus,
+ handleComponentDrop,
+ handleDeleteComponent,
+ isComponentVisible,
+ isFocused,
+ minColumnWidth,
+ onChangeTab,
+ onResize,
+ onResizeStart,
+ onResizeStop,
+ ],
+ );
+
+ return (
+
+ {renderChild}
+
+ );
+};
Column.propTypes = propTypes;
Column.defaultProps = defaultProps;
-export default Column;
+export default memo(Column);
diff --git a/superset-frontend/src/dashboard/components/gridComponents/DynamicComponent.tsx b/superset-frontend/src/dashboard/components/gridComponents/DynamicComponent.tsx
index e30b4a5455126..3b97c277d9c72 100644
--- a/superset-frontend/src/dashboard/components/gridComponents/DynamicComponent.tsx
+++ b/superset-frontend/src/dashboard/components/gridComponents/DynamicComponent.tsx
@@ -20,7 +20,7 @@ import { FC, Suspense } from 'react';
import { DashboardComponentMetadata, JsonObject, t } from '@superset-ui/core';
import backgroundStyleOptions from 'src/dashboard/util/backgroundStyleOptions';
import cx from 'classnames';
-import { useSelector } from 'react-redux';
+import { shallowEqual, useSelector } from 'react-redux';
import { Draggable } from '../dnd/DragDroppable';
import { COLUMN_TYPE, ROW_TYPE } from '../../util/componentTypes';
import WithPopoverMenu from '../menu/WithPopoverMenu';
@@ -103,6 +103,7 @@ const DynamicComponent: FC = ({
nativeFilters,
dataMask,
}),
+ shallowEqual,
);
return (
diff --git a/superset-frontend/src/dashboard/components/gridComponents/Row.jsx b/superset-frontend/src/dashboard/components/gridComponents/Row.jsx
index a22361e1421a9..bd59306ca97fa 100644
--- a/superset-frontend/src/dashboard/components/gridComponents/Row.jsx
+++ b/superset-frontend/src/dashboard/components/gridComponents/Row.jsx
@@ -16,10 +16,17 @@
* specific language governing permissions and limitations
* under the License.
*/
-import { createRef, PureComponent, Fragment } from 'react';
+import {
+ Fragment,
+ useState,
+ useCallback,
+ useRef,
+ useEffect,
+ useMemo,
+ memo,
+} from 'react';
import PropTypes from 'prop-types';
import cx from 'classnames';
-import { debounce } from 'lodash';
import {
css,
FAST_DEBOUNCE,
@@ -46,6 +53,7 @@ import backgroundStyleOptions from 'src/dashboard/util/backgroundStyleOptions';
import { BACKGROUND_TRANSPARENT } from 'src/dashboard/util/constants';
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,
@@ -126,285 +134,301 @@ const emptyRowContentStyles = theme => css`
color: ${theme.colors.text.label};
`;
-class Row extends PureComponent {
- constructor(props) {
- super(props);
- this.state = {
- isFocused: false,
- isInView: false,
- hoverMenuHovered: false,
- };
- this.handleDeleteComponent = this.handleDeleteComponent.bind(this);
- this.handleUpdateMeta = this.handleUpdateMeta.bind(this);
- this.handleChangeBackground = this.handleUpdateMeta.bind(
- this,
- 'background',
- );
- this.handleChangeFocus = this.handleChangeFocus.bind(this);
- this.handleMenuHover = this.handleMenuHover.bind(this);
- this.setVerticalEmptyContainerHeight = debounce(
- this.setVerticalEmptyContainerHeight.bind(this),
- FAST_DEBOUNCE,
- );
+const Row = props => {
+ const {
+ component: rowComponent,
+ parentComponent,
+ index,
+ availableColumnCount,
+ columnWidth,
+ occupiedColumnCount,
+ depth,
+ onResizeStart,
+ onResize,
+ onResizeStop,
+ handleComponentDrop,
+ editMode,
+ onChangeTab,
+ isComponentVisible,
+ updateComponents,
+ deleteComponent,
+ parentId,
+ } = 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 isComponentVisibleRef = useRef(isComponentVisible);
- this.containerRef = createRef();
- this.observerEnabler = null;
- this.observerDisabler = null;
- }
+ useEffect(() => {
+ isComponentVisibleRef.current = isComponentVisible;
+ }, [isComponentVisible]);
// 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
- componentDidMount() {
+ useEffect(() => {
+ let observerEnabler;
+ let observerDisabler;
if (
isFeatureEnabled(FeatureFlag.DashboardVirtualization) &&
!isCurrentUserBot()
) {
- this.observerEnabler = new IntersectionObserver(
+ observerEnabler = new IntersectionObserver(
([entry]) => {
- if (entry.isIntersecting && !this.state.isInView) {
- this.setState({ isInView: true });
+ if (entry.isIntersecting && isComponentVisibleRef.current) {
+ setIsInView(true);
}
},
{
rootMargin: '100% 0px',
},
);
- this.observerDisabler = new IntersectionObserver(
+ observerDisabler = new IntersectionObserver(
([entry]) => {
- if (!entry.isIntersecting && this.state.isInView) {
- this.setState({ isInView: false });
+ if (!entry.isIntersecting && isComponentVisibleRef.current) {
+ setIsInView(false);
}
},
{
rootMargin: '400% 0px',
},
);
- const element = this.containerRef.current;
+ const element = containerRef.current;
if (element) {
- this.observerEnabler.observe(element);
- this.observerDisabler.observe(element);
- this.setVerticalEmptyContainerHeight();
+ observerEnabler.observe(element);
+ observerDisabler.observe(element);
}
}
- }
-
- componentDidUpdate() {
- this.setVerticalEmptyContainerHeight();
- }
-
- setVerticalEmptyContainerHeight() {
- const { containerHeight } = this.state;
- const { editMode } = this.props;
- const updatedHeight = this.containerRef.current?.clientHeight;
- if (
- editMode &&
- this.containerRef.current &&
- updatedHeight !== containerHeight
- ) {
- this.setState({ containerHeight: updatedHeight });
- }
- }
+ return () => {
+ observerEnabler?.disconnect();
+ observerDisabler?.disconnect();
+ };
+ }, []);
- componentWillUnmount() {
- this.observerEnabler?.disconnect();
- this.observerDisabler?.disconnect();
- }
+ useDebouncedEffect(
+ () => {
+ const updatedHeight = containerRef.current?.clientHeight;
+ if (
+ editMode &&
+ containerRef.current &&
+ updatedHeight !== containerHeight
+ ) {
+ setContainerHeight(updatedHeight);
+ }
+ },
+ FAST_DEBOUNCE,
+ [editMode, containerHeight],
+ );
- handleChangeFocus(nextFocus) {
- this.setState(() => ({ isFocused: Boolean(nextFocus) }));
- }
+ const handleChangeFocus = useCallback(nextFocus => {
+ setIsFocused(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,
+ const handleChangeBackground = useCallback(
+ nextValue => {
+ const metaKey = 'background';
+ if (nextValue && rowComponent.meta[metaKey] !== nextValue) {
+ updateComponents({
+ [rowComponent.id]: {
+ ...rowComponent,
+ meta: {
+ ...rowComponent.meta,
+ [metaKey]: nextValue,
+ },
},
- },
- });
- }
- }
+ });
+ }
+ },
+ [updateComponents, rowComponent],
+ );
- handleDeleteComponent() {
- const { deleteComponent, component, parentId } = this.props;
- deleteComponent(component.id, parentId);
- }
+ const handleDeleteComponent = useCallback(() => {
+ deleteComponent(rowComponent.id, parentId);
+ }, [deleteComponent, rowComponent, parentId]);
- handleMenuHover = hovered => {
+ const handleMenuHover = useCallback(hovered => {
const { isHovered } = hovered;
- this.setState(() => ({ hoverMenuHovered: isHovered }));
- };
+ setHoverMenuHovered(isHovered);
+ }, []);
- render() {
- const {
- component: rowComponent,
- parentComponent,
- index,
- availableColumnCount,
- columnWidth,
- occupiedColumnCount,
- depth,
- onResizeStart,
- onResize,
- onResizeStop,
- handleComponentDrop,
- editMode,
- onChangeTab,
- isComponentVisible,
- } = this.props;
- const { containerHeight, hoverMenuHovered } = this.state;
+ const rowItems = useMemo(
+ () => rowComponent.children || [],
+ [rowComponent.children],
+ );
- const rowItems = rowComponent.children || [];
-
- const backgroundStyle = backgroundStyleOptions.find(
- opt =>
- opt.value === (rowComponent.meta.background || BACKGROUND_TRANSPARENT),
- );
- const remainColumnCount = availableColumnCount - occupiedColumnCount;
-
- return (
-
+ opt.value === (rowComponent.meta.background || BACKGROUND_TRANSPARENT),
+ );
+ const remainColumnCount = availableColumnCount - occupiedColumnCount;
+ const renderChild = useCallback(
+ ({ dragSourceRef }) => (
+ ,
+ ]}
editMode={editMode}
>
- {({ dragSourceRef }) => (
- ,
- ]}
- editMode={editMode}
+ {editMode && (
+
- {editMode && (
-
-
-
- }
- />
-
- )}
-
+
+ }
+ />
+
+ )}
+
+ {editMode && (
+ 0 && 'droptarget-side',
)}
- data-test={`grid-row-${backgroundStyle.className}`}
- ref={this.containerRef}
- editMode={editMode}
+ editMode
+ style={{
+ height: rowItems.length > 0 ? containerHeight : '100%',
+ ...(rowItems.length > 0 && { width: 16 }),
+ }}
>
- {editMode && (
- 0 && 'droptarget-side',
- )}
- editMode
- style={{
- height: rowItems.length > 0 ? containerHeight : '100%',
- ...(rowItems.length > 0 && { width: 16 }),
- }}
- >
- {({ dropIndicatorProps }) =>
- dropIndicatorProps &&
- }
-
- )}
- {rowItems.length === 0 && (
- {t('Empty row')}
- )}
- {rowItems.length > 0 &&
- rowItems.map((componentId, itemIndex) => (
-
-
- {editMode && (
-
- {({ dropIndicatorProps }) =>
- dropIndicatorProps &&
- }
-
+ {({ dropIndicatorProps }) =>
+ dropIndicatorProps &&
+ }
+
+ )}
+ {rowItems.length === 0 && (
+ {t('Empty row')}
+ )}
+ {rowItems.length > 0 &&
+ rowItems.map((componentId, itemIndex) => (
+
+
+ {editMode && (
+
- ))}
-
-
- )}
-
- );
- }
-}
+ editMode
+ style={{
+ height: containerHeight,
+ ...(remainColumnCount === 0 &&
+ itemIndex === rowItems.length - 1 && { width: 16 }),
+ }}
+ >
+ {({ dropIndicatorProps }) =>
+ dropIndicatorProps &&
+ }
+
+ )}
+
+ ))}
+
+
+ ),
+ [
+ backgroundStyle.className,
+ backgroundStyle.value,
+ columnWidth,
+ containerHeight,
+ depth,
+ editMode,
+ handleChangeBackground,
+ handleChangeFocus,
+ handleComponentDrop,
+ handleDeleteComponent,
+ handleMenuHover,
+ hoverMenuHovered,
+ isComponentVisible,
+ isFocused,
+ isInView,
+ onChangeTab,
+ onResize,
+ onResizeStart,
+ onResizeStop,
+ remainColumnCount,
+ rowComponent,
+ rowItems,
+ ],
+ );
+
+ return (
+
+ {renderChild}
+
+ );
+};
Row.propTypes = propTypes;
-export default Row;
+export default memo(Row);
diff --git a/superset-frontend/src/dashboard/components/gridComponents/Tab.jsx b/superset-frontend/src/dashboard/components/gridComponents/Tab.jsx
index 4d75f8edafd95..2c4695f8c7285 100644
--- a/superset-frontend/src/dashboard/components/gridComponents/Tab.jsx
+++ b/superset-frontend/src/dashboard/components/gridComponents/Tab.jsx
@@ -16,11 +16,10 @@
* specific language governing permissions and limitations
* under the License.
*/
-import { PureComponent, Fragment } from 'react';
+import { Fragment, useCallback, memo } from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
-import { bindActionCreators } from 'redux';
-import { connect } from 'react-redux';
+import { useDispatch, useSelector } from 'react-redux';
import { styled, t } from '@superset-ui/core';
import { EmptyStateMedium } from 'src/components/EmptyState';
@@ -51,7 +50,6 @@ const propTypes = {
onDragTab: PropTypes.func,
onHoverTab: PropTypes.func,
editMode: PropTypes.bool.isRequired,
- canEdit: PropTypes.bool.isRequired,
embeddedMode: PropTypes.bool,
// grid related
@@ -65,7 +63,6 @@ const propTypes = {
handleComponentDrop: PropTypes.func.isRequired,
updateComponents: PropTypes.func.isRequired,
setDirectPathToChild: PropTypes.func.isRequired,
- setEditMode: PropTypes.func.isRequired,
};
const defaultProps = {
@@ -102,62 +99,65 @@ const TitleDropIndicator = styled.div`
const renderDraggableContent = dropProps =>
dropProps.dropIndicatorProps && ;
-class Tab extends PureComponent {
- constructor(props) {
- super(props);
- this.handleChangeText = this.handleChangeText.bind(this);
- this.handleDrop = this.handleDrop.bind(this);
- this.handleOnHover = this.handleOnHover.bind(this);
- this.handleTopDropTargetDrop = this.handleTopDropTargetDrop.bind(this);
- this.handleChangeTab = this.handleChangeTab.bind(this);
- }
-
- handleChangeTab({ pathToTabIndex }) {
- this.props.setDirectPathToChild(pathToTabIndex);
- }
+const Tab = props => {
+ const dispatch = useDispatch();
+ const canEdit = useSelector(state => state.dashboardInfo.dash_edit_perm);
+ const handleChangeTab = useCallback(
+ ({ pathToTabIndex }) => {
+ props.setDirectPathToChild(pathToTabIndex);
+ },
+ [props.setDirectPathToChild],
+ );
- handleChangeText(nextTabText) {
- const { updateComponents, component } = this.props;
- if (nextTabText && nextTabText !== component.meta.text) {
- updateComponents({
- [component.id]: {
- ...component,
- meta: {
- ...component.meta,
- text: nextTabText,
+ const handleChangeText = useCallback(
+ nextTabText => {
+ const { updateComponents, component } = props;
+ if (nextTabText && nextTabText !== component.meta.text) {
+ updateComponents({
+ [component.id]: {
+ ...component,
+ meta: {
+ ...component.meta,
+ text: nextTabText,
+ },
},
- },
- });
- }
- }
+ });
+ }
+ },
+ [props.updateComponents, props.component],
+ );
- handleDrop(dropResult) {
- this.props.handleComponentDrop(dropResult);
- this.props.onDropOnTab(dropResult);
- }
+ const handleDrop = useCallback(
+ dropResult => {
+ props.handleComponentDrop(dropResult);
+ props.onDropOnTab(dropResult);
+ },
+ [props.handleComponentDrop, props.onDropOnTab],
+ );
- handleOnHover() {
- this.props.onHoverTab();
- }
+ const handleHoverTab = useCallback(() => {
+ props.onHoverTab?.();
+ }, [props.onHoverTab]);
- handleTopDropTargetDrop(dropResult) {
- if (dropResult) {
- this.props.handleComponentDrop({
- ...dropResult,
- destination: {
- ...dropResult.destination,
- // force appending as the first child if top drop target
- index: 0,
- },
- });
- }
- }
+ const handleTopDropTargetDrop = useCallback(
+ dropResult => {
+ if (dropResult) {
+ props.handleComponentDrop({
+ ...dropResult,
+ destination: {
+ ...dropResult.destination,
+ // force appending as the first child if top drop target
+ index: 0,
+ },
+ });
+ }
+ },
+ [props.handleComponentDrop],
+ );
- shouldDropToChild(item) {
- return item.type !== TAB_TYPE;
- }
+ const shouldDropToChild = useCallback(item => item.type !== TAB_TYPE, []);
- renderTabContent() {
+ const renderTabContent = useCallback(() => {
const {
component: tabComponent,
depth,
@@ -168,10 +168,8 @@ class Tab extends PureComponent {
onResizeStop,
editMode,
isComponentVisible,
- canEdit,
- setEditMode,
dashboardId,
- } = this.props;
+ } = props;
const shouldDisplayEmptyState = tabComponent.children.length === 0;
return (
@@ -185,8 +183,8 @@ class Tab extends PureComponent {
depth={depth}
onDrop={
tabComponent.children.length === 0
- ? this.handleTopDropTargetDrop
- : this.handleDrop
+ ? handleTopDropTargetDrop
+ : handleDrop
}
editMode
className={classNames({
@@ -225,7 +223,7 @@ class Tab extends PureComponent {
setEditMode(true)}
+ onClick={() => dispatch(setEditMode(true))}
>
{t('edit mode')}
@@ -242,15 +240,15 @@ class Tab extends PureComponent {
parentId={tabComponent.id}
depth={depth} // see isValidChild.js for why tabs don't increment child depth
index={componentIndex}
- onDrop={this.handleDrop}
- onHover={this.handleOnHover}
+ onDrop={handleDrop}
+ onHover={handleHoverTab}
availableColumnCount={availableColumnCount}
columnWidth={columnWidth}
onResizeStart={onResizeStart}
onResize={onResize}
onResizeStop={onResizeStop}
isComponentVisible={isComponentVisible}
- onChangeTab={this.handleChangeTab}
+ onChangeTab={handleChangeTab}
/>
{/* Make bottom of tab droppable */}
{editMode && (
@@ -259,7 +257,7 @@ class Tab extends PureComponent {
orientation="column"
index={componentIndex + 1}
depth={depth}
- onDrop={this.handleDrop}
+ onDrop={handleDrop}
editMode
className="empty-droptarget"
>
@@ -270,21 +268,95 @@ class Tab extends PureComponent {
))}
);
- }
+ }, [
+ dispatch,
+ props.component,
+ props.depth,
+ props.availableColumnCount,
+ props.columnWidth,
+ props.onResizeStart,
+ props.onResize,
+ props.onResizeStop,
+ props.editMode,
+ props.isComponentVisible,
+ props.dashboardId,
+ props.handleComponentDrop,
+ props.onDropOnTab,
+ props.setDirectPathToChild,
+ props.updateComponents,
+ handleHoverTab,
+ canEdit,
+ handleChangeTab,
+ handleChangeText,
+ handleDrop,
+ handleTopDropTargetDrop,
+ shouldDropToChild,
+ ]);
- renderTab() {
+ const renderTabChild = useCallback(
+ ({ dropIndicatorProps, dragSourceRef, draggingTabOnTab }) => {
+ const {
+ component,
+ index,
+ editMode,
+ isFocused,
+ isHighlighted,
+ dashboardId,
+ embeddedMode,
+ } = props;
+ return (
+
+
+ {!editMode && !embeddedMode && (
+ = 5 ? 'left' : 'right'}
+ />
+ )}
+
+ {dropIndicatorProps && !draggingTabOnTab && (
+
+ )}
+
+ );
+ },
+ [
+ props.component,
+ props.index,
+ props.editMode,
+ props.isFocused,
+ props.isHighlighted,
+ props.dashboardId,
+ handleChangeText,
+ ],
+ );
+
+ const renderTab = useCallback(() => {
const {
component,
parentComponent,
index,
depth,
editMode,
- isFocused,
- isHighlighted,
onDropPositionChange,
onDragTab,
- embeddedMode,
- } = this.props;
+ } = props;
return (
- {({ dropIndicatorProps, dragSourceRef, draggingTabOnTab }) => (
-
-
- {!editMode && !embeddedMode && (
- = 5 ? 'left' : 'right'}
- />
- )}
- {dropIndicatorProps && !draggingTabOnTab && (
-
- )}
-
- )}
+ {renderTabChild}
);
- }
+ }, [
+ props.component,
+ props.parentComponent,
+ props.index,
+ props.depth,
+ props.editMode,
+ handleDrop,
+ handleHoverTab,
+ shouldDropToChild,
+ renderTabChild,
+ ]);
- render() {
- const { renderType } = this.props;
- return renderType === RENDER_TAB
- ? this.renderTab()
- : this.renderTabContent();
- }
-}
+ return props.renderType === RENDER_TAB ? renderTab() : renderTabContent();
+};
Tab.propTypes = propTypes;
Tab.defaultProps = defaultProps;
-function mapStateToProps(state) {
- return {
- canEdit: state.dashboardInfo.dash_edit_perm,
- };
-}
-
-function mapDispatchToProps(dispatch) {
- return bindActionCreators(
- {
- setEditMode,
- },
- dispatch,
- );
-}
-
-export default connect(mapStateToProps, mapDispatchToProps)(Tab);
+export default memo(Tab);
diff --git a/superset-frontend/src/dashboard/components/gridComponents/Tabs.jsx b/superset-frontend/src/dashboard/components/gridComponents/Tabs.jsx
index 901d8729b1158..19d49254da09f 100644
--- a/superset-frontend/src/dashboard/components/gridComponents/Tabs.jsx
+++ b/superset-frontend/src/dashboard/components/gridComponents/Tabs.jsx
@@ -16,10 +16,10 @@
* specific language governing permissions and limitations
* under the License.
*/
-import { PureComponent } from 'react';
+import { useCallback, useEffect, useMemo, useState, memo } from 'react';
import PropTypes from 'prop-types';
-import { styled, t } from '@superset-ui/core';
-import { connect } from 'react-redux';
+import { styled, t, usePrevious } from '@superset-ui/core';
+import { useSelector } from 'react-redux';
import { LineEditableTabs } from 'src/components/Tabs';
import Icons from 'src/components/Icons';
import { LOG_ACTIONS_SELECT_DASHBOARD_TAB } from 'src/logger/LogUtils';
@@ -48,7 +48,6 @@ const propTypes = {
renderTabContent: PropTypes.bool, // whether to render tabs + content or just tabs
editMode: PropTypes.bool.isRequired,
renderHoverMenu: PropTypes.bool,
- directPathToChild: PropTypes.arrayOf(PropTypes.string),
activeTabs: PropTypes.arrayOf(PropTypes.string),
// actions (from DashboardComponent.jsx)
@@ -71,12 +70,6 @@ const propTypes = {
};
const defaultProps = {
- renderTabContent: true,
- renderHoverMenu: true,
- availableColumnCount: 0,
- columnWidth: 0,
- activeTabs: [],
- directPathToChild: [],
setActiveTab() {},
onResizeStart() {},
onResize() {},
@@ -133,58 +126,76 @@ const CloseIconWithDropIndicator = props => (
>
);
-export class Tabs extends PureComponent {
- constructor(props) {
- super(props);
- const { tabIndex, activeKey } = this.getTabInfo(props);
+const Tabs = props => {
+ const nativeFilters = useSelector(state => state.nativeFilters);
+ const activeTabs = useSelector(state => state.dashboardState.activeTabs);
+ const directPathToChild = useSelector(
+ state => state.dashboardState.directPathToChild,
+ );
- this.state = {
+ const { tabIndex: initTabIndex, activeKey: initActiveKey } = useMemo(() => {
+ let tabIndex = Math.max(
+ 0,
+ findTabIndexByComponentId({
+ currentComponent: props.component,
+ directPathToChild,
+ }),
+ );
+ if (tabIndex === 0 && activeTabs?.length) {
+ props.component.children.forEach((tabId, index) => {
+ if (tabIndex === 0 && activeTabs?.includes(tabId)) {
+ tabIndex = index;
+ }
+ });
+ }
+ const { children: tabIds } = props.component;
+ const activeKey = tabIds[tabIndex];
+
+ return {
tabIndex,
activeKey,
};
- this.handleClickTab = this.handleClickTab.bind(this);
- this.handleDeleteComponent = this.handleDeleteComponent.bind(this);
- this.handleDeleteTab = this.handleDeleteTab.bind(this);
- this.handleDropOnTab = this.handleDropOnTab.bind(this);
- this.handleDrop = this.handleDrop.bind(this);
- this.handleGetDropPosition = this.handleGetDropPosition.bind(this);
- this.handleDragggingTab = this.handleDragggingTab.bind(this);
- }
-
- componentDidMount() {
- this.props.setActiveTab(this.state.activeKey);
- }
-
- componentDidUpdate(prevProps, prevState) {
- if (prevState.activeKey !== this.state.activeKey) {
- this.props.setActiveTab(this.state.activeKey, prevState.activeKey);
+ }, [activeTabs, props.component, directPathToChild]);
+
+ const [activeKey, setActiveKey] = useState(initActiveKey);
+ const [selectedTabIndex, setSelectedTabIndex] = useState(initTabIndex);
+ const [dropPosition, setDropPosition] = useState(null);
+ const [dragOverTabIndex, setDragOverTabIndex] = useState(null);
+ const [draggingTabId, setDraggingTabId] = useState(null);
+ const prevActiveKey = usePrevious(activeKey);
+ const prevDashboardId = usePrevious(props.dashboardId);
+ const prevDirectPathToChild = usePrevious(directPathToChild);
+ const prevTabIds = usePrevious(props.component.children);
+
+ useEffect(() => {
+ if (prevActiveKey) {
+ props.setActiveTab(activeKey, prevActiveKey);
+ } else {
+ props.setActiveTab(activeKey);
}
- }
+ }, [props.setActiveTab, prevActiveKey, activeKey]);
- UNSAFE_componentWillReceiveProps(nextProps) {
- const maxIndex = Math.max(0, nextProps.component.children.length - 1);
- const currTabsIds = this.props.component.children;
- const nextTabsIds = nextProps.component.children;
-
- if (this.state.tabIndex > maxIndex) {
- this.setState(() => ({ tabIndex: maxIndex }));
+ useEffect(() => {
+ if (prevDashboardId && props.dashboardId !== prevDashboardId) {
+ setSelectedTabIndex(initTabIndex);
+ setActiveKey(initActiveKey);
}
+ }, [props.dashboardId, prevDashboardId, initTabIndex, initActiveKey]);
- // reset tab index if dashboard was changed
- if (nextProps.dashboardId !== this.props.dashboardId) {
- const { tabIndex, activeKey } = this.getTabInfo(nextProps);
- this.setState(() => ({
- tabIndex,
- activeKey,
- }));
+ useEffect(() => {
+ const maxIndex = Math.max(0, props.component.children.length - 1);
+ if (selectedTabIndex > maxIndex) {
+ setSelectedTabIndex(maxIndex);
}
+ }, [selectedTabIndex, props.component.children.length, setSelectedTabIndex]);
- if (nextProps.isComponentVisible) {
- const nextFocusComponent = getLeafComponentIdFromPath(
- nextProps.directPathToChild,
- );
+ useEffect(() => {
+ const currTabsIds = props.component.children;
+
+ if (props.isComponentVisible) {
+ const nextFocusComponent = getLeafComponentIdFromPath(directPathToChild);
const currentFocusComponent = getLeafComponentIdFromPath(
- this.props.directPathToChild,
+ prevDirectPathToChild,
);
// If the currently selected component is different than the new one,
@@ -193,328 +204,349 @@ export class Tabs extends PureComponent {
if (
nextFocusComponent !== currentFocusComponent ||
(nextFocusComponent === currentFocusComponent &&
- currTabsIds !== nextTabsIds)
+ currTabsIds !== prevTabIds)
) {
const nextTabIndex = findTabIndexByComponentId({
- currentComponent: nextProps.component,
- directPathToChild: nextProps.directPathToChild,
+ currentComponent: props.component,
+ directPathToChild,
});
// make sure nextFocusComponent is under this tabs component
- if (nextTabIndex > -1 && nextTabIndex !== this.state.tabIndex) {
- this.setState(() => ({
- tabIndex: nextTabIndex,
- activeKey: nextTabsIds[nextTabIndex],
- }));
+ if (nextTabIndex > -1 && nextTabIndex !== selectedTabIndex) {
+ setSelectedTabIndex(nextTabIndex);
+ setActiveKey(currTabsIds[nextTabIndex]);
}
}
}
- }
+ }, [
+ props.component,
+ directPathToChild,
+ props.isComponentVisible,
+ selectedTabIndex,
+ prevDirectPathToChild,
+ prevTabIds,
+ ]);
+
+ const handleClickTab = useCallback(
+ tabIndex => {
+ const { component } = props;
+ const { children: tabIds } = component;
+
+ if (tabIndex !== selectedTabIndex) {
+ const pathToTabIndex = getDirectPathToTabIndex(component, tabIndex);
+ const targetTabId = pathToTabIndex[pathToTabIndex.length - 1];
+ props.logEvent(LOG_ACTIONS_SELECT_DASHBOARD_TAB, {
+ target_id: targetTabId,
+ index: tabIndex,
+ });
- getTabInfo = props => {
- let tabIndex = Math.max(
- 0,
- findTabIndexByComponentId({
- currentComponent: props.component,
- directPathToChild: props.directPathToChild,
- }),
- );
- if (tabIndex === 0 && props.activeTabs?.length) {
- props.component.children.forEach((tabId, index) => {
- if (tabIndex === 0 && props.activeTabs.includes(tabId)) {
- tabIndex = index;
+ props.onChangeTab({ pathToTabIndex });
+ }
+ setActiveKey(tabIds[tabIndex]);
+ },
+ [
+ props.component,
+ props.logEvent,
+ props.onChangeTab,
+ selectedTabIndex,
+ setActiveKey,
+ ],
+ );
+
+ const handleDropOnTab = useCallback(
+ dropResult => {
+ const { component } = props;
+
+ // Ensure dropped tab is visible
+ const { destination } = dropResult;
+ if (destination) {
+ const dropTabIndex =
+ destination.id === component.id
+ ? destination.index // dropped ON tabs
+ : component.children.indexOf(destination.id); // dropped IN tab
+
+ if (dropTabIndex > -1) {
+ setTimeout(() => {
+ handleClickTab(dropTabIndex);
+ }, 30);
}
- });
- }
- const { children: tabIds } = props.component;
- const activeKey = tabIds[tabIndex];
-
- return {
- tabIndex,
- activeKey,
- };
- };
-
- showDeleteConfirmModal = key => {
- const { component, deleteComponent } = this.props;
- AntdModal.confirm({
- title: t('Delete dashboard tab?'),
- content: (
-
- {t(
- 'Deleting a tab will remove all content within it and will deactivate any related alerts or reports. You may still ' +
- 'reverse this action with the',
- )}{' '}
- {t('undo')}{' '}
- {t('button (cmd + z) until you save your changes.')}
-
- ),
- onOk: () => {
- deleteComponent(key, component.id);
- const tabIndex = component.children.indexOf(key);
- this.handleDeleteTab(tabIndex);
- },
- okType: 'danger',
- okText: t('DELETE'),
- cancelText: t('CANCEL'),
- icon: null,
- });
- };
-
- handleEdit = (event, action) => {
- const { component, createComponent } = this.props;
- if (action === 'add') {
- // Prevent the tab container to be selected
- event?.stopPropagation?.();
-
- createComponent({
- destination: {
- id: component.id,
- type: component.type,
- index: component.children.length,
- },
- dragging: {
- id: NEW_TAB_ID,
- type: TAB_TYPE,
+ }
+ },
+ [props.component, handleClickTab],
+ );
+
+ const handleDrop = useCallback(
+ dropResult => {
+ if (dropResult.dragging.type !== TABS_TYPE) {
+ props.handleComponentDrop(dropResult);
+ }
+ },
+ [props.handleComponentDrop],
+ );
+
+ const handleDeleteTab = useCallback(
+ tabIndex => {
+ // If we're removing the currently selected tab,
+ // select the previous one (if any)
+ if (selectedTabIndex === tabIndex) {
+ handleClickTab(Math.max(0, tabIndex - 1));
+ }
+ },
+ [selectedTabIndex, handleClickTab],
+ );
+
+ const showDeleteConfirmModal = useCallback(
+ key => {
+ const { component, deleteComponent } = props;
+ AntdModal.confirm({
+ title: t('Delete dashboard tab?'),
+ content: (
+
+ {t(
+ 'Deleting a tab will remove all content within it and will deactivate any related alerts or reports. You may still ' +
+ 'reverse this action with the',
+ )}{' '}
+ {t('undo')}{' '}
+ {t('button (cmd + z) until you save your changes.')}
+
+ ),
+ onOk: () => {
+ deleteComponent(key, component.id);
+ const tabIndex = component.children.indexOf(key);
+ handleDeleteTab(tabIndex);
},
+ okType: 'danger',
+ okText: t('DELETE'),
+ cancelText: t('CANCEL'),
+ icon: null,
});
- } else if (action === 'remove') {
- this.showDeleteConfirmModal(event);
- }
- };
-
- handleClickTab(tabIndex) {
- const { component } = this.props;
- const { children: tabIds } = component;
-
- if (tabIndex !== this.state.tabIndex) {
- const pathToTabIndex = getDirectPathToTabIndex(component, tabIndex);
- const targetTabId = pathToTabIndex[pathToTabIndex.length - 1];
- this.props.logEvent(LOG_ACTIONS_SELECT_DASHBOARD_TAB, {
- target_id: targetTabId,
- index: tabIndex,
- });
-
- this.props.onChangeTab({ pathToTabIndex });
- }
- this.setState(() => ({ activeKey: tabIds[tabIndex] }));
- }
+ },
+ [props.component, props.deleteComponent, handleDeleteTab],
+ );
+
+ const handleEdit = useCallback(
+ (event, action) => {
+ const { component, createComponent } = props;
+ if (action === 'add') {
+ // Prevent the tab container to be selected
+ event?.stopPropagation?.();
+
+ createComponent({
+ destination: {
+ id: component.id,
+ type: component.type,
+ index: component.children.length,
+ },
+ dragging: {
+ id: NEW_TAB_ID,
+ type: TAB_TYPE,
+ },
+ });
+ } else if (action === 'remove') {
+ showDeleteConfirmModal(event);
+ }
+ },
+ [props.component, props.createComponent, showDeleteConfirmModal],
+ );
- handleDeleteComponent() {
- const { deleteComponent, id, parentId } = this.props;
+ const handleDeleteComponent = useCallback(() => {
+ const { deleteComponent, id, parentId } = props;
deleteComponent(id, parentId);
- }
+ }, [props.deleteComponent, props.id, props.parentId]);
- handleDeleteTab(tabIndex) {
- // If we're removing the currently selected tab,
- // select the previous one (if any)
- if (this.state.tabIndex === tabIndex) {
- this.handleClickTab(Math.max(0, tabIndex - 1));
- }
- }
-
- handleGetDropPosition(dragObject) {
+ const handleGetDropPosition = useCallback(dragObject => {
const { dropIndicator, isDraggingOver, index } = dragObject;
if (isDraggingOver) {
- this.setState(() => ({
- dropPosition: dropIndicator,
- dragOverTabIndex: index,
- }));
+ setDropPosition(dropIndicator);
+ setDragOverTabIndex(index);
} else {
- this.setState(() => ({ dropPosition: null }));
- }
- }
-
- handleDropOnTab(dropResult) {
- const { component } = this.props;
-
- // Ensure dropped tab is visible
- const { destination } = dropResult;
- if (destination) {
- const dropTabIndex =
- destination.id === component.id
- ? destination.index // dropped ON tabs
- : component.children.indexOf(destination.id); // dropped IN tab
-
- if (dropTabIndex > -1) {
- setTimeout(() => {
- this.handleClickTab(dropTabIndex);
- }, 30);
- }
+ setDropPosition(null);
}
- }
-
- handleDrop(dropResult) {
- if (dropResult.dragging.type !== TABS_TYPE) {
- this.props.handleComponentDrop(dropResult);
- }
- }
+ }, []);
- handleDragggingTab(tabId) {
+ const handleDragggingTab = useCallback(tabId => {
if (tabId) {
- this.setState(() => ({ draggingTabId: tabId }));
+ setDraggingTabId(tabId);
} else {
- this.setState(() => ({ draggingTabId: null }));
+ setDraggingTabId(null);
}
- }
-
- render() {
- const {
- depth,
- component: tabsComponent,
- parentComponent,
- index,
- availableColumnCount,
- columnWidth,
- onResizeStart,
- onResize,
- onResizeStop,
- renderTabContent,
- renderHoverMenu,
- isComponentVisible: isCurrentTabVisible,
- editMode,
- nativeFilters,
- } = this.props;
-
- const { children: tabIds } = tabsComponent;
- const {
- tabIndex: selectedTabIndex,
- activeKey,
- dropPosition,
- dragOverTabIndex,
- } = this.state;
-
- const showDropIndicators = currentDropTabIndex =>
+ }, []);
+
+ const {
+ depth,
+ component: tabsComponent,
+ parentComponent,
+ index,
+ availableColumnCount = 0,
+ columnWidth = 0,
+ onResizeStart,
+ onResize,
+ onResizeStop,
+ renderTabContent = true,
+ renderHoverMenu = true,
+ isComponentVisible: isCurrentTabVisible,
+ editMode,
+ } = props;
+
+ const { children: tabIds } = tabsComponent;
+
+ const showDropIndicators = useCallback(
+ currentDropTabIndex =>
currentDropTabIndex === dragOverTabIndex && {
left: editMode && dropPosition === DROP_LEFT,
right: editMode && dropPosition === DROP_RIGHT,
- };
-
- const removeDraggedTab = tabID => this.state.draggingTabId === tabID;
+ },
+ [dragOverTabIndex, dropPosition, editMode],
+ );
+
+ const removeDraggedTab = useCallback(
+ tabID => draggingTabId === tabID,
+ [draggingTabId],
+ );
+
+ let tabsToHighlight;
+ const highlightedFilterId =
+ nativeFilters?.focusedFilterId || nativeFilters?.hoveredFilterId;
+ if (highlightedFilterId) {
+ tabsToHighlight = nativeFilters.filters[highlightedFilterId]?.tabsInScope;
+ }
- let tabsToHighlight;
- const highlightedFilterId =
- nativeFilters?.focusedFilterId || nativeFilters?.hoveredFilterId;
- if (highlightedFilterId) {
- tabsToHighlight = nativeFilters.filters[highlightedFilterId]?.tabsInScope;
- }
- return (
- (
+
- {({ dragSourceRef: tabsDragSourceRef }) => (
-
- {editMode && renderHoverMenu && (
-
-
-
-
- )}
-
- {
- this.handleClickTab(tabIds.indexOf(key));
- }}
- onEdit={this.handleEdit}
- data-test="nav-list"
- type={editMode ? 'editable-card' : 'card'}
- >
- {tabIds.map((tabId, tabIndex) => (
- >
- ) : (
- <>
- {showDropIndicators(tabIndex).left && (
-
- )}
- this.handleClickTab(tabIndex)}
- isFocused={activeKey === tabId}
- isHighlighted={
- activeKey !== tabId &&
- tabsToHighlight?.includes(tabId)
- }
- />
- >
- )
- }
- closeIcon={
- removeDraggedTab(tabId) ? (
- <>>
- ) : (
-
+
+
+
+ )}
+
+ {
+ handleClickTab(tabIds.indexOf(key));
+ }}
+ onEdit={handleEdit}
+ data-test="nav-list"
+ type={editMode ? 'editable-card' : 'card'}
+ >
+ {tabIds.map((tabId, tabIndex) => (
+ >
+ ) : (
+ <>
+ {showDropIndicators(tabIndex).left && (
+
- )
- }
- >
- {renderTabContent && (
+ )}
handleClickTab(tabIndex)}
+ isFocused={activeKey === tabId}
+ isHighlighted={
+ activeKey !== tabId && tabsToHighlight?.includes(tabId)
}
/>
- )}
-
- ))}
-
-
- )}
-
- );
- }
-}
+ >
+ )
+ }
+ closeIcon={
+ removeDraggedTab(tabId) ? (
+ <>>
+ ) : (
+
+ )
+ }
+ >
+ {renderTabContent && (
+
+ )}
+
+ ))}
+
+
+ ),
+ [
+ editMode,
+ renderHoverMenu,
+ handleDeleteComponent,
+ tabsComponent.id,
+ activeKey,
+ handleEdit,
+ tabIds,
+ handleClickTab,
+ removeDraggedTab,
+ showDropIndicators,
+ depth,
+ availableColumnCount,
+ columnWidth,
+ handleDropOnTab,
+ handleGetDropPosition,
+ handleDragggingTab,
+ tabsToHighlight,
+ renderTabContent,
+ onResizeStart,
+ onResize,
+ onResizeStop,
+ selectedTabIndex,
+ isCurrentTabVisible,
+ ],
+ );
+
+ return (
+
+ {renderChild}
+
+ );
+};
Tabs.propTypes = propTypes;
Tabs.defaultProps = defaultProps;
-function mapStateToProps(state) {
- return {
- nativeFilters: state.nativeFilters,
- activeTabs: state.dashboardState.activeTabs,
- directPathToChild: state.dashboardState.directPathToChild,
- };
-}
-
-export default connect(mapStateToProps)(Tabs);
+export default memo(Tabs);
diff --git a/superset-frontend/src/dashboard/components/gridComponents/Tabs.test.jsx b/superset-frontend/src/dashboard/components/gridComponents/Tabs.test.jsx
index 6d33091d8e40f..a477420249455 100644
--- a/superset-frontend/src/dashboard/components/gridComponents/Tabs.test.jsx
+++ b/superset-frontend/src/dashboard/components/gridComponents/Tabs.test.jsx
@@ -20,11 +20,10 @@ import { fireEvent, render } from 'spec/helpers/testing-library';
import { AntdModal } from 'src/components';
import fetchMock from 'fetch-mock';
-import { Tabs } from 'src/dashboard/components/gridComponents/Tabs';
+import Tabs from 'src/dashboard/components/gridComponents/Tabs';
import { DASHBOARD_ROOT_ID } from 'src/dashboard/util/constants';
import emptyDashboardLayout from 'src/dashboard/fixtures/emptyDashboardLayout';
import { dashboardLayoutWithTabs } from 'spec/fixtures/mockDashboardLayout';
-import { getMockStore } from 'spec/fixtures/mockStore';
import { nativeFilters } from 'spec/fixtures/mockNativeFilters';
import { initialState } from 'src/SqlLab/fixtures';
@@ -81,17 +80,17 @@ const props = {
nativeFilters: nativeFilters.filters,
};
-const mockStore = getMockStore({
- ...initialState,
- dashboardLayout: dashboardLayoutWithTabs,
- dashboardFilters: {},
-});
-
-function setup(overrideProps) {
+function setup(overrideProps, overrideState = {}) {
return render(, {
useDnd: true,
useRouter: true,
- store: mockStore,
+ useRedux: true,
+ initialState: {
+ ...initialState,
+ dashboardLayout: dashboardLayoutWithTabs,
+ dashboardFilters: {},
+ ...overrideState,
+ },
});
}
@@ -174,11 +173,7 @@ test('should direct display direct-link tab', () => {
// display child in directPathToChild list
const directPathToChild =
dashboardLayoutWithTabs.present.ROW_ID2.parents.slice();
- const directLinkProps = {
- ...props,
- directPathToChild,
- };
- const { getByRole } = setup(directLinkProps);
+ const { getByRole } = setup({}, { dashboardState: { directPathToChild } });
expect(getByRole('tab', { selected: true })).toHaveTextContent('TAB_ID2');
});
diff --git a/superset-frontend/src/dashboard/components/gridComponents/Tabs.test.tsx b/superset-frontend/src/dashboard/components/gridComponents/Tabs.test.tsx
index 98ae968fd697b..90a23c03583ef 100644
--- a/superset-frontend/src/dashboard/components/gridComponents/Tabs.test.tsx
+++ b/superset-frontend/src/dashboard/components/gridComponents/Tabs.test.tsx
@@ -25,7 +25,7 @@ import { Draggable } from 'src/dashboard/components/dnd/DragDroppable';
import DeleteComponentButton from 'src/dashboard/components/DeleteComponentButton';
import getLeafComponentIdFromPath from 'src/dashboard/util/getLeafComponentIdFromPath';
import emptyDashboardLayout from 'src/dashboard/fixtures/emptyDashboardLayout';
-import { Tabs } from './Tabs';
+import Tabs from './Tabs';
jest.mock('src/dashboard/containers/DashboardComponent', () =>
jest.fn(props => (
diff --git a/superset-frontend/src/dashboard/components/gridComponents/index.js b/superset-frontend/src/dashboard/components/gridComponents/index.js
index 95c524f5f7a64..38f3558864923 100644
--- a/superset-frontend/src/dashboard/components/gridComponents/index.js
+++ b/superset-frontend/src/dashboard/components/gridComponents/index.js
@@ -35,7 +35,7 @@ import Divider from './Divider';
import Header from './Header';
import Row from './Row';
import Tab from './Tab';
-import TabsConnected from './Tabs';
+import Tabs from './Tabs';
import DynamicComponent from './DynamicComponent';
export { default as ChartHolder } from './ChartHolder';
@@ -56,6 +56,6 @@ export const componentLookup = {
[HEADER_TYPE]: Header,
[ROW_TYPE]: Row,
[TAB_TYPE]: Tab,
- [TABS_TYPE]: TabsConnected,
+ [TABS_TYPE]: Tabs,
[DYNAMIC_TYPE]: DynamicComponent,
};
diff --git a/superset-frontend/src/dashboard/components/menu/ShareMenuItems/index.tsx b/superset-frontend/src/dashboard/components/menu/ShareMenuItems/index.tsx
index 96a00dd24cfaf..0d7211ba8ebe1 100644
--- a/superset-frontend/src/dashboard/components/menu/ShareMenuItems/index.tsx
+++ b/superset-frontend/src/dashboard/components/menu/ShareMenuItems/index.tsx
@@ -22,7 +22,7 @@ import { t, logging } from '@superset-ui/core';
import { Menu } from 'src/components/Menu';
import { getDashboardPermalink } from 'src/utils/urlUtils';
import { MenuKeys, RootState } from 'src/dashboard/types';
-import { useSelector } from 'react-redux';
+import { shallowEqual, useSelector } from 'react-redux';
interface ShareMenuItemProps {
url?: string;
@@ -54,10 +54,13 @@ const ShareMenuItems = (props: ShareMenuItemProps) => {
selectedKeys,
...rest
} = props;
- const { dataMask, activeTabs } = useSelector((state: RootState) => ({
- dataMask: state.dataMask,
- activeTabs: state.dashboardState.activeTabs,
- }));
+ const { dataMask, activeTabs } = useSelector(
+ (state: RootState) => ({
+ dataMask: state.dataMask,
+ activeTabs: state.dashboardState.activeTabs,
+ }),
+ shallowEqual,
+ );
async function generateUrl() {
return getDashboardPermalink({
diff --git a/superset-frontend/src/utils/colorScheme.ts b/superset-frontend/src/utils/colorScheme.ts
index 1b9f2d12ecef2..35f23d29f4d4d 100644
--- a/superset-frontend/src/utils/colorScheme.ts
+++ b/superset-frontend/src/utils/colorScheme.ts
@@ -19,10 +19,13 @@
import {
CategoricalColorNamespace,
+ ensureIsArray,
getCategoricalSchemeRegistry,
getLabelsColorMap,
} from '@superset-ui/core';
+const EMPTY_ARRAY: string[] = [];
+
/**
* Force falsy namespace values to undefined to default to GLOBAL
*
@@ -41,7 +44,7 @@ export const getColorNamespace = (namespace?: string) => namespace || undefined;
*/
export const enforceSharedLabelsColorsArray = (
sharedLabelsColors: string[] | Record | undefined,
-) => (Array.isArray(sharedLabelsColors) ? sharedLabelsColors : []);
+) => (Array.isArray(sharedLabelsColors) ? sharedLabelsColors : EMPTY_ARRAY);
/**
* Get labels shared across all charts in a dashboard.
@@ -67,7 +70,9 @@ export const getFreshSharedLabels = (
.filter(([, count]) => count > 1)
.map(([label]) => label);
- return Array.from(new Set([...currentSharedLabels, ...duplicates]));
+ return Array.from(
+ new Set([...ensureIsArray(currentSharedLabels), ...duplicates]),
+ );
};
export const getSharedLabelsColorMapEntries = (