[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:
parent
77b53db939
commit
43639cd9c0
File diff suppressed because it is too large
Load diff
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)}
|
||||||
|
|
|
@ -444,3 +444,13 @@ dashboard-viewport-provider {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dashboardLandingPage {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: @globalColorLightestGray;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboardLandingPage__content {
|
||||||
|
max-width: 1000px;
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue