feat(dashboard): Implement empty states for empty tabs (#19408)

* feat(dashboard): Implement empty states in empty tabs

* Change button to in text link

* Add edit dashboard button to dashboard empty state

* Add tests

* Fix test
This commit is contained in:
Kamil Gabryjelski 2022-04-01 15:23:00 +02:00 committed by GitHub
parent 11bf0d09cb
commit fc8cb22376
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 220 additions and 30 deletions

View File

@ -58,6 +58,15 @@ const EmptyStateContainer = styled.div`
& .ant-empty-image svg {
width: auto;
}
& a,
& span[role='button'] {
color: inherit;
text-decoration: underline;
&:hover {
color: ${theme.colors.grayscale.base};
}
}
`}
`;

View File

@ -34,7 +34,10 @@ import getDirectPathToTabIndex from 'src/dashboard/util/getDirectPathToTabIndex'
import { URL_PARAMS } from 'src/constants';
import { getUrlParam } from 'src/utils/urlUtils';
import { DashboardLayout, RootState } from 'src/dashboard/types';
import { setDirectPathToChild } from 'src/dashboard/actions/dashboardState';
import {
setDirectPathToChild,
setEditMode,
} from 'src/dashboard/actions/dashboardState';
import { useElementOnScreen } from 'src/hooks/useElementOnScreen';
import { FeatureFlag, isFeatureEnabled } from 'src/featureFlags';
import {
@ -398,6 +401,8 @@ const DashboardBuilder: FC<DashboardBuilderProps> = () => {
'Go to the edit mode to configure the dashboard and add charts',
)
}
buttonText={canEdit && t('Edit the dashboard')}
buttonAction={() => dispatch(setEditMode(true))}
image="dashboard.svg"
/>
)}

View File

@ -24,6 +24,7 @@ import { componentShape } from '../util/propShapes';
import DashboardComponent from '../containers/DashboardComponent';
import DragDroppable 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,
@ -137,9 +138,11 @@ class DashboardGrid extends React.PureComponent {
gridComponent,
handleComponentDrop,
depth,
editMode,
width,
isComponentVisible,
editMode,
canEdit,
setEditMode,
} = this.props;
const columnPlusGutterWidth =
(width + GRID_GUTTER_SIZE) / GRID_COLUMN_COUNT;
@ -147,26 +150,70 @@ class DashboardGrid extends React.PureComponent {
const columnWidth = columnPlusGutterWidth - GRID_GUTTER_SIZE;
const { isResizing, rowGuideTop } = this.state;
const shouldDisplayEmptyState = gridComponent?.children?.length === 0;
const shouldDisplayTopLevelTabEmptyState =
shouldDisplayEmptyState && gridComponent.type === TAB_TYPE;
const dashboardEmptyState = editMode && (
<EmptyStateBig
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',
)}
buttonText={
<>
<i className="fa fa-plus" />
{t('Create a new chart')}
</>
}
buttonAction={() => {
window.open('/chart/add', '_blank', 'noopener noreferrer');
}}
image="chart.svg"
/>
);
const topLevelTabEmptyState = editMode ? (
<EmptyStateBig
title={t('Drag and drop components to this tab')}
description={t(
`You can create a new chart or use existing ones from the panel on the right`,
)}
buttonText={
<>
<i className="fa fa-plus" />
{t('Create a new chart')}
</>
}
buttonAction={() => {
window.open('/chart/add', '_blank', 'noopener noreferrer');
}}
image="chart.svg"
/>
) : (
<EmptyStateBig
title={t('There are no components added to this tab')}
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 : (
<>
{editMode && gridComponent?.children?.length === 0 && (
{shouldDisplayEmptyState && (
<DashboardEmptyStateContainer>
<EmptyStateBig
title={t('Drag and drop components and charts to the dashboard')}
description={t(
'You can create new charts or use existing ones from the panel on the right',
)}
buttonText={
<>
<i className="fa fa-plus" />
{t('Create a new chart')}
</>
}
buttonAction={() => {
window.location.assign('/chart/add');
}}
image="chart.svg"
/>
{shouldDisplayTopLevelTabEmptyState
? topLevelTabEmptyState
: dashboardEmptyState}
</DashboardEmptyStateContainer>
)}
<div className="dashboard-grid" ref={this.setGridRef}>

View File

@ -18,8 +18,12 @@
*/
import React from 'react';
import PropTypes from 'prop-types';
import { styled } from '@superset-ui/core';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import { styled, t } from '@superset-ui/core';
import { EmptyStateMedium } from 'src/components/EmptyState';
import { setEditMode } from 'src/dashboard/actions/dashboardState';
import DashboardComponent from '../../containers/DashboardComponent';
import DragDroppable from '../dnd/DragDroppable';
import EditableTitle from '../../../components/EditableTitle';
@ -40,6 +44,7 @@ const propTypes = {
renderType: PropTypes.oneOf([RENDER_TAB, RENDER_TAB_CONTENT]).isRequired,
onDropOnTab: PropTypes.func,
editMode: PropTypes.bool.isRequired,
canEdit: PropTypes.bool.isRequired,
filters: PropTypes.object.isRequired,
// grid related
@ -53,6 +58,7 @@ const propTypes = {
handleComponentDrop: PropTypes.func.isRequired,
updateComponents: PropTypes.func.isRequired,
setDirectPathToChild: PropTypes.func.isRequired,
setEditMode: PropTypes.func.isRequired,
};
const defaultProps = {
@ -85,7 +91,7 @@ const renderDraggableContentTop = dropProps =>
<div className="drop-indicator drop-indicator--top" />
);
export default class Tab extends React.PureComponent {
class Tab extends React.PureComponent {
constructor(props) {
super(props);
this.handleChangeText = this.handleChangeText.bind(this);
@ -143,8 +149,11 @@ export default class Tab extends React.PureComponent {
onResizeStop,
editMode,
isComponentVisible,
canEdit,
setEditMode,
} = this.props;
const shouldDisplayEmptyState = tabComponent.children.length === 0;
return (
<div className="dashboard-component-tabs-content">
{/* Make top of tab droppable */}
@ -162,6 +171,43 @@ export default class Tab extends React.PureComponent {
{renderDraggableContentTop}
</DragDroppable>
)}
{shouldDisplayEmptyState && (
<EmptyStateMedium
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"
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={() => setEditMode(true)}
>
{t('edit mode')}
</span>
</span>
))
}
image="chart.svg"
/>
)}
{tabComponent.children.map((componentId, componentIndex) => (
<DashboardComponent
key={componentId}
@ -262,3 +308,20 @@ export default class Tab extends React.PureComponent {
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);

View File

@ -23,6 +23,7 @@ import { render, screen } from 'spec/helpers/testing-library';
import DashboardComponent from 'src/dashboard/containers/DashboardComponent';
import EditableTitle from 'src/components/EditableTitle';
import DragDroppable from 'src/dashboard/components/dnd/DragDroppable';
import { setEditMode } from 'src/dashboard/actions/dashboardState';
import Tab from './Tab';
@ -54,8 +55,13 @@ jest.mock('src/dashboard/components/dnd/DragDroppable', () =>
);
}),
);
jest.mock('src/dashboard/actions/dashboardState', () => ({
setEditMode: jest.fn(() => ({
type: 'SET_EDIT_MODE',
})),
}));
const creteProps = () => ({
const createProps = () => ({
id: 'TAB-YT6eNksV-',
parentId: 'TABS-L-d9eyOE-b',
depth: 2,
@ -98,7 +104,7 @@ beforeEach(() => {
});
test('Render tab (no content)', () => {
const props = creteProps();
const props = createProps();
props.renderType = 'RENDER_TAB';
render(<Tab {...props} />, { useRedux: true, useDnd: true });
expect(screen.getByText('🚀 Aspiring Developers')).toBeInTheDocument();
@ -107,7 +113,7 @@ test('Render tab (no content)', () => {
});
test('Render tab (no content) editMode:true', () => {
const props = creteProps();
const props = createProps();
props.editMode = true;
props.renderType = 'RENDER_TAB';
render(<Tab {...props} />, { useRedux: true, useDnd: true });
@ -117,7 +123,7 @@ test('Render tab (no content) editMode:true', () => {
});
test('Edit table title', () => {
const props = creteProps();
const props = createProps();
props.editMode = true;
props.renderType = 'RENDER_TAB';
render(<Tab {...props} />, { useRedux: true, useDnd: true });
@ -131,7 +137,7 @@ test('Edit table title', () => {
});
test('Render tab (with content)', () => {
const props = creteProps();
const props = createProps();
props.isFocused = true;
render(<Tab {...props} />, { useRedux: true, useDnd: true });
expect(DashboardComponent).toBeCalledTimes(2);
@ -174,8 +180,39 @@ test('Render tab (with content)', () => {
expect(DragDroppable).toBeCalledTimes(0);
});
test('Render tab content with no children', () => {
const props = createProps();
props.component.children = [];
render(<Tab {...props} />, {
useRedux: true,
useDnd: true,
});
expect(
screen.getByText('There are no components added to this tab'),
).toBeVisible();
expect(screen.getByAltText('empty')).toBeVisible();
expect(screen.queryByText('edit mode')).not.toBeInTheDocument();
});
test('Render tab content with no children, canEdit: true', () => {
const props = createProps();
props.component.children = [];
render(<Tab {...props} />, {
useRedux: true,
useDnd: true,
initialState: {
dashboardInfo: {
dash_edit_perm: true,
},
},
});
expect(screen.getByText('edit mode')).toBeVisible();
userEvent.click(screen.getByRole('button', { name: 'edit mode' }));
expect(setEditMode).toHaveBeenCalled();
});
test('Render tab (with content) editMode:true', () => {
const props = creteProps();
const props = createProps();
props.isFocused = true;
props.editMode = true;
render(<Tab {...props} />, { useRedux: true, useDnd: true });
@ -220,7 +257,7 @@ test('Render tab (with content) editMode:true', () => {
});
test('Should call "handleDrop" and "handleTopDropTargetDrop"', () => {
const props = creteProps();
const props = createProps();
props.isFocused = true;
props.editMode = true;
render(<Tab {...props} />, { useRedux: true, useDnd: true });
@ -233,3 +270,29 @@ test('Should call "handleDrop" and "handleTopDropTargetDrop"', () => {
expect(props.onDropOnTab).toBeCalledTimes(1);
expect(props.handleComponentDrop).toBeCalledTimes(2);
});
test('Render tab content with no children, editMode: true, canEdit: true', () => {
const props = createProps();
props.editMode = true;
// props.canEdit = true;
props.component.children = [];
render(<Tab {...props} />, {
useRedux: true,
useDnd: true,
initialState: {
dashboardInfo: {
dash_edit_perm: true,
},
},
});
expect(
screen.getByText('Drag and drop components to this tab'),
).toBeVisible();
expect(screen.getByAltText('empty')).toBeVisible();
expect(
screen.getByRole('link', { name: 'create a new chart' }),
).toBeVisible();
expect(
screen.getByRole('link', { name: 'create a new chart' }),
).toHaveAttribute('href', '/chart/add');
});

View File

@ -24,11 +24,12 @@ import {
handleComponentDrop,
resizeComponent,
} from '../actions/dashboardLayout';
import { setDirectPathToChild } from '../actions/dashboardState';
import { setDirectPathToChild, setEditMode } from '../actions/dashboardState';
function mapStateToProps({ dashboardState }) {
function mapStateToProps({ dashboardState, dashboardInfo }) {
return {
editMode: dashboardState.editMode,
canEdit: dashboardInfo.dash_edit_perm,
};
}
@ -38,6 +39,7 @@ function mapDispatchToProps(dispatch) {
handleComponentDrop,
resizeComponent,
setDirectPathToChild,
setEditMode,
},
dispatch,
);

View File

@ -22,6 +22,7 @@
flex-grow: 1;
display: flex;
flex-direction: column;
height: 100%;
}
/* only top-level tabs have popover, give it more padding to match header + tabs */