From a12ab5809fd419ab14082858155134046e31f0d8 Mon Sep 17 00:00:00 2001 From: Tim Sullivan Date: Sat, 6 Jan 2024 10:15:02 -0700 Subject: [PATCH] [Serverless Search] Update the "empty state" screens (#172225) ## Summary This PR offers customizations to the "Getting Started" experience for users of Serverless Elasticsearch projects. The changes involve checking Elasticsearch if the currently-logged-in user has created API Keys, and to use that information to customize the getting started messaging (destination of the call-to-action of the "no data" prompt) in the UI. ### Screenshots | | Before | After | |-|-|-| | Data Views Mgmt | before - serverless es data
views mgmt empty state | after - serverless es data views mgmt empty
state | | Analytics | before - serverless es discover
empty state | after - serverless es discover empty state | Elasticsearch documentation on API Key listing: https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-get-api-key.html Depends on: https://github.com/elastic/kibana/pull/172884 Closes: https://github.com/elastic/enterprise-search-team/issues/5974 Closes: https://github.com/elastic/enterprise-search-team/issues/5975 ### Other changes * Created a React hook to send and track a request to the Kibana server to ask whether the user has access to API Keys * Added unit test for this * See discussion, this hook may be [re-organized eventually](https://github.com/elastic/kibana/pull/172225#discussion_r1432307325) * Enable lazy loading for the "Analytics No Data" page within analytics apps. ### Checklist - [x] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md) - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- packages/kbn-optimizer/limits.yml | 2 +- .../no_data_card.component.test.tsx.snap | 4 + .../__snapshots__/no_data_card.test.tsx.snap | 32 +- .../impl/src/no_data_card.component.tsx | 8 +- .../shared-ux/card/no_data/types/index.d.ts | 2 +- .../page/analytics_no_data/impl/index.ts | 2 + .../impl/lib/get_has_api_keys.test.ts | 56 ++++ .../impl/lib/get_has_api_keys.ts | 54 +++ .../analytics_no_data_page.component.test.tsx | 47 ++- .../src/analytics_no_data_page.component.tsx | 39 ++- .../impl/src/analytics_no_data_page.test.tsx | 3 - .../impl/src/analytics_no_data_page.tsx | 18 +- .../analytics_no_data/impl/src/services.tsx | 7 +- .../page/analytics_no_data/impl/tsconfig.json | 2 + .../page/analytics_no_data/mocks/src/jest.ts | 2 + .../analytics_no_data/mocks/src/storybook.ts | 1 + .../page/analytics_no_data/types/index.d.ts | 11 +- .../analytics_no_data/types/tsconfig.json | 1 + .../impl/src/no_data_config_page.test.tsx | 4 +- .../prompt/no_data_views/impl/index.ts | 1 + .../impl/src/no_data_views.stories.tsx | 9 +- .../no_data/dashboard_app_no_data.tsx | 26 +- .../no_data_page/no_data_page_service.stub.ts | 6 +- src/plugins/dashboard/tsconfig.json | 3 +- src/plugins/data_view_management/kibana.jsonc | 1 + .../public/components/add_data_prompt.tsx | 128 +++++++ .../index_pattern_table.tsx | 46 +-- .../index_pattern_table/no_data.tsx | 91 +++++ .../mount_management_section.tsx | 8 +- .../data_view_management/public/mocks.ts | 3 + .../data_view_management/public/plugin.ts | 16 +- .../data_view_management/public/types.ts | 26 +- .../data_view_management/tsconfig.json | 6 + .../main/discover_main_route.test.tsx | 1 + .../application/main/discover_main_route.tsx | 21 +- .../__snapshots__/overview.test.tsx.snap | 312 +++++++++++++++++- .../public/components/overview/overview.tsx | 21 +- src/plugins/kibana_overview/tsconfig.json | 1 + .../no_data_page/public/mocks/index.ts | 21 ++ src/plugins/no_data_page/public/plugin.ts | 17 +- .../public/visualize_app/app.tsx | 22 +- src/plugins/visualizations/tsconfig.json | 3 +- .../lens/public/app_plugin/mounter.tsx | 21 +- x-pack/plugins/lens/tsconfig.json | 3 +- 44 files changed, 941 insertions(+), 167 deletions(-) create mode 100644 packages/shared-ux/page/analytics_no_data/impl/lib/get_has_api_keys.test.ts create mode 100644 packages/shared-ux/page/analytics_no_data/impl/lib/get_has_api_keys.ts create mode 100644 src/plugins/data_view_management/public/components/add_data_prompt.tsx create mode 100644 src/plugins/data_view_management/public/components/index_pattern_table/no_data.tsx create mode 100644 src/plugins/no_data_page/public/mocks/index.ts diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index 6811987dc146b..dae455d8bb8ac 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -33,7 +33,7 @@ pageLoadAssetSize: datasetQuality: 50624 dataViewEditor: 28082 dataViewFieldEditor: 27000 - dataViewManagement: 5100 + dataViewManagement: 5136 dataViews: 48300 dataVisualizer: 27530 devTools: 38637 diff --git a/packages/shared-ux/card/no_data/impl/src/__snapshots__/no_data_card.component.test.tsx.snap b/packages/shared-ux/card/no_data/impl/src/__snapshots__/no_data_card.component.test.tsx.snap index 17eb4ef8804cd..cd50761ed954b 100644 --- a/packages/shared-ux/card/no_data/impl/src/__snapshots__/no_data_card.component.test.tsx.snap +++ b/packages/shared-ux/card/no_data/impl/src/__snapshots__/no_data_card.component.test.tsx.snap @@ -11,6 +11,7 @@ exports[`NoDataCardComponent props button 1`] = ` description="Use Elastic Agent for a simple, unified way to collect data from your machines." footer={ Button @@ -40,7 +41,9 @@ exports[`NoDataCardComponent props href 1`] = ` description="Use Elastic Agent for a simple, unified way to collect data from your machines." footer={ Add Elastic Agent @@ -70,6 +73,7 @@ exports[`NoDataCardComponent renders 1`] = ` description="Use Elastic Agent for a simple, unified way to collect data from your machines." footer={ Add Elastic Agent diff --git a/packages/shared-ux/card/no_data/impl/src/__snapshots__/no_data_card.test.tsx.snap b/packages/shared-ux/card/no_data/impl/src/__snapshots__/no_data_card.test.tsx.snap index 474abd65a5991..915f1f6750bff 100644 --- a/packages/shared-ux/card/no_data/impl/src/__snapshots__/no_data_card.test.tsx.snap +++ b/packages/shared-ux/card/no_data/impl/src/__snapshots__/no_data_card.test.tsx.snap @@ -61,9 +61,11 @@ exports[`NoDataCard props button 1`] = ` @@ -142,9 +144,11 @@ exports[`NoDataCard props extends EuiCardProps 1`] = ` @@ -223,9 +227,11 @@ exports[`NoDataCard props href 1`] = ` @@ -370,9 +376,11 @@ exports[`NoDataCard renders 1`] = ` diff --git a/packages/shared-ux/card/no_data/impl/src/no_data_card.component.tsx b/packages/shared-ux/card/no_data/impl/src/no_data_card.component.tsx index 2c05f0319bfcd..264c4db677185 100644 --- a/packages/shared-ux/card/no_data/impl/src/no_data_card.component.tsx +++ b/packages/shared-ux/card/no_data/impl/src/no_data_card.component.tsx @@ -56,6 +56,7 @@ export const NoDataCard = ({ description: descriptionProp, canAccessFleet, button, + href, ...props }: Props) => { const styles = NoDataCardStyles(); @@ -72,7 +73,11 @@ export const NoDataCard = ({ } // Default footer action is a button with the provided or default string - return {button || titleProp || defaultTitle}; + return ( + + {button || titleProp || defaultTitle} + + ); }; const title = () => { @@ -103,6 +108,7 @@ export const NoDataCard = ({ description={description()} footer={footer()} isDisabled={!canAccessFleet} + href={href} image={} {...props} /> diff --git a/packages/shared-ux/card/no_data/types/index.d.ts b/packages/shared-ux/card/no_data/types/index.d.ts index e52843b160639..085d1629fd70a 100644 --- a/packages/shared-ux/card/no_data/types/index.d.ts +++ b/packages/shared-ux/card/no_data/types/index.d.ts @@ -56,7 +56,7 @@ export type NoDataCardKibanaDependencies = KibanaDependencies & RedirectAppLinks * Props for the `NoDataCard` pure component. */ export type NoDataCardComponentProps = Partial< - Omit + Pick > & { /** * Provide just a string for the button's label, or a whole component; diff --git a/packages/shared-ux/page/analytics_no_data/impl/index.ts b/packages/shared-ux/page/analytics_no_data/impl/index.ts index 2296d0e5d47bb..d977d4496d880 100644 --- a/packages/shared-ux/page/analytics_no_data/impl/index.ts +++ b/packages/shared-ux/page/analytics_no_data/impl/index.ts @@ -15,3 +15,5 @@ export type { AnalyticsNoDataPageKibanaDependencies, AnalyticsNoDataPageProps, } from '@kbn/shared-ux-page-analytics-no-data-types'; + +export { getHasApiKeys$ } from './lib/get_has_api_keys'; diff --git a/packages/shared-ux/page/analytics_no_data/impl/lib/get_has_api_keys.test.ts b/packages/shared-ux/page/analytics_no_data/impl/lib/get_has_api_keys.test.ts new file mode 100644 index 0000000000000..3ad7b42ff1bd7 --- /dev/null +++ b/packages/shared-ux/page/analytics_no_data/impl/lib/get_has_api_keys.test.ts @@ -0,0 +1,56 @@ +/* + * 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 { HttpSetup } from '@kbn/core-http-browser'; +import { httpServiceMock } from '@kbn/core-http-browser-mocks'; +import { HasApiKeysResponse, getHasApiKeys$ } from './get_has_api_keys'; + +describe('getHasApiKeys$', () => { + let mockHttp: HttpSetup; + beforeEach(() => { + mockHttp = httpServiceMock.createSetupContract({ basePath: '/test' }); + }); + + it('should return the correct sequence of states', (done) => { + const httpGetSpy = jest.spyOn(mockHttp, 'get'); + httpGetSpy.mockResolvedValue({ hasApiKeys: true }); + const source$ = getHasApiKeys$(mockHttp); + + const emittedValues: HasApiKeysResponse[] = []; + + source$.subscribe({ + next: (value) => emittedValues.push(value), + complete: () => { + expect(emittedValues).toEqual([ + { error: null, hasApiKeys: null, isLoading: true }, + { error: null, hasApiKeys: true, isLoading: false }, + ]); + done(); + }, + }); + }); + + it('should forward the error', (done) => { + const httpGetSpy = jest.spyOn(mockHttp, 'get'); + httpGetSpy.mockRejectedValue('something bad'); + const source$ = getHasApiKeys$(mockHttp); + + const emittedValues: HasApiKeysResponse[] = []; + + source$.subscribe({ + next: (value) => emittedValues.push(value), + complete: () => { + expect(emittedValues).toEqual([ + { error: null, hasApiKeys: null, isLoading: true }, + { error: 'something bad', hasApiKeys: null, isLoading: false }, + ]); + done(); + }, + }); + }); +}); diff --git a/packages/shared-ux/page/analytics_no_data/impl/lib/get_has_api_keys.ts b/packages/shared-ux/page/analytics_no_data/impl/lib/get_has_api_keys.ts new file mode 100644 index 0000000000000..7684f9ecdd682 --- /dev/null +++ b/packages/shared-ux/page/analytics_no_data/impl/lib/get_has_api_keys.ts @@ -0,0 +1,54 @@ +/* + * 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 type { AnalyticsNoDataPageServices } from '@kbn/shared-ux-page-analytics-no-data-types'; +import { of, Observable, catchError, from, map, startWith } from 'rxjs'; + +export interface HasApiKeysEndpointResponseData { + hasApiKeys: boolean; +} + +export interface HasApiKeysResponse { + hasApiKeys: boolean | null; + isLoading: boolean; + error: Error | null; +} + +const HAS_API_KEYS_ENDPOINT_PATH = '/internal/security/api_key/_has_active'; + +export const getHasApiKeys$ = ({ + get, +}: { + get: AnalyticsNoDataPageServices['getHttp']; +}): Observable => { + return from(get(HAS_API_KEYS_ENDPOINT_PATH)).pipe( + map((responseData) => { + return { + isLoading: false, + hasApiKeys: responseData.hasApiKeys, + error: null, + }; + }), + startWith({ + isLoading: true, + hasApiKeys: null, + error: null, + }), + // catch any errors + catchError((error) => { + // eslint-disable-next-line no-console + console.error('Could not determine whether user has API keys:', error); + + return of({ + hasApiKeys: null, + isLoading: false, + error, + }); + }) + ); +}; diff --git a/packages/shared-ux/page/analytics_no_data/impl/src/analytics_no_data_page.component.test.tsx b/packages/shared-ux/page/analytics_no_data/impl/src/analytics_no_data_page.component.test.tsx index ac59185e25ef9..da580f948995f 100644 --- a/packages/shared-ux/page/analytics_no_data/impl/src/analytics_no_data_page.component.test.tsx +++ b/packages/shared-ux/page/analytics_no_data/impl/src/analytics_no_data_page.component.test.tsx @@ -21,6 +21,7 @@ import { getAnalyticsNoDataPageServicesMock } from '@kbn/shared-ux-page-analytic describe('AnalyticsNoDataPageComponent', () => { const services = getAnalyticsNoDataPageServicesMock(); + services.kibanaGuideDocLink = 'http://www.test.com'; const onDataViewCreated = jest.fn(); it('renders correctly', async () => { @@ -28,10 +29,9 @@ describe('AnalyticsNoDataPageComponent', () => { // Include context so composed components will have access to their services. path} /> ); @@ -52,11 +52,10 @@ describe('AnalyticsNoDataPageComponent', () => { const component = mountWithIntl( path} /> ); @@ -74,17 +73,16 @@ describe('AnalyticsNoDataPageComponent', () => { false }}> path} /> ); await screen.findByTestId('kbnOverviewAddIntegrations'); - await screen.getAllByText('Add integrations'); + screen.getAllByText('Add integrations'); }); it('renders disabled add integrations card when fleet is not available', async () => { @@ -94,71 +92,72 @@ describe('AnalyticsNoDataPageComponent', () => { {...{ ...services, hasESData: async () => false, canAccessFleet: false }} > path} /> ); await screen.findByTestId('kbnOverviewAddIntegrations'); - await screen.getByText('Contact your administrator'); + screen.getByText('Contact your administrator'); }); }); describe('serverless_search flavor', () => { - it('renders getting started card', async () => { + beforeEach(() => { + services.pageFlavor = 'serverless_search'; + }); + + it('renders Add Data card', async () => { render( false }}> path} /> ); - await screen.findByTestId('kbnOverviewElasticsearchGettingStarted'); + await screen.findByTestId('kbnOverviewElasticsearchAddData'); }); - it('renders the same getting started card when fleet is not available', async () => { + it('renders the same Add Data card when fleet is not available', async () => { render( false, canAccessFleet: false }} > path} - pageFlavor={'serverless_search'} /> ); - await screen.findByTestId('kbnOverviewElasticsearchGettingStarted'); + await screen.findByTestId('kbnOverviewElasticsearchAddData'); }); }); describe('serverless_observability flavor', () => { - it('renders getting started card', async () => { + beforeEach(() => { + services.pageFlavor = 'serverless_observability'; + }); + + it('renders Add Data card', async () => { render( false }}> path} /> diff --git a/packages/shared-ux/page/analytics_no_data/impl/src/analytics_no_data_page.component.tsx b/packages/shared-ux/page/analytics_no_data/impl/src/analytics_no_data_page.component.tsx index a7cb75e2c1fb0..59ca3a9b77e33 100644 --- a/packages/shared-ux/page/analytics_no_data/impl/src/analytics_no_data_page.component.tsx +++ b/packages/shared-ux/page/analytics_no_data/impl/src/analytics_no_data_page.component.tsx @@ -5,32 +5,35 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ -import React from 'react'; + +import React, { useMemo } from 'react'; +import useObservable from 'react-use/lib/useObservable'; + import { i18n } from '@kbn/i18n'; +import { AnalyticsNoDataPageFlavor, Services } from '@kbn/shared-ux-page-analytics-no-data-types'; import { KibanaNoDataPage } from '@kbn/shared-ux-page-kibana-no-data'; import { KibanaNoDataPageProps } from '@kbn/shared-ux-page-kibana-no-data-types'; -import { AnalyticsNoDataPageFlavor } from '@kbn/shared-ux-page-analytics-no-data-types'; +import { getHasApiKeys$ } from '../lib/get_has_api_keys'; /** * Props for the pure component. */ export interface Props { - /** A link to documentation. */ - kibanaGuideDocLink: string; /** Handler for successfully creating a new data view. */ onDataViewCreated: (dataView: unknown) => void; /** if set to true allows creation of an ad-hoc dataview from data view editor */ allowAdHocDataView?: boolean; /** if the kibana instance is customly branded */ showPlainSpinner: boolean; - /** The flavor of the empty page to use. */ - pageFlavor?: AnalyticsNoDataPageFlavor; - prependBasePath: (path: string) => string; } +type AnalyticsNoDataPageProps = Props & + Pick; + const flavors: { [K in AnalyticsNoDataPageFlavor]: (deps: { kibanaGuideDocLink: string; + hasApiKeys: boolean; prependBasePath: (path: string) => string; }) => KibanaNoDataPageProps['noDataConfig']; } = { @@ -55,7 +58,7 @@ const flavors: { }, docsLink: kibanaGuideDocLink, }), - serverless_search: ({ prependBasePath }) => ({ + serverless_search: ({ hasApiKeys, prependBasePath }) => ({ solution: i18n.translate('sharedUXPackages.noDataConfig.elasticsearch', { defaultMessage: 'Elasticsearch', }), @@ -66,14 +69,16 @@ const flavors: { action: { elasticsearch: { title: i18n.translate('sharedUXPackages.noDataConfig.elasticsearchTitle', { - defaultMessage: 'Get started', + defaultMessage: 'Add data', }), description: i18n.translate('sharedUXPackages.noDataConfig.elasticsearchDescription', { defaultMessage: 'Set up your programming language client, ingest some data, and start searching.', }), - 'data-test-subj': 'kbnOverviewElasticsearchGettingStarted', - href: prependBasePath('/app/elasticsearch/'), + 'data-test-subj': 'kbnOverviewElasticsearchAddData', + href: hasApiKeys + ? prependBasePath('/app/elasticsearch/#ingestData') // use Ingest Data section of Home page if project has ES API keys + : prependBasePath('/app/elasticsearch/'), /** force the no data card to be shown **/ canAccessFleet: true, }, @@ -109,17 +114,19 @@ const flavors: { /** * A pure component of an entire page that can be displayed when Kibana "has no data", specifically for Analytics. */ -export const AnalyticsNoDataPage = ({ - kibanaGuideDocLink, +export const AnalyticsNoDataPage: React.FC = ({ onDataViewCreated, allowAdHocDataView, showPlainSpinner, - prependBasePath, - pageFlavor = 'kibana', -}: Props) => { + ...services +}) => { + const { prependBasePath, kibanaGuideDocLink, getHttp: get, pageFlavor } = services; + const { hasApiKeys } = useObservable(useMemo(() => getHasApiKeys$({ get }), [get])) ?? {}; + const noDataConfig: KibanaNoDataPageProps['noDataConfig'] = flavors[pageFlavor]({ kibanaGuideDocLink, prependBasePath, + hasApiKeys: Boolean(hasApiKeys), }); return ( diff --git a/packages/shared-ux/page/analytics_no_data/impl/src/analytics_no_data_page.test.tsx b/packages/shared-ux/page/analytics_no_data/impl/src/analytics_no_data_page.test.tsx index 6a8eb777db73b..2b36648ce1204 100644 --- a/packages/shared-ux/page/analytics_no_data/impl/src/analytics_no_data_page.test.tsx +++ b/packages/shared-ux/page/analytics_no_data/impl/src/analytics_no_data_page.test.tsx @@ -38,11 +38,8 @@ describe('AnalyticsNoDataPage', () => { await act(() => new Promise(setImmediate)); expect(component.find(Component).length).toBe(1); - expect(component.find(Component).props().kibanaGuideDocLink).toBe(services.kibanaGuideDocLink); expect(component.find(Component).props().onDataViewCreated).toBe(onDataViewCreated); expect(component.find(Component).props().allowAdHocDataView).toBe(true); - expect(component.find(Component).props().prependBasePath).toBe(services.prependBasePath); - expect(component.find(Component).props().pageFlavor).toBe(services.pageFlavor); }); it('passes correct boolean value to showPlainSpinner', () => { diff --git a/packages/shared-ux/page/analytics_no_data/impl/src/analytics_no_data_page.tsx b/packages/shared-ux/page/analytics_no_data/impl/src/analytics_no_data_page.tsx index e9d3ee318d3fd..3ad51b54c1feb 100644 --- a/packages/shared-ux/page/analytics_no_data/impl/src/analytics_no_data_page.tsx +++ b/packages/shared-ux/page/analytics_no_data/impl/src/analytics_no_data_page.tsx @@ -20,21 +20,15 @@ export const AnalyticsNoDataPage = ({ onDataViewCreated, allowAdHocDataView, }: AnalyticsNoDataPageProps) => { - const services = useServices(); - const { kibanaGuideDocLink, customBranding, prependBasePath, pageFlavor } = services; - const { hasCustomBranding$ } = customBranding; - const showPlainSpinner = useObservable(hasCustomBranding$) ?? false; + const { customBranding, ...services } = useServices(); + const showPlainSpinner = useObservable(customBranding.hasCustomBranding$) ?? false; return ( ); }; diff --git a/packages/shared-ux/page/analytics_no_data/impl/src/services.tsx b/packages/shared-ux/page/analytics_no_data/impl/src/services.tsx index 4d514ba032ec9..f79861f680a0d 100644 --- a/packages/shared-ux/page/analytics_no_data/impl/src/services.tsx +++ b/packages/shared-ux/page/analytics_no_data/impl/src/services.tsx @@ -27,10 +27,12 @@ export const AnalyticsNoDataPageProvider: FC = ({ children, ...services }) => { - const { kibanaGuideDocLink, customBranding, prependBasePath, pageFlavor } = services; + const { kibanaGuideDocLink, customBranding, getHttp, prependBasePath, pageFlavor } = services; return ( - + {children} ); @@ -48,6 +50,7 @@ export const AnalyticsNoDataPageKibanaProvider: FC { kibanaGuideDocLink: 'Kibana guide', customBranding: { hasCustomBranding$: of(false) }, prependBasePath: (path) => path, + getHttp: () => Promise.resolve({} as T), pageFlavor: 'kibana', }; @@ -29,6 +30,7 @@ export const getServicesMockCustomBranding = () => { customBranding: { hasCustomBranding$: of(true) }, kibanaGuideDocLink: 'Kibana guide', prependBasePath: (path) => path, + getHttp: () => Promise.resolve({} as T), pageFlavor: 'kibana', }; diff --git a/packages/shared-ux/page/analytics_no_data/mocks/src/storybook.ts b/packages/shared-ux/page/analytics_no_data/mocks/src/storybook.ts index 6bb3f07e34a87..5b5f26fd18a7f 100644 --- a/packages/shared-ux/page/analytics_no_data/mocks/src/storybook.ts +++ b/packages/shared-ux/page/analytics_no_data/mocks/src/storybook.ts @@ -53,6 +53,7 @@ export class StorybookMock extends AbstractStorybookMock< }, pageFlavor: 'kibana', prependBasePath: (path) => path, + getHttp: () => Promise.resolve({} as T), ...kibanaNoDataMock.getServices(params), }; } diff --git a/packages/shared-ux/page/analytics_no_data/types/index.d.ts b/packages/shared-ux/page/analytics_no_data/types/index.d.ts index 1b71ac172f0de..f744595a2fe29 100644 --- a/packages/shared-ux/page/analytics_no_data/types/index.d.ts +++ b/packages/shared-ux/page/analytics_no_data/types/index.d.ts @@ -5,11 +5,14 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ + +import { Observable } from 'rxjs'; + +import { NoDataPagePluginSetup } from '@kbn/no-data-page-plugin/public'; import { KibanaNoDataPageServices, KibanaNoDataPageKibanaDependencies, } from '@kbn/shared-ux-page-kibana-no-data-types'; -import { Observable } from 'rxjs'; /** * A list of services that are consumed by this component. @@ -18,6 +21,7 @@ export interface Services { kibanaGuideDocLink: string; customBranding: { hasCustomBranding$: Observable }; prependBasePath: (path: string) => string; + getHttp: (path: string) => Promise; pageFlavor: AnalyticsNoDataPageFlavor; } @@ -44,11 +48,10 @@ export interface KibanaDependencies { basePath: { prepend: (path: string) => string; }; + get: (path: string, options?: object) => Promise; }; }; - noDataPage?: { - getAnalyticsNoDataPageFlavor: () => AnalyticsNoDataPageFlavor; - }; + noDataPage?: NoDataPagePluginSetup; } /** diff --git a/packages/shared-ux/page/analytics_no_data/types/tsconfig.json b/packages/shared-ux/page/analytics_no_data/types/tsconfig.json index e34fff5c01f9b..e46f95f52599b 100644 --- a/packages/shared-ux/page/analytics_no_data/types/tsconfig.json +++ b/packages/shared-ux/page/analytics_no_data/types/tsconfig.json @@ -12,5 +12,6 @@ ], "kbn_references": [ "@kbn/shared-ux-page-kibana-no-data-types", + "@kbn/no-data-page-plugin", ] } diff --git a/packages/shared-ux/page/no_data_config/impl/src/no_data_config_page.test.tsx b/packages/shared-ux/page/no_data_config/impl/src/no_data_config_page.test.tsx index fbd897314eceb..da932bff12d8a 100644 --- a/packages/shared-ux/page/no_data_config/impl/src/no_data_config_page.test.tsx +++ b/packages/shared-ux/page/no_data_config/impl/src/no_data_config_page.test.tsx @@ -34,6 +34,8 @@ describe('NoDataConfigPage', () => { ); expect(component.find('h1').html()).toContain('Welcome to Elastic Solution!'); - expect(component.find('button').html()).toContain('Click me'); + expect(component.find('a[data-test-subj="noDataDefaultFooterAction"]').html()).toContain( + 'Click me' + ); }); }); diff --git a/packages/shared-ux/prompt/no_data_views/impl/index.ts b/packages/shared-ux/prompt/no_data_views/impl/index.ts index 1b068a2267052..69a602f9eac3b 100644 --- a/packages/shared-ux/prompt/no_data_views/impl/index.ts +++ b/packages/shared-ux/prompt/no_data_views/impl/index.ts @@ -16,3 +16,4 @@ export type { export { NoDataViewsPrompt } from './src/no_data_views'; export { NoDataViewsPrompt as NoDataViewsPromptComponent } from './src/no_data_views.component'; export { NoDataViewsPromptKibanaProvider, NoDataViewsPromptProvider } from './src/services'; +export { DataViewIllustration } from './src/data_view_illustration'; diff --git a/packages/shared-ux/prompt/no_data_views/impl/src/no_data_views.stories.tsx b/packages/shared-ux/prompt/no_data_views/impl/src/no_data_views.stories.tsx index 748687fbda5e8..dc7ca32ed2d93 100644 --- a/packages/shared-ux/prompt/no_data_views/impl/src/no_data_views.stories.tsx +++ b/packages/shared-ux/prompt/no_data_views/impl/src/no_data_views.stories.tsx @@ -5,6 +5,7 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ + import React from 'react'; import { @@ -12,7 +13,7 @@ import { NoDataViewsPromptStorybookParams, } from '@kbn/shared-ux-prompt-no-data-views-mocks'; -import { NoDataViewsPrompt } from './no_data_views'; +import { NoDataViewsPrompt as Component } from './no_data_views'; import { NoDataViewsPromptProvider } from './services'; import mdx from '../README.mdx'; @@ -29,12 +30,12 @@ export default { const mock = new NoDataViewsPromptStorybookMock(); -export const Prompt = (params: NoDataViewsPromptStorybookParams) => { +export const CreateDataView = (params: NoDataViewsPromptStorybookParams) => { return ( - + ); }; -Prompt.argTypes = mock.getArgumentTypes(); +CreateDataView.argTypes = mock.getArgumentTypes(); diff --git a/src/plugins/dashboard/public/dashboard_app/no_data/dashboard_app_no_data.tsx b/src/plugins/dashboard/public/dashboard_app/no_data/dashboard_app_no_data.tsx index 8580ae2f168d3..8394237976f9a 100644 --- a/src/plugins/dashboard/public/dashboard_app/no_data/dashboard_app_no_data.tsx +++ b/src/plugins/dashboard/public/dashboard_app/no_data/dashboard_app_no_data.tsx @@ -7,11 +7,8 @@ */ import React from 'react'; -import { - AnalyticsNoDataPageKibanaProvider, - AnalyticsNoDataPage, -} from '@kbn/shared-ux-page-analytics-no-data'; +import { withSuspense } from '@kbn/shared-ux-utility'; import { pluginServices } from '../../services/plugin_services'; export const DashboardAppNoDataPage = ({ @@ -23,7 +20,7 @@ export const DashboardAppNoDataPage = ({ application, data: { dataViews }, dataViewEditor, - http: { basePath }, + http: { basePath, get }, documentationLinks: { indexPatternsDocLink, kibanaGuideDocLink }, customBranding, noDataPage, @@ -38,7 +35,7 @@ export const DashboardAppNoDataPage = ({ }, }, application, - http: { basePath }, + http: { basePath, get }, customBranding: { hasCustomBranding$: customBranding.hasCustomBranding$, }, @@ -47,6 +44,23 @@ export const DashboardAppNoDataPage = ({ dataViewEditor, noDataPage, }; + + const importPromise = import('@kbn/shared-ux-page-analytics-no-data'); + const AnalyticsNoDataPageKibanaProvider = withSuspense( + React.lazy(() => + importPromise.then(({ AnalyticsNoDataPageKibanaProvider: NoDataProvider }) => { + return { default: NoDataProvider }; + }) + ) + ); + const AnalyticsNoDataPage = withSuspense( + React.lazy(() => + importPromise.then(({ AnalyticsNoDataPage: NoDataPage }) => { + return { default: NoDataPage }; + }) + ) + ); + return ( diff --git a/src/plugins/dashboard/public/services/no_data_page/no_data_page_service.stub.ts b/src/plugins/dashboard/public/services/no_data_page/no_data_page_service.stub.ts index c1af1452176a8..f761b0d113daf 100644 --- a/src/plugins/dashboard/public/services/no_data_page/no_data_page_service.stub.ts +++ b/src/plugins/dashboard/public/services/no_data_page/no_data_page_service.stub.ts @@ -6,11 +6,11 @@ * Side Public License, v 1. */ +import { noDataPagePublicMock } from '@kbn/no-data-page-plugin/public/mocks'; import { PluginServiceFactory } from '@kbn/presentation-util-plugin/public'; import { NoDataPageService } from './types'; export type NoDataPageServiceFactory = PluginServiceFactory; -export const noDataPageServiceFactory: NoDataPageServiceFactory = () => { - return { getAnalyticsNoDataPageFlavor: () => 'kibana' }; -}; +export const noDataPageServiceFactory: NoDataPageServiceFactory = () => + noDataPagePublicMock.createSetup(); diff --git a/src/plugins/dashboard/tsconfig.json b/src/plugins/dashboard/tsconfig.json index f01342cdc4b8d..1f37320d775f2 100644 --- a/src/plugins/dashboard/tsconfig.json +++ b/src/plugins/dashboard/tsconfig.json @@ -69,7 +69,8 @@ "@kbn/react-kibana-mount", "@kbn/core-lifecycle-browser", "@kbn/logging", - "@kbn/content-management-table-list-view-common" + "@kbn/content-management-table-list-view-common", + "@kbn/shared-ux-utility" ], "exclude": ["target/**/*"] } diff --git a/src/plugins/data_view_management/kibana.jsonc b/src/plugins/data_view_management/kibana.jsonc index cd9018c04e543..479e357804140 100644 --- a/src/plugins/data_view_management/kibana.jsonc +++ b/src/plugins/data_view_management/kibana.jsonc @@ -19,6 +19,7 @@ "savedObjectsManagement" ], "optionalPlugins": [ + "noDataPage", "spaces" ], "requiredBundles": [ diff --git a/src/plugins/data_view_management/public/components/add_data_prompt.tsx b/src/plugins/data_view_management/public/components/add_data_prompt.tsx new file mode 100644 index 0000000000000..136786324a062 --- /dev/null +++ b/src/plugins/data_view_management/public/components/add_data_prompt.tsx @@ -0,0 +1,128 @@ +/* + * 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 { css } from '@emotion/react'; +import React from 'react'; + +import { EuiButton, EuiEmptyPrompt, EuiPanel } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { withSuspense } from '@kbn/shared-ux-utility'; + +import { EuiLink, EuiTitle } from '@elastic/eui'; + +interface DocumentationLinkProps { + href: string; +} + +export function DocumentationLink({ href }: DocumentationLinkProps) { + return ( +
+ +
+ +
+
+   +
+ + + +
+
+ ); +} + +export interface AddDataPromptComponentProps { + addDataHref: string; + docLink?: string; +} + +// Using raw value because it is content dependent +const MAX_WIDTH = 830; + +/** + * A presentational component that is shown in cases when there are no data views created yet. + */ +export const AddDataPrompt: React.FC = ({ + addDataHref, + docLink: docLink, +}) => { + const createDataViewText = i18n.translate('indexPatternManagement.addDataPrompt.addDataText', { + defaultMessage: 'Add data', + }); + + const actions = ( + + {createDataViewText} + + ); + + const title = ( +

