[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

@ -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,46 +228,57 @@ 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) {
if (this.props.hideWriteControls) {
return (
<EuiText>
<h2>
<EuiTextColor color="subdued">
{`Looks like you don't have any dashboards.`}
</EuiTextColor>
</h2>
</EuiText>
);
}
return 'No dashboards matched your search.';
}
renderNoItemsMessage() {
if (this.props.hideWriteControls) {
return (
<React.Fragment>
<EuiText>
<h2>
<EuiTextColor color="subdued">
{`Looks like you don't have any dashboards. Let's create some!`}
</EuiTextColor>
</h2>
</EuiText>
<EuiButton
href={`#${DashboardConstants.CREATE_NEW_DASHBOARD_URL}`}
fill
iconType="plusInCircle"
data-test-subj="createDashboardPromptButton"
>
Create new dashboard
</EuiButton>
</React.Fragment>
<EuiText>
<h2>
<EuiTextColor color="subdued">
{`Looks like you don't have any dashboards.`}
</EuiTextColor>
</h2>
</EuiText>
);
}
return 'No dashboards matched your search.';
return (
<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
iconType="plusInCircle"
data-test-subj="createDashboardPromptButton"
>
Create new dashboard
</EuiButton>
}
/>
</div>
);
}
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,18 +67,24 @@ test('renders table in loading state', () => {
expect(component).toMatchSnapshot();
});
test('initialFilter', () => {
const component = shallow(<DashboardListing
find={find.bind(null, 2)}
delete={() => {}}
listingLimit={1000}
hideWriteControls={false}
initialFilter="my dashboard"
/>);
expect(component).toMatchSnapshot();
});
describe('after fetch', () => {
test('initialFilter', async () => {
const component = shallow(<DashboardListing
find={find.bind(null, 2)}
delete={() => {}}
listingLimit={1000}
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();
});
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,7 +187,14 @@ export function DashboardPageProvider({ getService, getPageObjects }) {
}
async clickNewDashboard() {
return await testSubjects.click('newDashboardLink');
// 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() {

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();
}