blob: 0f4997d24e1e7215ab5cf5cf1cdb32a218088936 [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 { PureComponent, Fragment } from 'react';
import { withTheme } from '@emotion/react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { addAlpha, css, styled, t } from '@superset-ui/core';
import { EmptyState } from '@superset-ui/core/components';
import { Icons } from '@superset-ui/core/components/Icons';
import { navigateTo } from 'src/utils/navigationUtils';
import { componentShape } from '../util/propShapes';
import DashboardComponent from '../containers/DashboardComponent';
import { Droppable } from './dnd/DragDroppable';
import { GRID_GUTTER_SIZE, GRID_COLUMN_COUNT } from '../util/constants';
import { TAB_TYPE } from '../util/componentTypes';
const propTypes = {
depth: PropTypes.number.isRequired,
editMode: PropTypes.bool,
gridComponent: componentShape,
handleComponentDrop: PropTypes.func.isRequired,
isComponentVisible: PropTypes.bool.isRequired,
resizeComponent: PropTypes.func.isRequired,
setDirectPathToChild: PropTypes.func.isRequired,
width: PropTypes.number.isRequired,
dashboardId: PropTypes.number,
};
const defaultProps = {};
const renderDraggableContent = dropProps =>
dropProps.dropIndicatorProps && <div {...dropProps.dropIndicatorProps} />;
const DashboardEmptyStateContainer = styled.div`
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
`;
const GridContent = styled.div`
${({ theme, editMode }) => css`
display: flex;
flex-direction: column;
/* gutters between rows */
& > div:not(:last-child):not(.empty-droptarget) {
${!editMode && `margin-bottom: ${theme.sizeUnit * 4}px`};
}
.empty-droptarget {
width: 100%;
height: ${theme.sizeUnit * 4}px;
display: flex;
align-items: center;
justify-content: center;
border-radius: ${theme.borderRadius}px;
overflow: hidden;
&:before {
content: '';
display: block;
width: calc(100% - ${theme.sizeUnit * 2}px);
height: calc(100% - ${theme.sizeUnit * 2}px);
border: 1px dashed transparent;
border-radius: ${theme.borderRadius}px;
opacity: 0.5;
}
}
& > .empty-droptarget:first-child {
height: ${theme.sizeUnit * 4}px;
margin-top: ${theme.sizeUnit * -4}px;
}
& > .empty-droptarget:last-child {
height: ${theme.sizeUnit * 24}px;
}
& > .empty-droptarget.empty-droptarget--full:only-child {
height: 80vh;
}
`}
`;
const GridColumnGuide = styled.div`
${({ theme }) => css`
// /* Editing guides */
&.grid-column-guide {
position: absolute;
top: 0;
min-height: 100%;
background-color: ${addAlpha(theme.colorPrimary, 0.1)};
pointer-events: none;
box-shadow: inset 0 0 0 1px ${addAlpha(theme.colorPrimary, 0.6)};
}
`};
`;
class DashboardGrid extends PureComponent {
constructor(props) {
super(props);
this.state = {
isResizing: false,
};
this.theme = this;
this.handleResizeStart = this.handleResizeStart.bind(this);
this.handleResizeStop = this.handleResizeStop.bind(this);
this.handleTopDropTargetDrop = this.handleTopDropTargetDrop.bind(this);
this.getRowGuidePosition = this.getRowGuidePosition.bind(this);
this.setGridRef = this.setGridRef.bind(this);
this.handleChangeTab = this.handleChangeTab.bind(this);
}
getRowGuidePosition(resizeRef) {
if (resizeRef && this.grid) {
return (
resizeRef.getBoundingClientRect().bottom -
this.grid.getBoundingClientRect().top -
2
);
}
return null;
}
setGridRef(ref) {
this.grid = ref;
}
handleResizeStart() {
this.setState(() => ({
isResizing: true,
}));
}
handleResizeStop(_event, _direction, _elementRef, delta, id) {
this.props.resizeComponent({
id,
width: delta.width,
height: delta.height,
});
this.setState(() => ({
isResizing: false,
}));
}
handleTopDropTargetDrop(dropResult) {
if (dropResult) {
this.props.handleComponentDrop({
...dropResult,
destination: {
...dropResult.destination,
// force appending as the first child if top drop target
index: 0,
},
});
}
}
handleChangeTab({ pathToTabIndex }) {
this.props.setDirectPathToChild(pathToTabIndex);
}
render() {
const {
gridComponent,
handleComponentDrop,
depth,
width,
isComponentVisible,
editMode,
canEdit,
setEditMode,
dashboardId,
theme,
} = this.props;
const columnPlusGutterWidth =
(width + GRID_GUTTER_SIZE) / GRID_COLUMN_COUNT;
const columnWidth = columnPlusGutterWidth - GRID_GUTTER_SIZE;
const { isResizing } = this.state;
const shouldDisplayEmptyState = gridComponent?.children?.length === 0;
const shouldDisplayTopLevelTabEmptyState =
shouldDisplayEmptyState && gridComponent.type === TAB_TYPE;
const dashboardEmptyState = editMode && (
<EmptyState
title={t('Drag and drop components and charts to the dashboard')}
description={t(
'You can create a new chart or use existing ones from the panel on the right',
)}
size="large"
buttonText={
<>
<Icons.PlusOutlined iconSize="m" color={theme.colorPrimary} />
{t('Create a new chart')}
</>
}
buttonAction={() => {
navigateTo(`/chart/add?dashboard_id=${dashboardId}`, {
newWindow: true,
});
}}
image="chart.svg"
/>
);
const topLevelTabEmptyState = editMode ? (
<EmptyState
title={t('Drag and drop components to this tab')}
size="large"
description={t(
`You can create a new chart or use existing ones from the panel on the right`,
)}
buttonText={
<>
<Icons.PlusOutlined iconSize="m" color={theme.colorPrimary} />
{t('Create a new chart')}
</>
}
buttonAction={() => {
navigateTo(`/chart/add?dashboard_id=${dashboardId}`, {
newWindow: true,
});
}}
image="chart.svg"
/>
) : (
<EmptyState
title={t('There are no components added to this tab')}
size="large"
description={
canEdit && t('You can add the components in the edit mode.')
}
buttonText={canEdit && t('Edit the dashboard')}
buttonAction={
canEdit &&
(() => {
setEditMode(true);
})
}
image="chart.svg"
/>
);
return width < 100 ? null : (
<>
{shouldDisplayEmptyState && (
<DashboardEmptyStateContainer>
{shouldDisplayTopLevelTabEmptyState
? topLevelTabEmptyState
: dashboardEmptyState}
</DashboardEmptyStateContainer>
)}
<div className="dashboard-grid" ref={this.setGridRef}>
<GridContent
className="grid-content"
data-test="grid-content"
editMode={editMode}
>
{/* make the area above components droppable */}
{editMode && (
<Droppable
component={gridComponent}
depth={depth}
parentComponent={null}
index={0}
orientation="column"
onDrop={this.handleTopDropTargetDrop}
className={classNames({
'empty-droptarget': true,
'empty-droptarget--full':
gridComponent?.children?.length === 0,
})}
editMode
dropToChild={gridComponent?.children?.length === 0}
>
{renderDraggableContent}
</Droppable>
)}
{gridComponent?.children?.map((id, index) => (
<Fragment key={id}>
<DashboardComponent
id={id}
parentId={gridComponent.id}
depth={depth + 1}
index={index}
availableColumnCount={GRID_COLUMN_COUNT}
columnWidth={columnWidth}
isComponentVisible={isComponentVisible}
onResizeStart={this.handleResizeStart}
onResize={this.handleResize}
onResizeStop={this.handleResizeStop}
onChangeTab={this.handleChangeTab}
/>
{/* make the area below components droppable */}
{editMode && (
<Droppable
component={gridComponent}
depth={depth}
parentComponent={null}
index={index + 1}
orientation="column"
onDrop={handleComponentDrop}
className="empty-droptarget"
editMode
>
{renderDraggableContent}
</Droppable>
)}
</Fragment>
))}
{isResizing &&
Array(GRID_COLUMN_COUNT)
.fill(null)
.map((_, i) => (
<GridColumnGuide
key={`grid-column-${i}`}
className="grid-column-guide"
style={{
left: i * GRID_GUTTER_SIZE + i * columnWidth,
width: columnWidth,
}}
/>
))}
</GridContent>
</div>
</>
);
}
}
DashboardGrid.propTypes = propTypes;
DashboardGrid.defaultProps = defaultProps;
export default withTheme(DashboardGrid);