blob: 8902e84a4675dc74703f0971abcf6feeb12948ff [file] [log] [blame]
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { Fragment, useCallback, memo, useEffect } from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { useDispatch, useSelector } from 'react-redux';
import { t } from '@superset-ui/core';
import { styled } from '@apache-superset/core/ui';
import { EditableTitle, EmptyState } from '@superset-ui/core/components';
import { setEditMode, onRefresh } from 'src/dashboard/actions/dashboardState';
import getChartIdsFromComponent from 'src/dashboard/util/getChartIdsFromComponent';
import DashboardComponent from 'src/dashboard/containers/DashboardComponent';
import AnchorLink from 'src/dashboard/components/AnchorLink';
import {
DragDroppable,
Droppable,
} from 'src/dashboard/components/dnd/DragDroppable';
import { componentShape } from 'src/dashboard/util/propShapes';
import { TAB_TYPE } from 'src/dashboard/util/componentTypes';
export const RENDER_TAB = 'RENDER_TAB';
export const RENDER_TAB_CONTENT = 'RENDER_TAB_CONTENT';
// Delay before refreshing charts to ensure they are fully mounted
const CHART_MOUNT_DELAY = 100;
const propTypes = {
dashboardId: PropTypes.number.isRequired,
id: PropTypes.string.isRequired,
parentId: PropTypes.string.isRequired,
component: componentShape.isRequired,
parentComponent: componentShape.isRequired,
index: PropTypes.number.isRequired,
depth: PropTypes.number.isRequired,
renderType: PropTypes.oneOf([RENDER_TAB, RENDER_TAB_CONTENT]).isRequired,
onDropOnTab: PropTypes.func,
onDropPositionChange: PropTypes.func,
onDragTab: PropTypes.func,
onHoverTab: PropTypes.func,
editMode: PropTypes.bool.isRequired,
embeddedMode: PropTypes.bool,
onTabTitleEditingChange: PropTypes.func,
// grid related
availableColumnCount: PropTypes.number,
columnWidth: PropTypes.number,
onResizeStart: PropTypes.func,
onResize: PropTypes.func,
onResizeStop: PropTypes.func,
// redux
handleComponentDrop: PropTypes.func.isRequired,
updateComponents: PropTypes.func.isRequired,
setDirectPathToChild: PropTypes.func.isRequired,
isComponentVisible: PropTypes.bool,
};
const defaultProps = {
availableColumnCount: 0,
columnWidth: 0,
onDropOnTab() {},
onDropPositionChange() {},
onDragTab() {},
onHoverTab() {},
onResizeStart() {},
onResize() {},
onResizeStop() {},
onTabTitleEditingChange() {},
};
const TabTitleContainer = styled.div`
${({ isHighlighted, theme: { sizeUnit, colorPrimaryBg } }) => `
display: inline-flex;
position: relative;
align-items: center;
margin: 0 ${sizeUnit * 2}px;
transition: box-shadow 0.2s ease-in-out;
${isHighlighted ? `box-shadow: 0 0 ${sizeUnit}px ${colorPrimaryBg};` : ''}
.anchor-link-container {
position: absolute;
left: 100%;
opacity: 0;
transition: opacity 0.2s ease-in-out;
}
&:hover .anchor-link-container {
opacity: 1;
}
`}
`;
const TitleDropIndicator = styled.div`
&.drop-indicator {
position: absolute;
top: 0;
border-radius: 4px;
}
`;
const renderDraggableContent = dropProps =>
dropProps.dropIndicatorProps && <div {...dropProps.dropIndicatorProps} />;
const Tab = props => {
const dispatch = useDispatch();
const canEdit = useSelector(state => state.dashboardInfo.dash_edit_perm);
const dashboardLayout = useSelector(state => state.dashboardLayout.present);
const lastRefreshTime = useSelector(
state => state.dashboardState.lastRefreshTime,
);
const tabActivationTime = useSelector(
state => state.dashboardState.tabActivationTimes?.[props.id] || 0,
);
const dashboardInfo = useSelector(state => state.dashboardInfo);
useEffect(() => {
if (props.renderType === RENDER_TAB_CONTENT && props.isComponentVisible) {
if (
lastRefreshTime &&
tabActivationTime &&
lastRefreshTime > tabActivationTime
) {
const chartIds = getChartIdsFromComponent(props.id, dashboardLayout);
if (chartIds.length > 0) {
requestAnimationFrame(() => {
setTimeout(() => {
dispatch(onRefresh(chartIds, true, 0, dashboardInfo.id));
}, CHART_MOUNT_DELAY);
});
}
}
}
}, [
props.isComponentVisible,
props.renderType,
props.id,
lastRefreshTime,
tabActivationTime,
dashboardLayout,
dashboardInfo.id,
dispatch,
]);
const handleChangeTab = useCallback(
({ pathToTabIndex }) => {
props.setDirectPathToChild(pathToTabIndex);
},
[props.setDirectPathToChild],
);
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],
);
const handleDrop = useCallback(
dropResult => {
props.handleComponentDrop(dropResult);
props.onDropOnTab(dropResult);
},
[props.handleComponentDrop, props.onDropOnTab],
);
const handleHoverTab = useCallback(() => {
props.onHoverTab?.();
}, [props.onHoverTab]);
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],
);
const shouldDropToChild = useCallback(item => item.type !== TAB_TYPE, []);
const renderTabContent = useCallback(() => {
const {
component: tabComponent,
depth,
availableColumnCount,
columnWidth,
onResizeStart,
onResize,
onResizeStop,
editMode,
isComponentVisible,
dashboardId,
} = props;
const shouldDisplayEmptyState = tabComponent.children.length === 0;
return (
<div className="dashboard-component-tabs-content">
{/* Make top of tab droppable */}
{editMode && (
<Droppable
component={tabComponent}
orientation="column"
index={0}
depth={depth}
onDrop={
tabComponent.children.length === 0
? handleTopDropTargetDrop
: handleDrop
}
editMode
className={classNames({
'empty-droptarget': true,
'empty-droptarget--full': tabComponent.children.length === 0,
})}
dropToChild={tabComponent.children.length === 0}
>
{renderDraggableContent}
</Droppable>
)}
{shouldDisplayEmptyState && (
<EmptyState
title={
editMode
? t('Drag and drop components to this tab')
: t('There are no components added to this tab')
}
description={
canEdit &&
(editMode ? (
<span>
{t('You can')}{' '}
<a
href={`/chart/add?dashboard_id=${dashboardId}`}
rel="noopener noreferrer"
target="_blank"
>
{t('create a new chart')}
</a>{' '}
{t('or use existing ones from the panel on the right')}
</span>
) : (
<span>
{t('You can add the components in the')}{' '}
<span
role="button"
tabIndex={0}
onClick={() => dispatch(setEditMode(true))}
>
{t('edit mode')}
</span>
</span>
))
}
image="chart.svg"
/>
)}
{tabComponent.children.map((componentId, componentIndex) => (
<Fragment key={componentId}>
<DashboardComponent
id={componentId}
parentId={tabComponent.id}
depth={depth} // see isValidChild.js for why tabs don't increment child depth
index={componentIndex}
onDrop={handleDrop}
onHover={handleHoverTab}
availableColumnCount={availableColumnCount}
columnWidth={columnWidth}
onResizeStart={onResizeStart}
onResize={onResize}
onResizeStop={onResizeStop}
isComponentVisible={isComponentVisible}
onChangeTab={handleChangeTab}
/>
{/* Make bottom of tab droppable */}
{editMode && (
<Droppable
component={tabComponent}
orientation="column"
index={componentIndex + 1}
depth={depth}
onDrop={handleDrop}
editMode
className="empty-droptarget"
>
{renderDraggableContent}
</Droppable>
)}
</Fragment>
))}
</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,
]);
const renderTabChild = useCallback(
({ dropIndicatorProps, dragSourceRef, draggingTabOnTab }) => {
const {
component,
index,
editMode,
isFocused,
isHighlighted,
dashboardId,
embeddedMode,
onTabTitleEditingChange,
} = 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}
onEditingChange={onTabTitleEditingChange}
/>
{!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,
props.onTabTitleEditingChange,
handleChangeText,
],
);
const renderTab = useCallback(() => {
const {
component,
parentComponent,
index,
depth,
editMode,
onDropPositionChange,
onDragTab,
} = props;
return (
<DragDroppable
component={component}
parentComponent={parentComponent}
orientation="column"
index={index}
depth={depth}
onDrop={handleDrop}
onHover={handleHoverTab}
onDropIndicatorChange={onDropPositionChange}
onDragTab={onDragTab}
editMode={editMode}
dropToChild={shouldDropToChild}
>
{renderTabChild}
</DragDroppable>
);
}, [
props.component,
props.parentComponent,
props.index,
props.depth,
props.editMode,
handleDrop,
handleHoverTab,
shouldDropToChild,
renderTabChild,
]);
return props.renderType === RENDER_TAB ? renderTab() : renderTabContent();
};
Tab.propTypes = propTypes;
Tab.defaultProps = defaultProps;
export default memo(Tab);