diff --git a/packages/deeplinks/analytics/constants.ts b/packages/deeplinks/analytics/constants.ts index 81d8646cfa074..9793f7bb1864f 100644 --- a/packages/deeplinks/analytics/constants.ts +++ b/packages/deeplinks/analytics/constants.ts @@ -11,3 +11,5 @@ export const DISCOVER_APP_ID = 'discover'; export const DASHBOARD_APP_ID = 'dashboards'; export const VISUALIZE_APP_ID = 'visualize'; + +export const DISCOVER_ESQL_LOCATOR = 'DISCOVER_ESQL_LOCATOR'; diff --git a/packages/deeplinks/analytics/deep_links.ts b/packages/deeplinks/analytics/deep_links.ts index e2a19ee30b80d..048b8217cda74 100644 --- a/packages/deeplinks/analytics/deep_links.ts +++ b/packages/deeplinks/analytics/deep_links.ts @@ -6,8 +6,15 @@ * Side Public License, v 1. */ -import { DASHBOARD_APP_ID, DISCOVER_APP_ID, VISUALIZE_APP_ID } from './constants'; +import { + DASHBOARD_APP_ID, + DISCOVER_APP_ID, + DISCOVER_ESQL_LOCATOR, + VISUALIZE_APP_ID, +} from './constants'; export type AppId = typeof DISCOVER_APP_ID | typeof DASHBOARD_APP_ID | typeof VISUALIZE_APP_ID; export type DeepLinkId = AppId; + +export type LocatorId = typeof DISCOVER_ESQL_LOCATOR; diff --git a/packages/deeplinks/analytics/index.ts b/packages/deeplinks/analytics/index.ts index e2d605988231a..aa01b0036a52d 100644 --- a/packages/deeplinks/analytics/index.ts +++ b/packages/deeplinks/analytics/index.ts @@ -6,6 +6,11 @@ * Side Public License, v 1. */ -export { DASHBOARD_APP_ID, DISCOVER_APP_ID, VISUALIZE_APP_ID } from './constants'; +export { + DASHBOARD_APP_ID, + DISCOVER_APP_ID, + VISUALIZE_APP_ID, + DISCOVER_ESQL_LOCATOR, +} from './constants'; export type { AppId, DeepLinkId } from './deep_links'; diff --git a/packages/kbn-es-query/index.ts b/packages/kbn-es-query/index.ts index 3793f0efb8855..5a7e55b73a75f 100644 --- a/packages/kbn-es-query/index.ts +++ b/packages/kbn-es-query/index.ts @@ -51,6 +51,7 @@ export { migrateFilter, fromCombinedFilter, isOfQueryType, + isOfEsqlQueryType, isOfAggregateQueryType, getAggregateQueryMode, getLanguageDisplayName, diff --git a/packages/kbn-es-query/src/es_query/es_aggregate_query.ts b/packages/kbn-es-query/src/es_query/es_aggregate_query.ts index 9eae919db7f6f..ba3e67be19cee 100644 --- a/packages/kbn-es-query/src/es_query/es_aggregate_query.ts +++ b/packages/kbn-es-query/src/es_query/es_aggregate_query.ts @@ -24,6 +24,15 @@ export function isOfAggregateQueryType( return Boolean(query && ('sql' in query || 'esql' in query)); } +/** + * True if the query is of type AggregateQuery and is of type esql, false otherwise. + */ +export function isOfEsqlQueryType( + query?: AggregateQuery | Query | { [key: string]: any } +): query is { esql: string } { + return Boolean(query && 'esql' in query && !('sql' in query)); +} + // returns the language of the aggregate Query, sql, esql etc export function getAggregateQueryMode(query: AggregateQuery): Language { return Object.keys(query)[0] as Language; diff --git a/packages/kbn-es-query/src/es_query/index.ts b/packages/kbn-es-query/src/es_query/index.ts index bb84ddbc16f97..887ad2ea3214f 100644 --- a/packages/kbn-es-query/src/es_query/index.ts +++ b/packages/kbn-es-query/src/es_query/index.ts @@ -16,6 +16,7 @@ export { decorateQuery } from './decorate_query'; export { isOfQueryType, isOfAggregateQueryType, + isOfEsqlQueryType, getAggregateQueryMode, getLanguageDisplayName, } from './es_aggregate_query'; diff --git a/packages/kbn-esql-utils/index.ts b/packages/kbn-esql-utils/index.ts index c4f5def0b4802..d9b57701801b7 100644 --- a/packages/kbn-esql-utils/index.ts +++ b/packages/kbn-esql-utils/index.ts @@ -12,4 +12,5 @@ export { getIndexPatternFromESQLQuery, getLimitFromESQLQuery, removeDropCommandsFromESQLQuery, + getIndexForESQLQuery, } from './src'; diff --git a/packages/kbn-esql-utils/src/index.ts b/packages/kbn-esql-utils/src/index.ts index 224b4fb2788ae..33f35c213c739 100644 --- a/packages/kbn-esql-utils/src/index.ts +++ b/packages/kbn-esql-utils/src/index.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -export { getESQLAdHocDataview } from './utils/get_esql_adhoc_dataview'; +export { getESQLAdHocDataview, getIndexForESQLQuery } from './utils/get_esql_adhoc_dataview'; export { getIndexPatternFromSQLQuery, getIndexPatternFromESQLQuery, diff --git a/packages/kbn-esql-utils/src/utils/get_esql_adhoc_dataview.ts b/packages/kbn-esql-utils/src/utils/get_esql_adhoc_dataview.ts index 7ac96e272fcf7..a1866526742cc 100644 --- a/packages/kbn-esql-utils/src/utils/get_esql_adhoc_dataview.ts +++ b/packages/kbn-esql-utils/src/utils/get_esql_adhoc_dataview.ts @@ -25,7 +25,7 @@ async function sha256(str: string) { // This is a helper to create one. The id is constructed from the indexpattern. // As there are no runtime fields or field formatters or default time fields // the same adhoc dataview can be constructed/used. This comes with great advantages such -// as solving the problem descibed here https://github.com/elastic/kibana/issues/168131 +// as solving the problem described here https://github.com/elastic/kibana/issues/168131 export async function getESQLAdHocDataview( indexPattern: string, dataViewsService: DataViewsPublicPluginStart @@ -35,3 +35,31 @@ export async function getESQLAdHocDataview( id: await sha256(`esql-${indexPattern}`), }); } + +/** + * This can be used to get an initial index for a default ES|QL query. + * Could be used during onboarding when data views to get a better index are not yet available. + * Can be used in combination with {@link getESQLAdHocDataview} to create a dataview for the index. + */ +export async function getIndexForESQLQuery(deps: { + dataViews: { getIndices: DataViewsPublicPluginStart['getIndices'] }; +}): Promise { + const indices = ( + await deps.dataViews.getIndices({ + showAllIndices: false, + pattern: '*', + isRollupIndex: () => false, + }) + ) + .filter((index) => !index.name.startsWith('.')) + .map((index) => index.name); + + let indexName = indices[0]; + if (indices.length > 0) { + if (indices.find((index) => index.startsWith('logs'))) { + indexName = 'logs*'; + } + } + + return indexName ?? null; +} 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 59ca3a9b77e33..c3eae7ae06542 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 @@ -21,6 +21,8 @@ import { getHasApiKeys$ } from '../lib/get_has_api_keys'; export interface Props { /** Handler for successfully creating a new data view. */ onDataViewCreated: (dataView: unknown) => void; + /** Handler for when try ES|QL is clicked and user has been navigated to try ES|QL in discover. */ + onESQLNavigationComplete?: () => 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 */ @@ -116,6 +118,7 @@ const flavors: { */ export const AnalyticsNoDataPage: React.FC = ({ onDataViewCreated, + onESQLNavigationComplete, allowAdHocDataView, showPlainSpinner, ...services @@ -131,7 +134,13 @@ export const AnalyticsNoDataPage: React.FC = ({ return ( ); }; 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 3ad51b54c1feb..47a76fc9a0c9b 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 @@ -18,6 +18,7 @@ import { AnalyticsNoDataPage as Component } from './analytics_no_data_page.compo */ export const AnalyticsNoDataPage = ({ onDataViewCreated, + onESQLNavigationComplete, allowAdHocDataView, }: AnalyticsNoDataPageProps) => { const { customBranding, ...services } = useServices(); @@ -29,6 +30,7 @@ export const AnalyticsNoDataPage = ({ showPlainSpinner={showPlainSpinner} allowAdHocDataView={allowAdHocDataView} onDataViewCreated={onDataViewCreated} + onESQLNavigationComplete={onESQLNavigationComplete} /> ); }; 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 f744595a2fe29..1e4af95715b12 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 @@ -69,4 +69,6 @@ export interface AnalyticsNoDataPageProps { onDataViewCreated: (dataView: unknown) => void; /** if set to true allows creation of an ad-hoc data view from data view editor */ allowAdHocDataView?: boolean; + /** Handler for when try ES|QL is clicked and user has been navigated to try ES|QL in discover. */ + onESQLNavigationComplete?: () => void; } diff --git a/packages/shared-ux/page/kibana_no_data/impl/src/kibana_no_data_page.tsx b/packages/shared-ux/page/kibana_no_data/impl/src/kibana_no_data_page.tsx index 2773184b087bb..12cebacbf9973 100644 --- a/packages/shared-ux/page/kibana_no_data/impl/src/kibana_no_data_page.tsx +++ b/packages/shared-ux/page/kibana_no_data/impl/src/kibana_no_data_page.tsx @@ -18,6 +18,7 @@ import { useServices } from './services'; */ export const KibanaNoDataPage = ({ onDataViewCreated, + onESQLNavigationComplete, noDataConfig, allowAdHocDataView, showPlainSpinner, @@ -55,6 +56,7 @@ export const KibanaNoDataPage = ({ return ( ); diff --git a/packages/shared-ux/page/kibana_no_data/types/index.d.ts b/packages/shared-ux/page/kibana_no_data/types/index.d.ts index ff9b4d845f597..888925a146e9d 100644 --- a/packages/shared-ux/page/kibana_no_data/types/index.d.ts +++ b/packages/shared-ux/page/kibana_no_data/types/index.d.ts @@ -59,4 +59,6 @@ export interface KibanaNoDataPageProps { allowAdHocDataView?: boolean; /** Set to true if the kibana is customly branded */ showPlainSpinner: boolean; + /** Handler for when try ES|QL is clicked and user has been navigated to try ES|QL in discover. */ + onESQLNavigationComplete?: () => void; } 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 69a602f9eac3b..3a71331ebbc9d 100644 --- a/packages/shared-ux/prompt/no_data_views/impl/index.ts +++ b/packages/shared-ux/prompt/no_data_views/impl/index.ts @@ -17,3 +17,4 @@ 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'; +export { useOnTryESQL } from './src/hooks'; diff --git a/packages/shared-ux/prompt/no_data_views/impl/src/actions.tsx b/packages/shared-ux/prompt/no_data_views/impl/src/actions.tsx new file mode 100644 index 0000000000000..d71b460e26a3e --- /dev/null +++ b/packages/shared-ux/prompt/no_data_views/impl/src/actions.tsx @@ -0,0 +1,75 @@ +/* + * 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 { EuiButton, EuiLink, EuiSpacer, EuiText } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; +import React from 'react'; + +interface NoDataButtonProps { + onClickCreate: (() => void) | undefined; + canCreateNewDataView: boolean; + onTryESQL?: () => void; + esqlDocLink?: string; +} + +const createDataViewText = i18n.translate('sharedUXPackages.noDataViewsPrompt.addDataViewText', { + defaultMessage: 'Create data view', +}); + +export const NoDataButtonLink = ({ + onClickCreate, + canCreateNewDataView, + onTryESQL, + esqlDocLink, +}: NoDataButtonProps) => { + if (!onTryESQL && !canCreateNewDataView) { + return null; + } + + return ( + <> + {canCreateNewDataView && ( + + {createDataViewText} + + )} + {canCreateNewDataView && onTryESQL && } + {onTryESQL && ( + + + + + ), + }} + /> + + + + + + )} + + ); +}; diff --git a/packages/shared-ux/prompt/no_data_views/impl/src/hooks/index.ts b/packages/shared-ux/prompt/no_data_views/impl/src/hooks/index.ts new file mode 100644 index 0000000000000..23b2d1134fd97 --- /dev/null +++ b/packages/shared-ux/prompt/no_data_views/impl/src/hooks/index.ts @@ -0,0 +1,9 @@ +/* + * 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. + */ + +export { useOnTryESQL, type UseOnTryEsqlParams } from './use_on_try_esql'; diff --git a/packages/shared-ux/prompt/no_data_views/impl/src/hooks/use_on_try_esql.ts b/packages/shared-ux/prompt/no_data_views/impl/src/hooks/use_on_try_esql.ts new file mode 100644 index 0000000000000..e026886c1ca7f --- /dev/null +++ b/packages/shared-ux/prompt/no_data_views/impl/src/hooks/use_on_try_esql.ts @@ -0,0 +1,39 @@ +/* + * 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 { useEffect, useState } from 'react'; +import { DISCOVER_ESQL_LOCATOR } from '@kbn/deeplinks-analytics'; + +import { NavigateToAppFn, LocatorClient } from '@kbn/shared-ux-prompt-no-data-views-types'; + +export interface UseOnTryEsqlParams { + locatorClient?: LocatorClient; + navigateToApp: NavigateToAppFn; +} + +export const useOnTryESQL = ({ locatorClient, navigateToApp }: UseOnTryEsqlParams) => { + const [onTryESQL, setOnTryEsql] = useState<(() => void) | undefined>(); + + useEffect(() => { + (async () => { + const location = await locatorClient?.get(DISCOVER_ESQL_LOCATOR)?.getLocation({}); + + if (!location) { + return; + } + + const { app, path, state } = location; + + setOnTryEsql(() => () => { + navigateToApp(app, { path, state }); + }); + })(); + }, [locatorClient, navigateToApp]); + + return onTryESQL; +}; diff --git a/packages/shared-ux/prompt/no_data_views/impl/src/no_data_views.component.tsx b/packages/shared-ux/prompt/no_data_views/impl/src/no_data_views.component.tsx index 5bc428f051968..2803df4cc6eb1 100644 --- a/packages/shared-ux/prompt/no_data_views/impl/src/no_data_views.component.tsx +++ b/packages/shared-ux/prompt/no_data_views/impl/src/no_data_views.component.tsx @@ -9,17 +9,13 @@ import React from 'react'; import { css } from '@emotion/react'; -import { EuiButton, EuiEmptyPrompt, EuiPanel } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; +import { EuiEmptyPrompt, EuiPanel } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; import { withSuspense } from '@kbn/shared-ux-utility'; import { NoDataViewsPromptComponentProps } from '@kbn/shared-ux-prompt-no-data-views-types'; import { DocumentationLink } from './documentation_link'; - -const createDataViewText = i18n.translate('sharedUXPackages.noDataViewsPrompt.addDataViewText', { - defaultMessage: 'Create data view', -}); +import { NoDataButtonLink } from './actions'; // Using raw value because it is content dependent const MAX_WIDTH = 830; @@ -31,19 +27,10 @@ export const NoDataViewsPrompt = ({ onClickCreate, canCreateNewDataView, dataViewsDocLink, + onTryESQL, + esqlDocLink, emptyPromptColor = 'plain', }: NoDataViewsPromptComponentProps) => { - const actions = canCreateNewDataView && ( - - {createDataViewText} - - ); - const title = canCreateNewDataView ? (

; + const actions = ( + + ); return ( ', () => { const component = mount(); expect(services.openDataViewEditor).not.toHaveBeenCalled(); - component.find('button').simulate('click'); + component.find('button[data-test-subj="createDataViewButton"]').simulate('click'); component.unmount(); expect(services.openDataViewEditor).toHaveBeenCalled(); }); + + test('on ES|QL try', () => { + const component = mount(); + + expect(services.onTryESQL).not.toHaveBeenCalled(); + component.find('button[data-test-subj="tryESQLLink"]').simulate('click'); + + component.unmount(); + + expect(services.onTryESQL).toHaveBeenCalled(); + }); }); diff --git a/packages/shared-ux/prompt/no_data_views/impl/src/no_data_views.tsx b/packages/shared-ux/prompt/no_data_views/impl/src/no_data_views.tsx index 4f668a1017b28..837de4c856949 100644 --- a/packages/shared-ux/prompt/no_data_views/impl/src/no_data_views.tsx +++ b/packages/shared-ux/prompt/no_data_views/impl/src/no_data_views.tsx @@ -26,9 +26,12 @@ type CloseDataViewEditorFn = ReturnType { - const { canCreateNewDataView, openDataViewEditor, dataViewsDocLink } = useServices(); + const { canCreateNewDataView, openDataViewEditor, dataViewsDocLink, onTryESQL, esqlDocLink } = + useServices(); + const closeDataViewEditor = useRef(); useEffect(() => { @@ -72,6 +75,21 @@ export const NoDataViewsPrompt = ({ ]); return ( - + { + onTryESQL(); + if (onESQLNavigationComplete) { + onESQLNavigationComplete(); + } + } + : undefined, + }} + /> ); }; diff --git a/packages/shared-ux/prompt/no_data_views/impl/src/services.tsx b/packages/shared-ux/prompt/no_data_views/impl/src/services.tsx index b8fe90e1ef135..6e282fc6b3a18 100644 --- a/packages/shared-ux/prompt/no_data_views/impl/src/services.tsx +++ b/packages/shared-ux/prompt/no_data_views/impl/src/services.tsx @@ -7,10 +7,12 @@ */ import React, { FC, useContext } from 'react'; + import type { NoDataViewsPromptServices, NoDataViewsPromptKibanaDependencies, } from '@kbn/shared-ux-prompt-no-data-views-types'; +import { useOnTryESQL } from './hooks'; const NoDataViewsPromptContext = React.createContext(null); @@ -23,11 +25,18 @@ export const NoDataViewsPromptProvider: FC = ({ }) => { // Typescript types are widened to accept more than what is needed. Take only what is necessary // so the context remains clean. - const { canCreateNewDataView, dataViewsDocLink, openDataViewEditor } = services; + const { canCreateNewDataView, dataViewsDocLink, openDataViewEditor, onTryESQL, esqlDocLink } = + services; return ( {children} @@ -41,12 +50,22 @@ export const NoDataViewsPromptKibanaProvider: FC { + const { + share, + coreStart: { + application: { navigateToApp }, + }, + } = services; + const onTryESQL = useOnTryESQL({ locatorClient: share?.url.locators, navigateToApp }); + return ( {children} diff --git a/packages/shared-ux/prompt/no_data_views/impl/tsconfig.json b/packages/shared-ux/prompt/no_data_views/impl/tsconfig.json index 5da3eb228470f..673823e620474 100644 --- a/packages/shared-ux/prompt/no_data_views/impl/tsconfig.json +++ b/packages/shared-ux/prompt/no_data_views/impl/tsconfig.json @@ -21,6 +21,7 @@ "@kbn/test-jest-helpers", "@kbn/shared-ux-prompt-no-data-views-types", "@kbn/shared-ux-prompt-no-data-views-mocks", + "@kbn/deeplinks-analytics", ], "exclude": [ "target/**/*", diff --git a/packages/shared-ux/prompt/no_data_views/mocks/src/jest.ts b/packages/shared-ux/prompt/no_data_views/mocks/src/jest.ts index af27a7acb8aff..41af02dcf4c71 100644 --- a/packages/shared-ux/prompt/no_data_views/mocks/src/jest.ts +++ b/packages/shared-ux/prompt/no_data_views/mocks/src/jest.ts @@ -16,12 +16,14 @@ const defaultParams = {}; export const getNoDataViewsPromptServicesMock = ( params: Partial = defaultParams ) => { - const { canCreateNewDataView, dataViewsDocLink } = params || {}; + const { canCreateNewDataView, dataViewsDocLink, esqlDocLink } = params || {}; const services: NoDataViewsPromptServices = { canCreateNewDataView: canCreateNewDataView || true, dataViewsDocLink: dataViewsDocLink || 'some/link', + esqlDocLink: esqlDocLink || 'some/link', openDataViewEditor: jest.fn(), + onTryESQL: jest.fn(), }; return services; diff --git a/packages/shared-ux/prompt/no_data_views/mocks/src/storybook.ts b/packages/shared-ux/prompt/no_data_views/mocks/src/storybook.ts index 2dd5a22186eb4..4f0ddc0311300 100644 --- a/packages/shared-ux/prompt/no_data_views/mocks/src/storybook.ts +++ b/packages/shared-ux/prompt/no_data_views/mocks/src/storybook.ts @@ -15,8 +15,8 @@ import { type ServiceArguments = Pick< NoDataViewsPromptServices, - 'canCreateNewDataView' | 'dataViewsDocLink' ->; + 'canCreateNewDataView' | 'dataViewsDocLink' | 'esqlDocLink' +> & { canTryEsql: boolean }; export type Params = Record; @@ -36,6 +36,14 @@ export class StorybookMock extends AbstractStorybookMock< options: ['some/link', undefined], control: { type: 'radio' }, }, + esqlDocLink: { + options: ['some/link', undefined], + control: { type: 'radio' }, + }, + canTryEsql: { + control: 'boolean', + defaultValue: true, + }, }; dependencies = []; @@ -46,15 +54,22 @@ export class StorybookMock extends AbstractStorybookMock< } getServices(params: Params): NoDataViewsPromptServices { - const { canCreateNewDataView, dataViewsDocLink } = params; + const { canCreateNewDataView, dataViewsDocLink, canTryEsql, esqlDocLink } = params; + let onTryESQL; + + if (canTryEsql !== false) { + onTryESQL = action('onTryESQL'); + } return { canCreateNewDataView, dataViewsDocLink, + esqlDocLink, openDataViewEditor: (options) => { action('openDataViewEditor')(options); return () => {}; }, + onTryESQL, }; } } diff --git a/packages/shared-ux/prompt/no_data_views/types/index.d.ts b/packages/shared-ux/prompt/no_data_views/types/index.d.ts index eff6ad60e2aa4..6c2c409899d58 100644 --- a/packages/shared-ux/prompt/no_data_views/types/index.d.ts +++ b/packages/shared-ux/prompt/no_data_views/types/index.d.ts @@ -7,6 +7,7 @@ */ import { EuiEmptyPromptProps } from '@elastic/eui'; +import type { ILocatorClient } from '@kbn/share-plugin/common/url_service'; /** * TODO: `DataView` is a class exported by `src/plugins/data_views/public`. Since this service @@ -40,7 +41,15 @@ export interface NoDataViewsPromptServices { openDataViewEditor: (options: DataViewEditorOptions) => () => void; /** A link to information about Data Views in Kibana */ dataViewsDocLink: string; + /** Get a handler for trying ES|QL */ + onTryESQL: (() => void) | undefined; + /** A link to the documentation for ES|QL */ + esqlDocLink: string; } + +export type NavigateToAppFn = (appId: string, options?: { path?: string; state?: unknown }) => void; +export type LocatorClient = ILocatorClient; + /** * Kibana-specific service types. */ @@ -51,8 +60,14 @@ export interface NoDataViewsPromptKibanaDependencies { indexPatterns: { introduction: string; }; + query: { + queryESQL: string; + }; }; }; + application: { + navigateToApp: NavigateToAppFn; + }; }; dataViewEditor: { userPermissions: { @@ -60,23 +75,34 @@ export interface NoDataViewsPromptKibanaDependencies { }; openEditor: (options: DataViewEditorOptions) => () => void; }; + share?: { + url: { + locators: LocatorClient; + }; + }; } export interface NoDataViewsPromptComponentProps { /** True if the user has permission to create a data view, false otherwise. */ canCreateNewDataView: boolean; - /** Click handler for create button. **/ - onClickCreate?: () => void; /** Link to documentation on data views. */ dataViewsDocLink?: string; /** The background color of the prompt; defaults to `plain`. */ emptyPromptColor?: EuiEmptyPromptProps['color']; + /** Click handler for create button. **/ + onClickCreate?: () => void; + /** Handler for someone wanting to try ES|QL. */ + onTryESQL?: () => void; + /** Link to documentation on ES|QL. */ + esqlDocLink?: string; } // TODO: https://github.com/elastic/kibana/issues/127695 export interface NoDataViewsPromptProps { - /** Handler for successfully creating a new data view. */ - onDataViewCreated: (dataView: unknown) => void; /** if set to true allows creation of an ad-hoc data view from data view editor */ allowAdHocDataView?: boolean; + /** Handler for successfully creating a new data view. */ + onDataViewCreated: (dataView: unknown) => void; + /** Handler for when try ES|QL is clicked and user has been navigated to try ES|QL in discover. */ + onESQLNavigationComplete?: () => void; } diff --git a/packages/shared-ux/prompt/no_data_views/types/tsconfig.json b/packages/shared-ux/prompt/no_data_views/types/tsconfig.json index 362cc9e727b9f..b82ab394b8afc 100644 --- a/packages/shared-ux/prompt/no_data_views/types/tsconfig.json +++ b/packages/shared-ux/prompt/no_data_views/types/tsconfig.json @@ -9,5 +9,8 @@ ], "exclude": [ "target/**/*", + ], + "kbn_references": [ + "@kbn/share-plugin", ] } 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 8394237976f9a..c9c460a2c7a39 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 @@ -10,6 +10,7 @@ import React from 'react'; import { withSuspense } from '@kbn/shared-ux-utility'; import { pluginServices } from '../../services/plugin_services'; +import { DASHBOARD_APP_ID } from '../../dashboard_constants'; export const DashboardAppNoDataPage = ({ onDataViewCreated, @@ -21,9 +22,10 @@ export const DashboardAppNoDataPage = ({ data: { dataViews }, dataViewEditor, http: { basePath, get }, - documentationLinks: { indexPatternsDocLink, kibanaGuideDocLink }, + documentationLinks: { indexPatternsDocLink, kibanaGuideDocLink, esqlDocLink }, customBranding, noDataPage, + share, } = pluginServices.getServices(); const analyticsServices = { @@ -32,6 +34,7 @@ export const DashboardAppNoDataPage = ({ links: { kibana: { guide: kibanaGuideDocLink }, indexPatterns: { introduction: indexPatternsDocLink }, + query: { queryESQL: esqlDocLink }, }, }, application, @@ -43,6 +46,7 @@ export const DashboardAppNoDataPage = ({ dataViews, dataViewEditor, noDataPage, + share: share.url ? { url: share.url } : undefined, }; const importPromise = import('@kbn/shared-ux-page-analytics-no-data'); @@ -71,8 +75,29 @@ export const DashboardAppNoDataPage = ({ export const isDashboardAppInNoDataState = async () => { const { data: { dataViews }, + embeddable, + dashboardContentManagement, + dashboardBackup, } = pluginServices.getServices(); const hasUserDataView = await dataViews.hasData.hasUserDataView().catch(() => false); - return !hasUserDataView; + + if (hasUserDataView) return false; + + // consider has data if there is an incoming embeddable + const hasIncomingEmbeddable = embeddable + .getStateTransfer() + .getIncomingEmbeddablePackage(DASHBOARD_APP_ID, false); + if (hasIncomingEmbeddable) return false; + + // consider has data if there is unsaved dashboard with edits + if (dashboardBackup.dashboardHasUnsavedEdits()) return false; + + // consider has data if there is at least one dashboard + const { total } = await dashboardContentManagement.findDashboards + .search({ search: '', size: 1 }) + .catch(() => ({ total: 0 })); + if (total > 0) return false; + + return true; }; diff --git a/src/plugins/dashboard/public/dashboard_container/embeddable/create/create_dashboard.test.ts b/src/plugins/dashboard/public/dashboard_container/embeddable/create/create_dashboard.test.ts index fe21437028e73..5ae88b7c3bcd3 100644 --- a/src/plugins/dashboard/public/dashboard_container/embeddable/create/create_dashboard.test.ts +++ b/src/plugins/dashboard/public/dashboard_container/embeddable/create/create_dashboard.test.ts @@ -30,13 +30,11 @@ import { pluginServices } from '../../../services/plugin_services'; import { DashboardCreationOptions } from '../dashboard_container_factory'; import { DEFAULT_DASHBOARD_INPUT } from '../../../dashboard_constants'; -test('throws error when no data views are available', async () => { +test("doesn't throw error when no data views are available", async () => { pluginServices.getServices().data.dataViews.defaultDataViewExists = jest .fn() .mockReturnValue(false); - await expect(async () => { - await createDashboard(); - }).rejects.toThrow('Dashboard requires at least one data view before it can be initialized.'); + expect(await createDashboard()).toBeDefined(); // reset get default data view pluginServices.getServices().data.dataViews.defaultDataViewExists = jest diff --git a/src/plugins/dashboard/public/dashboard_container/embeddable/create/create_dashboard.ts b/src/plugins/dashboard/public/dashboard_container/embeddable/create/create_dashboard.ts index 9621b8f01476c..93b849a122ce0 100644 --- a/src/plugins/dashboard/public/dashboard_container/embeddable/create/create_dashboard.ts +++ b/src/plugins/dashboard/public/dashboard_container/embeddable/create/create_dashboard.ts @@ -82,16 +82,12 @@ export const createDashboard = async ( const defaultDataViewExistsPromise = dataViews.defaultDataViewExists(); const dashboardSavedObjectPromise = loadDashboardState({ id: savedObjectId }); - const [reduxEmbeddablePackage, savedObjectResult, defaultDataView] = await Promise.all([ + const [reduxEmbeddablePackage, savedObjectResult] = await Promise.all([ reduxEmbeddablePackagePromise, dashboardSavedObjectPromise, - defaultDataViewExistsPromise, + defaultDataViewExistsPromise /* the result is not used, but the side effect of setting the default data view is needed. */, ]); - if (!defaultDataView) { - throw new Error('Dashboard requires at least one data view before it can be initialized.'); - } - // -------------------------------------------------------------------------------------- // Initialize Dashboard integrations // -------------------------------------------------------------------------------------- diff --git a/src/plugins/dashboard/public/services/documentation_links/documentation_links.stub.ts b/src/plugins/dashboard/public/services/documentation_links/documentation_links.stub.ts index dfdd74bfe4184..3c6255e423419 100644 --- a/src/plugins/dashboard/public/services/documentation_links/documentation_links.stub.ts +++ b/src/plugins/dashboard/public/services/documentation_links/documentation_links.stub.ts @@ -19,5 +19,6 @@ export const documentationLinksServiceFactory: DocumentationLinksServiceFactory indexPatternsDocLink: corePluginMock.docLinks.links.indexPatterns.introduction, kibanaGuideDocLink: corePluginMock.docLinks.links.kibana.guide, dashboardDocLink: corePluginMock.docLinks.links.dashboard.guide, + esqlDocLink: corePluginMock.docLinks.links.query.queryESQL, }; }; diff --git a/src/plugins/dashboard/public/services/documentation_links/documentation_links_service.ts b/src/plugins/dashboard/public/services/documentation_links/documentation_links_service.ts index eb65f639d57fe..0c9e86f78c758 100644 --- a/src/plugins/dashboard/public/services/documentation_links/documentation_links_service.ts +++ b/src/plugins/dashboard/public/services/documentation_links/documentation_links_service.ts @@ -24,6 +24,7 @@ export const documentationLinksServiceFactory: DocumentationLinksServiceFactory kibana, indexPatterns: { introduction }, dashboard, + query: { queryESQL }, }, }, } = coreStart; @@ -32,5 +33,6 @@ export const documentationLinksServiceFactory: DocumentationLinksServiceFactory indexPatternsDocLink: introduction, kibanaGuideDocLink: kibana.guide, dashboardDocLink: dashboard.guide, + esqlDocLink: queryESQL, }; }; diff --git a/src/plugins/dashboard/public/services/documentation_links/types.ts b/src/plugins/dashboard/public/services/documentation_links/types.ts index d47fbee6ed772..80e544adc63ed 100644 --- a/src/plugins/dashboard/public/services/documentation_links/types.ts +++ b/src/plugins/dashboard/public/services/documentation_links/types.ts @@ -12,4 +12,5 @@ export interface DashboardDocumentationLinksService { indexPatternsDocLink: CoreStart['docLinks']['links']['indexPatterns']['introduction']; kibanaGuideDocLink: CoreStart['docLinks']['links']['kibana']['guide']; dashboardDocLink: CoreStart['docLinks']['links']['dashboard']['guide']; + esqlDocLink: CoreStart['docLinks']['links']['query']['queryESQL']; } diff --git a/src/plugins/discover/common/locator.test.ts b/src/plugins/discover/common/app_locator.test.ts similarity index 99% rename from src/plugins/discover/common/locator.test.ts rename to src/plugins/discover/common/app_locator.test.ts index 93da54ad365e9..fc8d33010aa91 100644 --- a/src/plugins/discover/common/locator.test.ts +++ b/src/plugins/discover/common/app_locator.test.ts @@ -13,7 +13,7 @@ import { } from '@kbn/kibana-utils-plugin/public'; import { mockStorage } from '@kbn/kibana-utils-plugin/public/storage/hashed_item_store/mock'; import { FilterStateStore } from '@kbn/es-query'; -import { DiscoverAppLocatorDefinition } from './locator'; +import { DiscoverAppLocatorDefinition } from './app_locator'; import { SerializableRecord } from '@kbn/utility-types'; import { addProfile } from './customizations'; diff --git a/src/plugins/discover/common/locator.ts b/src/plugins/discover/common/app_locator.ts similarity index 100% rename from src/plugins/discover/common/locator.ts rename to src/plugins/discover/common/app_locator.ts diff --git a/src/plugins/discover/common/esql_locator.ts b/src/plugins/discover/common/esql_locator.ts new file mode 100644 index 0000000000000..68a09b67584bc --- /dev/null +++ b/src/plugins/discover/common/esql_locator.ts @@ -0,0 +1,41 @@ +/* + * 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 { DISCOVER_ESQL_LOCATOR } from '@kbn/deeplinks-analytics'; +import { LocatorDefinition, LocatorPublic } from '@kbn/share-plugin/common'; +import { SerializableRecord } from '@kbn/utility-types'; +import { getIndexForESQLQuery } from '@kbn/esql-utils'; +import type { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public'; + +export type DiscoverESQLLocatorParams = SerializableRecord; + +export interface DiscoverESQLLocatorDependencies { + discoverAppLocator: LocatorPublic; + getIndices: DataViewsPublicPluginStart['getIndices']; +} + +export type DiscoverESQLLocator = LocatorPublic; + +export class DiscoverESQLLocatorDefinition implements LocatorDefinition { + public readonly id = DISCOVER_ESQL_LOCATOR; + + constructor(protected readonly deps: DiscoverESQLLocatorDependencies) {} + + public readonly getLocation = async () => { + const { discoverAppLocator, getIndices } = this.deps; + + const indexName = await getIndexForESQLQuery({ dataViews: { getIndices } }); + const esql = `from ${indexName ?? '*'} | limit 10`; + + const params = { + query: { esql }, + }; + + return await discoverAppLocator.getLocation(params); + }; +} diff --git a/src/plugins/discover/common/index.ts b/src/plugins/discover/common/index.ts index 67ef77fdb9dee..fd74e77a9ab60 100644 --- a/src/plugins/discover/common/index.ts +++ b/src/plugins/discover/common/index.ts @@ -9,5 +9,12 @@ export const PLUGIN_ID = 'discover'; export const APP_ICON = 'discoverApp'; -export { DISCOVER_APP_LOCATOR, DiscoverAppLocatorDefinition } from './locator'; -export type { DiscoverAppLocator, DiscoverAppLocatorParams } from './locator'; +export { DISCOVER_APP_LOCATOR, DiscoverAppLocatorDefinition } from './app_locator'; +export type { + DiscoverAppLocator, + DiscoverAppLocatorParams, + MainHistoryLocationState, +} from './app_locator'; + +export { DiscoverESQLLocatorDefinition } from './esql_locator'; +export type { DiscoverESQLLocator, DiscoverESQLLocatorParams } from './esql_locator'; 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 4b1eebd153ec4..7fd8ebb22b3cd 100644 --- a/src/plugins/discover/public/application/main/discover_main_route.tsx +++ b/src/plugins/discover/public/application/main/discover_main_route.tsx @@ -19,10 +19,10 @@ 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 { isOfEsqlQueryType } from '@kbn/es-query'; import { useUrl } from './hooks/use_url'; -import { useSingleton } from './hooks/use_singleton'; -import { MainHistoryLocationState } from '../../../common/locator'; -import { DiscoverStateContainer, getDiscoverStateContainer } from './services/discover_state'; +import { useDiscoverStateContainer } from './hooks/use_discover_state_container'; +import { MainHistoryLocationState } from '../../../common'; import { DiscoverMainApp } from './discover_main_app'; import { setBreadcrumbs } from '../../utils/breadcrumbs'; import { LoadingIndicator } from '../../components/common/loading_indicator'; @@ -66,16 +66,16 @@ export function DiscoverMainRoute({ toastNotifications, http: { basePath }, dataViewEditor, + share, } = services; const { id: savedSearchId } = useParams(); - const stateContainer = useSingleton(() => - getDiscoverStateContainer({ - history, - services, - customizationContext, - stateStorageContainer, - }) - ); + const [stateContainer, { reset: resetStateContainer }] = useDiscoverStateContainer({ + history, + services, + customizationContext, + stateStorageContainer, + }); + const { customizationService, isInitialized: isCustomizationServiceInitialized } = useDiscoverCustomizationService({ customizationCallbacks, @@ -112,6 +112,11 @@ export function DiscoverMainRoute({ if (savedSearchId) { return true; // bypass NoData screen } + + if (isOfEsqlQueryType(stateContainer.appState.getState().query)) { + return true; + } + const hasUserDataViewValue = await data.dataViews.hasData .hasUserDataView() .catch(() => false); @@ -140,7 +145,7 @@ export function DiscoverMainRoute({ setError(e); return false; } - }, [data.dataViews, savedSearchId]); + }, [data.dataViews, savedSearchId, stateContainer.appState]); const loadSavedSearch = useCallback( async (nextDataView?: DataView) => { @@ -252,6 +257,10 @@ export function DiscoverMainRoute({ [loadSavedSearch] ); + const onESQLNavigationComplete = useCallback(async () => { + resetStateContainer(); + }, [resetStateContainer]); + const noDataDependencies = useMemo( () => ({ coreStart: core, @@ -266,10 +275,11 @@ export function DiscoverMainRoute({ hasUserDataView: () => Promise.resolve(hasUserDataView), }, }, + share, dataViewEditor, noDataPage: services.noDataPage, }), - [core, data.dataViews, dataViewEditor, hasESData, hasUserDataView, services.noDataPage] + [core, data.dataViews, dataViewEditor, hasESData, hasUserDataView, services.noDataPage, share] ); const loadingIndicator = useMemo( @@ -297,7 +307,10 @@ export function DiscoverMainRoute({ return ( - + ); } @@ -312,6 +325,7 @@ export function DiscoverMainRoute({ loadingIndicator, noDataDependencies, onDataViewCreated, + onESQLNavigationComplete, showNoDataPage, stateContainer, ]); diff --git a/src/plugins/discover/public/application/main/hooks/use_discover_state_container.ts b/src/plugins/discover/public/application/main/hooks/use_discover_state_container.ts new file mode 100644 index 0000000000000..5ae3d0d6085f7 --- /dev/null +++ b/src/plugins/discover/public/application/main/hooks/use_discover_state_container.ts @@ -0,0 +1,34 @@ +/* + * 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 { useState } from 'react'; +import { + DiscoverStateContainer, + getDiscoverStateContainer, + DiscoverStateContainerParams, +} from '../services/discover_state'; + +/** + * Creates a state container using the initial params and allows to reset it. + * The container is recreated only when reset is called. This is useful to reset Discover to its initial state. + * @param params + */ +export const useDiscoverStateContainer = (params: DiscoverStateContainerParams) => { + const [stateContainer, setStateContainer] = useState(() => + getDiscoverStateContainer(params) + ); + + return [ + stateContainer, + { + reset: () => { + setStateContainer(getDiscoverStateContainer(params)); + }, + }, + ] as const; +}; diff --git a/src/plugins/discover/public/application/main/hooks/use_singleton.ts b/src/plugins/discover/public/application/main/hooks/use_singleton.ts deleted file mode 100644 index bac195896caa3..0000000000000 --- a/src/plugins/discover/public/application/main/hooks/use_singleton.ts +++ /dev/null @@ -1,26 +0,0 @@ -/* - * 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 { useRef } from 'react'; - -/** - * Allows lazy initialization of a singleton - * Context: https://reactjs.org/docs/hooks-faq.html#how-to-create-expensive-objects-lazily - * Why not using useMemo: We're using the useMemo here also kind of as a guarantee to - * only instantiate that subject once. Unfortunately useMemo explicitly does not give - * those guarantees: - * https://reactjs.org/docs/hooks-reference.html#usememo - */ -export function useSingleton(initialize: () => T): T { - const ref = useRef(null); - - if (ref.current === null) { - ref.current = initialize(); - } - - return ref.current; -} diff --git a/src/plugins/discover/public/application/main/services/discover_state.ts b/src/plugins/discover/public/application/main/services/discover_state.ts index 8994afb8a5f96..af3675156a93d 100644 --- a/src/plugins/discover/public/application/main/services/discover_state.ts +++ b/src/plugins/discover/public/application/main/services/discover_state.ts @@ -54,7 +54,8 @@ import { getDiscoverGlobalStateContainer, DiscoverGlobalStateContainer, } from './discover_global_state_container'; -interface DiscoverStateContainerParams { + +export interface DiscoverStateContainerParams { /** * Browser history */ diff --git a/src/plugins/discover/public/application/view_alert/view_alert_route.tsx b/src/plugins/discover/public/application/view_alert/view_alert_route.tsx index 5d8383be5e935..ee5e9f85a090d 100644 --- a/src/plugins/discover/public/application/view_alert/view_alert_route.tsx +++ b/src/plugins/discover/public/application/view_alert/view_alert_route.tsx @@ -8,7 +8,7 @@ import { useEffect, useMemo } from 'react'; import { useHistory, useLocation, useParams } from 'react-router-dom'; -import { DiscoverAppLocatorParams } from '../../../common/locator'; +import { DiscoverAppLocatorParams } from '../../../common/app_locator'; import { useDiscoverServices } from '../../hooks/use_discover_services'; import { displayPossibleDocsDiffInfoAlert } from '../main/hooks/use_alert_results_toast'; import { getAlertUtils, QueryParams } from './view_alert_utils'; diff --git a/src/plugins/discover/public/application/view_alert/view_alert_utils.tsx b/src/plugins/discover/public/application/view_alert/view_alert_utils.tsx index 2a8337aa45f39..fc732ad08c124 100644 --- a/src/plugins/discover/public/application/view_alert/view_alert_utils.tsx +++ b/src/plugins/discover/public/application/view_alert/view_alert_utils.tsx @@ -19,7 +19,7 @@ import type { DataPublicPluginStart } from '@kbn/data-plugin/public'; import { MarkdownSimple } from '@kbn/kibana-react-plugin/public'; import { toMountPoint } from '@kbn/react-kibana-mount'; import { Filter } from '@kbn/es-query'; -import { DiscoverAppLocatorParams } from '../../../common/locator'; +import { DiscoverAppLocatorParams } from '../../../common/app_locator'; export interface SearchThresholdAlertParams extends RuleTypeParams { searchConfiguration: SerializedSearchSourceFields; diff --git a/src/plugins/discover/public/plugin.tsx b/src/plugins/discover/public/plugin.tsx index 5997e9dfced85..f43477c5da100 100644 --- a/src/plugins/discover/public/plugin.tsx +++ b/src/plugins/discover/public/plugin.tsx @@ -44,7 +44,7 @@ import type { UnifiedDocViewerStart } from '@kbn/unified-doc-viewer-plugin/publi import { setStateToKbnUrl } from '@kbn/kibana-utils-plugin/public'; import type { LensPublicStart } from '@kbn/lens-plugin/public'; import { TRUNCATE_MAX_HEIGHT, ENABLE_ESQL } from '@kbn/discover-utils'; -import { NoDataPagePluginStart } from '@kbn/no-data-page-plugin/public'; +import type { NoDataPagePluginStart } from '@kbn/no-data-page-plugin/public'; import type { ServerlessPluginStart } from '@kbn/serverless/public'; import { PLUGIN_ID } from '../common'; import { @@ -68,7 +68,11 @@ import { DiscoverSingleDocLocator, DiscoverSingleDocLocatorDefinition, } from './application/doc/locator'; -import { DiscoverAppLocator, DiscoverAppLocatorDefinition } from '../common'; +import { + DiscoverAppLocator, + DiscoverAppLocatorDefinition, + DiscoverESQLLocatorDefinition, +} from '../common'; import type { RegisterCustomizationProfile } from './customizations'; import { createRegisterCustomizationProfile, @@ -159,6 +163,7 @@ export interface DiscoverStart { * @internal */ export interface DiscoverSetupPlugins { + dataViews: DataViewsServicePublic; share?: SharePluginSetup; uiActions: UiActionsSetup; embeddable: EmbeddableSetup; @@ -400,6 +405,17 @@ export class DiscoverPlugin return this.getDiscoverServices(core, plugins); }; + const isEsqlEnabled = core.uiSettings.get(ENABLE_ESQL); + + if (plugins.share && this.locator && isEsqlEnabled) { + plugins.share?.url.locators.create( + new DiscoverESQLLocatorDefinition({ + discoverAppLocator: this.locator, + getIndices: plugins.dataViews.getIndices, + }) + ); + } + return { locator: this.locator, DiscoverContainer: (props: DiscoverContainerProps) => { diff --git a/src/plugins/discover/server/plugin.ts b/src/plugins/discover/server/plugin.ts index dc7761b2ba20d..19800cdc0dbb2 100644 --- a/src/plugins/discover/server/plugin.ts +++ b/src/plugins/discover/server/plugin.ts @@ -14,7 +14,7 @@ import { setStateToKbnUrl } from '@kbn/kibana-utils-plugin/common'; import type { SharePluginSetup } from '@kbn/share-plugin/server'; import { PluginInitializerContext } from '@kbn/core/server'; import type { DiscoverServerPluginStart, DiscoverServerPluginStartDeps } from '.'; -import { DiscoverAppLocatorDefinition } from '../common/locator'; +import { DiscoverAppLocatorDefinition } from '../common'; import { capabilitiesProvider } from './capabilities_provider'; import { createSearchEmbeddableFactory } from './embeddable'; import { initializeLocatorServices } from './locator'; diff --git a/src/plugins/discover/tsconfig.json b/src/plugins/discover/tsconfig.json index bbb9d7495b2b6..24417d17f70d4 100644 --- a/src/plugins/discover/tsconfig.json +++ b/src/plugins/discover/tsconfig.json @@ -82,7 +82,8 @@ "@kbn/serverless", "@kbn/deeplinks-observability", "@kbn/esql-utils", - "@kbn/managed-content-badge" + "@kbn/managed-content-badge", + "@kbn/deeplinks-analytics" ], "exclude": ["target/**/*"] } 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 b2741eaf8d3a1..61aa40604fa95 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 @@ -727,6 +727,15 @@ exports[`Overview renders correctly when there is no user data view 1`] = ` "hasUserDataView": [MockFunction], } } + share={ + Object { + "url": Object { + "locators": Object { + "get": [Function], + }, + }, + } + } > @@ -1028,6 +1037,15 @@ exports[`Overview renders correctly when there is no user data view 1`] = ` "hasUserDataView": [MockFunction], } } + share={ + Object { + "url": Object { + "locators": Object { + "get": [Function], + }, + }, + } + } > diff --git a/src/plugins/kibana_overview/public/components/overview/overview.tsx b/src/plugins/kibana_overview/public/components/overview/overview.tsx index bdc8c9e0a755e..2b3f532a0fb52 100644 --- a/src/plugins/kibana_overview/public/components/overview/overview.tsx +++ b/src/plugins/kibana_overview/public/components/overview/overview.tsx @@ -197,6 +197,7 @@ export const Overview: FC = ({ newsFetchResult, solutions, features }) => }, }, dataViewEditor, + share, }; const importPromise = import('@kbn/shared-ux-page-analytics-no-data'); diff --git a/src/plugins/visualizations/public/visualize_app/app.tsx b/src/plugins/visualizations/public/visualize_app/app.tsx index 8ff102e101479..bba97b60acec8 100644 --- a/src/plugins/visualizations/public/visualize_app/app.tsx +++ b/src/plugins/visualizations/public/visualize_app/app.tsx @@ -18,6 +18,7 @@ import type { NoDataPagePluginStart } from '@kbn/no-data-page-plugin/public'; import { useKibana } from '@kbn/kibana-react-plugin/public'; import type { DataViewsContract } from '@kbn/data-views-plugin/public'; import { withSuspense } from '@kbn/shared-ux-utility'; +import { SharePluginStart } from '@kbn/share-plugin/public'; import { VisualizeServices } from './types'; import { VisualizeEditor, @@ -37,6 +38,7 @@ interface NoDataComponentProps { dataViewEditor: DataViewEditorStart; onDataViewCreated: (dataView: unknown) => void; noDataPage?: NoDataPagePluginStart; + share?: SharePluginStart; } const NoDataComponent = ({ @@ -45,12 +47,14 @@ const NoDataComponent = ({ dataViewEditor, onDataViewCreated, noDataPage, + share, }: NoDataComponentProps) => { const analyticsServices = { coreStart: core, dataViews, dataViewEditor, noDataPage, + share, }; const importPromise = import('@kbn/shared-ux-page-analytics-no-data'); @@ -84,6 +88,7 @@ export const VisualizeApp = ({ onAppLeave }: VisualizeAppProps) => { kbnUrlStateStorage, dataViewEditor, noDataPage, + share, }, } = useKibana(); const { pathname } = useLocation(); @@ -145,6 +150,7 @@ export const VisualizeApp = ({ onAppLeave }: VisualizeAppProps) => { dataViews={dataViews} onDataViewCreated={onDataViewCreated} noDataPage={noDataPage} + share={share} /> ); } diff --git a/test/functional/apps/discover/group1/_no_data.ts b/test/functional/apps/discover/group1/_no_data.ts index 8efc1b1390ef6..1a80955068bd0 100644 --- a/test/functional/apps/discover/group1/_no_data.ts +++ b/test/functional/apps/discover/group1/_no_data.ts @@ -5,6 +5,7 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ +import expect from '@kbn/expect'; import { FtrProviderContext } from '../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { @@ -14,6 +15,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const kibanaServer = getService('kibanaServer'); const testSubjects = getService('testSubjects'); + const dataGrid = getService('dataGrid'); const PageObjects = getPageObjects(['common', 'discover', 'header', 'timePicker']); const createDataView = async (dataViewName: string) => { @@ -53,8 +55,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await kibanaServer.savedObjects.clean({ types: ['search', 'index-pattern'] }); await PageObjects.common.navigateToApp('discover'); - const button = await testSubjects.find('createDataViewButton'); - button.click(); + await testSubjects.click('createDataViewButton'); await retry.waitForWithTimeout('data view editor form to be visible', 15000, async () => { return await (await find.byClassName('indexPatternEditor__form')).isDisplayed(); }); @@ -73,5 +74,23 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { } ); }); + + it('skips to Discover to try ES|QL', async () => { + await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/logstash_functional'); + await kibanaServer.savedObjects.clean({ types: ['search', 'index-pattern'] }); + await kibanaServer.uiSettings.update({ + 'timepicker:timeDefaults': '{ "from": "2015-09-18T19:37:13.000Z", "to": "now"}', + }); + await PageObjects.common.navigateToApp('discover'); + + await testSubjects.click('tryESQLLink'); + + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.discover.waitUntilSearchingHasFinished(); + await testSubjects.existOrFail('TextBasedLangEditor'); + await testSubjects.existOrFail('unifiedHistogramChart'); + const rows = await dataGrid.getDocTableRows(); + expect(rows.length).to.be.above(0); + }); }); } diff --git a/x-pack/plugins/lens/public/app_plugin/mounter.tsx b/x-pack/plugins/lens/public/app_plugin/mounter.tsx index 3893b1b3f0e44..483e57e6e9f56 100644 --- a/x-pack/plugins/lens/public/app_plugin/mounter.tsx +++ b/x-pack/plugins/lens/public/app_plugin/mounter.tsx @@ -185,7 +185,7 @@ export async function mountApp( locator ); - const { stateTransfer, data, savedObjectStore } = lensServices; + const { stateTransfer, data, savedObjectStore, share } = lensServices; const embeddableEditorIncomingState = stateTransfer?.getIncomingEditorState(APP_ID); @@ -343,6 +343,7 @@ export async function mountApp( coreStart, dataViews: data.dataViews, dataViewEditor: startDependencies.dataViewEditor, + share, }; const importPromise = import('@kbn/shared-ux-page-analytics-no-data'); const AnalyticsNoDataPageKibanaProvider = withSuspense( diff --git a/x-pack/plugins/lens/public/trigger_actions/open_lens_config/create_action_helpers.ts b/x-pack/plugins/lens/public/trigger_actions/open_lens_config/create_action_helpers.ts index 1eddc499f4170..7fb732cd72ccf 100644 --- a/x-pack/plugins/lens/public/trigger_actions/open_lens_config/create_action_helpers.ts +++ b/x-pack/plugins/lens/public/trigger_actions/open_lens_config/create_action_helpers.ts @@ -12,6 +12,7 @@ import type { IEmbeddable, } from '@kbn/embeddable-plugin/public'; import { IncompatibleActionError } from '@kbn/ui-actions-plugin/public'; +import { getESQLAdHocDataview, getIndexForESQLQuery } from '@kbn/esql-utils'; import type { Datasource, Visualization } from '../../types'; import type { LensPluginStartDependencies } from '../../plugin'; import { fetchDataFromAggregateQuery } from '../../datasources/text_based/fetch_data_from_aggregate_query'; @@ -52,13 +53,23 @@ export async function executeCreateAction({ const defaultDataView = await deps.dataViews.getDefaultDataView({ displayErrors: false, }); - if (!isCompatibleAction || !defaultDataView) { + + const getFallbackDataView = async () => { + const indexName = await getIndexForESQLQuery({ dataViews: deps.dataViews }); + if (!indexName) return null; + const dataView = await getESQLAdHocDataview(indexName, deps.dataViews); + return dataView; + }; + + const dataView = defaultDataView ?? (await getFallbackDataView()); + + if (!isCompatibleAction || !dataView) { throw new IncompatibleActionError(); } const visualizationMap = getVisualizationMap(); const datasourceMap = getDatasourceMap(); + const defaultIndex = dataView.getIndexPattern(); - const defaultIndex = defaultDataView.getIndexPattern(); const defaultEsqlQuery = { esql: `from ${defaultIndex} | limit 10`, }; @@ -73,13 +84,13 @@ export async function executeCreateAction({ const table = await fetchDataFromAggregateQuery( performantQuery, - defaultDataView, + dataView, deps.data, deps.expressions ); const context = { - dataViewSpec: defaultDataView.toSpec(), + dataViewSpec: dataView.toSpec(), fieldName: '', textBasedColumns: table?.columns, query: defaultEsqlQuery, @@ -87,7 +98,7 @@ export async function executeCreateAction({ // get the initial attributes from the suggestions api const allSuggestions = - suggestionsApi({ context, dataView: defaultDataView, datasourceMap, visualizationMap }) ?? []; + suggestionsApi({ context, dataView, datasourceMap, visualizationMap }) ?? []; // Lens might not return suggestions for some cases, i.e. in case of errors if (!allSuggestions.length) return undefined; @@ -96,7 +107,7 @@ export async function executeCreateAction({ filters: [], query: defaultEsqlQuery, suggestion: firstSuggestion, - dataView: defaultDataView, + dataView, }); const input = {