+ +

+ ); + + const body = ( +

+ +

+ ); + + const footer = docLink ? : undefined; + + // Load this illustration lazily + const Illustration = withSuspense( + React.lazy(() => + import('@kbn/shared-ux-prompt-no-data-views').then(({ DataViewIllustration }) => { + return { default: DataViewIllustration }; + }) + ), + + ); + + return ( + , + }} + /> + ); +}; diff --git a/src/plugins/data_view_management/public/components/index_pattern_table/index_pattern_table.tsx b/src/plugins/data_view_management/public/components/index_pattern_table/index_pattern_table.tsx index a64d75bf63196..6f6df3e77bf09 100644 --- a/src/plugins/data_view_management/public/components/index_pattern_table/index_pattern_table.tsx +++ b/src/plugins/data_view_management/public/components/index_pattern_table/index_pattern_table.tsx @@ -8,34 +8,36 @@ import { EuiBadge, + EuiBasicTableColumn, EuiButton, - EuiLink, + EuiIconTip, EuiInMemoryTable, + EuiLink, + EuiLoadingSpinner, EuiPageHeader, EuiSpacer, - EuiIconTip, - EuiBasicTableColumn, - EuiLoadingSpinner, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; -import { RouteComponentProps, withRouter, useLocation } from 'react-router-dom'; -import useObservable from 'react-use/lib/useObservable'; -import React, { useState, useMemo } from 'react'; import { i18n } from '@kbn/i18n'; + +import React, { useMemo, useState } from 'react'; +import { RouteComponentProps, useLocation, withRouter } from 'react-router-dom'; +import useObservable from 'react-use/lib/useObservable'; + import { reactRouterNavigate, useKibana } from '@kbn/kibana-react-plugin/public'; -import type { SpacesContextProps } from '@kbn/spaces-plugin/public'; import { NoDataViewsPromptComponent } from '@kbn/shared-ux-prompt-no-data-views'; -import { EmptyIndexListPrompt } from '../empty_index_list_prompt'; -import { IndexPatternManagmentContext } from '../../types'; -import { IndexPatternTableItem } from '../types'; +import type { SpacesContextProps } from '@kbn/spaces-plugin/public'; +import type { IndexPatternManagmentContext } from '../../types'; import { getListBreadcrumbs } from '../breadcrumbs'; -import { SpacesList } from './spaces_list'; -import { removeDataView, RemoveDataViewProps } from '../edit_index_pattern'; -import { deleteModalMsg } from './delete_modal_msg'; +import { type RemoveDataViewProps, removeDataView } from '../edit_index_pattern'; +import { IndexPatternTableItem } from '../types'; import { DataViewTableController, dataViewTableControllerStateDefaults as defaults, } from './data_view_table_controller'; +import { deleteModalMsg } from './delete_modal_msg'; +import { NoData } from './no_data'; +import { SpacesList } from './spaces_list'; const pagination = { initialPageSize: 10, @@ -76,6 +78,7 @@ export const IndexPatternTable = ({ }: Props) => { const { setBreadcrumbs, + http, uiSettings, application, chrome, @@ -84,6 +87,7 @@ export const IndexPatternTable = ({ spaces, overlays, docLinks, + noDataPage, } = useKibana().services; const [query, setQuery] = useState(''); const [showCreateDialog, setShowCreateDialog] = useState(showCreateDialogProp); @@ -362,12 +366,14 @@ export const IndexPatternTable = ({ displayIndexPatternSection = ( <> - setShowCreateDialog(true)} - canSaveIndexPattern={!!application.capabilities.indexPatterns.save} - navigateToApp={application.navigateToApp} - addDataUrl={docLinks.links.indexPatterns.introduction} + ); diff --git a/src/plugins/data_view_management/public/components/index_pattern_table/no_data.tsx b/src/plugins/data_view_management/public/components/index_pattern_table/no_data.tsx new file mode 100644 index 0000000000000..49437f57122fa --- /dev/null +++ b/src/plugins/data_view_management/public/components/index_pattern_table/no_data.tsx @@ -0,0 +1,91 @@ +/* + * 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, { useMemo } from 'react'; +import useObservable from 'react-use/lib/useObservable'; + +import { EuiLoadingSpinner } from '@elastic/eui'; +import type { ApplicationStart } from '@kbn/core-application-browser'; +import type { DocLinksStart } from '@kbn/core-doc-links-browser'; +import type { IUiSettingsClient } from '@kbn/core-ui-settings-browser'; +import type { NoDataPagePluginStart } from '@kbn/no-data-page-plugin/public'; +import { getHasApiKeys$ } from '@kbn/shared-ux-page-analytics-no-data'; +import { HttpStart } from '@kbn/core-http-browser'; +import { AddDataPrompt } from '../add_data_prompt'; +import { EmptyIndexListPrompt } from '../empty_index_list_prompt'; +import type { DataViewTableController } from './data_view_table_controller'; + +/** + * @internal + */ +export interface NoDataProps { + noDataPage?: NoDataPagePluginStart; + docLinks: DocLinksStart; + uiSettings: IUiSettingsClient; + http: HttpStart; + application: ApplicationStart; + dataViewController: DataViewTableController; + setShowCreateDialog: React.Dispatch>; +} + +const NoDataServerlessSearch: React.FC> = ({ + uiSettings, + http, + docLinks, +}) => { + const { hasApiKeys, error, isLoading } = + useObservable(useMemo(() => getHasApiKeys$(http), [http])) ?? {}; + + if (error) { + throw error; + } + + if (isLoading) { + return ; + } + + const addDataHref = hasApiKeys + ? uiSettings.get('defaultRoute') + '#ingestData' + : uiSettings.get('defaultRoute'); + + return ( + + ); +}; + +/** + * @internal + */ +export const NoData: React.FC = ({ + noDataPage, + docLinks, + http, + uiSettings, + application, + dataViewController, + setShowCreateDialog, +}) => { + const flavor = noDataPage?.getAnalyticsNoDataPageFlavor() ?? 'kibana'; + + switch (flavor) { + case 'serverless_search': { + return ; + } + + default: + return ( + setShowCreateDialog(true)} + canSaveIndexPattern={!!application.capabilities.indexPatterns.save} + navigateToApp={application.navigateToApp} + addDataUrl={docLinks.links.indexPatterns.introduction} + /> + ); + } +}; diff --git a/src/plugins/data_view_management/public/management_app/mount_management_section.tsx b/src/plugins/data_view_management/public/management_app/mount_management_section.tsx index 8bd8745f7dfd9..2b7c928146ae6 100644 --- a/src/plugins/data_view_management/public/management_app/mount_management_section.tsx +++ b/src/plugins/data_view_management/public/management_app/mount_management_section.tsx @@ -21,7 +21,11 @@ import { EditIndexPatternContainer, CreateEditFieldContainer, } from '../components'; -import { IndexPatternManagementStartDependencies, IndexPatternManagementStart } from '../plugin'; +import { + IndexPatternManagementStartDependencies, + IndexPatternManagementStart, + IndexPatternManagementSetupDependencies, +} from '../plugin'; import { IndexPatternManagmentContext } from '../types'; const readOnlyBadge = { @@ -36,6 +40,7 @@ const readOnlyBadge = { export async function mountManagementSection( getStartServices: StartServicesAccessor, + { noDataPage }: Pick, params: ManagementAppMountParams ) { const [ @@ -90,6 +95,7 @@ export async function mountManagementSection( spaces: spaces?.hasOnlyDefaultSpace ? undefined : spaces, theme, savedObjectsManagement, + noDataPage, }; const editPath = '/dataView/:id/field/:fieldName'; diff --git a/src/plugins/data_view_management/public/mocks.ts b/src/plugins/data_view_management/public/mocks.ts index e4753af88e752..4c0f573447a69 100644 --- a/src/plugins/data_view_management/public/mocks.ts +++ b/src/plugins/data_view_management/public/mocks.ts @@ -9,6 +9,7 @@ import { PluginInitializerContext } from '@kbn/core/public'; import { coreMock } from '@kbn/core/public/mocks'; import { managementPluginMock } from '@kbn/management-plugin/public/mocks'; +import { noDataPagePublicMock } from '@kbn/no-data-page-plugin/public/mocks'; import { urlForwardingPluginMock } from '@kbn/url-forwarding-plugin/public/mocks'; import { dataPluginMock } from '@kbn/data-plugin/public/mocks'; import { unifiedSearchPluginMock } from '@kbn/unified-search-plugin/public/mocks'; @@ -34,6 +35,7 @@ const createInstance = async () => { const setup = plugin.setup(coreMock.createSetup(), { management: managementPluginMock.createSetupContract(), urlForwarding: urlForwardingPluginMock.createSetupContract(), + noDataPage: noDataPagePublicMock.createSetup(), }); const doStart = () => plugin.start(); @@ -77,6 +79,7 @@ const createIndexPatternManagmentContext = (): { docLinks, data, dataViews, + noDataPage: noDataPagePublicMock.createStart(), unifiedSearch, dataViewFieldEditor, indexPatternManagementStart: createStartContract(), diff --git a/src/plugins/data_view_management/public/plugin.ts b/src/plugins/data_view_management/public/plugin.ts index 3aefcbc047f16..ee91c0666a13f 100644 --- a/src/plugins/data_view_management/public/plugin.ts +++ b/src/plugins/data_view_management/public/plugin.ts @@ -6,23 +6,25 @@ * Side Public License, v 1. */ -import { i18n } from '@kbn/i18n'; -import { PluginInitializerContext, CoreSetup, Plugin } from '@kbn/core/public'; +import { CoreSetup, Plugin, PluginInitializerContext } from '@kbn/core/public'; import { DataPublicPluginStart } from '@kbn/data-plugin/public'; import { FieldFormatsStart } from '@kbn/field-formats-plugin/public'; +import { i18n } from '@kbn/i18n'; +import { NoDataPagePluginSetup } from '@kbn/no-data-page-plugin/public'; import { UrlForwardingSetup } from '@kbn/url-forwarding-plugin/public'; -import { ManagementSetup } from '@kbn/management-plugin/public'; -import { IndexPatternFieldEditorStart } from '@kbn/data-view-field-editor-plugin/public'; import { DataViewEditorStart } from '@kbn/data-view-editor-plugin/public'; +import { IndexPatternFieldEditorStart } from '@kbn/data-view-field-editor-plugin/public'; import { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public'; +import { ManagementSetup } from '@kbn/management-plugin/public'; +import { SavedObjectsManagementPluginStart } from '@kbn/saved-objects-management-plugin/public'; import { SpacesPluginStart } from '@kbn/spaces-plugin/public'; import { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public'; -import { SavedObjectsManagementPluginStart } from '@kbn/saved-objects-management-plugin/public'; export interface IndexPatternManagementSetupDependencies { management: ManagementSetup; urlForwarding: UrlForwardingSetup; + noDataPage?: NoDataPagePluginSetup; } export interface IndexPatternManagementStartDependencies { @@ -61,7 +63,7 @@ export class IndexPatternManagementPlugin public setup( core: CoreSetup, - { management, urlForwarding }: IndexPatternManagementSetupDependencies + { management, urlForwarding, ...deps }: IndexPatternManagementSetupDependencies ) { const kibanaSection = management.sections.section.kibana; @@ -87,7 +89,7 @@ export class IndexPatternManagementPlugin mount: async (params) => { const { mountManagementSection } = await import('./management_app'); - return mountManagementSection(core.getStartServices, params); + return mountManagementSection(core.getStartServices, deps, params); }, }); return {}; diff --git a/src/plugins/data_view_management/public/types.ts b/src/plugins/data_view_management/public/types.ts index 064ef60d3a22d..f26ccefd8ee75 100644 --- a/src/plugins/data_view_management/public/types.ts +++ b/src/plugins/data_view_management/public/types.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { +import type { ChromeStart, IUiSettingsClient, OverlayStart, @@ -16,18 +16,19 @@ import { ApplicationStart, ThemeServiceStart, } from '@kbn/core/public'; -import { DataPublicPluginStart } from '@kbn/data-plugin/public'; -import { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public'; -import { ManagementAppMountParams } from '@kbn/management-plugin/public'; -import { KibanaReactContextValue } from '@kbn/kibana-react-plugin/public'; -import { IndexPatternFieldEditorStart } from '@kbn/data-view-field-editor-plugin/public'; -import { DataViewEditorStart } from '@kbn/data-view-editor-plugin/public'; -import { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public'; -import { FieldFormatsStart } from '@kbn/field-formats-plugin/public'; -import { SpacesPluginStart } from '@kbn/spaces-plugin/public'; -import { SavedObjectsManagementPluginStart } from '@kbn/saved-objects-management-plugin/public'; +import type { DataPublicPluginStart } from '@kbn/data-plugin/public'; +import type { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public'; +import type { ManagementAppMountParams } from '@kbn/management-plugin/public'; +import type { KibanaReactContextValue } from '@kbn/kibana-react-plugin/public'; +import type { IndexPatternFieldEditorStart } from '@kbn/data-view-field-editor-plugin/public'; +import type { DataViewEditorStart } from '@kbn/data-view-editor-plugin/public'; +import type { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public'; +import type { FieldFormatsStart } from '@kbn/field-formats-plugin/public'; +import type { SpacesPluginStart } from '@kbn/spaces-plugin/public'; +import type { SavedObjectsManagementPluginStart } from '@kbn/saved-objects-management-plugin/public'; import type { SettingsStart } from '@kbn/core-ui-settings-browser'; -import { IndexPatternManagementStart } from '.'; +import type { NoDataPagePluginSetup } from '@kbn/no-data-page-plugin/public'; +import type { IndexPatternManagementStart } from '.'; export interface IndexPatternManagmentContext { application: ApplicationStart; @@ -50,6 +51,7 @@ export interface IndexPatternManagmentContext { spaces?: SpacesPluginStart; theme: ThemeServiceStart; savedObjectsManagement: SavedObjectsManagementPluginStart; + noDataPage?: NoDataPagePluginSetup; } export type IndexPatternManagmentContextValue = diff --git a/src/plugins/data_view_management/tsconfig.json b/src/plugins/data_view_management/tsconfig.json index 5fd9fff16ea3a..ec09cf2f7d1ca 100644 --- a/src/plugins/data_view_management/tsconfig.json +++ b/src/plugins/data_view_management/tsconfig.json @@ -35,6 +35,12 @@ "@kbn/shared-ux-router", "@kbn/core-ui-settings-browser", "@kbn/react-kibana-context-render", + "@kbn/no-data-page-plugin", + "@kbn/core-application-browser", + "@kbn/core-doc-links-browser", + "@kbn/shared-ux-utility", + "@kbn/shared-ux-page-analytics-no-data", + "@kbn/core-http-browser", "@kbn/code-editor", ], "exclude": [ diff --git a/src/plugins/discover/public/application/main/discover_main_route.test.tsx b/src/plugins/discover/public/application/main/discover_main_route.test.tsx index 63296bfb927e9..5cc73a4200f9f 100644 --- a/src/plugins/discover/public/application/main/discover_main_route.test.tsx +++ b/src/plugins/discover/public/application/main/discover_main_route.test.tsx @@ -137,6 +137,7 @@ function getServicesMock(hasESData = true, hasUserDataView = true) { hasUserDataView: jest.fn(() => Promise.resolve(hasUserDataView)), hasDataView: jest.fn(() => Promise.resolve(true)), }; + discoverServiceMock.core.http.get = jest.fn().mockResolvedValue({}); return discoverServiceMock; } diff --git a/src/plugins/discover/public/application/main/discover_main_route.tsx b/src/plugins/discover/public/application/main/discover_main_route.tsx index 2a2575a773524..4b1eebd153ec4 100644 --- a/src/plugins/discover/public/application/main/discover_main_route.tsx +++ b/src/plugins/discover/public/application/main/discover_main_route.tsx @@ -15,13 +15,10 @@ import { SavedObjectNotFound, } from '@kbn/kibana-utils-plugin/public'; import { useExecutionContext } from '@kbn/kibana-react-plugin/public'; -import { - AnalyticsNoDataPageKibanaProvider, - AnalyticsNoDataPage, -} from '@kbn/shared-ux-page-analytics-no-data'; import { getSavedSearchFullPathUrl } from '@kbn/saved-search-plugin/public'; import useObservable from 'react-use/lib/useObservable'; import { reportPerformanceMetricEvent } from '@kbn/ebt-tools'; +import { withSuspense } from '@kbn/shared-ux-utility'; import { useUrl } from './hooks/use_url'; import { useSingleton } from './hooks/use_singleton'; import { MainHistoryLocationState } from '../../../common/locator'; @@ -282,6 +279,22 @@ export function DiscoverMainRoute({ const mainContent = useMemo(() => { if (showNoDataPage) { + const importPromise = import('@kbn/shared-ux-page-analytics-no-data'); + const AnalyticsNoDataPageKibanaProvider = withSuspense( + React.lazy(() => + importPromise.then(({ AnalyticsNoDataPageKibanaProvider: NoDataProvider }) => { + return { default: NoDataProvider }; + }) + ) + ); + const AnalyticsNoDataPage = withSuspense( + React.lazy(() => + importPromise.then(({ AnalyticsNoDataPage: NoDataPage }) => { + return { default: NoDataPage }; + }) + ) + ); + return ( diff --git a/src/plugins/kibana_overview/public/components/overview/__snapshots__/overview.test.tsx.snap b/src/plugins/kibana_overview/public/components/overview/__snapshots__/overview.test.tsx.snap index 656a59a6425f6..b2741eaf8d3a1 100644 --- a/src/plugins/kibana_overview/public/components/overview/__snapshots__/overview.test.tsx.snap +++ b/src/plugins/kibana_overview/public/components/overview/__snapshots__/overview.test.tsx.snap @@ -442,7 +442,7 @@ exports[`Overview renders correctly when there is no user data view 1`] = ` ] } > - - - + + + + } + > + + + + + + + + `; diff --git a/src/plugins/kibana_overview/public/components/overview/overview.tsx b/src/plugins/kibana_overview/public/components/overview/overview.tsx index 0df0e583699f7..bdc8c9e0a755e 100644 --- a/src/plugins/kibana_overview/public/components/overview/overview.tsx +++ b/src/plugins/kibana_overview/public/components/overview/overview.tsx @@ -26,10 +26,6 @@ import { } from '@kbn/kibana-react-plugin/public'; import { KibanaPageTemplate } from '@kbn/shared-ux-page-kibana-template'; import { KibanaSolutionAvatar } from '@kbn/shared-ux-avatar-solution'; -import { - AnalyticsNoDataPageKibanaProvider, - AnalyticsNoDataPage, -} from '@kbn/shared-ux-page-analytics-no-data'; import { RedirectAppLinksContainer as RedirectAppLinks, RedirectAppLinksKibanaProvider, @@ -40,6 +36,7 @@ import { FeatureCatalogueSolution, FeatureCatalogueCategory, } from '@kbn/home-plugin/public'; +import { withSuspense } from '@kbn/shared-ux-utility'; import { PLUGIN_ID, PLUGIN_PATH } from '../../../common'; import { AppPluginStartDependencies } from '../../types'; import { AddData } from '../add_data'; @@ -202,6 +199,22 @@ export const Overview: FC = ({ newsFetchResult, solutions, features }) => dataViewEditor, }; + const importPromise = import('@kbn/shared-ux-page-analytics-no-data'); + const AnalyticsNoDataPageKibanaProvider = withSuspense( + React.lazy(() => + importPromise.then(({ AnalyticsNoDataPageKibanaProvider: NoDataProvider }) => { + return { default: NoDataProvider }; + }) + ) + ); + const AnalyticsNoDataPage = withSuspense( + React.lazy(() => + importPromise.then(({ AnalyticsNoDataPage: NoDataPage }) => { + return { default: NoDataPage }; + }) + ) + ); + return ( diff --git a/src/plugins/kibana_overview/tsconfig.json b/src/plugins/kibana_overview/tsconfig.json index 976eed58d31aa..09e9d232901c4 100644 --- a/src/plugins/kibana_overview/tsconfig.json +++ b/src/plugins/kibana_overview/tsconfig.json @@ -26,6 +26,7 @@ "@kbn/shared-ux-link-redirect-app", "@kbn/shared-ux-router", "@kbn/shared-ux-avatar-solution", + "@kbn/shared-ux-utility", ], "exclude": [ "target/**/*", diff --git a/src/plugins/no_data_page/public/mocks/index.ts b/src/plugins/no_data_page/public/mocks/index.ts new file mode 100644 index 0000000000000..5735f2d4876c1 --- /dev/null +++ b/src/plugins/no_data_page/public/mocks/index.ts @@ -0,0 +1,21 @@ +/* + * 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 { NoDataPagePublicSetup, NoDataPagePublicStart } from '../types'; + +const initialize = () => { + return () => + ({ + getAnalyticsNoDataPageFlavor: () => 'kibana', + } as T); +}; + +export const noDataPagePublicMock = { + createSetup: initialize(), + createStart: initialize(), +}; diff --git a/src/plugins/no_data_page/public/plugin.ts b/src/plugins/no_data_page/public/plugin.ts index a9f5dd0fe1f95..910208f0f94be 100644 --- a/src/plugins/no_data_page/public/plugin.ts +++ b/src/plugins/no_data_page/public/plugin.ts @@ -7,23 +7,10 @@ */ import type { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from '@kbn/core/public'; -import type { - NoDataPagePublicSetup, - NoDataPagePublicSetupDependencies, - NoDataPagePublicStart, - NoDataPagePublicStartDependencies, -} from './types'; +import type { NoDataPagePublicSetup, NoDataPagePublicStart } from './types'; import type { NoDataPageConfig } from '../config'; -export class NoDataPagePlugin - implements - Plugin< - NoDataPagePublicSetup, - NoDataPagePublicStart, - NoDataPagePublicSetupDependencies, - NoDataPagePublicStartDependencies - > -{ +export class NoDataPagePlugin implements Plugin { constructor(private initializerContext: PluginInitializerContext) {} public setup(_core: CoreSetup): NoDataPagePublicSetup { diff --git a/src/plugins/visualizations/public/visualize_app/app.tsx b/src/plugins/visualizations/public/visualize_app/app.tsx index 88453b1d902f4..8ff102e101479 100644 --- a/src/plugins/visualizations/public/visualize_app/app.tsx +++ b/src/plugins/visualizations/public/visualize_app/app.tsx @@ -16,11 +16,8 @@ import type { DataViewEditorStart } from '@kbn/data-view-editor-plugin/public'; import { syncGlobalQueryStateWithUrl } from '@kbn/data-plugin/public'; import type { NoDataPagePluginStart } from '@kbn/no-data-page-plugin/public'; import { useKibana } from '@kbn/kibana-react-plugin/public'; -import { - AnalyticsNoDataPageKibanaProvider, - AnalyticsNoDataPage, -} from '@kbn/shared-ux-page-analytics-no-data'; import type { DataViewsContract } from '@kbn/data-views-plugin/public'; +import { withSuspense } from '@kbn/shared-ux-utility'; import { VisualizeServices } from './types'; import { VisualizeEditor, @@ -55,6 +52,23 @@ const NoDataComponent = ({ dataViewEditor, noDataPage, }; + + const importPromise = import('@kbn/shared-ux-page-analytics-no-data'); + const AnalyticsNoDataPageKibanaProvider = withSuspense( + React.lazy(() => + importPromise.then(({ AnalyticsNoDataPageKibanaProvider: NoDataProvider }) => { + return { default: NoDataProvider }; + }) + ) + ); + const AnalyticsNoDataPage = withSuspense( + React.lazy(() => + importPromise.then(({ AnalyticsNoDataPage: NoDataPage }) => { + return { default: NoDataPage }; + }) + ) + ); + return ( diff --git a/src/plugins/visualizations/tsconfig.json b/src/plugins/visualizations/tsconfig.json index a52401eaf65da..813c47ca83872 100644 --- a/src/plugins/visualizations/tsconfig.json +++ b/src/plugins/visualizations/tsconfig.json @@ -65,7 +65,8 @@ "@kbn/no-data-page-plugin", "@kbn/search-response-warnings", "@kbn/logging", - "@kbn/content-management-table-list-view-common" + "@kbn/content-management-table-list-view-common", + "@kbn/shared-ux-utility" ], "exclude": [ "target/**/*", diff --git a/x-pack/plugins/lens/public/app_plugin/mounter.tsx b/x-pack/plugins/lens/public/app_plugin/mounter.tsx index 77c0685e2858c..3893b1b3f0e44 100644 --- a/x-pack/plugins/lens/public/app_plugin/mounter.tsx +++ b/x-pack/plugins/lens/public/app_plugin/mounter.tsx @@ -19,16 +19,13 @@ import { Storage, withNotifyOnErrors, } from '@kbn/kibana-utils-plugin/public'; -import { - AnalyticsNoDataPageKibanaProvider, - AnalyticsNoDataPage, -} from '@kbn/shared-ux-page-analytics-no-data'; import { ACTION_VISUALIZE_LENS_FIELD, VisualizeFieldContext } from '@kbn/ui-actions-plugin/public'; import { ACTION_CONVERT_TO_LENS } from '@kbn/visualizations-plugin/public'; import { KibanaContextProvider, KibanaThemeProvider } from '@kbn/kibana-react-plugin/public'; import { EuiLoadingSpinner } from '@elastic/eui'; import { syncGlobalQueryStateWithUrl } from '@kbn/data-plugin/public'; +import { withSuspense } from '@kbn/shared-ux-utility'; import { App } from './app'; import { EditorFrameStart, LensTopNavMenuEntryGenerator, VisualizeEditorContext } from '../types'; @@ -347,6 +344,22 @@ export async function mountApp( dataViews: data.dataViews, dataViewEditor: startDependencies.dataViewEditor, }; + const importPromise = import('@kbn/shared-ux-page-analytics-no-data'); + const AnalyticsNoDataPageKibanaProvider = withSuspense( + React.lazy(() => + importPromise.then(({ AnalyticsNoDataPageKibanaProvider: NoDataProvider }) => { + return { default: NoDataProvider }; + }) + ) + ); + const AnalyticsNoDataPage = withSuspense( + React.lazy(() => + importPromise.then(({ AnalyticsNoDataPage: NoDataPage }) => { + return { default: NoDataPage }; + }) + ) + ); + return (