[Design] Clean up dashboard listing page (#19657)

Along with @nreese, cleaned up the dashboard listing view to add an empty state.
This commit is contained in:
dave.snider@gmail.com 2018-06-12 17:43:20 -07:00 committed by GitHub
parent 77b53db939
commit 43639cd9c0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 664 additions and 810 deletions

View file

@ -2,81 +2,17 @@
exports[`after fetch hideWriteControls 1`] = `
<EuiPage
className="dashboardLandingPage"
data-test-subj="dashboardLandingPage"
restrictWidth={false}
>
<EuiFlexGroup
alignItems="flexEnd"
component="div"
data-test-subj="top-nav"
direction="row"
gutterSize="l"
justifyContent="spaceBetween"
responsive={true}
wrap={false}
<EuiPageBody>
<EuiPageContent
className="dashboardLandingPage__content"
horizontalPosition="center"
panelPaddingSize="l"
verticalPosition="center"
>
<EuiFlexItem
component="div"
grow={false}
>
<EuiTitle
size="l"
>
<h1>
Dashboard
</h1>
</EuiTitle>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer
size="m"
/>
<EuiFlexGroup
alignItems="stretch"
component="div"
direction="row"
gutterSize="l"
justifyContent="flexStart"
responsive={true}
wrap={false}
>
<EuiFlexItem
component="div"
grow={true}
>
<EuiFieldSearch
compressed={false}
data-test-subj="searchFilter"
fullWidth={true}
incremental={false}
isLoading={false}
onChange={[Function]}
placeholder="Search..."
value=""
/>
</EuiFlexItem>
</EuiFlexGroup>
<EuiBasicTable
columns={
Array [
Object {
"field": "title",
"name": "Title",
"render": [Function],
"sortable": true,
},
Object {
"dataType": "string",
"field": "description",
"name": "Description",
"sortable": true,
},
]
}
itemId="id"
items={Array []}
loading={false}
noItemsMessage={
<EuiText
grow={true}
>
@ -88,36 +24,25 @@ exports[`after fetch hideWriteControls 1`] = `
</EuiTextColor>
</h2>
</EuiText>
}
onChange={[Function]}
pagination={
Object {
"pageIndex": 0,
"pageSize": 20,
"pageSizeOptions": Array [
10,
20,
50,
],
"totalItemCount": 0,
}
}
responsive={true}
selection={
Object {
"onSelectionChange": [Function],
}
}
sorting={Object {}}
/>
</EuiPageContent>
</EuiPageBody>
</EuiPage>
`;
exports[`after fetch renders call to action when no dashboards exist 1`] = `
exports[`after fetch initialFilter 1`] = `
<EuiPage
className="dashboardLandingPage"
data-test-subj="dashboardLandingPage"
restrictWidth={false}
>
<EuiPageBody>
<EuiPageContent
className="dashboardLandingPage__content"
horizontalPosition="center"
panelPaddingSize="l"
verticalPosition="center"
>
<div>
<EuiFlexGroup
alignItems="flexEnd"
component="div"
@ -136,7 +61,7 @@ exports[`after fetch renders call to action when no dashboards exist 1`] = `
size="l"
>
<h1>
Dashboard
Dashboards
</h1>
</EuiTitle>
</EuiFlexItem>
@ -180,159 +105,13 @@ exports[`after fetch renders call to action when no dashboards exist 1`] = `
isLoading={false}
onChange={[Function]}
placeholder="Search..."
value=""
value="my dashboard"
/>
</EuiFlexItem>
</EuiFlexGroup>
<EuiBasicTable
columns={
Array [
Object {
"field": "title",
"name": "Title",
"render": [Function],
"sortable": true,
},
Object {
"dataType": "string",
"field": "description",
"name": "Description",
"sortable": true,
},
Object {
"actions": Array [
Object {
"render": [Function],
},
],
"name": "Actions",
},
]
}
itemId="id"
items={Array []}
loading={false}
noItemsMessage={
<UNDEFINED>
<EuiText
grow={true}
>
<h2>
<EuiTextColor
color="subdued"
>
Looks like you don't have any dashboards. Let's create some!
</EuiTextColor>
</h2>
</EuiText>
<EuiButton
color="primary"
data-test-subj="createDashboardPromptButton"
fill={true}
href="#/dashboard"
iconSide="left"
iconType="plusInCircle"
type="button"
>
Create new dashboard
</EuiButton>
</UNDEFINED>
}
onChange={[Function]}
pagination={
Object {
"pageIndex": 0,
"pageSize": 20,
"pageSizeOptions": Array [
10,
20,
50,
],
"totalItemCount": 0,
}
}
responsive={true}
selection={
Object {
"onSelectionChange": [Function],
}
}
sorting={Object {}}
/>
</EuiPage>
`;
exports[`after fetch renders table rows 1`] = `
<EuiPage
data-test-subj="dashboardLandingPage"
restrictWidth={false}
>
<EuiFlexGroup
alignItems="flexEnd"
component="div"
data-test-subj="top-nav"
direction="row"
gutterSize="l"
justifyContent="spaceBetween"
responsive={true}
wrap={false}
>
<EuiFlexItem
component="div"
grow={false}
>
<EuiTitle
size="l"
>
<h1>
Dashboard
</h1>
</EuiTitle>
</EuiFlexItem>
<EuiFlexItem
component="div"
grow={false}
>
<EuiButton
color="primary"
data-test-subj="newDashboardLink"
fill={false}
href="#/dashboard"
iconSide="left"
type="button"
>
Create new dashboard
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer
size="m"
/>
<EuiFlexGroup
alignItems="stretch"
component="div"
direction="row"
gutterSize="l"
justifyContent="flexStart"
responsive={true}
wrap={false}
>
<EuiFlexItem
component="div"
grow={true}
>
<EuiFieldSearch
compressed={false}
data-test-subj="searchFilter"
fullWidth={true}
incremental={false}
isLoading={false}
onChange={[Function]}
placeholder="Search..."
value=""
/>
</EuiFlexItem>
</EuiFlexGroup>
<EuiBasicTable
columns={
Array [
@ -396,14 +175,86 @@ exports[`after fetch renders table rows 1`] = `
}
sorting={Object {}}
/>
</div>
</EuiPageContent>
</EuiPageBody>
</EuiPage>
`;
exports[`after fetch renders warning when listingLimit is exceeded 1`] = `
exports[`after fetch renders call to action when no dashboards exist 1`] = `
<EuiPage
className="dashboardLandingPage"
data-test-subj="dashboardLandingPage"
restrictWidth={false}
>
<EuiPageBody>
<EuiPageContent
className="dashboardLandingPage__content"
horizontalPosition="center"
panelPaddingSize="l"
verticalPosition="center"
>
<div>
<EuiEmptyPrompt
actions={
<EuiButton
color="primary"
data-test-subj="createDashboardPromptButton"
fill={true}
href="#/dashboard"
iconSide="left"
iconType="plusInCircle"
type="button"
>
Create new dashboard
</EuiButton>
}
body={
<UNDEFINED>
<p>
You can combine data views from any Kibana app into one dashboard and see everything in one place.
</p>
<p>
New to Kibana?
<EuiLink
color="primary"
href="#/home/tutorial_directory/sampleData"
type="button"
>
Install some sample data
</EuiLink>
to take a test drive.
</p>
</UNDEFINED>
}
iconColor="subdued"
iconType="dashboardApp"
title={
<h2>
Create your first dashboard
</h2>
}
/>
</div>
</EuiPageContent>
</EuiPageBody>
</EuiPage>
`;
exports[`after fetch renders table rows 1`] = `
<EuiPage
className="dashboardLandingPage"
data-test-subj="dashboardLandingPage"
restrictWidth={false}
>
<EuiPageBody>
<EuiPageContent
className="dashboardLandingPage__content"
horizontalPosition="center"
panelPaddingSize="l"
verticalPosition="center"
>
<div>
<EuiFlexGroup
alignItems="flexEnd"
component="div"
@ -422,7 +273,159 @@ exports[`after fetch renders warning when listingLimit is exceeded 1`] = `
size="l"
>
<h1>
Dashboard
Dashboards
</h1>
</EuiTitle>
</EuiFlexItem>
<EuiFlexItem
component="div"
grow={false}
>
<EuiButton
color="primary"
data-test-subj="newDashboardLink"
fill={false}
href="#/dashboard"
iconSide="left"
type="button"
>
Create new dashboard
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer
size="m"
/>
<EuiFlexGroup
alignItems="stretch"
component="div"
direction="row"
gutterSize="l"
justifyContent="flexStart"
responsive={true}
wrap={false}
>
<EuiFlexItem
component="div"
grow={true}
>
<EuiFieldSearch
compressed={false}
data-test-subj="searchFilter"
fullWidth={true}
incremental={false}
isLoading={false}
onChange={[Function]}
placeholder="Search..."
value=""
/>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer
size="m"
/>
<EuiBasicTable
columns={
Array [
Object {
"field": "title",
"name": "Title",
"render": [Function],
"sortable": true,
},
Object {
"dataType": "string",
"field": "description",
"name": "Description",
"sortable": true,
},
Object {
"actions": Array [
Object {
"render": [Function],
},
],
"name": "Actions",
},
]
}
itemId="id"
items={
Array [
Object {
"description": "dashboard0 desc",
"id": "dashboard0",
"title": "dashboard0 title",
},
Object {
"description": "dashboard1 desc",
"id": "dashboard1",
"title": "dashboard1 title",
},
]
}
loading={false}
noItemsMessage="No dashboards matched your search."
onChange={[Function]}
pagination={
Object {
"pageIndex": 0,
"pageSize": 20,
"pageSizeOptions": Array [
10,
20,
50,
],
"totalItemCount": 2,
}
}
responsive={true}
selection={
Object {
"onSelectionChange": [Function],
}
}
sorting={Object {}}
/>
</div>
</EuiPageContent>
</EuiPageBody>
</EuiPage>
`;
exports[`after fetch renders warning when listingLimit is exceeded 1`] = `
<EuiPage
className="dashboardLandingPage"
data-test-subj="dashboardLandingPage"
restrictWidth={false}
>
<EuiPageBody>
<EuiPageContent
className="dashboardLandingPage__content"
horizontalPosition="center"
panelPaddingSize="l"
verticalPosition="center"
>
<div>
<EuiFlexGroup
alignItems="flexEnd"
component="div"
data-test-subj="top-nav"
direction="row"
gutterSize="l"
justifyContent="spaceBetween"
responsive={true}
wrap={false}
>
<EuiFlexItem
component="div"
grow={false}
>
<EuiTitle
size="l"
>
<h1>
Dashboards
</h1>
</EuiTitle>
</EuiFlexItem>
@ -501,6 +504,9 @@ exports[`after fetch renders warning when listingLimit is exceeded 1`] = `
/>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer
size="m"
/>
<EuiBasicTable
columns={
Array [
@ -564,253 +570,18 @@ exports[`after fetch renders warning when listingLimit is exceeded 1`] = `
}
sorting={Object {}}
/>
</div>
</EuiPageContent>
</EuiPageBody>
</EuiPage>
`;
exports[`initialFilter 1`] = `
exports[`renders empty page in before initial fetch to avoid flickering 1`] = `
<EuiPage
className="dashboardLandingPage"
data-test-subj="dashboardLandingPage"
restrictWidth={false}
>
<EuiFlexGroup
alignItems="flexEnd"
component="div"
data-test-subj="top-nav"
direction="row"
gutterSize="l"
justifyContent="spaceBetween"
responsive={true}
wrap={false}
>
<EuiFlexItem
component="div"
grow={false}
>
<EuiTitle
size="l"
>
<h1>
Dashboard
</h1>
</EuiTitle>
</EuiFlexItem>
<EuiFlexItem
component="div"
grow={false}
>
<EuiButton
color="primary"
data-test-subj="newDashboardLink"
fill={false}
href="#/dashboard"
iconSide="left"
type="button"
>
Create new dashboard
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer
size="m"
/>
<EuiFlexGroup
alignItems="stretch"
component="div"
direction="row"
gutterSize="l"
justifyContent="flexStart"
responsive={true}
wrap={false}
>
<EuiFlexItem
component="div"
grow={true}
>
<EuiFieldSearch
compressed={false}
data-test-subj="searchFilter"
fullWidth={true}
incremental={false}
isLoading={false}
onChange={[Function]}
placeholder="Search..."
value="my dashboard"
/>
</EuiFlexItem>
</EuiFlexGroup>
<EuiBasicTable
columns={
Array [
Object {
"field": "title",
"name": "Title",
"render": [Function],
"sortable": true,
},
Object {
"dataType": "string",
"field": "description",
"name": "Description",
"sortable": true,
},
Object {
"actions": Array [
Object {
"render": [Function],
},
],
"name": "Actions",
},
]
}
itemId="id"
items={Array []}
loading={true}
noItemsMessage=""
onChange={[Function]}
pagination={
Object {
"pageIndex": 0,
"pageSize": 20,
"pageSizeOptions": Array [
10,
20,
50,
],
"totalItemCount": 0,
}
}
responsive={true}
selection={
Object {
"onSelectionChange": [Function],
}
}
sorting={Object {}}
/>
</EuiPage>
`;
exports[`renders table in loading state 1`] = `
<EuiPage
data-test-subj="dashboardLandingPage"
restrictWidth={false}
>
<EuiFlexGroup
alignItems="flexEnd"
component="div"
data-test-subj="top-nav"
direction="row"
gutterSize="l"
justifyContent="spaceBetween"
responsive={true}
wrap={false}
>
<EuiFlexItem
component="div"
grow={false}
>
<EuiTitle
size="l"
>
<h1>
Dashboard
</h1>
</EuiTitle>
</EuiFlexItem>
<EuiFlexItem
component="div"
grow={false}
>
<EuiButton
color="primary"
data-test-subj="newDashboardLink"
fill={false}
href="#/dashboard"
iconSide="left"
type="button"
>
Create new dashboard
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer
size="m"
/>
<EuiFlexGroup
alignItems="stretch"
component="div"
direction="row"
gutterSize="l"
justifyContent="flexStart"
responsive={true}
wrap={false}
>
<EuiFlexItem
component="div"
grow={true}
>
<EuiFieldSearch
compressed={false}
data-test-subj="searchFilter"
fullWidth={true}
incremental={false}
isLoading={false}
onChange={[Function]}
placeholder="Search..."
value=""
/>
</EuiFlexItem>
</EuiFlexGroup>
<EuiBasicTable
columns={
Array [
Object {
"field": "title",
"name": "Title",
"render": [Function],
"sortable": true,
},
Object {
"dataType": "string",
"field": "description",
"name": "Description",
"sortable": true,
},
Object {
"actions": Array [
Object {
"render": [Function],
},
],
"name": "Actions",
},
]
}
itemId="id"
items={Array []}
loading={true}
noItemsMessage=""
onChange={[Function]}
pagination={
Object {
"pageIndex": 0,
"pageSize": 20,
"pageSizeOptions": Array [
10,
20,
50,
],
"totalItemCount": 0,
}
}
responsive={true}
selection={
Object {
"onSelectionChange": [Function],
}
}
sorting={Object {}}
/>
<EuiPageBody />
</EuiPage>
`;

