[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. * under the License.
*/ */
import React from 'react'; import React, { Fragment } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import _ from 'lodash'; import _ from 'lodash';
import { toastNotifications } from 'ui/notify'; import { toastNotifications } from 'ui/notify';
@ -26,6 +26,8 @@ import {
EuiFieldSearch, EuiFieldSearch,
EuiBasicTable, EuiBasicTable,
EuiPage, EuiPage,
EuiPageBody,
EuiPageContent,
EuiLink, EuiLink,
EuiFlexGroup, EuiFlexGroup,
EuiFlexItem, EuiFlexItem,
@ -36,6 +38,7 @@ import {
EuiCallOut, EuiCallOut,
EuiText, EuiText,
EuiTextColor, EuiTextColor,
EuiEmptyPrompt,
} from '@elastic/eui'; } from '@elastic/eui';
import { DashboardConstants, createDashboardEditUrl } from '../dashboard_constants'; import { DashboardConstants, createDashboardEditUrl } from '../dashboard_constants';
@ -52,6 +55,7 @@ export class DashboardListing extends React.Component {
super(props); super(props);
this.state = { this.state = {
hasInitialFetchReturned: false,
isFetchingItems: false, isFetchingItems: false,
showDeleteModal: false, showDeleteModal: false,
showLimitError: 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. // order than they were sent out. Only load results for the most recent search.
if (filter === this.state.filter) { if (filter === this.state.filter) {
this.setState({ this.setState({
hasInitialFetchReturned: true,
isFetchingItems: false, isFetchingItems: false,
dashboards: response.hits, dashboards: response.hits,
totalDashboards: response.total, totalDashboards: response.total,
@ -177,6 +182,14 @@ export class DashboardListing extends React.Component {
return dashboardsCopy.slice(startIndex, lastIndex); return dashboardsCopy.slice(startIndex, lastIndex);
} }
hasNoDashboards() {
if (!this.state.isFetchingItems && this.state.dashboards.length === 0 && !this.state.filter) {
return true;
}
return false;
}
renderConfirmDeleteModal() { renderConfirmDeleteModal() {
return ( return (
<EuiOverlayMask> <EuiOverlayMask>
@ -215,46 +228,57 @@ export class DashboardListing extends React.Component {
} }
} }
renderNoItemsMessage() { renderNoResultsMessage() {
if (this.state.isFetchingItems) { if (this.state.isFetchingItems) {
return ''; return '';
} }
if (!this.state.isFetchingItems && this.state.dashboards.length === 0 && !this.state.filter) { return 'No dashboards matched your search.';
if (this.props.hideWriteControls) { }
return (
<EuiText>
<h2>
<EuiTextColor color="subdued">
{`Looks like you don't have any dashboards.`}
</EuiTextColor>
</h2>
</EuiText>
);
}
renderNoItemsMessage() {
if (this.props.hideWriteControls) {
return ( return (
<React.Fragment> <EuiText>
<EuiText> <h2>
<h2> <EuiTextColor color="subdued">
<EuiTextColor color="subdued"> {`Looks like you don't have any dashboards.`}
{`Looks like you don't have any dashboards. Let's create some!`} </EuiTextColor>
</EuiTextColor> </h2>
</h2> </EuiText>
</EuiText>
<EuiButton
href={`#${DashboardConstants.CREATE_NEW_DASHBOARD_URL}`}
fill
iconType="plusInCircle"
data-test-subj="createDashboardPromptButton"
>
Create new dashboard
</EuiButton>
</React.Fragment>
); );
} }
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() { renderSearchBar() {
@ -356,6 +380,7 @@ export class DashboardListing extends React.Component {
}; };
} }
const items = this.state.dashboards.length === 0 ? [] : this.getPageOfItems(); const items = this.state.dashboards.length === 0 ? [] : this.getPageOfItems();
return ( return (
<EuiBasicTable <EuiBasicTable
itemId={'id'} itemId={'id'}
@ -363,7 +388,7 @@ export class DashboardListing extends React.Component {
loading={this.state.isFetchingItems} loading={this.state.isFetchingItems}
columns={tableColumns} columns={tableColumns}
selection={selection} selection={selection}
noItemsMessage={this.renderNoItemsMessage()} noItemsMessage={this.renderNoResultsMessage()}
pagination={pagination} pagination={pagination}
sorting={sorting} sorting={sorting}
onChange={this.onTableChange} 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; let createButton;
if (!this.props.hideWriteControls) { if (!this.props.hideWriteControls) {
createButton = ( createButton = (
@ -386,15 +419,14 @@ export class DashboardListing extends React.Component {
); );
} }
return ( return (
<EuiPage data-test-subj="dashboardLandingPage"> <div>
{this.state.showDeleteModal && this.renderConfirmDeleteModal()} {this.state.showDeleteModal && this.renderConfirmDeleteModal()}
<EuiFlexGroup justifyContent="spaceBetween" alignItems="flexEnd" data-test-subj="top-nav"> <EuiFlexGroup justifyContent="spaceBetween" alignItems="flexEnd" data-test-subj="top-nav">
<EuiFlexItem grow={false}> <EuiFlexItem grow={false}>
<EuiTitle size="l"> <EuiTitle size="l">
<h1> <h1>
Dashboard Dashboards
</h1> </h1>
</EuiTitle> </EuiTitle>
</EuiFlexItem> </EuiFlexItem>
@ -409,8 +441,31 @@ export class DashboardListing extends React.Component {
{this.renderSearchBar()} {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> </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 const component = shallow(<DashboardListing
find={find.bind(null, 2)} find={find.bind(null, 2)}
delete={() => {}} delete={() => {}}
@ -67,18 +67,24 @@ test('renders table in loading state', () => {
expect(component).toMatchSnapshot(); 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', () => { 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 () => { test('renders table rows', async () => {
const component = shallow(<DashboardListing const component = shallow(<DashboardListing
find={find.bind(null, 2)} find={find.bind(null, 2)}

View file

@ -444,3 +444,13 @@ dashboard-viewport-provider {
display: block; 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() { 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() { async clickCreateDashboardPrompt() {

View file

@ -62,8 +62,13 @@ export function HeaderPageProvider({ getService, getPageObjects }) {
async clickDashboard() { async clickDashboard() {
log.debug('click Dashboard tab'); log.debug('click Dashboard tab');
await this.clickSelector('a[href*=\'dashboard\']'); await this.clickSelector('a[href*=\'dashboard\']');
await PageObjects.common.waitForTopNavToBeVisible(); await retry.try(async () => {
await this.confirmTopNavTextContains('dashboard'); 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(); await this.awaitGlobalLoadingIndicatorHidden();
} }