perf: Optimize dashboard grid components (#31240)
This commit is contained in:
parent
24d001e498
commit
5006f97f70
|
|
@ -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 (
|
||||
<Draggable
|
||||
component={columnComponent}
|
||||
parentComponent={parentComponent}
|
||||
orientation="column"
|
||||
index={index}
|
||||
depth={depth}
|
||||
onDrop={handleComponentDrop}
|
||||
const renderChild = useCallback(
|
||||
({ dragSourceRef }) => (
|
||||
<ResizableContainer
|
||||
id={columnComponent.id}
|
||||
adjustableWidth
|
||||
adjustableHeight={false}
|
||||
widthStep={columnWidth}
|
||||
widthMultiple={columnComponent.meta.width}
|
||||
minWidthMultiple={minColumnWidth}
|
||||
maxWidthMultiple={
|
||||
availableColumnCount + (columnComponent.meta.width || 0)
|
||||
}
|
||||
onResizeStart={onResizeStart}
|
||||
onResize={onResize}
|
||||
onResizeStop={onResizeStop}
|
||||
editMode={editMode}
|
||||
>
|
||||
{({ dragSourceRef }) => (
|
||||
<ResizableContainer
|
||||
id={columnComponent.id}
|
||||
adjustableWidth
|
||||
adjustableHeight={false}
|
||||
widthStep={columnWidth}
|
||||
widthMultiple={columnComponent.meta.width}
|
||||
minWidthMultiple={minColumnWidth}
|
||||
maxWidthMultiple={
|
||||
availableColumnCount + (columnComponent.meta.width || 0)
|
||||
}
|
||||
onResizeStart={onResizeStart}
|
||||
onResize={onResize}
|
||||
onResizeStop={onResizeStop}
|
||||
<WithPopoverMenu
|
||||
isFocused={isFocused}
|
||||
onChangeFocus={handleChangeFocus}
|
||||
disableClick
|
||||
menuItems={[
|
||||
<BackgroundStyleDropdown
|
||||
id={`${columnComponent.id}-background`}
|
||||
value={columnComponent.meta.background}
|
||||
onChange={handleChangeBackground}
|
||||
/>,
|
||||
]}
|
||||
editMode={editMode}
|
||||
>
|
||||
{editMode && (
|
||||
<HoverMenu innerRef={dragSourceRef} position="top">
|
||||
<DragHandle position="top" />
|
||||
<DeleteComponentButton onDelete={handleDeleteComponent} />
|
||||
<IconButton
|
||||
onClick={handleChangeFocus}
|
||||
icon={<Icons.Cog iconSize="xl" />}
|
||||
/>
|
||||
</HoverMenu>
|
||||
)}
|
||||
<ColumnStyles
|
||||
className={cx('grid-column', backgroundStyle.className)}
|
||||
editMode={editMode}
|
||||
>
|
||||
<WithPopoverMenu
|
||||
isFocused={this.state.isFocused}
|
||||
onChangeFocus={this.handleChangeFocus}
|
||||
disableClick
|
||||
menuItems={[
|
||||
<BackgroundStyleDropdown
|
||||
id={`${columnComponent.id}-background`}
|
||||
value={columnComponent.meta.background}
|
||||
onChange={this.handleChangeBackground}
|
||||
/>,
|
||||
]}
|
||||
editMode={editMode}
|
||||
>
|
||||
{editMode && (
|
||||
<HoverMenu innerRef={dragSourceRef} position="top">
|
||||
<DragHandle position="top" />
|
||||
<DeleteComponentButton
|
||||
onDelete={this.handleDeleteComponent}
|
||||
/>
|
||||
<IconButton
|
||||
onClick={this.handleChangeFocus}
|
||||
icon={<Icons.Cog iconSize="xl" />}
|
||||
/>
|
||||
</HoverMenu>
|
||||
)}
|
||||
<ColumnStyles
|
||||
className={cx('grid-column', backgroundStyle.className)}
|
||||
editMode={editMode}
|
||||
>
|
||||
{editMode && (
|
||||
<Droppable
|
||||
component={columnComponent}
|
||||
parentComponent={columnComponent}
|
||||
{...(columnItems.length === 0
|
||||
? {
|
||||
dropToChild: true,
|
||||
}
|
||||
: {
|
||||
component: columnItems[0],
|
||||
})}
|
||||
depth={depth}
|
||||
index={0}
|
||||
orientation="column"
|
||||
onDrop={handleComponentDrop}
|
||||
className={cx(
|
||||
'empty-droptarget',
|
||||
columnItems.length > 0 && 'droptarget-edge',
|
||||
)}
|
||||
editMode
|
||||
>
|
||||
{({ dropIndicatorProps }) =>
|
||||
dropIndicatorProps && <div {...dropIndicatorProps} />
|
||||
{editMode && (
|
||||
<Droppable
|
||||
component={columnComponent}
|
||||
parentComponent={columnComponent}
|
||||
{...(columnItems.length === 0
|
||||
? {
|
||||
dropToChild: true,
|
||||
}
|
||||
</Droppable>
|
||||
: {
|
||||
component: columnItems[0],
|
||||
})}
|
||||
depth={depth}
|
||||
index={0}
|
||||
orientation="column"
|
||||
onDrop={handleComponentDrop}
|
||||
className={cx(
|
||||
'empty-droptarget',
|
||||
columnItems.length > 0 && 'droptarget-edge',
|
||||
)}
|
||||
{columnItems.length === 0 ? (
|
||||
<div css={emptyColumnContentStyles}>{t('Empty column')}</div>
|
||||
) : (
|
||||
columnItems.map((componentId, itemIndex) => (
|
||||
<Fragment key={componentId}>
|
||||
<DashboardComponent
|
||||
id={componentId}
|
||||
parentId={columnComponent.id}
|
||||
depth={depth + 1}
|
||||
index={itemIndex}
|
||||
availableColumnCount={columnComponent.meta.width}
|
||||
columnWidth={columnWidth}
|
||||
onResizeStart={onResizeStart}
|
||||
onResize={onResize}
|
||||
onResizeStop={onResizeStop}
|
||||
isComponentVisible={isComponentVisible}
|
||||
onChangeTab={onChangeTab}
|
||||
/>
|
||||
{editMode && (
|
||||
<Droppable
|
||||
component={columnItems}
|
||||
parentComponent={columnComponent}
|
||||
depth={depth}
|
||||
index={itemIndex + 1}
|
||||
orientation="column"
|
||||
onDrop={handleComponentDrop}
|
||||
className={cx(
|
||||
'empty-droptarget',
|
||||
itemIndex === columnItems.length - 1 &&
|
||||
'droptarget-edge',
|
||||
)}
|
||||
editMode
|
||||
>
|
||||
{({ dropIndicatorProps }) =>
|
||||
dropIndicatorProps && (
|
||||
<div {...dropIndicatorProps} />
|
||||
)
|
||||
}
|
||||
</Droppable>
|
||||
editMode
|
||||
>
|
||||
{({ dropIndicatorProps }) =>
|
||||
dropIndicatorProps && <div {...dropIndicatorProps} />
|
||||
}
|
||||
</Droppable>
|
||||
)}
|
||||
{columnItems.length === 0 ? (
|
||||
<div css={emptyColumnContentStyles}>{t('Empty column')}</div>
|
||||
) : (
|
||||
columnItems.map((componentId, itemIndex) => (
|
||||
<Fragment key={componentId}>
|
||||
<DashboardComponent
|
||||
id={componentId}
|
||||
parentId={columnComponent.id}
|
||||
depth={depth + 1}
|
||||
index={itemIndex}
|
||||
availableColumnCount={columnComponent.meta.width}
|
||||
columnWidth={columnWidth}
|
||||
onResizeStart={onResizeStart}
|
||||
onResize={onResize}
|
||||
onResizeStop={onResizeStop}
|
||||
isComponentVisible={isComponentVisible}
|
||||
onChangeTab={onChangeTab}
|
||||
/>
|
||||
{editMode && (
|
||||
<Droppable
|
||||
component={columnItems}
|
||||
parentComponent={columnComponent}
|
||||
depth={depth}
|
||||
index={itemIndex + 1}
|
||||
orientation="column"
|
||||
onDrop={handleComponentDrop}
|
||||
className={cx(
|
||||
'empty-droptarget',
|
||||
itemIndex === columnItems.length - 1 &&
|
||||
'droptarget-edge',
|
||||
)}
|
||||
</Fragment>
|
||||
))
|
||||
)}
|
||||
</ColumnStyles>
|
||||
</WithPopoverMenu>
|
||||
</ResizableContainer>
|
||||
)}
|
||||
</Draggable>
|
||||
);
|
||||
}
|
||||
}
|
||||
editMode
|
||||
>
|
||||
{({ dropIndicatorProps }) =>
|
||||
dropIndicatorProps && <div {...dropIndicatorProps} />
|
||||
}
|
||||
</Droppable>
|
||||
)}
|
||||
</Fragment>
|
||||
))
|
||||
)}
|
||||
</ColumnStyles>
|
||||
</WithPopoverMenu>
|
||||
</ResizableContainer>
|
||||
),
|
||||
[
|
||||
availableColumnCount,
|
||||
backgroundStyle.className,
|
||||
columnComponent,
|
||||
columnItems,
|
||||
columnWidth,
|
||||
depth,
|
||||
editMode,
|
||||
handleChangeBackground,
|
||||
handleChangeFocus,
|
||||
handleComponentDrop,
|
||||
handleDeleteComponent,
|
||||
isComponentVisible,
|
||||
isFocused,
|
||||
minColumnWidth,
|
||||
onChangeTab,
|
||||
onResize,
|
||||
onResizeStart,
|
||||
onResizeStop,
|
||||
],
|
||||
);
|
||||
|
||||
return (
|
||||
<Draggable
|
||||
component={columnComponent}
|
||||
parentComponent={parentComponent}
|
||||
orientation="column"
|
||||
index={index}
|
||||
depth={depth}
|
||||
onDrop={handleComponentDrop}
|
||||
editMode={editMode}
|
||||
>
|
||||
{renderChild}
|
||||
</Draggable>
|
||||
);
|
||||
};
|
||||
|
||||
Column.propTypes = propTypes;
|
||||
Column.defaultProps = defaultProps;
|
||||
|
||||
export default Column;
|
||||
export default memo(Column);
|
||||
|
|
|
|||
|
|
@ -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<FilterSummaryType> = ({
|
|||
nativeFilters,
|
||||
dataMask,
|
||||
}),
|
||||
shallowEqual,
|
||||
);
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
this.containerRef = createRef();
|
||||
this.observerEnabler = null;
|
||||
this.observerDisabler = null;
|
||||
}
|
||||
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);
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
return () => {
|
||||
observerEnabler?.disconnect();
|
||||
observerDisabler?.disconnect();
|
||||
};
|
||||
}, []);
|
||||
|
||||
componentDidUpdate() {
|
||||
this.setVerticalEmptyContainerHeight();
|
||||
}
|
||||
useDebouncedEffect(
|
||||
() => {
|
||||
const updatedHeight = containerRef.current?.clientHeight;
|
||||
if (
|
||||
editMode &&
|
||||
containerRef.current &&
|
||||
updatedHeight !== containerHeight
|
||||
) {
|
||||
setContainerHeight(updatedHeight);
|
||||
}
|
||||
},
|
||||
FAST_DEBOUNCE,
|
||||
[editMode, containerHeight],
|
||||
);
|
||||
|
||||
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 });
|
||||
}
|
||||
}
|
||||
const handleChangeFocus = useCallback(nextFocus => {
|
||||
setIsFocused(Boolean(nextFocus));
|
||||
}, []);
|
||||
|
||||
componentWillUnmount() {
|
||||
this.observerEnabler?.disconnect();
|
||||
this.observerDisabler?.disconnect();
|
||||
}
|
||||
|
||||
handleChangeFocus(nextFocus) {
|
||||
this.setState(() => ({ isFocused: 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 (
|
||||
<Draggable
|
||||
component={rowComponent}
|
||||
parentComponent={parentComponent}
|
||||
orientation="row"
|
||||
index={index}
|
||||
depth={depth}
|
||||
onDrop={handleComponentDrop}
|
||||
const backgroundStyle = backgroundStyleOptions.find(
|
||||
opt =>
|
||||
opt.value === (rowComponent.meta.background || BACKGROUND_TRANSPARENT),
|
||||
);
|
||||
const remainColumnCount = availableColumnCount - occupiedColumnCount;
|
||||
const renderChild = useCallback(
|
||||
({ dragSourceRef }) => (
|
||||
<WithPopoverMenu
|
||||
isFocused={isFocused}
|
||||
onChangeFocus={handleChangeFocus}
|
||||
disableClick
|
||||
menuItems={[
|
||||
<BackgroundStyleDropdown
|
||||
id={`${rowComponent.id}-background`}
|
||||
value={backgroundStyle.value}
|
||||
onChange={handleChangeBackground}
|
||||
/>,
|
||||
]}
|
||||
editMode={editMode}
|
||||
>
|
||||
{({ dragSourceRef }) => (
|
||||
<WithPopoverMenu
|
||||
isFocused={this.state.isFocused}
|
||||
onChangeFocus={this.handleChangeFocus}
|
||||
disableClick
|
||||
menuItems={[
|
||||
<BackgroundStyleDropdown
|
||||
id={`${rowComponent.id}-background`}
|
||||
value={backgroundStyle.value}
|
||||
onChange={this.handleChangeBackground}
|
||||
/>,
|
||||
]}
|
||||
editMode={editMode}
|
||||
{editMode && (
|
||||
<HoverMenu
|
||||
onHover={handleMenuHover}
|
||||
innerRef={dragSourceRef}
|
||||
position="left"
|
||||
>
|
||||
{editMode && (
|
||||
<HoverMenu
|
||||
onHover={this.handleMenuHover}
|
||||
innerRef={dragSourceRef}
|
||||
position="left"
|
||||
>
|
||||
<DragHandle position="left" />
|
||||
<DeleteComponentButton onDelete={this.handleDeleteComponent} />
|
||||
<IconButton
|
||||
onClick={this.handleChangeFocus}
|
||||
icon={<Icons.Cog iconSize="xl" />}
|
||||
/>
|
||||
</HoverMenu>
|
||||
)}
|
||||
<GridRow
|
||||
className={cx(
|
||||
'grid-row',
|
||||
rowItems.length === 0 && 'grid-row--empty',
|
||||
hoverMenuHovered && 'grid-row--hovered',
|
||||
backgroundStyle.className,
|
||||
)}
|
||||
data-test={`grid-row-${backgroundStyle.className}`}
|
||||
ref={this.containerRef}
|
||||
editMode={editMode}
|
||||
>
|
||||
{editMode && (
|
||||
<Droppable
|
||||
{...(rowItems.length === 0
|
||||
? {
|
||||
component: rowComponent,
|
||||
parentComponent: rowComponent,
|
||||
dropToChild: true,
|
||||
}
|
||||
: {
|
||||
component: rowItems[0],
|
||||
parentComponent: rowComponent,
|
||||
})}
|
||||
depth={depth}
|
||||
index={0}
|
||||
orientation="row"
|
||||
onDrop={handleComponentDrop}
|
||||
className={cx(
|
||||
'empty-droptarget',
|
||||
'empty-droptarget--vertical',
|
||||
rowItems.length > 0 && 'droptarget-side',
|
||||
)}
|
||||
editMode
|
||||
style={{
|
||||
height: rowItems.length > 0 ? containerHeight : '100%',
|
||||
...(rowItems.length > 0 && { width: 16 }),
|
||||
}}
|
||||
>
|
||||
{({ dropIndicatorProps }) =>
|
||||
dropIndicatorProps && <div {...dropIndicatorProps} />
|
||||
}
|
||||
</Droppable>
|
||||
)}
|
||||
{rowItems.length === 0 && (
|
||||
<div css={emptyRowContentStyles}>{t('Empty row')}</div>
|
||||
)}
|
||||
{rowItems.length > 0 &&
|
||||
rowItems.map((componentId, itemIndex) => (
|
||||
<Fragment key={componentId}>
|
||||
<DashboardComponent
|
||||
key={componentId}
|
||||
id={componentId}
|
||||
parentId={rowComponent.id}
|
||||
depth={depth + 1}
|
||||
index={itemIndex}
|
||||
availableColumnCount={remainColumnCount}
|
||||
columnWidth={columnWidth}
|
||||
onResizeStart={onResizeStart}
|
||||
onResize={onResize}
|
||||
onResizeStop={onResizeStop}
|
||||
isComponentVisible={isComponentVisible}
|
||||
onChangeTab={onChangeTab}
|
||||
isInView={this.state.isInView}
|
||||
/>
|
||||
{editMode && (
|
||||
<Droppable
|
||||
component={rowItems}
|
||||
parentComponent={rowComponent}
|
||||
depth={depth}
|
||||
index={itemIndex + 1}
|
||||
orientation="row"
|
||||
onDrop={handleComponentDrop}
|
||||
className={cx(
|
||||
'empty-droptarget',
|
||||
'empty-droptarget--vertical',
|
||||
remainColumnCount === 0 &&
|
||||
itemIndex === rowItems.length - 1 &&
|
||||
'droptarget-side',
|
||||
)}
|
||||
editMode
|
||||
style={{
|
||||
height: containerHeight,
|
||||
...(remainColumnCount === 0 &&
|
||||
itemIndex === rowItems.length - 1 && { width: 16 }),
|
||||
}}
|
||||
>
|
||||
{({ dropIndicatorProps }) =>
|
||||
dropIndicatorProps && <div {...dropIndicatorProps} />
|
||||
}
|
||||
</Droppable>
|
||||
)}
|
||||
</Fragment>
|
||||
))}
|
||||
</GridRow>
|
||||
</WithPopoverMenu>
|
||||
<DragHandle position="left" />
|
||||
<DeleteComponentButton onDelete={handleDeleteComponent} />
|
||||
<IconButton
|
||||
onClick={handleChangeFocus}
|
||||
icon={<Icons.Cog iconSize="xl" />}
|
||||
/>
|
||||
</HoverMenu>
|
||||
)}
|
||||
</Draggable>
|
||||
);
|
||||
}
|
||||
}
|
||||
<GridRow
|
||||
className={cx(
|
||||
'grid-row',
|
||||
rowItems.length === 0 && 'grid-row--empty',
|
||||
hoverMenuHovered && 'grid-row--hovered',
|
||||
backgroundStyle.className,
|
||||
)}
|
||||
data-test={`grid-row-${backgroundStyle.className}`}
|
||||
ref={containerRef}
|
||||
editMode={editMode}
|
||||
>
|
||||
{editMode && (
|
||||
<Droppable
|
||||
{...(rowItems.length === 0
|
||||
? {
|
||||
component: rowComponent,
|
||||
parentComponent: rowComponent,
|
||||
dropToChild: true,
|
||||
}
|
||||
: {
|
||||
component: rowItems[0],
|
||||
parentComponent: rowComponent,
|
||||
})}
|
||||
depth={depth}
|
||||
index={0}
|
||||
orientation="row"
|
||||
onDrop={handleComponentDrop}
|
||||
className={cx(
|
||||
'empty-droptarget',
|
||||
'empty-droptarget--vertical',
|
||||
rowItems.length > 0 && 'droptarget-side',
|
||||
)}
|
||||
editMode
|
||||
style={{
|
||||
height: rowItems.length > 0 ? containerHeight : '100%',
|
||||
...(rowItems.length > 0 && { width: 16 }),
|
||||
}}
|
||||
>
|
||||
{({ dropIndicatorProps }) =>
|
||||
dropIndicatorProps && <div {...dropIndicatorProps} />
|
||||
}
|
||||
</Droppable>
|
||||
)}
|
||||
{rowItems.length === 0 && (
|
||||
<div css={emptyRowContentStyles}>{t('Empty row')}</div>
|
||||
)}
|
||||
{rowItems.length > 0 &&
|
||||
rowItems.map((componentId, itemIndex) => (
|
||||
<Fragment key={componentId}>
|
||||
<DashboardComponent
|
||||
key={componentId}
|
||||
id={componentId}
|
||||
parentId={rowComponent.id}
|
||||
depth={depth + 1}
|
||||
index={itemIndex}
|
||||
availableColumnCount={remainColumnCount}
|
||||
columnWidth={columnWidth}
|
||||
onResizeStart={onResizeStart}
|
||||
onResize={onResize}
|
||||
onResizeStop={onResizeStop}
|
||||
isComponentVisible={isComponentVisible}
|
||||
onChangeTab={onChangeTab}
|
||||
isInView={isInView}
|
||||
/>
|
||||
{editMode && (
|
||||
<Droppable
|
||||
component={rowItems}
|
||||
parentComponent={rowComponent}
|
||||
depth={depth}
|
||||
index={itemIndex + 1}
|
||||
orientation="row"
|
||||
onDrop={handleComponentDrop}
|
||||
className={cx(
|
||||
'empty-droptarget',
|
||||
'empty-droptarget--vertical',
|
||||
remainColumnCount === 0 &&
|
||||
itemIndex === rowItems.length - 1 &&
|
||||
'droptarget-side',
|
||||
)}
|
||||
editMode
|
||||
style={{
|
||||
height: containerHeight,
|
||||
...(remainColumnCount === 0 &&
|
||||
itemIndex === rowItems.length - 1 && { width: 16 }),
|
||||
}}
|
||||
>
|
||||
{({ dropIndicatorProps }) =>
|
||||
dropIndicatorProps && <div {...dropIndicatorProps} />
|
||||
}
|
||||
</Droppable>
|
||||
)}
|
||||
</Fragment>
|
||||
))}
|
||||
</GridRow>
|
||||
</WithPopoverMenu>
|
||||
),
|
||||
[
|
||||
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 (
|
||||
<Draggable
|
||||
component={rowComponent}
|
||||
parentComponent={parentComponent}
|
||||
orientation="row"
|
||||
index={index}
|
||||
depth={depth}
|
||||
onDrop={handleComponentDrop}
|
||||
editMode={editMode}
|
||||
>
|
||||
{renderChild}
|
||||
</Draggable>
|
||||
);
|
||||
};
|
||||
|
||||
Row.propTypes = propTypes;
|
||||
|
||||
export default Row;
|
||||
export default memo(Row);
|
||||
|
|
|
|||
|
|
@ -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 && <div {...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);
|
||||
}
|
||||
const Tab = props => {
|
||||
const dispatch = useDispatch();
|
||||
const canEdit = useSelector(state => state.dashboardInfo.dash_edit_perm);
|
||||
const handleChangeTab = useCallback(
|
||||
({ pathToTabIndex }) => {
|
||||
props.setDirectPathToChild(pathToTabIndex);
|
||||
},
|
||||
[props.setDirectPathToChild],
|
||||
);
|
||||
|
||||
handleChangeTab({ pathToTabIndex }) {
|
||||
this.props.setDirectPathToChild(pathToTabIndex);
|
||||
}
|
||||
|
||||
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 {
|
|||
<span
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => setEditMode(true)}
|
||||
onClick={() => dispatch(setEditMode(true))}
|
||||
>
|
||||
{t('edit mode')}
|
||||
</span>
|
||||
|
|
@ -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 {
|
|||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}, [
|
||||
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 (
|
||||
<TabTitleContainer
|
||||
isHighlighted={isHighlighted}
|
||||
className="dragdroppable-tab"
|
||||
ref={dragSourceRef}
|
||||
>
|
||||
<EditableTitle
|
||||
title={component.meta.text}
|
||||
defaultTitle={component.meta.defaultText}
|
||||
placeholder={component.meta.placeholder}
|
||||
canEdit={editMode && isFocused}
|
||||
onSaveTitle={handleChangeText}
|
||||
showTooltip={false}
|
||||
editing={editMode && isFocused}
|
||||
/>
|
||||
{!editMode && !embeddedMode && (
|
||||
<AnchorLink
|
||||
id={component.id}
|
||||
dashboardId={dashboardId}
|
||||
placement={index >= 5 ? 'left' : 'right'}
|
||||
/>
|
||||
)}
|
||||
|
||||
{dropIndicatorProps && !draggingTabOnTab && (
|
||||
<TitleDropIndicator
|
||||
className={dropIndicatorProps.className}
|
||||
data-test="title-drop-indicator"
|
||||
/>
|
||||
)}
|
||||
</TabTitleContainer>
|
||||
);
|
||||
},
|
||||
[
|
||||
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 (
|
||||
<DragDroppable
|
||||
|
|
@ -293,71 +365,32 @@ class Tab extends PureComponent {
|
|||
orientation="column"
|
||||
index={index}
|
||||
depth={depth}
|
||||
onDrop={this.handleDrop}
|
||||
onHover={this.handleOnHover}
|
||||
onDrop={handleDrop}
|
||||
onHover={handleHoverTab}
|
||||
onDropIndicatorChange={onDropPositionChange}
|
||||
onDragTab={onDragTab}
|
||||
editMode={editMode}
|
||||
dropToChild={this.shouldDropToChild}
|
||||
dropToChild={shouldDropToChild}
|
||||
>
|
||||
{({ dropIndicatorProps, dragSourceRef, draggingTabOnTab }) => (
|
||||
<TabTitleContainer
|
||||
isHighlighted={isHighlighted}
|
||||
className="dragdroppable-tab"
|
||||
ref={dragSourceRef}
|
||||
>
|
||||
<EditableTitle
|
||||
title={component.meta.text}
|
||||
defaultTitle={component.meta.defaultText}
|
||||
placeholder={component.meta.placeholder}
|
||||
canEdit={editMode && isFocused}
|
||||
onSaveTitle={this.handleChangeText}
|
||||
showTooltip={false}
|
||||
editing={editMode && isFocused}
|
||||
/>
|
||||
{!editMode && !embeddedMode && (
|
||||
<AnchorLink
|
||||
id={component.id}
|
||||
dashboardId={this.props.dashboardId}
|
||||
placement={index >= 5 ? 'left' : 'right'}
|
||||
/>
|
||||
)}
|
||||
{dropIndicatorProps && !draggingTabOnTab && (
|
||||
<TitleDropIndicator
|
||||
className={dropIndicatorProps.className}
|
||||
data-test="title-drop-indicator"
|
||||
/>
|
||||
)}
|
||||
</TabTitleContainer>
|
||||
)}
|
||||
{renderTabChild}
|
||||
</DragDroppable>
|
||||
);
|
||||
}
|
||||
}, [
|
||||
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);
|
||||
|
|
|
|||
|
|
@ -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,95 +126,24 @@ 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 = {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
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 }));
|
||||
}
|
||||
|
||||
// reset tab index if dashboard was changed
|
||||
if (nextProps.dashboardId !== this.props.dashboardId) {
|
||||
const { tabIndex, activeKey } = this.getTabInfo(nextProps);
|
||||
this.setState(() => ({
|
||||
tabIndex,
|
||||
activeKey,
|
||||
}));
|
||||
}
|
||||
|
||||
if (nextProps.isComponentVisible) {
|
||||
const nextFocusComponent = getLeafComponentIdFromPath(
|
||||
nextProps.directPathToChild,
|
||||
);
|
||||
const currentFocusComponent = getLeafComponentIdFromPath(
|
||||
this.props.directPathToChild,
|
||||
);
|
||||
|
||||
// If the currently selected component is different than the new one,
|
||||
// or the tab length/order changed, calculate the new tab index and
|
||||
// replace it if it's different than the current one
|
||||
if (
|
||||
nextFocusComponent !== currentFocusComponent ||
|
||||
(nextFocusComponent === currentFocusComponent &&
|
||||
currTabsIds !== nextTabsIds)
|
||||
) {
|
||||
const nextTabIndex = findTabIndexByComponentId({
|
||||
currentComponent: nextProps.component,
|
||||
directPathToChild: nextProps.directPathToChild,
|
||||
});
|
||||
|
||||
// make sure nextFocusComponent is under this tabs component
|
||||
if (nextTabIndex > -1 && nextTabIndex !== this.state.tabIndex) {
|
||||
this.setState(() => ({
|
||||
tabIndex: nextTabIndex,
|
||||
activeKey: nextTabsIds[nextTabIndex],
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getTabInfo = props => {
|
||||
const { tabIndex: initTabIndex, activeKey: initActiveKey } = useMemo(() => {
|
||||
let tabIndex = Math.max(
|
||||
0,
|
||||
findTabIndexByComponentId({
|
||||
currentComponent: props.component,
|
||||
directPathToChild: props.directPathToChild,
|
||||
directPathToChild,
|
||||
}),
|
||||
);
|
||||
if (tabIndex === 0 && props.activeTabs?.length) {
|
||||
if (tabIndex === 0 && activeTabs?.length) {
|
||||
props.component.children.forEach((tabId, index) => {
|
||||
if (tabIndex === 0 && props.activeTabs.includes(tabId)) {
|
||||
if (tabIndex === 0 && activeTabs?.includes(tabId)) {
|
||||
tabIndex = index;
|
||||
}
|
||||
});
|
||||
|
|
@ -233,288 +155,398 @@ export class Tabs extends PureComponent {
|
|||
tabIndex,
|
||||
activeKey,
|
||||
};
|
||||
};
|
||||
}, [activeTabs, props.component, directPathToChild]);
|
||||
|
||||
showDeleteConfirmModal = key => {
|
||||
const { component, deleteComponent } = this.props;
|
||||
AntdModal.confirm({
|
||||
title: t('Delete dashboard tab?'),
|
||||
content: (
|
||||
<span>
|
||||
{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',
|
||||
)}{' '}
|
||||
<b>{t('undo')}</b>{' '}
|
||||
{t('button (cmd + z) until you save your changes.')}
|
||||
</span>
|
||||
),
|
||||
onOk: () => {
|
||||
deleteComponent(key, component.id);
|
||||
const tabIndex = component.children.indexOf(key);
|
||||
this.handleDeleteTab(tabIndex);
|
||||
},
|
||||
okType: 'danger',
|
||||
okText: t('DELETE'),
|
||||
cancelText: t('CANCEL'),
|
||||
icon: null,
|
||||
});
|
||||
};
|
||||
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);
|
||||
|
||||
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,
|
||||
},
|
||||
});
|
||||
} else if (action === 'remove') {
|
||||
this.showDeleteConfirmModal(event);
|
||||
useEffect(() => {
|
||||
if (prevActiveKey) {
|
||||
props.setActiveTab(activeKey, prevActiveKey);
|
||||
} else {
|
||||
props.setActiveTab(activeKey);
|
||||
}
|
||||
};
|
||||
}, [props.setActiveTab, prevActiveKey, activeKey]);
|
||||
|
||||
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 });
|
||||
useEffect(() => {
|
||||
if (prevDashboardId && props.dashboardId !== prevDashboardId) {
|
||||
setSelectedTabIndex(initTabIndex);
|
||||
setActiveKey(initActiveKey);
|
||||
}
|
||||
this.setState(() => ({ activeKey: tabIds[tabIndex] }));
|
||||
}
|
||||
}, [props.dashboardId, prevDashboardId, initTabIndex, initActiveKey]);
|
||||
|
||||
handleDeleteComponent() {
|
||||
const { deleteComponent, id, parentId } = this.props;
|
||||
useEffect(() => {
|
||||
const maxIndex = Math.max(0, props.component.children.length - 1);
|
||||
if (selectedTabIndex > maxIndex) {
|
||||
setSelectedTabIndex(maxIndex);
|
||||
}
|
||||
}, [selectedTabIndex, props.component.children.length, setSelectedTabIndex]);
|
||||
|
||||
useEffect(() => {
|
||||
const currTabsIds = props.component.children;
|
||||
|
||||
if (props.isComponentVisible) {
|
||||
const nextFocusComponent = getLeafComponentIdFromPath(directPathToChild);
|
||||
const currentFocusComponent = getLeafComponentIdFromPath(
|
||||
prevDirectPathToChild,
|
||||
);
|
||||
|
||||
// If the currently selected component is different than the new one,
|
||||
// or the tab length/order changed, calculate the new tab index and
|
||||
// replace it if it's different than the current one
|
||||
if (
|
||||
nextFocusComponent !== currentFocusComponent ||
|
||||
(nextFocusComponent === currentFocusComponent &&
|
||||
currTabsIds !== prevTabIds)
|
||||
) {
|
||||
const nextTabIndex = findTabIndexByComponentId({
|
||||
currentComponent: props.component,
|
||||
directPathToChild,
|
||||
});
|
||||
|
||||
// make sure nextFocusComponent is under this tabs component
|
||||
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,
|
||||
});
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
},
|
||||
[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: (
|
||||
<span>
|
||||
{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',
|
||||
)}{' '}
|
||||
<b>{t('undo')}</b>{' '}
|
||||
{t('button (cmd + z) until you save your changes.')}
|
||||
</span>
|
||||
),
|
||||
onOk: () => {
|
||||
deleteComponent(key, component.id);
|
||||
const tabIndex = component.children.indexOf(key);
|
||||
handleDeleteTab(tabIndex);
|
||||
},
|
||||
okType: 'danger',
|
||||
okText: t('DELETE'),
|
||||
cancelText: t('CANCEL'),
|
||||
icon: null,
|
||||
});
|
||||
},
|
||||
[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],
|
||||
);
|
||||
|
||||
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 }));
|
||||
setDropPosition(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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
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 {
|
||||
tabIndex: selectedTabIndex,
|
||||
activeKey,
|
||||
dropPosition,
|
||||
dragOverTabIndex,
|
||||
} = this.state;
|
||||
const { children: tabIds } = tabsComponent;
|
||||
|
||||
const showDropIndicators = currentDropTabIndex =>
|
||||
const showDropIndicators = useCallback(
|
||||
currentDropTabIndex =>
|
||||
currentDropTabIndex === dragOverTabIndex && {
|
||||
left: editMode && dropPosition === DROP_LEFT,
|
||||
right: editMode && dropPosition === DROP_RIGHT,
|
||||
};
|
||||
},
|
||||
[dragOverTabIndex, dropPosition, editMode],
|
||||
);
|
||||
|
||||
const removeDraggedTab = tabID => this.state.draggingTabId === tabID;
|
||||
const removeDraggedTab = useCallback(
|
||||
tabID => draggingTabId === tabID,
|
||||
[draggingTabId],
|
||||
);
|
||||
|
||||
let tabsToHighlight;
|
||||
const highlightedFilterId =
|
||||
nativeFilters?.focusedFilterId || nativeFilters?.hoveredFilterId;
|
||||
if (highlightedFilterId) {
|
||||
tabsToHighlight = nativeFilters.filters[highlightedFilterId]?.tabsInScope;
|
||||
}
|
||||
return (
|
||||
<Draggable
|
||||
component={tabsComponent}
|
||||
parentComponent={parentComponent}
|
||||
orientation="row"
|
||||
index={index}
|
||||
depth={depth}
|
||||
onDrop={this.handleDrop}
|
||||
editMode={editMode}
|
||||
let tabsToHighlight;
|
||||
const highlightedFilterId =
|
||||
nativeFilters?.focusedFilterId || nativeFilters?.hoveredFilterId;
|
||||
if (highlightedFilterId) {
|
||||
tabsToHighlight = nativeFilters.filters[highlightedFilterId]?.tabsInScope;
|
||||
}
|
||||
|
||||
const renderChild = useCallback(
|
||||
({ dragSourceRef: tabsDragSourceRef }) => (
|
||||
<StyledTabsContainer
|
||||
className="dashboard-component dashboard-component-tabs"
|
||||
data-test="dashboard-component-tabs"
|
||||
>
|
||||
{({ dragSourceRef: tabsDragSourceRef }) => (
|
||||
<StyledTabsContainer
|
||||
className="dashboard-component dashboard-component-tabs"
|
||||
data-test="dashboard-component-tabs"
|
||||
>
|
||||
{editMode && renderHoverMenu && (
|
||||
<HoverMenu innerRef={tabsDragSourceRef} position="left">
|
||||
<DragHandle position="left" />
|
||||
<DeleteComponentButton onDelete={this.handleDeleteComponent} />
|
||||
</HoverMenu>
|
||||
)}
|
||||
{editMode && renderHoverMenu && (
|
||||
<HoverMenu innerRef={tabsDragSourceRef} position="left">
|
||||
<DragHandle position="left" />
|
||||
<DeleteComponentButton onDelete={handleDeleteComponent} />
|
||||
</HoverMenu>
|
||||
)}
|
||||
|
||||
<LineEditableTabs
|
||||
id={tabsComponent.id}
|
||||
activeKey={activeKey}
|
||||
onChange={key => {
|
||||
this.handleClickTab(tabIds.indexOf(key));
|
||||
}}
|
||||
onEdit={this.handleEdit}
|
||||
data-test="nav-list"
|
||||
type={editMode ? 'editable-card' : 'card'}
|
||||
>
|
||||
{tabIds.map((tabId, tabIndex) => (
|
||||
<LineEditableTabs.TabPane
|
||||
key={tabId}
|
||||
tab={
|
||||
removeDraggedTab(tabId) ? (
|
||||
<></>
|
||||
) : (
|
||||
<>
|
||||
{showDropIndicators(tabIndex).left && (
|
||||
<DropIndicator
|
||||
className="drop-indicator-left"
|
||||
pos="left"
|
||||
/>
|
||||
)}
|
||||
<DashboardComponent
|
||||
id={tabId}
|
||||
parentId={tabsComponent.id}
|
||||
depth={depth}
|
||||
index={tabIndex}
|
||||
renderType={RENDER_TAB}
|
||||
availableColumnCount={availableColumnCount}
|
||||
columnWidth={columnWidth}
|
||||
onDropOnTab={this.handleDropOnTab}
|
||||
onDropPositionChange={this.handleGetDropPosition}
|
||||
onDragTab={this.handleDragggingTab}
|
||||
onHoverTab={() => this.handleClickTab(tabIndex)}
|
||||
isFocused={activeKey === tabId}
|
||||
isHighlighted={
|
||||
activeKey !== tabId &&
|
||||
tabsToHighlight?.includes(tabId)
|
||||
}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
closeIcon={
|
||||
removeDraggedTab(tabId) ? (
|
||||
<></>
|
||||
) : (
|
||||
<CloseIconWithDropIndicator
|
||||
role="button"
|
||||
tabIndex={tabIndex}
|
||||
showDropIndicators={showDropIndicators(tabIndex)}
|
||||
<LineEditableTabs
|
||||
id={tabsComponent.id}
|
||||
activeKey={activeKey}
|
||||
onChange={key => {
|
||||
handleClickTab(tabIds.indexOf(key));
|
||||
}}
|
||||
onEdit={handleEdit}
|
||||
data-test="nav-list"
|
||||
type={editMode ? 'editable-card' : 'card'}
|
||||
>
|
||||
{tabIds.map((tabId, tabIndex) => (
|
||||
<LineEditableTabs.TabPane
|
||||
key={tabId}
|
||||
tab={
|
||||
removeDraggedTab(tabId) ? (
|
||||
<></>
|
||||
) : (
|
||||
<>
|
||||
{showDropIndicators(tabIndex).left && (
|
||||
<DropIndicator
|
||||
className="drop-indicator-left"
|
||||
pos="left"
|
||||
/>
|
||||
)
|
||||
}
|
||||
>
|
||||
{renderTabContent && (
|
||||
)}
|
||||
<DashboardComponent
|
||||
id={tabId}
|
||||
parentId={tabsComponent.id}
|
||||
depth={depth} // see isValidChild.js for why tabs don't increment child depth
|
||||
depth={depth}
|
||||
index={tabIndex}
|
||||
renderType={RENDER_TAB_CONTENT}
|
||||
renderType={RENDER_TAB}
|
||||
availableColumnCount={availableColumnCount}
|
||||
columnWidth={columnWidth}
|
||||
onResizeStart={onResizeStart}
|
||||
onResize={onResize}
|
||||
onResizeStop={onResizeStop}
|
||||
onDropOnTab={this.handleDropOnTab}
|
||||
isComponentVisible={
|
||||
selectedTabIndex === tabIndex && isCurrentTabVisible
|
||||
onDropOnTab={handleDropOnTab}
|
||||
onDropPositionChange={handleGetDropPosition}
|
||||
onDragTab={handleDragggingTab}
|
||||
onHoverTab={() => handleClickTab(tabIndex)}
|
||||
isFocused={activeKey === tabId}
|
||||
isHighlighted={
|
||||
activeKey !== tabId && tabsToHighlight?.includes(tabId)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</LineEditableTabs.TabPane>
|
||||
))}
|
||||
</LineEditableTabs>
|
||||
</StyledTabsContainer>
|
||||
)}
|
||||
</Draggable>
|
||||
);
|
||||
}
|
||||
}
|
||||
</>
|
||||
)
|
||||
}
|
||||
closeIcon={
|
||||
removeDraggedTab(tabId) ? (
|
||||
<></>
|
||||
) : (
|
||||
<CloseIconWithDropIndicator
|
||||
role="button"
|
||||
tabIndex={tabIndex}
|
||||
showDropIndicators={showDropIndicators(tabIndex)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
>
|
||||
{renderTabContent && (
|
||||
<DashboardComponent
|
||||
id={tabId}
|
||||
parentId={tabsComponent.id}
|
||||
depth={depth} // see isValidChild.js for why tabs don't increment child depth
|
||||
index={tabIndex}
|
||||
renderType={RENDER_TAB_CONTENT}
|
||||
availableColumnCount={availableColumnCount}
|
||||
columnWidth={columnWidth}
|
||||
onResizeStart={onResizeStart}
|
||||
onResize={onResize}
|
||||
onResizeStop={onResizeStop}
|
||||
onDropOnTab={handleDropOnTab}
|
||||
isComponentVisible={
|
||||
selectedTabIndex === tabIndex && isCurrentTabVisible
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</LineEditableTabs.TabPane>
|
||||
))}
|
||||
</LineEditableTabs>
|
||||
</StyledTabsContainer>
|
||||
),
|
||||
[
|
||||
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 (
|
||||
<Draggable
|
||||
component={tabsComponent}
|
||||
parentComponent={parentComponent}
|
||||
orientation="row"
|
||||
index={index}
|
||||
depth={depth}
|
||||
onDrop={handleDrop}
|
||||
editMode={editMode}
|
||||
>
|
||||
{renderChild}
|
||||
</Draggable>
|
||||
);
|
||||
};
|
||||
|
||||
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);
|
||||
|
|
|
|||
|
|
@ -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(<Tabs {...props} {...overrideProps} />, {
|
||||
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');
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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 => (
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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<string, string> | 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 = (
|
||||
|
|
|
|||
Loading…
Reference in New Issue