diff --git a/src/plugins/discover/public/index.ts b/src/plugins/discover/public/index.ts index 5af7c2bf1142a..ca3c58a4d2899 100644 --- a/src/plugins/discover/public/index.ts +++ b/src/plugins/discover/public/index.ts @@ -15,6 +15,7 @@ export function plugin(initializerContext: PluginInitializerContext) { } export type { ISearchEmbeddable, SearchInput } from './embeddable'; +export type { DiscoverAppState } from './application/main/services/discover_app_state_container'; export type { DiscoverStateContainer } from './application/main/services/discover_state'; export type { DiscoverContainerProps } from './components/discover_container'; export type { diff --git a/x-pack/plugins/log_explorer/common/constants.ts b/x-pack/plugins/log_explorer/common/constants.ts index 37f56942f332a..fc1c572ebae26 100644 --- a/x-pack/plugins/log_explorer/common/constants.ts +++ b/x-pack/plugins/log_explorer/common/constants.ts @@ -8,4 +8,5 @@ export const LOG_EXPLORER_PROFILE_ID = 'log-explorer'; // Fields constants +export const TIMESTAMP_FIELD = '@timestamp'; export const MESSAGE_FIELD = 'message'; diff --git a/x-pack/plugins/log_explorer/common/datasets/models/dataset.ts b/x-pack/plugins/log_explorer/common/datasets/models/dataset.ts index 68119fb6015b1..974a9fd4ca37f 100644 --- a/x-pack/plugins/log_explorer/common/datasets/models/dataset.ts +++ b/x-pack/plugins/log_explorer/common/datasets/models/dataset.ts @@ -8,6 +8,7 @@ import { IconType } from '@elastic/eui'; import { DataViewSpec } from '@kbn/data-views-plugin/common'; import { IndexPattern } from '@kbn/io-ts-utils'; +import { TIMESTAMP_FIELD } from '../../constants'; import { DatasetId, DatasetType, IntegrationType } from '../types'; type IntegrationBase = Pick; @@ -53,6 +54,7 @@ export class Dataset { return { id: this.id, name: this.getFullTitle(), + timeFieldName: TIMESTAMP_FIELD, title: this.name as string, }; } diff --git a/x-pack/plugins/log_explorer/public/components/log_explorer/log_explorer.tsx b/x-pack/plugins/log_explorer/public/components/log_explorer/log_explorer.tsx index fa76cadeb727b..6a945afa19ab2 100644 --- a/x-pack/plugins/log_explorer/public/components/log_explorer/log_explorer.tsx +++ b/x-pack/plugins/log_explorer/public/components/log_explorer/log_explorer.tsx @@ -5,22 +5,30 @@ * 2.0. */ +import React, { useMemo } from 'react'; import { ScopedHistory } from '@kbn/core-application-browser'; import { DataPublicPluginStart } from '@kbn/data-plugin/public'; -import { DiscoverStart } from '@kbn/discover-plugin/public'; -import React from 'react'; +import { DiscoverAppState, DiscoverStart } from '@kbn/discover-plugin/public'; +import type { BehaviorSubject } from 'rxjs'; import { createLogExplorerProfileCustomizations, CreateLogExplorerProfileCustomizationsDeps, } from '../../customizations/log_explorer_profile'; import { createPropertyGetProxy } from '../../utils/proxies'; +import { LogExplorerProfileContext } from '../../state_machines/log_explorer_profile'; export interface CreateLogExplorerArgs extends CreateLogExplorerProfileCustomizationsDeps { discover: DiscoverStart; } +export interface LogExplorerStateContainer { + appState?: DiscoverAppState; + logExplorerState?: Partial; +} + export interface LogExplorerProps { scopedHistory: ScopedHistory; + state$?: BehaviorSubject; } export const createLogExplorer = ({ @@ -28,13 +36,16 @@ export const createLogExplorer = ({ data, discover: { DiscoverContainer }, }: CreateLogExplorerArgs) => { - const logExplorerCustomizations = [createLogExplorerProfileCustomizations({ core, data })]; - const overrideServices = { data: createDataServiceProxy(data), }; - return ({ scopedHistory }: LogExplorerProps) => { + return ({ scopedHistory, state$ }: LogExplorerProps) => { + const logExplorerCustomizations = useMemo( + () => [createLogExplorerProfileCustomizations({ core, data, state$ })], + [state$] + ); + return ( import('./custom_dataset_selector')); const LazyCustomDatasetFilters = dynamic(() => import('./custom_dataset_filters')); @@ -17,10 +20,11 @@ const LazyCustomDatasetFilters = dynamic(() => import('./custom_dataset_filters' export interface CreateLogExplorerProfileCustomizationsDeps { core: CoreStart; data: DataPublicPluginStart; + state$?: BehaviorSubject; } export const createLogExplorerProfileCustomizations = - ({ core, data }: CreateLogExplorerProfileCustomizationsDeps): CustomizationCallback => + ({ core, data, state$ }: CreateLogExplorerProfileCustomizationsDeps): CustomizationCallback => async ({ customizations, stateContainer }) => { // Lazy load dependencies const datasetServiceModuleLoadable = import('../services/datasets'); @@ -38,13 +42,26 @@ export const createLogExplorerProfileCustomizations = toasts: core.notifications.toasts, }); - // /** * Wait for the machine to be fully initialized to set the restored selection * create the DataView and set it in the stateContainer from Discover */ await waitForState(logExplorerProfileStateService, 'initialized'); + /** + * Subscribe the state$ BehaviorSubject when the consumer app wants to react to state changes. + * It emits a combined state of: + * - log explorer state machine context + * - appState from the discover stateContainer + */ + let stateSubscription: Subscription; + if (state$) { + stateSubscription = createStateUpdater({ + logExplorerProfileStateService, + stateContainer, + }).subscribe(state$); + } + /** * Replace the DataViewPicker with a custom `DatasetSelector` to pick integrations streams * Prepend the search bar with custom filter control groups depending on the selected dataset @@ -76,4 +93,25 @@ export const createLogExplorerProfileCustomizations = saveItem: { disabled: true }, }, }); + + return () => { + if (stateSubscription) { + stateSubscription.unsubscribe(); + } + }; }; + +const createStateUpdater = ({ + logExplorerProfileStateService, + stateContainer, +}: { + logExplorerProfileStateService: LogExplorerProfileStateService; + stateContainer: DiscoverStateContainer; +}) => { + return combineLatest([from(logExplorerProfileStateService), stateContainer.appState.state$]).pipe( + map(([logExplorerState, appState]) => ({ + logExplorerState: logExplorerState.context, + appState, + })) + ); +}; diff --git a/x-pack/plugins/log_explorer/public/index.ts b/x-pack/plugins/log_explorer/public/index.ts index c145f6fd88864..00750926517e6 100644 --- a/x-pack/plugins/log_explorer/public/index.ts +++ b/x-pack/plugins/log_explorer/public/index.ts @@ -9,6 +9,7 @@ import type { PluginInitializerContext } from '@kbn/core/public'; import type { LogExplorerConfig } from '../common/plugin_config'; import { LogExplorerPlugin } from './plugin'; export type { LogExplorerPluginSetup, LogExplorerPluginStart } from './types'; +export type { LogExplorerStateContainer } from './components/log_explorer'; export function plugin(context: PluginInitializerContext) { return new LogExplorerPlugin(context); diff --git a/x-pack/plugins/observability_log_explorer/common/constants.ts b/x-pack/plugins/observability_log_explorer/common/constants.ts new file mode 100644 index 0000000000000..90cd311f05940 --- /dev/null +++ b/x-pack/plugins/observability_log_explorer/common/constants.ts @@ -0,0 +1,8 @@ +/* + * 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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const OBSERVABILITY_LOG_EXPLORER_APP_ID = 'observability-log-explorer'; diff --git a/x-pack/plugins/observability_log_explorer/common/translations.ts b/x-pack/plugins/observability_log_explorer/common/translations.ts index 5ec1940fa8dff..2abf660538260 100644 --- a/x-pack/plugins/observability_log_explorer/common/translations.ts +++ b/x-pack/plugins/observability_log_explorer/common/translations.ts @@ -21,3 +21,10 @@ export const betaBadgeDescription = i18n.translate( defaultMessage: 'This application is in beta and therefore subject to change.', } ); + +export const discoverLinkTitle = i18n.translate( + 'xpack.observabilityLogExplorer.discoverLinkTitle', + { + defaultMessage: 'Discover', + } +); diff --git a/x-pack/plugins/observability_log_explorer/kibana.jsonc b/x-pack/plugins/observability_log_explorer/kibana.jsonc index 35121b578c39c..529f879a56386 100644 --- a/x-pack/plugins/observability_log_explorer/kibana.jsonc +++ b/x-pack/plugins/observability_log_explorer/kibana.jsonc @@ -13,12 +13,13 @@ ], "requiredPlugins": [ "data", + "discover", "logExplorer", "observabilityShared" ], "optionalPlugins": [ "serverless" ], - "requiredBundles": [] + "requiredBundles": ["kibanaReact"] } } diff --git a/x-pack/plugins/observability_log_explorer/public/applications/observability_log_explorer.tsx b/x-pack/plugins/observability_log_explorer/public/applications/observability_log_explorer.tsx index 7d6863e4eb45a..999ebdd3095bf 100644 --- a/x-pack/plugins/observability_log_explorer/public/applications/observability_log_explorer.tsx +++ b/x-pack/plugins/observability_log_explorer/public/applications/observability_log_explorer.tsx @@ -5,28 +5,29 @@ * 2.0. */ -import { AppMountParameters, CoreStart, ScopedHistory } from '@kbn/core/public'; +import { AppMountParameters, CoreStart } from '@kbn/core/public'; import { KibanaRenderContextProvider } from '@kbn/react-kibana-context-render'; import { Route, Router, Routes } from '@kbn/shared-ux-router'; import React from 'react'; import ReactDOM from 'react-dom'; import { ObservablityLogExplorerMainRoute } from '../routes/main'; import { ObservabilityLogExplorerPluginStart, ObservabilityLogExplorerStartDeps } from '../types'; +import { useKibanaContextForPluginProvider } from '../utils/use_kibana'; export const renderObservabilityLogExplorer = ( core: CoreStart, pluginsStart: ObservabilityLogExplorerStartDeps, ownPluginStart: ObservabilityLogExplorerPluginStart, - { element, history }: AppMountParameters + appParams: AppMountParameters ) => { ReactDOM.render( , - element + appParams.element ); return () => { @@ -34,40 +35,51 @@ export const renderObservabilityLogExplorer = ( // observable in the search session service pluginsStart.data.search.session.clear(); - ReactDOM.unmountComponentAtNode(element); + ReactDOM.unmountComponentAtNode(appParams.element); }; }; export interface ObservabilityLogExplorerAppProps { + appParams: AppMountParameters; core: CoreStart; plugins: ObservabilityLogExplorerStartDeps; pluginStart: ObservabilityLogExplorerPluginStart; - history: ScopedHistory; } export const ObservabilityLogExplorerApp = ({ + appParams, core, - plugins: { logExplorer, observabilityShared, serverless }, + plugins, pluginStart, - history, -}: ObservabilityLogExplorerAppProps) => ( - - - - ( - { + const { logExplorer, observabilityShared, serverless } = plugins; + const KibanaContextProviderForPlugin = useKibanaContextForPluginProvider( + core, + plugins, + pluginStart + ); + + return ( + + + + + ( + + )} /> - )} - /> - - - -); + + + + + ); +}; diff --git a/x-pack/plugins/observability_log_explorer/public/components/log_explorer_top_nav_menu.tsx b/x-pack/plugins/observability_log_explorer/public/components/log_explorer_top_nav_menu.tsx new file mode 100644 index 0000000000000..0e8ec200da871 --- /dev/null +++ b/x-pack/plugins/observability_log_explorer/public/components/log_explorer_top_nav_menu.tsx @@ -0,0 +1,105 @@ +/* + * 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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import deepEqual from 'fast-deep-equal'; +import useObservable from 'react-use/lib/useObservable'; +import { type BehaviorSubject, distinctUntilChanged } from 'rxjs'; +import { HeaderMenuPortal } from '@kbn/observability-shared-plugin/public'; +import { AppMountParameters } from '@kbn/core-application-browser'; +import { + EuiBetaBadge, + EuiHeaderLink, + EuiHeaderLinks, + EuiHeaderSection, + EuiHeaderSectionItem, + useEuiTheme, +} from '@elastic/eui'; +import { css } from '@emotion/react'; +import { LogExplorerStateContainer } from '@kbn/log-explorer-plugin/public'; +import { useKibanaContextForPlugin } from '../utils/use_kibana'; +import { betaBadgeDescription, betaBadgeTitle, discoverLinkTitle } from '../../common/translations'; + +interface LogExplorerTopNavMenuProps { + setHeaderActionMenu: AppMountParameters['setHeaderActionMenu']; + state$: BehaviorSubject; + theme$: AppMountParameters['theme$']; +} + +export const LogExplorerTopNavMenu = ({ + setHeaderActionMenu, + state$, + theme$, +}: LogExplorerTopNavMenuProps) => { + const { euiTheme } = useEuiTheme(); + + return ( + + + + + + + + + + + ); +}; + +const DiscoverLink = React.memo( + ({ state$ }: { state$: BehaviorSubject }) => { + const { + services: { discover }, + } = useKibanaContextForPlugin(); + + const { appState, logExplorerState } = useObservable( + state$.pipe( + distinctUntilChanged((prev, curr) => { + if (!prev.appState || !curr.appState) return false; + return deepEqual( + [ + prev.appState.columns, + prev.appState.filters, + prev.appState.index, + prev.appState.query, + ], + [curr.appState.columns, curr.appState.filters, curr.appState.index, curr.appState.query] + ); + }) + ), + { appState: {}, logExplorerState: {} } + ); + + const discoverLinkParams = { + columns: appState?.columns, + filters: appState?.filters, + query: appState?.query, + dataViewSpec: logExplorerState?.datasetSelection?.selection.dataset.toDataviewSpec(), + }; + + return ( + discover.locator?.navigate(discoverLinkParams)} + color="primary" + iconType="discoverApp" + data-test-subj="logExplorerDiscoverFallbackLink" + > + {discoverLinkTitle} + + ); + } +); diff --git a/x-pack/plugins/observability_log_explorer/public/plugin.ts b/x-pack/plugins/observability_log_explorer/public/plugin.ts index 6afb62235ba15..8ca79e4ec7be4 100644 --- a/x-pack/plugins/observability_log_explorer/public/plugin.ts +++ b/x-pack/plugins/observability_log_explorer/public/plugin.ts @@ -14,6 +14,7 @@ import { PluginInitializerContext, } from '@kbn/core/public'; import { type ObservabilityLogExplorerConfig } from '../common/plugin_config'; +import { OBSERVABILITY_LOG_EXPLORER_APP_ID } from '../common/constants'; import { logExplorerAppTitle } from '../common/translations'; import { renderObservabilityLogExplorer } from './applications/observability_log_explorer'; import type { @@ -37,7 +38,7 @@ export class ObservabilityLogExplorerPlugin _pluginsSetup: ObservabilityLogExplorerSetupDeps ) { core.application.register({ - id: 'observability-log-explorer', + id: OBSERVABILITY_LOG_EXPLORER_APP_ID, title: logExplorerAppTitle, category: DEFAULT_APP_CATEGORIES.observability, euiIconType: 'logoLogging', diff --git a/x-pack/plugins/observability_log_explorer/public/routes/main/main_route.tsx b/x-pack/plugins/observability_log_explorer/public/routes/main/main_route.tsx index 5e9b22fb1ad5d..7b224da830433 100644 --- a/x-pack/plugins/observability_log_explorer/public/routes/main/main_route.tsx +++ b/x-pack/plugins/observability_log_explorer/public/routes/main/main_route.tsx @@ -5,34 +5,45 @@ * 2.0. */ -import { CoreStart, ScopedHistory } from '@kbn/core/public'; +import { AppMountParameters, CoreStart } from '@kbn/core/public'; import { LogExplorerPluginStart } from '@kbn/log-explorer-plugin/public'; import { ObservabilitySharedPluginStart } from '@kbn/observability-shared-plugin/public'; import { ServerlessPluginStart } from '@kbn/serverless/public'; -import React from 'react'; +import React, { useState } from 'react'; +import { BehaviorSubject } from 'rxjs'; +import { LogExplorerTopNavMenu } from '../../components/log_explorer_top_nav_menu'; import { ObservabilityLogExplorerPageTemplate } from '../../components/page_template'; import { noBreadcrumbs, useBreadcrumbs } from '../../utils/breadcrumbs'; export interface ObservablityLogExplorerMainRouteProps { + appParams: AppMountParameters; core: CoreStart; - history: ScopedHistory; logExplorer: LogExplorerPluginStart; observabilityShared: ObservabilitySharedPluginStart; serverless?: ServerlessPluginStart; } export const ObservablityLogExplorerMainRoute = ({ + appParams: { history, setHeaderActionMenu, theme$ }, core, - history, logExplorer, observabilityShared, serverless, }: ObservablityLogExplorerMainRouteProps) => { useBreadcrumbs(noBreadcrumbs, core.chrome, serverless); + const [state$] = useState(() => new BehaviorSubject({})); + return ( - - - + <> + + + + + ); }; diff --git a/x-pack/plugins/observability_log_explorer/public/types.ts b/x-pack/plugins/observability_log_explorer/public/types.ts index f5e6526c502d9..e52ece9ca1624 100644 --- a/x-pack/plugins/observability_log_explorer/public/types.ts +++ b/x-pack/plugins/observability_log_explorer/public/types.ts @@ -6,6 +6,7 @@ */ import { DataPublicPluginStart } from '@kbn/data-plugin/public'; +import { DiscoverStart } from '@kbn/discover-plugin/public'; import { LogExplorerPluginStart } from '@kbn/log-explorer-plugin/public'; import { ObservabilitySharedPluginStart } from '@kbn/observability-shared-plugin/public'; import { ServerlessPluginStart } from '@kbn/serverless/public'; @@ -22,6 +23,7 @@ export interface ObservabilityLogExplorerSetupDeps { export interface ObservabilityLogExplorerStartDeps { data: DataPublicPluginStart; + discover: DiscoverStart; logExplorer: LogExplorerPluginStart; observabilityShared: ObservabilitySharedPluginStart; serverless?: ServerlessPluginStart; diff --git a/x-pack/plugins/observability_log_explorer/public/utils/breadcrumbs.tsx b/x-pack/plugins/observability_log_explorer/public/utils/breadcrumbs.tsx index a8b575d5341dc..c1eaca45b7855 100644 --- a/x-pack/plugins/observability_log_explorer/public/utils/breadcrumbs.tsx +++ b/x-pack/plugins/observability_log_explorer/public/utils/breadcrumbs.tsx @@ -9,11 +9,7 @@ import { EuiBreadcrumb } from '@elastic/eui'; import type { ChromeStart } from '@kbn/core-chrome-browser'; import type { ServerlessPluginStart } from '@kbn/serverless/public'; import { useEffect } from 'react'; -import { - betaBadgeDescription, - betaBadgeTitle, - logExplorerAppTitle, -} from '../../common/translations'; +import { logExplorerAppTitle } from '../../common/translations'; export const useBreadcrumbs = ( breadcrumbs: EuiBreadcrumb[], @@ -40,10 +36,6 @@ export function setBreadcrumbs( ...breadcrumbs, ]); } - chromeService.setBadge({ - text: betaBadgeTitle, - tooltip: betaBadgeDescription, - }); } export const noBreadcrumbs: EuiBreadcrumb[] = []; diff --git a/x-pack/plugins/observability_log_explorer/tsconfig.json b/x-pack/plugins/observability_log_explorer/tsconfig.json index 5f94d15d30fea..ae9660b421359 100644 --- a/x-pack/plugins/observability_log_explorer/tsconfig.json +++ b/x-pack/plugins/observability_log_explorer/tsconfig.json @@ -22,6 +22,8 @@ "@kbn/serverless", "@kbn/core-chrome-browser", "@kbn/config-schema", + "@kbn/core-application-browser", + "@kbn/discover-plugin", ], "exclude": [ "target/**/*" diff --git a/x-pack/test/functional/apps/observability_log_explorer/header_menu.ts b/x-pack/test/functional/apps/observability_log_explorer/header_menu.ts new file mode 100644 index 0000000000000..0831bec27b7ed --- /dev/null +++ b/x-pack/test/functional/apps/observability_log_explorer/header_menu.ts @@ -0,0 +1,76 @@ +/* + * 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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const kibanaServer = getService('kibanaServer'); + const retry = getService('retry'); + const PageObjects = getPageObjects(['discover', 'observabilityLogExplorer', 'timePicker']); + + describe('Header menu', () => { + before(async () => { + await kibanaServer.importExport.load('test/functional/fixtures/kbn_archiver/discover'); + await esArchiver.load( + 'x-pack/test/functional/es_archives/observability_log_explorer/data_streams' + ); + await PageObjects.observabilityLogExplorer.navigateTo(); + }); + + after(async () => { + await kibanaServer.importExport.unload('test/functional/fixtures/kbn_archiver/discover'); + await esArchiver.unload( + 'x-pack/test/functional/es_archives/observability_log_explorer/data_streams' + ); + }); + + it('should inject the app header menu on the top navbar', async () => { + const headerMenu = await PageObjects.observabilityLogExplorer.getHeaderMenu(); + expect(await headerMenu.isDisplayed()).to.be(true); + }); + + describe('Discover fallback link', () => { + it('should render a button link ', async () => { + const discoverLink = await PageObjects.observabilityLogExplorer.getDiscoverFallbackLink(); + expect(await discoverLink.isDisplayed()).to.be(true); + }); + + it('should navigate to discover keeping the current columns/filters/query/time/data view', async () => { + // Set timerange to specific values to match data and retrieve config + await PageObjects.discover.expandTimeRangeAsSuggestedInNoResultsMessage(); + const timeConfig = await PageObjects.timePicker.getTimeConfig(); + + // Set query bar value + await PageObjects.observabilityLogExplorer.submitQuery('*favicon*'); + + const discoverLink = await PageObjects.observabilityLogExplorer.getDiscoverFallbackLink(); + discoverLink.click(); + + await PageObjects.discover.waitForDocTableLoadingComplete(); + + await retry.try(async () => { + expect(await PageObjects.discover.getCurrentlySelectedDataView()).to.eql( + 'All log datasets' + ); + }); + + await retry.try(async () => { + expect(await PageObjects.discover.getColumnHeaders()).to.eql(['@timestamp', 'message']); + }); + + await retry.try(async () => { + expect(await PageObjects.timePicker.getTimeConfig()).to.eql(timeConfig); + }); + + await retry.try(async () => { + expect(await PageObjects.observabilityLogExplorer.getQueryBarValue()).to.eql('*favicon*'); + }); + }); + }); + }); +} diff --git a/x-pack/test/functional/apps/observability_log_explorer/index.ts b/x-pack/test/functional/apps/observability_log_explorer/index.ts index 90a52663e34ce..aec38a6bb8308 100644 --- a/x-pack/test/functional/apps/observability_log_explorer/index.ts +++ b/x-pack/test/functional/apps/observability_log_explorer/index.ts @@ -14,5 +14,6 @@ export default function ({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./dataset_selection_state')); loadTestFile(require.resolve('./dataset_selector')); loadTestFile(require.resolve('./filter_controls')); + loadTestFile(require.resolve('./header_menu')); }); } diff --git a/x-pack/test/functional/page_objects/observability_log_explorer.ts b/x-pack/test/functional/page_objects/observability_log_explorer.ts index 33e85e06a16a9..7e4b83083ace0 100644 --- a/x-pack/test/functional/page_objects/observability_log_explorer.ts +++ b/x-pack/test/functional/page_objects/observability_log_explorer.ts @@ -314,5 +314,34 @@ export function ObservabilityLogExplorerPageObject({ expect(await promptTitle.getVisibleText()).to.be('No data streams found'); }, + + getHeaderMenu() { + return testSubjects.find('logExplorerHeaderMenu'); + }, + + getDiscoverFallbackLink() { + return testSubjects.find('logExplorerDiscoverFallbackLink'); + }, + + // Query Bar + getQueryBar() { + return testSubjects.find('queryInput'); + }, + + async getQueryBarValue() { + const queryBar = await testSubjects.find('queryInput'); + return queryBar.getAttribute('value'); + }, + + async typeInQueryBar(query: string) { + const queryBar = await this.getQueryBar(); + await queryBar.clearValueWithKeyboard(); + return queryBar.type(query); + }, + + async submitQuery(query: string) { + await this.typeInQueryBar(query); + await testSubjects.click('querySubmitButton'); + }, }; } diff --git a/x-pack/test_serverless/functional/test_suites/observability/observability_log_explorer/header_menu.ts b/x-pack/test_serverless/functional/test_suites/observability/observability_log_explorer/header_menu.ts new file mode 100644 index 0000000000000..038e28a442c24 --- /dev/null +++ b/x-pack/test_serverless/functional/test_suites/observability/observability_log_explorer/header_menu.ts @@ -0,0 +1,76 @@ +/* + * 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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const kibanaServer = getService('kibanaServer'); + const retry = getService('retry'); + const PageObjects = getPageObjects(['discover', 'observabilityLogExplorer', 'timePicker']); + + describe('Header menu', () => { + before(async () => { + await kibanaServer.importExport.load('test/functional/fixtures/kbn_archiver/discover'); + await esArchiver.load( + 'x-pack/test/functional/es_archives/observability_log_explorer/data_streams' + ); + await PageObjects.observabilityLogExplorer.navigateTo(); + }); + + after(async () => { + await kibanaServer.importExport.unload('test/functional/fixtures/kbn_archiver/discover'); + await esArchiver.unload( + 'x-pack/test/functional/es_archives/observability_log_explorer/data_streams' + ); + }); + + it('should inject the app header menu on the top navbar', async () => { + const headerMenu = await PageObjects.observabilityLogExplorer.getHeaderMenu(); + expect(await headerMenu.isDisplayed()).to.be(true); + }); + + describe('Discover fallback link', () => { + it('should render a button link ', async () => { + const discoverLink = await PageObjects.observabilityLogExplorer.getDiscoverFallbackLink(); + expect(await discoverLink.isDisplayed()).to.be(true); + }); + + it('should navigate to discover keeping the current columns/filters/query/time/data view', async () => { + // Set timerange to specific values to match data and retrieve config + await PageObjects.discover.expandTimeRangeAsSuggestedInNoResultsMessage(); + const timeConfig = await PageObjects.timePicker.getTimeConfig(); + + // Set query bar value + await PageObjects.observabilityLogExplorer.submitQuery('*favicon*'); + + const discoverLink = await PageObjects.observabilityLogExplorer.getDiscoverFallbackLink(); + discoverLink.click(); + + await PageObjects.discover.waitForDocTableLoadingComplete(); + + await retry.try(async () => { + expect(await PageObjects.discover.getCurrentlySelectedDataView()).to.eql( + 'All log datasets' + ); + }); + + await retry.try(async () => { + expect(await PageObjects.discover.getColumnHeaders()).to.eql(['@timestamp', 'message']); + }); + + await retry.try(async () => { + expect(await PageObjects.timePicker.getTimeConfig()).to.eql(timeConfig); + }); + + await retry.try(async () => { + expect(await PageObjects.observabilityLogExplorer.getQueryBarValue()).to.eql('*favicon*'); + }); + }); + }); + }); +} diff --git a/x-pack/test_serverless/functional/test_suites/observability/observability_log_explorer/index.ts b/x-pack/test_serverless/functional/test_suites/observability/observability_log_explorer/index.ts index b0555b4447d27..77f89dad01f77 100644 --- a/x-pack/test_serverless/functional/test_suites/observability/observability_log_explorer/index.ts +++ b/x-pack/test_serverless/functional/test_suites/observability/observability_log_explorer/index.ts @@ -14,5 +14,6 @@ export default function ({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./dataset_selection_state')); loadTestFile(require.resolve('./dataset_selector')); loadTestFile(require.resolve('./filter_controls')); + loadTestFile(require.resolve('./header_menu')); }); }