[Dashboard] Listing Page Callout When New Dashboard In Progress (#117237)

* Added dashboard listing state for when no dashboards are available, but the user has one in progress
This commit is contained in:
Devon Thomson 2021-11-05 10:21:26 -04:00 committed by GitHub
parent 18f601dc49
commit 5d5fb3f91c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 300 additions and 83 deletions

View file

@ -34,13 +34,13 @@ exports[`after fetch When given a title that matches multiple dashboards, filter
iconType="plusInCircle"
onClick={[Function]}
>
Create new dashboard
Create a dashboard
</EuiButton>
}
body={
<React.Fragment>
<p>
You can combine data views from any Kibana app into one dashboard and see everything in one place.
Analyze all of your Elastic data in one place by creating a dashboard and adding visualizations.
</p>
<p>
<FormattedMessage
@ -51,7 +51,7 @@ exports[`after fetch When given a title that matches multiple dashboards, filter
"sampleDataInstallLink": <EuiLink
onClick={[Function]}
>
Install some sample data
Add some sample data
</EuiLink>,
}
}
@ -146,13 +146,13 @@ exports[`after fetch initialFilter 1`] = `
iconType="plusInCircle"
onClick={[Function]}
>
Create new dashboard
Create a dashboard
</EuiButton>
}
body={
<React.Fragment>
<p>
You can combine data views from any Kibana app into one dashboard and see everything in one place.
Analyze all of your Elastic data in one place by creating a dashboard and adding visualizations.
</p>
<p>
<FormattedMessage
@ -163,7 +163,7 @@ exports[`after fetch initialFilter 1`] = `
"sampleDataInstallLink": <EuiLink
onClick={[Function]}
>
Install some sample data
Add some sample data
</EuiLink>,
}
}
@ -257,13 +257,13 @@ exports[`after fetch renders all table rows 1`] = `
iconType="plusInCircle"
onClick={[Function]}
>
Create new dashboard
Create a dashboard
</EuiButton>
}
body={
<React.Fragment>
<p>
You can combine data views from any Kibana app into one dashboard and see everything in one place.
Analyze all of your Elastic data in one place by creating a dashboard and adding visualizations.
</p>
<p>
<FormattedMessage
@ -274,7 +274,7 @@ exports[`after fetch renders all table rows 1`] = `
"sampleDataInstallLink": <EuiLink
onClick={[Function]}
>
Install some sample data
Add some sample data
</EuiLink>,
}
}
@ -368,13 +368,13 @@ exports[`after fetch renders call to action when no dashboards exist 1`] = `
iconType="plusInCircle"
onClick={[Function]}
>
Create new dashboard
Create a dashboard
</EuiButton>
}
body={
<React.Fragment>
<p>
You can combine data views from any Kibana app into one dashboard and see everything in one place.
Analyze all of your Elastic data in one place by creating a dashboard and adding visualizations.
</p>
<p>
<FormattedMessage
@ -385,7 +385,7 @@ exports[`after fetch renders call to action when no dashboards exist 1`] = `
"sampleDataInstallLink": <EuiLink
onClick={[Function]}
>
Install some sample data
Add some sample data
</EuiLink>,
}
}
@ -446,6 +446,128 @@ exports[`after fetch renders call to action when no dashboards exist 1`] = `
</DashboardListing>
`;
exports[`after fetch renders call to action with continue when no dashboards exist but one is in progress 1`] = `
<DashboardListing
kbnUrlStateStorage={
Object {
"cancel": [Function],
"change$": [Function],
"get": [Function],
"kbnUrlControls": Object {
"cancel": [Function],
"flush": [Function],
"getPendingUrl": [Function],
"listen": [Function],
"update": [Function],
"updateAsync": [Function],
},
"set": [Function],
}
}
redirectTo={[MockFunction]}
>
<TableListView
createItem={[Function]}
deleteItems={[Function]}
editItem={[Function]}
emptyPrompt={
<EuiEmptyPrompt
actions={
<EuiFlexGroup
alignItems="center"
gutterSize="s"
justifyContent="center"
responsive={false}
>
<EuiFlexItem
grow={false}
>
<EuiButtonEmpty
aria-label="Discard changes to New Dashboard"
color="danger"
data-test-subj="discardDashboardPromptButton"
onClick={[Function]}
size="s"
>
Discard changes
</EuiButtonEmpty>
</EuiFlexItem>
<EuiFlexItem
grow={false}
>
<EuiButton
aria-label="Continue editing New Dashboard"
color="primary"
data-test-subj="createDashboardPromptButton"
fill={true}
iconType="pencil"
onClick={[Function]}
>
Continue editing
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
}
body={
<React.Fragment>
<p>
Analyze all of your Elastic data in one place by creating a dashboard and adding visualizations.
</p>
</React.Fragment>
}
iconType="dashboardApp"
title={
<h1
id="dashboardListingHeading"
>
Dashboard in progress
</h1>
}
/>
}
entityName="dashboard"
entityNamePlural="dashboards"
findItems={[Function]}
headingId="dashboardListingHeading"
initialFilter=""
initialPageSize={20}
listingLimit={100}
rowHeader="title"
searchFilters={Array []}
tableCaption="Dashboards"
tableColumns={
Array [
Object {
"field": "title",
"name": "Title",
"render": [Function],
"sortable": true,
},
Object {
"field": "description",
"name": "Description",
"render": [Function],
"sortable": true,
},
]
}
tableListTitle="Dashboards"
toastNotifications={
Object {
"add": [MockFunction],
"addDanger": [MockFunction],
"addError": [MockFunction],
"addInfo": [MockFunction],
"addSuccess": [MockFunction],
"addWarning": [MockFunction],
"get$": [MockFunction],
"remove": [MockFunction],
}
}
/>
</DashboardListing>
`;
exports[`after fetch renders warning when listingLimit is exceeded 1`] = `
<DashboardListing
kbnUrlStateStorage={
@ -479,13 +601,13 @@ exports[`after fetch renders warning when listingLimit is exceeded 1`] = `
iconType="plusInCircle"
onClick={[Function]}
>
Create new dashboard
Create a dashboard
</EuiButton>
}
body={
<React.Fragment>
<p>
You can combine data views from any Kibana app into one dashboard and see everything in one place.
Analyze all of your Elastic data in one place by creating a dashboard and adding visualizations.
</p>
<p>
<FormattedMessage
@ -496,7 +618,7 @@ exports[`after fetch renders warning when listingLimit is exceeded 1`] = `
"sampleDataInstallLink": <EuiLink
onClick={[Function]}
>
Install some sample data
Add some sample data
</EuiLink>,
}
}

View file

@ -16,6 +16,7 @@ import { KibanaContextProvider } from '../../services/kibana_react';
import { createKbnUrlStateStorage } from '../../services/kibana_utils';
import { DashboardListing, DashboardListingProps } from './dashboard_listing';
import { makeDefaultServices } from '../test_helpers';
import { DASHBOARD_PANELS_UNSAVED_ID } from '../lib/dashboard_session_storage';
function makeDefaultProps(): DashboardListingProps {
return {
@ -72,6 +73,25 @@ describe('after fetch', () => {
expect(component).toMatchSnapshot();
});
test('renders call to action with continue when no dashboards exist but one is in progress', async () => {
const services = makeDefaultServices();
services.savedDashboards.find = () => {
return Promise.resolve({
total: 0,
hits: [],
});
};
services.dashboardSessionStorage.getDashboardIdsWithUnsavedChanges = () => [
DASHBOARD_PANELS_UNSAVED_ID,
];
const { component } = mountWith({ services });
// Ensure all promises resolve
await new Promise((resolve) => process.nextTick(resolve));
// Ensure the state changes are reflected
component.update();
expect(component).toMatchSnapshot();
});
test('initialFilter', async () => {
const props = makeDefaultProps();
props.initialFilter = 'testFilter';

View file

@ -7,7 +7,15 @@
*/
import { FormattedMessage } from '@kbn/i18n/react';
import { EuiLink, EuiButton, EuiEmptyPrompt, EuiBasicTableColumn } from '@elastic/eui';
import {
EuiLink,
EuiButton,
EuiEmptyPrompt,
EuiBasicTableColumn,
EuiFlexGroup,
EuiFlexItem,
EuiButtonEmpty,
} from '@elastic/eui';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { attemptLoadDashboardByTitle } from '../lib';
import { DashboardAppServices, DashboardRedirect } from '../../types';
@ -15,6 +23,8 @@ import {
getDashboardBreadcrumb,
dashboardListingTable,
noItemsStrings,
dashboardUnsavedListingStrings,
getNewDashboardTitle,
} from '../../dashboard_strings';
import { ApplicationStart, SavedObjectsFindOptionsReference } from '../../../../../core/public';
import { syncQueryStateWithUrl } from '../../services/data';
@ -22,8 +32,9 @@ import { IKbnUrlStateStorage } from '../../services/kibana_utils';
import { TableListView, useKibana } from '../../services/kibana_react';
import { SavedObjectsTaggingApi } from '../../services/saved_objects_tagging_oss';
import { DashboardUnsavedListing } from './dashboard_unsaved_listing';
import { confirmCreateWithUnsaved } from './confirm_overlays';
import { confirmCreateWithUnsaved, confirmDiscardUnsavedChanges } from './confirm_overlays';
import { getDashboardListItemLink } from './get_dashboard_list_item_link';
import { DASHBOARD_PANELS_UNSAVED_ID } from '../lib/dashboard_session_storage';
export interface DashboardListingProps {
kbnUrlStateStorage: IKbnUrlStateStorage;
@ -117,10 +128,109 @@ export const DashboardListing = ({
}
}, [dashboardSessionStorage, redirectTo, core.overlays]);
const emptyPrompt = useMemo(
() => getNoItemsMessage(showWriteControls, core.application, createItem),
[createItem, core.application, showWriteControls]
);
const emptyPrompt = useMemo(() => {
if (!showWriteControls) {
return (
<EuiEmptyPrompt
iconType="glasses"
title={<h1 id="dashboardListingHeading">{noItemsStrings.getReadonlyTitle()}</h1>}
body={<p>{noItemsStrings.getReadonlyBody()}</p>}
/>
);
}
const isEditingFirstDashboard = unsavedDashboardIds.length === 1;
const emptyAction = isEditingFirstDashboard ? (
<EuiFlexGroup alignItems="center" justifyContent="center" gutterSize="s" responsive={false}>
<EuiFlexItem grow={false}>
<EuiButtonEmpty
size="s"
color="danger"
onClick={() =>
confirmDiscardUnsavedChanges(core.overlays, () => {
dashboardSessionStorage.clearState(DASHBOARD_PANELS_UNSAVED_ID);
setUnsavedDashboardIds(dashboardSessionStorage.getDashboardIdsWithUnsavedChanges());
})
}
data-test-subj="discardDashboardPromptButton"
aria-label={dashboardUnsavedListingStrings.getDiscardAriaLabel(getNewDashboardTitle())}
>
{dashboardUnsavedListingStrings.getDiscardTitle()}
</EuiButtonEmpty>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton
fill
iconType="pencil"
color="primary"
onClick={() => redirectTo({ destination: 'dashboard' })}
data-test-subj="createDashboardPromptButton"
aria-label={dashboardUnsavedListingStrings.getEditAriaLabel(getNewDashboardTitle())}
>
{dashboardUnsavedListingStrings.getEditTitle()}
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
) : (
<EuiButton
onClick={createItem}
fill
iconType="plusInCircle"
data-test-subj="createDashboardPromptButton"
>
{noItemsStrings.getCreateNewDashboardText()}
</EuiButton>
);
return (
<EuiEmptyPrompt
iconType="dashboardApp"
title={
<h1 id="dashboardListingHeading">
{isEditingFirstDashboard
? noItemsStrings.getReadEditInProgressTitle()
: noItemsStrings.getReadEditTitle()}
</h1>
}
body={
<>
<p>{noItemsStrings.getReadEditDashboardDescription()}</p>
{!isEditingFirstDashboard && (
<p>
<FormattedMessage
id="dashboard.listing.createNewDashboard.newToKibanaDescription"
defaultMessage="New to Kibana? {sampleDataInstallLink} to take a test drive."
values={{
sampleDataInstallLink: (
<EuiLink
onClick={() =>
core.application.navigateToApp('home', {
path: '#/tutorial_directory/sampleData',
})
}
>
{noItemsStrings.getSampleDataLinkText()}
</EuiLink>
),
}}
/>
</p>
)}
</>
}
actions={emptyAction}
/>
);
}, [
redirectTo,
createItem,
core.overlays,
core.application,
showWriteControls,
unsavedDashboardIds,
dashboardSessionStorage,
]);
const fetchItems = useCallback(
(filter: string) => {
@ -233,60 +343,3 @@ const getTableColumns = (
...(savedObjectsTagging ? [savedObjectsTagging.ui.getTableColumnDefinition()] : []),
] as unknown as Array<EuiBasicTableColumn<Record<string, unknown>>>;
};
const getNoItemsMessage = (
showWriteControls: boolean,
application: ApplicationStart,
createItem: () => void
) => {
if (!showWriteControls) {
return (
<EuiEmptyPrompt
iconType="glasses"
title={<h1 id="dashboardListingHeading">{noItemsStrings.getReadonlyTitle()}</h1>}
body={<p>{noItemsStrings.getReadonlyBody()}</p>}
/>
);
}
return (
<EuiEmptyPrompt
iconType="dashboardApp"
title={<h1 id="dashboardListingHeading">{noItemsStrings.getReadEditTitle()}</h1>}
body={
<>
<p>{noItemsStrings.getReadEditDashboardDescription()}</p>
<p>
<FormattedMessage
id="dashboard.listing.createNewDashboard.newToKibanaDescription"
defaultMessage="New to Kibana? {sampleDataInstallLink} to take a test drive."
values={{
sampleDataInstallLink: (
<EuiLink
onClick={() =>
application.navigateToApp('home', {
path: '#/tutorial_directory/sampleData',
})
}
>
{noItemsStrings.getSampleDataLinkText()}
</EuiLink>
),
}}
/>
</p>
</>
}
actions={
<EuiButton
onClick={createItem}
fill
iconType="plusInCircle"
data-test-subj="createDashboardPromptButton"
>
{noItemsStrings.getCreateNewDashboardText()}
</EuiButton>
}
/>
);
};

View file

@ -321,7 +321,7 @@ export const createConfirmStrings = {
}),
getCreateSubtitle: () =>
i18n.translate('dashboard.createConfirmModal.unsavedChangesSubtitle', {
defaultMessage: 'You can continue editing or start with a blank dashboard.',
defaultMessage: 'Continue editing or start over with a blank dashboard.',
}),
getStartOverButtonText: () =>
i18n.translate('dashboard.createConfirmModal.confirmButtonLabel', {
@ -420,7 +420,7 @@ export const dashboardListingTable = {
export const dashboardUnsavedListingStrings = {
getUnsavedChangesTitle: (plural = false) =>
i18n.translate('dashboard.listing.unsaved.unsavedChangesTitle', {
defaultMessage: 'You have unsaved changes in the following {dash}.',
defaultMessage: 'You have unsaved changes in the following {dash}:',
values: {
dash: plural
? dashboardListingTable.getEntityNamePlural()
@ -469,17 +469,21 @@ export const noItemsStrings = {
i18n.translate('dashboard.listing.createNewDashboard.title', {
defaultMessage: 'Create your first dashboard',
}),
getReadEditInProgressTitle: () =>
i18n.translate('dashboard.listing.createNewDashboard.inProgressTitle', {
defaultMessage: 'Dashboard in progress',
}),
getReadEditDashboardDescription: () =>
i18n.translate('dashboard.listing.createNewDashboard.combineDataViewFromKibanaAppDescription', {
defaultMessage:
'You can combine data views from any Kibana app into one dashboard and see everything in one place.',
'Analyze all of your Elastic data in one place by creating a dashboard and adding visualizations.',
}),
getSampleDataLinkText: () =>
i18n.translate('dashboard.listing.createNewDashboard.sampleDataInstallLinkText', {
defaultMessage: `Install some sample data`,
defaultMessage: `Add some sample data`,
}),
getCreateNewDashboardText: () =>
i18n.translate('dashboard.listing.createNewDashboard.createButtonLabel', {
defaultMessage: `Create new dashboard`,
defaultMessage: `Create a dashboard`,
}),
};

View file

@ -292,6 +292,15 @@ export class DashboardPageObject extends FtrService {
}
public async clickNewDashboard(continueEditing = false) {
const discardButtonExists = await this.testSubjects.exists('discardDashboardPromptButton');
if (!continueEditing && discardButtonExists) {
this.log.debug('found discard button');
await this.testSubjects.click('discardDashboardPromptButton');
const confirmation = await this.testSubjects.exists('confirmModalTitleText');
if (confirmation) {
await this.common.clickConfirmOnModal();
}
}
await this.listingTable.clickNewButton('createDashboardPromptButton');
if (await this.testSubjects.exists('dashboardCreateConfirm')) {
if (continueEditing) {
@ -305,6 +314,15 @@ export class DashboardPageObject extends FtrService {
}
public async clickNewDashboardExpectWarning(continueEditing = false) {
const discardButtonExists = await this.testSubjects.exists('discardDashboardPromptButton');
if (!continueEditing && discardButtonExists) {
this.log.debug('found discard button');
await this.testSubjects.click('discardDashboardPromptButton');
const confirmation = await this.testSubjects.exists('confirmModalTitleText');
if (confirmation) {
await this.common.clickConfirmOnModal();
}
}
await this.listingTable.clickNewButton('createDashboardPromptButton');
await this.testSubjects.existOrFail('dashboardCreateConfirm');
if (continueEditing) {