View file

@ -17,7 +17,7 @@
* under the License.
*/
import React from 'react';
import React, { Fragment } from 'react';
import PropTypes from 'prop-types';
import _ from 'lodash';
import { toastNotifications } from 'ui/notify';
@ -26,6 +26,8 @@ import {
EuiFieldSearch,
EuiBasicTable,
EuiPage,
EuiPageBody,
EuiPageContent,
EuiLink,
EuiFlexGroup,
EuiFlexItem,
@ -36,6 +38,7 @@ import {
EuiCallOut,
EuiText,
EuiTextColor,
EuiEmptyPrompt,
} from '@elastic/eui';
import { DashboardConstants, createDashboardEditUrl } from '../dashboard_constants';
@ -52,6 +55,7 @@ export class DashboardListing extends React.Component {
super(props);
this.state = {
hasInitialFetchReturned: false,
isFetchingItems: false,
showDeleteModal: false,
showLimitError: false,
@ -87,6 +91,7 @@ export class DashboardListing extends React.Component {
// order than they were sent out. Only load results for the most recent search.
if (filter === this.state.filter) {
this.setState({
hasInitialFetchReturned: true,
isFetchingItems: false,
dashboards: response.hits,
totalDashboards: response.total,
@ -177,6 +182,14 @@ export class DashboardListing extends React.Component {
return dashboardsCopy.slice(startIndex, lastIndex);
}
hasNoDashboards() {
if (!this.state.isFetchingItems && this.state.dashboards.length === 0 && !this.state.filter) {
return true;
}
return false;
}
renderConfirmDeleteModal() {
return (
<EuiOverlayMask>
@ -215,12 +228,16 @@ export class DashboardListing extends React.Component {
}
}
renderNoItemsMessage() {
renderNoResultsMessage() {
if (this.state.isFetchingItems) {
return '';
}
if (!this.state.isFetchingItems && this.state.dashboards.length === 0 && !this.state.filter) {
return 'No dashboards matched your search.';
}
renderNoItemsMessage() {
if (this.props.hideWriteControls) {
return (
<EuiText>
@ -234,14 +251,21 @@ export class DashboardListing extends React.Component {
}
return (
<React.Fragment>
<EuiText>
<h2>
<EuiTextColor color="subdued">
{`Looks like you don't have any dashboards. Let's create some!`}
</EuiTextColor>
</h2>
</EuiText>
<div>
<EuiEmptyPrompt
iconType="dashboardApp"
title={<h2>Create your first dashboard</h2>}
body={
<Fragment>
<p>
You can combine data views from any Kibana app into one dashboard and see everything in one place.
</p>
<p>
New to Kibana? <EuiLink href="#/home/tutorial_directory/sampleData">Install some sample data</EuiLink> to take a test drive.
</p>
</Fragment>
}
actions={
<EuiButton
href={`#${DashboardConstants.CREATE_NEW_DASHBOARD_URL}`}
fill
@ -250,11 +274,11 @@ export class DashboardListing extends React.Component {
>
Create new dashboard
</EuiButton>
</React.Fragment>
);
}
/>
</div>
);
return 'No dashboards matched your search.';
}
renderSearchBar() {
@ -356,6 +380,7 @@ export class DashboardListing extends React.Component {
};
}
const items = this.state.dashboards.length === 0 ? [] : this.getPageOfItems();
return (
<EuiBasicTable
itemId={'id'}
@ -363,7 +388,7 @@ export class DashboardListing extends React.Component {
loading={this.state.isFetchingItems}
columns={tableColumns}
selection={selection}
noItemsMessage={this.renderNoItemsMessage()}
noItemsMessage={this.renderNoResultsMessage()}
pagination={pagination}
sorting={sorting}
onChange={this.onTableChange}
@ -371,7 +396,15 @@ export class DashboardListing extends React.Component {
);
}
render() {
renderListingOrEmptyState() {
if (this.hasNoDashboards()) {
return this.renderNoItemsMessage();
}
return this.renderListing();
}
renderListing() {
let createButton;
if (!this.props.hideWriteControls) {
createButton = (
@ -386,15 +419,14 @@ export class DashboardListing extends React.Component {
);
}
return (
<EuiPage data-test-subj="dashboardLandingPage">
<div>
{this.state.showDeleteModal && this.renderConfirmDeleteModal()}
<EuiFlexGroup justifyContent="spaceBetween" alignItems="flexEnd" data-test-subj="top-nav">
<EuiFlexItem grow={false}>
<EuiTitle size="l">
<h1>
Dashboard
Dashboards
</h1>
</EuiTitle>
</EuiFlexItem>
@ -409,8 +441,31 @@ export class DashboardListing extends React.Component {
{this.renderSearchBar()}
{this.renderTable()}
<EuiSpacer size="m" />
{this.renderTable()}
</div>
);
}
renderPageContent() {
if (!this.state.hasInitialFetchReturned) {
return;
}
return (
<EuiPageContent verticalPosition="center" horizontalPosition="center" className="dashboardLandingPage__content">
{this.renderListingOrEmptyState()}
</EuiPageContent>
);
}
render() {
return (
<EuiPage data-test-subj="dashboardLandingPage" className="dashboardLandingPage">
<EuiPageBody>
{this.renderPageContent()}
</EuiPageBody>
</EuiPage>
);
}

View file

@ -57,7 +57,7 @@ const find = (num) => {
});
};
test('renders table in loading state', () => {
test('renders empty page in before initial fetch to avoid flickering', () => {
const component = shallow(<DashboardListing
find={find.bind(null, 2)}
delete={() => {}}
@ -67,7 +67,8 @@ test('renders table in loading state', () => {
expect(component).toMatchSnapshot();
});
test('initialFilter', () => {
describe('after fetch', () => {
test('initialFilter', async () => {
const component = shallow(<DashboardListing
find={find.bind(null, 2)}
delete={() => {}}
@ -75,10 +76,15 @@ test('initialFilter', () => {
hideWriteControls={false}
initialFilter="my dashboard"
/>);
// Ensure all promises resolve
await new Promise(resolve => process.nextTick(resolve));
// Ensure the state changes are reflected
component.update();
expect(component).toMatchSnapshot();
});
describe('after fetch', () => {
test('renders table rows', async () => {
const component = shallow(<DashboardListing
find={find.bind(null, 2)}

View file

@ -444,3 +444,13 @@ dashboard-viewport-provider {
display: block;
}
}
.dashboardLandingPage {
min-height: 100vh;
background: @globalColorLightestGray;
}
.dashboardLandingPage__content {
max-width: 1000px;
margin: auto;
}

View file

@ -187,9 +187,16 @@ export function DashboardPageProvider({ getService, getPageObjects }) {
}
async clickNewDashboard() {
// newDashboardLink button is only visible when dashboard listing table is displayed (at least one dashboard).
const exists = await testSubjects.exists('newDashboardLink');
if (exists) {
return await testSubjects.click('newDashboardLink');
}
// no dashboards exist, click createDashboardPromptButton to create new dashboard
return await this.clickCreateDashboardPrompt();
}
async clickCreateDashboardPrompt() {
await testSubjects.click('createDashboardPromptButton');
}

View file

@ -62,8 +62,13 @@ export function HeaderPageProvider({ getService, getPageObjects }) {
async clickDashboard() {
log.debug('click Dashboard tab');
await this.clickSelector('a[href*=\'dashboard\']');
await PageObjects.common.waitForTopNavToBeVisible();
await this.confirmTopNavTextContains('dashboard');
await retry.try(async () => {
const isNavVisible = await testSubjects.exists('top-nav');
const isLandingPageVisible = await testSubjects.exists('dashboardLandingPage');
if (!isNavVisible && !isLandingPageVisible) {
throw new Error('Dashboard application not loaded yet');
}
});
await this.awaitGlobalLoadingIndicatorHidden();
}