mirror of
https://github.com/apache/superset.git
synced 2024-09-16 10:39:55 -04:00
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:
parent
11bf0d09cb
commit
fc8cb22376
@ -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};
|
||||
}
|
||||
}
|
||||
`}
|
||||
`;
|
||||
|
||||
|
@ -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"
|
||||
/>
|
||||
)}
|
||||
|
@ -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}>
|
||||
|
@ -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);
|
||||
|
@ -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');
|
||||
});
|
||||
|
@ -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,
|
||||
);
|
||||
|
@ -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 */
|
||||
|
Loading…
Reference in New Issue
Block a user