diff --git a/src/plugins/dashboard/public/application/listing/__snapshots__/dashboard_listing.test.tsx.snap b/src/plugins/dashboard/public/application/listing/__snapshots__/dashboard_listing.test.tsx.snap index 2e37dc61fe85..2f383adb3f5c 100644 --- a/src/plugins/dashboard/public/application/listing/__snapshots__/dashboard_listing.test.tsx.snap +++ b/src/plugins/dashboard/public/application/listing/__snapshots__/dashboard_listing.test.tsx.snap @@ -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 } body={

- 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.

- Install some sample data + Add some sample data , } } @@ -146,13 +146,13 @@ exports[`after fetch initialFilter 1`] = ` iconType="plusInCircle" onClick={[Function]} > - Create new dashboard + Create a dashboard } body={

- 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.

- Install some sample data + Add some sample data , } } @@ -257,13 +257,13 @@ exports[`after fetch renders all table rows 1`] = ` iconType="plusInCircle" onClick={[Function]} > - Create new dashboard + Create a dashboard } body={

- 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.

- Install some sample data + Add some sample data , } } @@ -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 } body={

- 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.

- Install some sample data + Add some sample data , } } @@ -446,6 +446,128 @@ exports[`after fetch renders call to action when no dashboards exist 1`] = ` `; +exports[`after fetch renders call to action with continue when no dashboards exist but one is in progress 1`] = ` + + + + + Discard changes + + + + + Continue editing + + + + } + body={ + +

+ Analyze all of your Elastic data in one place by creating a dashboard and adding visualizations. +

+
+ } + iconType="dashboardApp" + title={ +

+ Dashboard in progress +

+ } + /> + } + 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], + } + } + /> + +`; + exports[`after fetch renders warning when listingLimit is exceeded 1`] = ` - Create new dashboard + Create a dashboard } body={

- 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.

- Install some sample data + Add some sample data , } } diff --git a/src/plugins/dashboard/public/application/listing/dashboard_listing.test.tsx b/src/plugins/dashboard/public/application/listing/dashboard_listing.test.tsx index 37ee0ec13d7c..ff34a63bdce1 100644 --- a/src/plugins/dashboard/public/application/listing/dashboard_listing.test.tsx +++ b/src/plugins/dashboard/public/application/listing/dashboard_listing.test.tsx @@ -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'; diff --git a/src/plugins/dashboard/public/application/listing/dashboard_listing.tsx b/src/plugins/dashboard/public/application/listing/dashboard_listing.tsx index 827e5abf2bd6..8b99b5c51598 100644 --- a/src/plugins/dashboard/public/application/listing/dashboard_listing.tsx +++ b/src/plugins/dashboard/public/application/listing/dashboard_listing.tsx @@ -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 ( + {noItemsStrings.getReadonlyTitle()}} + body={

{noItemsStrings.getReadonlyBody()}

} + /> + ); + } + + const isEditingFirstDashboard = unsavedDashboardIds.length === 1; + + const emptyAction = isEditingFirstDashboard ? ( + + + + confirmDiscardUnsavedChanges(core.overlays, () => { + dashboardSessionStorage.clearState(DASHBOARD_PANELS_UNSAVED_ID); + setUnsavedDashboardIds(dashboardSessionStorage.getDashboardIdsWithUnsavedChanges()); + }) + } + data-test-subj="discardDashboardPromptButton" + aria-label={dashboardUnsavedListingStrings.getDiscardAriaLabel(getNewDashboardTitle())} + > + {dashboardUnsavedListingStrings.getDiscardTitle()} + + + + redirectTo({ destination: 'dashboard' })} + data-test-subj="createDashboardPromptButton" + aria-label={dashboardUnsavedListingStrings.getEditAriaLabel(getNewDashboardTitle())} + > + {dashboardUnsavedListingStrings.getEditTitle()} + + + + ) : ( + + {noItemsStrings.getCreateNewDashboardText()} + + ); + + return ( + + {isEditingFirstDashboard + ? noItemsStrings.getReadEditInProgressTitle() + : noItemsStrings.getReadEditTitle()} + + } + body={ + <> +

{noItemsStrings.getReadEditDashboardDescription()}

+ {!isEditingFirstDashboard && ( +

+ + core.application.navigateToApp('home', { + path: '#/tutorial_directory/sampleData', + }) + } + > + {noItemsStrings.getSampleDataLinkText()} + + ), + }} + /> +

+ )} + + } + 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>>; }; - -const getNoItemsMessage = ( - showWriteControls: boolean, - application: ApplicationStart, - createItem: () => void -) => { - if (!showWriteControls) { - return ( - {noItemsStrings.getReadonlyTitle()}} - body={

{noItemsStrings.getReadonlyBody()}

} - /> - ); - } - - return ( - {noItemsStrings.getReadEditTitle()}} - body={ - <> -

{noItemsStrings.getReadEditDashboardDescription()}

-

- - application.navigateToApp('home', { - path: '#/tutorial_directory/sampleData', - }) - } - > - {noItemsStrings.getSampleDataLinkText()} - - ), - }} - /> -

- - } - actions={ - - {noItemsStrings.getCreateNewDashboardText()} - - } - /> - ); -}; diff --git a/src/plugins/dashboard/public/dashboard_strings.ts b/src/plugins/dashboard/public/dashboard_strings.ts index a32acf8d3bdf..ca0f51976f3f 100644 --- a/src/plugins/dashboard/public/dashboard_strings.ts +++ b/src/plugins/dashboard/public/dashboard_strings.ts @@ -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`, }), }; diff --git a/test/functional/page_objects/dashboard_page.ts b/test/functional/page_objects/dashboard_page.ts index 3d2ba53e7ba9..77ea098c7687 100644 --- a/test/functional/page_objects/dashboard_page.ts +++ b/test/functional/page_objects/dashboard_page.ts @@ -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) {