[Dashboard] Add Analytics No Data Page (#132188)

Added no data and no data view states to dashboard app and dashboard listing page
Co-authored-by: Maja Grubic <maja.grubic@elastic.co>
This commit is contained in:
Devon Thomson 2022-05-24 16:04:12 -04:00 committed by GitHub
parent b60e7ad758
commit 556b49133b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 240 additions and 44 deletions

View file

@ -9,6 +9,7 @@
"requiredPlugins": [
"data",
"dataViews",
"dataViewEditor",
"embeddable",
"controls",
"inspector",

View file

@ -7,9 +7,10 @@
*/
import { History } from 'history';
import React, { useEffect, useMemo } from 'react';
import React, { useEffect, useMemo, useState } from 'react';
import { useKibana, useExecutionContext } from '@kbn/kibana-react-plugin/public';
import { useDashboardSelector } from './state';
import { useDashboardAppState } from './hooks';
import {
@ -22,6 +23,7 @@ import { EmbeddableRenderer, ViewMode } from '../services/embeddable';
import { DashboardTopNav, isCompleteDashboardAppState } from './top_nav/dashboard_top_nav';
import { DashboardAppServices, DashboardEmbedSettings, DashboardRedirect } from '../types';
import { createKbnUrlStateStorage, withNotifyOnErrors } from '../services/kibana_utils';
import { DashboardAppNoDataPage } from './dashboard_app_no_data';
export interface DashboardAppProps {
history: History;
savedDashboardId?: string;
@ -46,6 +48,8 @@ export function DashboardApp({
screenshotModeService,
} = useKibana<DashboardAppServices>().services;
const [showNoDataPage, setShowNoDataPage] = useState<boolean>(false);
const kbnUrlStateStorage = useMemo(
() =>
createKbnUrlStateStorage({
@ -65,6 +69,8 @@ export function DashboardApp({
const dashboardState = useDashboardSelector((state) => state.dashboardStateReducer);
const dashboardAppState = useDashboardAppState({
history,
showNoDataPage,
setShowNoDataPage,
savedDashboardId,
kbnUrlStateStorage,
isEmbeddedExternally: Boolean(embedSettings),
@ -125,7 +131,10 @@ export function DashboardApp({
return (
<>
{isCompleteDashboardAppState(dashboardAppState) && (
{showNoDataPage && (
<DashboardAppNoDataPage onDataViewCreated={() => setShowNoDataPage(false)} />
)}
{!showNoDataPage && isCompleteDashboardAppState(dashboardAppState) && (
<>
<DashboardTopNav
printMode={printMode}

View file

@ -0,0 +1,47 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React from 'react';
import {
AnalyticsNoDataPageKibanaProvider,
AnalyticsNoDataPage,
} from '@kbn/shared-ux-page-analytics-no-data';
import { DataPublicPluginStart } from '@kbn/data-plugin/public';
import { DashboardAppServices } from '../types';
import { useKibana } from '../services/kibana_react';
export const DashboardAppNoDataPage = ({
onDataViewCreated,
}: {
onDataViewCreated: () => void;
}) => {
const {
services: { core, data, dataViewEditor },
} = useKibana<DashboardAppServices>();
const analyticsServices = {
coreStart: core as unknown as React.ComponentProps<
typeof AnalyticsNoDataPageKibanaProvider
>['coreStart'],
dataViews: data.dataViews,
dataViewEditor,
};
return (
<AnalyticsNoDataPageKibanaProvider {...analyticsServices}>
<AnalyticsNoDataPage onDataViewCreated={onDataViewCreated} />;
</AnalyticsNoDataPageKibanaProvider>
);
};
export const isDashboardAppInNoDataState = async (
dataViews: DataPublicPluginStart['dataViews']
) => {
const hasUserDataView = await dataViews.hasData.hasUserDataView().catch(() => false);
const hasEsData = await dataViews.hasData.hasESData().catch(() => true);
return !hasUserDataView || !hasEsData;
};

View file

@ -85,6 +85,7 @@ export async function mountApp({
visualizations,
presentationUtil,
screenshotMode,
dataViewEditor,
} = pluginsStart;
const activeSpaceId =
@ -97,6 +98,7 @@ export async function mountApp({
onAppLeave,
savedObjects,
urlForwarding,
dataViewEditor,
visualizations,
usageCollection,
core: coreStart,

View file

@ -30,7 +30,6 @@ import {
getSavedDashboardMock,
makeDefaultServices,
} from '../test_helpers';
import { DataViewsContract } from '../../services/data';
import { DataView } from '../../services/data_views';
import type { Filter } from '@kbn/es-query';
@ -54,14 +53,20 @@ const createDashboardAppStateProps = (): UseDashboardStateProps => ({
savedDashboardId: 'testDashboardId',
history: createBrowserHistory(),
isEmbeddedExternally: false,
showNoDataPage: false,
setShowNoDataPage: () => {},
});
const createDashboardAppStateServices = () => {
const defaults = makeDefaultServices();
const dataViews = {} as DataViewsContract;
const defaultDataView = { id: 'foo', fields: [{ name: 'bar' }] } as DataView;
dataViews.ensureDefaultDataView = jest.fn().mockImplementation(() => Promise.resolve(true));
dataViews.getDefault = jest.fn().mockImplementation(() => Promise.resolve(defaultDataView));
defaults.dataViews.getDefaultDataView = jest
.fn()
.mockImplementation(() => Promise.resolve(defaultDataView));
defaults.dataViews.getDefault = jest
.fn()
.mockImplementation(() => Promise.resolve(defaultDataView));
const data = dataPluginMock.createStartContract();
data.query.filterManager.getUpdates$ = jest.fn().mockImplementation(() => of(void 0));
@ -71,7 +76,7 @@ const createDashboardAppStateServices = () => {
.fn()
.mockImplementation(() => of(void 0));
return { ...defaults, dataViews, data };
return { ...defaults, data };
};
const setupEmbeddableFactory = (

View file

@ -39,17 +39,22 @@ import {
areTimeRangesEqual,
areRefreshIntervalsEqual,
} from '../lib';
import { isDashboardAppInNoDataState } from '../dashboard_app_no_data';
export interface UseDashboardStateProps {
history: History;
showNoDataPage: boolean;
savedDashboardId?: string;
isEmbeddedExternally: boolean;
setShowNoDataPage: (showNoData: boolean) => void;
kbnUrlStateStorage: IKbnUrlStateStorage;
}
export const useDashboardAppState = ({
history,
savedDashboardId,
showNoDataPage,
setShowNoDataPage,
kbnUrlStateStorage,
isEmbeddedExternally,
}: UseDashboardStateProps) => {
@ -138,6 +143,21 @@ export const useDashboardAppState = ({
};
(async () => {
/**
* Ensure default data view exists and there is data in elasticsearch
*/
const isEmpty = await isDashboardAppInNoDataState(dataViews);
if (showNoDataPage || isEmpty) {
setShowNoDataPage(true);
return;
}
const defaultDataView = await dataViews.getDefaultDataView();
if (!defaultDataView) {
return;
}
/**
* Load and unpack state from dashboard saved object.
*/
@ -375,6 +395,8 @@ export const useDashboardAppState = ({
search,
query,
data,
showNoDataPage,
setShowNoDataPage,
spacesService?.ui,
screenshotModeService,
]);

View file

@ -49,7 +49,6 @@ export const loadSavedDashboardState = async ({
notifications.toasts.addWarning(getDashboard60Warning());
return;
}
await dataViews.ensureDefaultDataView();
try {
const savedDashboard = (await savedDashboards.get({
id: savedDashboardId,

View file

@ -19,6 +19,7 @@ import {
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { ApplicationStart, SavedObjectsFindOptionsReference } from '@kbn/core/public';
import { useExecutionContext } from '@kbn/kibana-react-plugin/public';
import useMount from 'react-use/lib/useMount';
import { attemptLoadDashboardByTitle } from '../lib';
import { DashboardAppServices, DashboardRedirect } from '../../types';
import {
@ -36,6 +37,7 @@ import { DashboardUnsavedListing } from './dashboard_unsaved_listing';
import { confirmCreateWithUnsaved, confirmDiscardUnsavedChanges } from './confirm_overlays';
import { getDashboardListItemLink } from './get_dashboard_list_item_link';
import { DASHBOARD_PANELS_UNSAVED_ID } from '../lib/dashboard_session_storage';
import { DashboardAppNoDataPage, isDashboardAppInNoDataState } from '../dashboard_app_no_data';
const SAVED_OBJECTS_LIMIT_SETTING = 'savedObjects:listingLimit';
const SAVED_OBJECTS_PER_PAGE_SETTING = 'savedObjects:perPage';
@ -57,6 +59,7 @@ export const DashboardListing = ({
services: {
core,
data,
dataViews,
savedDashboards,
savedObjectsClient,
savedObjectsTagging,
@ -66,6 +69,11 @@ export const DashboardListing = ({
},
} = useKibana<DashboardAppServices>();
const [showNoDataPage, setShowNoDataPage] = useState<boolean>(false);
useMount(() => {
(async () => setShowNoDataPage(await isDashboardAppInNoDataState(dataViews)))();
});
const [unsavedDashboardIds, setUnsavedDashboardIds] = useState<string[]>(
dashboardSessionStorage.getDashboardIdsWithUnsavedChanges()
);
@ -286,37 +294,44 @@ export const DashboardListing = ({
const { getEntityName, getTableCaption, getTableListTitle, getEntityNamePlural } =
dashboardListingTable;
return (
<TableListView
createItem={!showWriteControls ? undefined : createItem}
deleteItems={!showWriteControls ? undefined : deleteItems}
initialPageSize={initialPageSize}
editItem={!showWriteControls ? undefined : editItem}
initialFilter={initialFilter ?? defaultFilter}
toastNotifications={core.notifications.toasts}
headingId="dashboardListingHeading"
findItems={fetchItems}
rowHeader="title"
entityNamePlural={getEntityNamePlural()}
tableListTitle={getTableListTitle()}
tableCaption={getTableCaption()}
entityName={getEntityName()}
{...{
emptyPrompt,
searchFilters,
listingLimit,
tableColumns,
}}
theme={core.theme}
application={core.application}
>
<DashboardUnsavedListing
redirectTo={redirectTo}
unsavedDashboardIds={unsavedDashboardIds}
refreshUnsavedDashboards={() =>
setUnsavedDashboardIds(dashboardSessionStorage.getDashboardIdsWithUnsavedChanges())
}
/>
</TableListView>
<>
{showNoDataPage && (
<DashboardAppNoDataPage onDataViewCreated={() => setShowNoDataPage(false)} />
)}
{!showNoDataPage && (
<TableListView
createItem={!showWriteControls ? undefined : createItem}
deleteItems={!showWriteControls ? undefined : deleteItems}
initialPageSize={initialPageSize}
editItem={!showWriteControls ? undefined : editItem}
initialFilter={initialFilter ?? defaultFilter}
toastNotifications={core.notifications.toasts}
headingId="dashboardListingHeading"
findItems={fetchItems}
rowHeader="title"
entityNamePlural={getEntityNamePlural()}
tableListTitle={getTableListTitle()}
tableCaption={getTableCaption()}
entityName={getEntityName()}
{...{
emptyPrompt,
searchFilters,
listingLimit,
tableColumns,
}}
theme={core.theme}
application={core.application}
>
<DashboardUnsavedListing
redirectTo={redirectTo}
unsavedDashboardIds={unsavedDashboardIds}
refreshUnsavedDashboards={() =>
setUnsavedDashboardIds(dashboardSessionStorage.getDashboardIdsWithUnsavedChanges())
}
/>
</TableListView>
)}
</>
);
};

View file

@ -9,13 +9,16 @@
import { dataPluginMock } from '@kbn/data-plugin/public/mocks';
import { UrlForwardingStart } from '@kbn/url-forwarding-plugin/public';
import { embeddablePluginMock } from '@kbn/embeddable-plugin/public/mocks';
import { savedObjectsPluginMock } from '@kbn/saved-objects-plugin/public/mocks';
import { screenshotModePluginMock } from '@kbn/screenshot-mode-plugin/public/mocks';
import { visualizationsPluginMock } from '@kbn/visualizations-plugin/public/mocks';
import { PluginInitializerContext, ScopedHistory } from '@kbn/core/public';
import { savedObjectsPluginMock } from '@kbn/saved-objects-plugin/public/mocks';
import { visualizationsPluginMock } from '@kbn/visualizations-plugin/public/mocks';
import { screenshotModePluginMock } from '@kbn/screenshot-mode-plugin/public/mocks';
import { indexPatternEditorPluginMock } from '@kbn/data-view-editor-plugin/public/mocks';
import { dataViewPluginMocks } from '@kbn/data-views-plugin/public/mocks';
import { chromeServiceMock, coreMock, uiSettingsServiceMock } from '@kbn/core/public/mocks';
import { SavedObjectLoader, SavedObjectLoaderFindOptions } from '../../services/saved_objects';
import { DataViewsContract, SavedQueryService } from '../../services/data';
import { SavedQueryService } from '../../services/data';
import { DashboardAppServices, DashboardAppCapabilities } from '../../types';
import { NavigationPublicPluginStart } from '../../services/navigation';
import { getSavedDashboardMock } from './get_saved_dashboard_mock';
@ -70,16 +73,17 @@ export function makeDefaultServices(): DashboardAppServices {
return {
screenshotModeService: screenshotModePluginMock.createSetupContract(),
dataViewEditor: indexPatternEditorPluginMock.createStartContract(),
visualizations: visualizationsPluginMock.createStartContract(),
savedObjects: savedObjectsPluginMock.createStartContract(),
embeddable: embeddablePluginMock.createInstance().doStart(),
uiSettings: uiSettingsServiceMock.createStartContract(),
dataViews: dataViewPluginMocks.createStartContract(),
chrome: chromeServiceMock.createStartContract(),
navigation: {} as NavigationPublicPluginStart,
savedObjectsClient: core.savedObjects.client,
dashboardCapabilities: defaultCapabilities,
data: dataPluginMock.createStartContract(),
dataViews: {} as DataViewsContract,
savedQueryService: {} as SavedQueryService,
scopedHistory: () => ({} as ScopedHistory),
setHeaderActionMenu: (mountPoint) => {},

View file

@ -28,6 +28,7 @@ import {
import { VisualizationsStart } from '@kbn/visualizations-plugin/public';
import { replaceUrlHashQuery } from '@kbn/kibana-utils-plugin/public';
import { DataViewEditorStart } from '@kbn/data-view-editor-plugin/public';
import { createKbnUrlTracker } from './services/kibana_utils';
import { UsageCollectionSetup } from './services/usage_collection';
import { UiActionsSetup, UiActionsStart } from './services/ui_actions';
@ -109,6 +110,7 @@ export interface DashboardStartDependencies {
spaces?: SpacesPluginStart;
visualizations: VisualizationsStart;
screenshotMode: ScreenshotModePluginStart;
dataViewEditor: DataViewEditorStart;
}
export interface DashboardSetup {

View file

@ -24,6 +24,7 @@ import { BehaviorSubject, Subject } from 'rxjs';
import { UrlForwardingStart } from '@kbn/url-forwarding-plugin/public';
import { VisualizationsStart } from '@kbn/visualizations-plugin/public';
import { PersistableControlGroupInput } from '@kbn/controls-plugin/common';
import { DataViewEditorStart } from '@kbn/data-view-editor-plugin/public';
import { DataView } from './services/data_views';
import { SharePluginStart } from './services/share';
import { EmbeddableStart } from './services/embeddable';
@ -205,6 +206,7 @@ export interface DashboardAppServices {
savedDashboards: SavedObjectLoader;
scopedHistory: () => ScopedHistory;
visualizations: VisualizationsStart;
dataViewEditor: DataViewEditorStart;
dataViews: DataViewsContract;
usageCollection?: UsageCollectionSetup;
navigation: NavigationPublicPluginStart;

View file

@ -25,6 +25,12 @@ const createStartContract = (): Start => {
fetchForWildcard: jest.fn(),
},
}),
hasData: {
hasESData: jest.fn().mockReturnValue(Promise.resolve(true)),
hasUserDataView: jest.fn().mockReturnValue(Promise.resolve(true)),
hasDataView: jest.fn().mockReturnValue(Promise.resolve(true)),
},
getDefaultDataView: jest.fn().mockReturnValue(Promise.resolve({})),
get: jest.fn().mockReturnValue(Promise.resolve({})),
clearCache: jest.fn(),
getCanSaveSync: jest.fn(),

View file

@ -0,0 +1,80 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { FtrProviderContext } from '../../../ftr_provider_context';
export default function ({ getService, getPageObjects }: FtrProviderContext) {
const log = getService('log');
const retry = getService('retry');
const find = getService('find');
const filterBar = getService('filterBar');
const esArchiver = getService('esArchiver');
const kibanaServer = getService('kibanaServer');
const testSubjects = getService('testSubjects');
const PageObjects = getPageObjects(['common', 'dashboard', 'header', 'timePicker']);
const createDataView = async (dataViewName: string) => {
await testSubjects.setValue('createIndexPatternNameInput', dataViewName, {
clearWithKeyboard: true,
typeCharByChar: true,
});
await testSubjects.click('saveIndexPatternButton');
};
describe('dashboard empty state', () => {
const kbnDirectory = 'test/functional/fixtures/kbn_archiver/dashboard/current/kibana';
before(async function () {
await esArchiver.unload('test/functional/fixtures/es_archiver/logstash_functional');
await kibanaServer.savedObjects.clean({ types: ['search', 'index-pattern'] });
log.debug('load kibana with no data');
await kibanaServer.importExport.unload(kbnDirectory);
await PageObjects.common.navigateToApp('dashboard');
});
after(async () => {
await kibanaServer.savedObjects.clean({ types: ['search', 'index-pattern'] });
});
it('Opens the integrations page when there is no data', async () => {
await PageObjects.header.waitUntilLoadingHasFinished();
const addIntegrations = await testSubjects.find('kbnOverviewAddIntegrations');
await addIntegrations.click();
await PageObjects.common.waitUntilUrlIncludes('integrations/browse');
});
it('adds a new data view when no data views', async () => {
await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/logstash_functional');
await kibanaServer.savedObjects.clean({ types: ['search', 'index-pattern'] });
// create the new data view from the dashboards/create route in order to test that the dashboard is loaded properly as soon as the data view is created...
await PageObjects.common.navigateToUrl('dashboard', '/create');
const button = await testSubjects.find('createDataViewButtonFlyout');
button.click();
await retry.waitForWithTimeout('index pattern editor form to be visible', 15000, async () => {
return await (await find.byClassName('indexPatternEditor__form')).isDisplayed();
});
const dataViewToCreate = 'logstash';
await createDataView(dataViewToCreate);
await PageObjects.header.waitUntilLoadingHasFinished();
await retry.waitForWithTimeout(
'filter manager to be able to create a filter with the new data view',
5000,
async () => {
await testSubjects.click('addFilter');
const fields = await filterBar.getFilterEditorFields();
await filterBar.ensureFieldEditorModalIsClosed();
return fields.length > 0;
}
);
});
});
}

View file

@ -21,6 +21,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
const dashboardNameEnterKey = 'Dashboard Save Test with Enter Key';
before(async function () {
await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/logstash_functional');
await PageObjects.dashboard.initTests();
});

View file

@ -25,6 +25,7 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) {
before(loadLogstash);
after(unloadLogstash);
loadTestFile(require.resolve('./dashboard_empty'));
loadTestFile(require.resolve('./dashboard_save'));
loadTestFile(require.resolve('./dashboard_time'));
loadTestFile(require.resolve('./dashboard_listing'));