From fc8cb223761ef78f888d5d4dbf038caa1028b277 Mon Sep 17 00:00:00 2001 From: Kamil Gabryjelski Date: Fri, 1 Apr 2022 15:23:00 +0200 Subject: [PATCH] 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 --- .../src/components/EmptyState/index.tsx | 9 ++ .../DashboardBuilder/DashboardBuilder.tsx | 7 +- .../dashboard/components/DashboardGrid.jsx | 83 +++++++++++++++---- .../components/gridComponents/Tab.jsx | 67 ++++++++++++++- .../components/gridComponents/Tab.test.tsx | 77 +++++++++++++++-- .../dashboard/containers/DashboardGrid.jsx | 6 +- .../src/dashboard/stylesheets/builder.less | 1 + 7 files changed, 220 insertions(+), 30 deletions(-) diff --git a/superset-frontend/src/components/EmptyState/index.tsx b/superset-frontend/src/components/EmptyState/index.tsx index d3e0c5701a..02c1d7c4a2 100644 --- a/superset-frontend/src/components/EmptyState/index.tsx +++ b/superset-frontend/src/components/EmptyState/index.tsx @@ -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}; + } + } `} `; diff --git a/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardBuilder.tsx b/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardBuilder.tsx index 4f7ef563ce..ab1e95b9a1 100644 --- a/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardBuilder.tsx +++ b/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardBuilder.tsx @@ -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 = () => { '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" /> )} diff --git a/superset-frontend/src/dashboard/components/DashboardGrid.jsx b/superset-frontend/src/dashboard/components/DashboardGrid.jsx index 5a9d2ff812..4be8d6bc05 100644 --- a/superset-frontend/src/dashboard/components/DashboardGrid.jsx +++ b/superset-frontend/src/dashboard/components/DashboardGrid.jsx @@ -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 && ( + + + {t('Create a new chart')} + + } + buttonAction={() => { + window.open('/chart/add', '_blank', 'noopener noreferrer'); + }} + image="chart.svg" + /> + ); + + const topLevelTabEmptyState = editMode ? ( + + + {t('Create a new chart')} + + } + buttonAction={() => { + window.open('/chart/add', '_blank', 'noopener noreferrer'); + }} + image="chart.svg" + /> + ) : ( + { + setEditMode(true); + }) + } + image="chart.svg" + /> + ); + return width < 100 ? null : ( <> - {editMode && gridComponent?.children?.length === 0 && ( + {shouldDisplayEmptyState && ( - - - {t('Create a new chart')} - - } - buttonAction={() => { - window.location.assign('/chart/add'); - }} - image="chart.svg" - /> + {shouldDisplayTopLevelTabEmptyState + ? topLevelTabEmptyState + : dashboardEmptyState} )}
diff --git a/superset-frontend/src/dashboard/components/gridComponents/Tab.jsx b/superset-frontend/src/dashboard/components/gridComponents/Tab.jsx index cf051baefb..7715627850 100644 --- a/superset-frontend/src/dashboard/components/gridComponents/Tab.jsx +++ b/superset-frontend/src/dashboard/components/gridComponents/Tab.jsx @@ -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 =>
); -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 (
{/* Make top of tab droppable */} @@ -162,6 +171,43 @@ export default class Tab extends React.PureComponent { {renderDraggableContentTop} )} + {shouldDisplayEmptyState && ( + + {t('You can')}{' '} + + {t('create a new chart')} + {' '} + {t('or use existing ones from the panel on the right')} + + ) : ( + + {t('You can add the components in the')}{' '} + setEditMode(true)} + > + {t('edit mode')} + + + )) + } + image="chart.svg" + /> + )} {tabComponent.children.map((componentId, componentIndex) => ( ); }), ); +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(, { 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(, { 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(, { 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(, { 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(, { + 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(, { + 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(, { 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(, { 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(, { + 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'); +}); diff --git a/superset-frontend/src/dashboard/containers/DashboardGrid.jsx b/superset-frontend/src/dashboard/containers/DashboardGrid.jsx index cbec708e54..9668847611 100644 --- a/superset-frontend/src/dashboard/containers/DashboardGrid.jsx +++ b/superset-frontend/src/dashboard/containers/DashboardGrid.jsx @@ -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, ); diff --git a/superset-frontend/src/dashboard/stylesheets/builder.less b/superset-frontend/src/dashboard/stylesheets/builder.less index 1512e4c6fa..422d455622 100644 --- a/superset-frontend/src/dashboard/stylesheets/builder.less +++ b/superset-frontend/src/dashboard/stylesheets/builder.less @@ -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 */