diff --git a/.buildkite/ftr_platform_stateful_configs.yml b/.buildkite/ftr_platform_stateful_configs.yml index 23297f9f834ea..8cf067edecc42 100644 --- a/.buildkite/ftr_platform_stateful_configs.yml +++ b/.buildkite/ftr_platform_stateful_configs.yml @@ -91,6 +91,7 @@ enabled: - src/platform/test/functional/apps/discover/group10/config.ts - src/platform/test/functional/apps/discover/context_awareness/config.ts - src/platform/test/functional/apps/discover/observability/config.ts + - src/platform/test/functional/apps/discover/query_mode/config.ts - src/platform/test/functional/apps/discover/tabs/config.ts - src/platform/test/functional/apps/discover/tabs2/config.ts - src/platform/test/functional/apps/discover/tabs3/config.ts diff --git a/src/platform/plugins/shared/discover/common/constants.ts b/src/platform/plugins/shared/discover/common/constants.ts index 881d257c19cff..0298b0a09656d 100644 --- a/src/platform/plugins/shared/discover/common/constants.ts +++ b/src/platform/plugins/shared/discover/common/constants.ts @@ -26,6 +26,9 @@ export const getDefaultRowsPerPage = (uiSettings: IUiSettingsClient): number => // local storage key for the ES|QL to Dataviews transition modal export const ESQL_TRANSITION_MODAL_KEY = 'data.textLangTransitionModal'; +// local storage key for the query mode when starting a new discover session +export const DISCOVER_QUERY_MODE_KEY = 'discover.defaultQueryMode'; + /** * The id value used to indicate that a link should open in a new Discover tab. * It will be used in the `_tab` URL param to indicate that a new tab should be created. diff --git a/src/platform/plugins/shared/discover/public/application/main/state_management/redux/actions/initialize_single_tab.ts b/src/platform/plugins/shared/discover/public/application/main/state_management/redux/actions/initialize_single_tab.ts index f28200049d8bc..b649735cece49 100644 --- a/src/platform/plugins/shared/discover/public/application/main/state_management/redux/actions/initialize_single_tab.ts +++ b/src/platform/plugins/shared/discover/public/application/main/state_management/redux/actions/initialize_single_tab.ts @@ -232,6 +232,7 @@ export const initializeSingleTab = createInternalStateAsyncThunk( // then get an updated copy of the saved search with the applied initial state const initialAppState = getInitialAppState({ initialUrlState: urlAppState, + hasGlobalState: Object.keys(urlGlobalState || {}).length > 0, persistedTab, dataView, services, diff --git a/src/platform/plugins/shared/discover/public/application/main/state_management/redux/actions/tab_state.ts b/src/platform/plugins/shared/discover/public/application/main/state_management/redux/actions/tab_state.ts index 47b1e5382ed6b..21cc4995ef65b 100644 --- a/src/platform/plugins/shared/discover/public/application/main/state_management/redux/actions/tab_state.ts +++ b/src/platform/plugins/shared/discover/public/application/main/state_management/redux/actions/tab_state.ts @@ -26,6 +26,8 @@ import { internalStateSlice, type InternalStateThunkActionCreator, type TabActionPayload, + transitionedFromEsqlToDataView, + transitionedFromDataViewToEsql, } from '../internal_state'; import { selectTab } from '../selectors'; import { selectTabRuntimeState } from '../runtime_state'; @@ -206,6 +208,8 @@ export const transitionFromESQLToDataView: InternalStateThunkActionCreator< }, }) ); + + dispatch(transitionedFromEsqlToDataView({ tabId })); }; const clearTimeFieldFromSort = ( @@ -251,6 +255,8 @@ export const transitionFromDataViewToESQL: InternalStateThunkActionCreator< // clears pinned filters dispatch(updateGlobalState({ tabId, globalState: { filters: [] } })); + + dispatch(transitionedFromDataViewToEsql({ tabId })); }; /** diff --git a/src/platform/plugins/shared/discover/public/application/main/state_management/redux/internal_state.ts b/src/platform/plugins/shared/discover/public/application/main/state_management/redux/internal_state.ts index 51e647808e88f..0099d23a573db 100644 --- a/src/platform/plugins/shared/discover/public/application/main/state_management/redux/internal_state.ts +++ b/src/platform/plugins/shared/discover/public/application/main/state_management/redux/internal_state.ts @@ -29,6 +29,7 @@ import type { IKbnUrlStateStorage } from '@kbn/kibana-utils-plugin/public'; import type { ESQLControlVariable } from '@kbn/esql-types'; import type { DiscoverSession } from '@kbn/saved-search-plugin/common'; import { isOfAggregateQueryType } from '@kbn/es-query'; +import { DISCOVER_QUERY_MODE_KEY } from '../../../../../common/constants'; import type { DiscoverCustomizationContext } from '../../../../customizations'; import type { DiscoverServices } from '../../../../build_services'; import { type RuntimeStateManager, selectTabRuntimeInternalState } from './runtime_state'; @@ -446,6 +447,14 @@ export const syncLocallyPersistedTabState = createAction( export const discardFlyoutsOnTabChange = createAction('internalState/discardFlyoutsOnTabChange'); +export const transitionedFromEsqlToDataView = createAction( + 'internalState/transitionedFromEsqlToDataView' +); + +export const transitionedFromDataViewToEsql = createAction( + 'internalState/transitionedFromDataViewToEsql' +); + type InternalStateListenerEffect< TActionCreator extends PayloadActionCreator, TPayload = TActionCreator extends PayloadActionCreator ? T : never @@ -510,6 +519,28 @@ const createMiddleware = (options: InternalStateDependencies) => { }, }); + // This pair of listeners updates the default query mode based on the last used query type (ES|QL vs Data View), we use + // this so new discover sessions use that query mode as a default. + // + // NOTE: In the short term we will add a feature flag to default to ES|QL when there is no existing preference saved. + // Right now we use classic - this means that users will have to switch to ES|QL manually the first time if they already + // had classic stored as their last used mode. + startListening({ + actionCreator: transitionedFromDataViewToEsql, + effect: (action, listenerApi) => { + const { services } = listenerApi.extra; + services.storage.set(DISCOVER_QUERY_MODE_KEY, 'esql'); + }, + }); + + startListening({ + actionCreator: transitionedFromEsqlToDataView, + effect: (action, listenerApi) => { + const { services } = listenerApi.extra; + services.storage.set(DISCOVER_QUERY_MODE_KEY, 'classic'); + }, + }); + return listenerMiddleware.middleware; }; diff --git a/src/platform/plugins/shared/discover/public/application/main/state_management/utils/get_initial_app_state.test.ts b/src/platform/plugins/shared/discover/public/application/main/state_management/utils/get_initial_app_state.test.ts index 4e6098b148293..2c953ce93d5d8 100644 --- a/src/platform/plugins/shared/discover/public/application/main/state_management/utils/get_initial_app_state.test.ts +++ b/src/platform/plugins/shared/discover/public/application/main/state_management/utils/get_initial_app_state.test.ts @@ -18,6 +18,7 @@ import { dataViewWithTimefieldMock } from '../../../../__mocks__/data_view_with_ import type { DiscoverServices } from '../../../../build_services'; import { VIEW_MODE } from '@kbn/saved-search-plugin/common'; import { DEFAULT_COLUMNS_SETTING } from '@kbn/discover-utils'; +import { DataView } from '@kbn/data-views-plugin/common'; describe('getInitialAppState', () => { const customQuery = { @@ -77,6 +78,7 @@ describe('getInitialAppState', () => { services, }); const appState = getInitialAppState({ + hasGlobalState: false, initialUrlState: undefined, persistedTab, dataView: dataViewMock, @@ -122,6 +124,7 @@ describe('getInitialAppState', () => { services, }); const appState = getInitialAppState({ + hasGlobalState: false, initialUrlState: undefined, persistedTab, dataView: dataViewMock, @@ -151,6 +154,7 @@ describe('getInitialAppState', () => { test('data view with timefield', () => { const services = createDiscoverServicesMock(); const actual = getInitialAppState({ + hasGlobalState: false, initialUrlState: undefined, persistedTab: undefined, dataView: dataViewWithTimefieldMock, @@ -192,6 +196,7 @@ describe('getInitialAppState', () => { test('data view without timefield', () => { const services = createDiscoverServicesMock(); const actual = getInitialAppState({ + hasGlobalState: false, initialUrlState: undefined, persistedTab: undefined, dataView: dataViewMock, @@ -235,6 +240,7 @@ describe('getInitialAppState', () => { test('should set view mode correctly', () => { const services = createDiscoverServicesMock(); const actualForUndefinedViewMode = getInitialAppState({ + hasGlobalState: false, initialUrlState: undefined, persistedTab: { ...getPersistedTab({ services }), @@ -246,6 +252,7 @@ describe('getInitialAppState', () => { expect(actualForUndefinedViewMode.viewMode).toBeUndefined(); const actualForEsqlWithAggregatedViewMode = getInitialAppState({ + hasGlobalState: false, initialUrlState: undefined, persistedTab: { ...getPersistedTab({ services }), @@ -258,6 +265,7 @@ describe('getInitialAppState', () => { expect(actualForEsqlWithAggregatedViewMode.viewMode).toBe(VIEW_MODE.AGGREGATED_LEVEL); const actualForEsqlWithInvalidPatternLevelViewMode = getInitialAppState({ + hasGlobalState: false, initialUrlState: undefined, persistedTab: { ...getPersistedTab({ services }), @@ -270,6 +278,7 @@ describe('getInitialAppState', () => { expect(actualForEsqlWithInvalidPatternLevelViewMode.viewMode).toBe(VIEW_MODE.DOCUMENT_LEVEL); const actualForEsqlWithValidViewMode = getInitialAppState({ + hasGlobalState: false, initialUrlState: undefined, persistedTab: { ...getPersistedTab({ services }), @@ -283,6 +292,7 @@ describe('getInitialAppState', () => { expect(actualForEsqlWithValidViewMode.dataSource).toEqual(createEsqlDataSource()); const actualForWithValidAggLevelViewMode = getInitialAppState({ + hasGlobalState: false, initialUrlState: undefined, persistedTab: { ...getPersistedTab({ services }), @@ -297,6 +307,7 @@ describe('getInitialAppState', () => { ); const actualForWithValidPatternLevelViewMode = getInitialAppState({ + hasGlobalState: false, initialUrlState: undefined, persistedTab: { ...getPersistedTab({ services }), @@ -314,6 +325,7 @@ describe('getInitialAppState', () => { test('should return expected dataSource', () => { const services = createDiscoverServicesMock(); const actualForEsql = getInitialAppState({ + hasGlobalState: false, initialUrlState: undefined, persistedTab: { ...getPersistedTab({ services }), @@ -328,6 +340,7 @@ describe('getInitialAppState', () => { } `); const actualForDataView = getInitialAppState({ + hasGlobalState: false, initialUrlState: undefined, persistedTab: getPersistedTab({ services }), dataView: dataViewMock, @@ -341,10 +354,184 @@ describe('getInitialAppState', () => { `); }); + describe('when there is no persistedTab', () => { + describe('when there is a global state', () => { + describe('when there is a query in the url', () => { + it('should use the query from the url state', () => { + // Given + const services = createDiscoverServicesMock(); + const query = { language: 'kuery', query: 'url state query' }; + + // When + const appState = getInitialAppState({ + hasGlobalState: true, + initialUrlState: { query }, + persistedTab: undefined, + dataView: dataViewMock, + services, + }); + + // Then + expect(appState).toEqual( + expect.objectContaining({ + query, + }) + ); + }); + }); + + describe('when there is no query in the url', () => { + it('should use the default query', () => { + // Given + const services = createDiscoverServicesMock(); + const dataSource = createDataViewDataSource({ dataViewId: 'some-data-view-id' }); + services.data.query.queryString.getDefaultQuery = jest.fn().mockReturnValue(defaultQuery); + + // When + const appState = getInitialAppState({ + hasGlobalState: true, + initialUrlState: { dataSource }, + persistedTab: undefined, + dataView: dataViewMock, + services, + }); + + // Then + expect(appState).toEqual( + expect.objectContaining({ + query: defaultQuery, + }) + ); + }); + }); + }); + + describe('when there is no global state', () => { + describe('when there is initial url state', () => { + describe('when there is a query in the url state', () => { + it('should use the query from the url state', () => { + // Given + const services = createDiscoverServicesMock(); + const query = { language: 'kuery', query: 'url state query' }; + + // When + const appState = getInitialAppState({ + hasGlobalState: false, + initialUrlState: { query }, + persistedTab: undefined, + dataView: dataViewMock, + services, + }); + + // Then + expect(appState).toEqual( + expect.objectContaining({ + query, + }) + ); + }); + }); + + describe('when there is no query in the url state', () => { + it('should use the default query', () => { + // Given + const services = createDiscoverServicesMock(); + const dataSource = createDataViewDataSource({ dataViewId: 'some-data-view-id' }); + services.data.query.queryString.getDefaultQuery = jest + .fn() + .mockReturnValue(defaultQuery); + + // When + const appState = getInitialAppState({ + hasGlobalState: false, + initialUrlState: { dataSource }, + persistedTab: undefined, + dataView: dataViewMock, + services, + }); + + // Then + expect(appState).toEqual( + expect.objectContaining({ + query: defaultQuery, + }) + ); + }); + }); + }); + + describe('when there is no initial url state', () => { + describe('when the query mode is esql', () => { + it('should return an esql initial query', () => { + // Given + const services = createDiscoverServicesMock(); + services.storage.get = jest.fn().mockReturnValue('esql'); + services.uiSettings.get = jest.fn().mockReturnValue(true); + + // When + const appState = getInitialAppState({ + hasGlobalState: false, + initialUrlState: undefined, + persistedTab: undefined, + dataView: new DataView({ + spec: dataViewMock.toSpec(), + fieldFormats: {} as DataView['fieldFormats'], + }), + services, + }); + + // Then + expect(appState).toEqual( + expect.objectContaining({ + query: { esql: 'FROM the-data-view-title' }, + }) + ); + }); + }); + + describe.each([ + { queryMode: 'esql', description: 'esql but esql is disabled' }, + { queryMode: 'classic', description: 'classic' }, + { queryMode: undefined, description: 'unset' }, + ])('when the query mode is $description', ({ queryMode }) => { + it('should return the default query', () => { + // Given + const services = createDiscoverServicesMock(); + services.storage.get = jest.fn().mockReturnValue(queryMode); + services.uiSettings.get = jest.fn().mockReturnValue(false); + services.data.query.queryString.getDefaultQuery = jest + .fn() + .mockReturnValue(defaultQuery); + + // When + const appState = getInitialAppState({ + hasGlobalState: false, + initialUrlState: undefined, + persistedTab: undefined, + dataView: new DataView({ + spec: dataViewMock.toSpec(), + fieldFormats: {} as DataView['fieldFormats'], + }), + services, + }); + + // Then + expect(appState).toEqual( + expect.objectContaining({ + query: defaultQuery, + }) + ); + }); + }); + }); + }); + }); + describe('default sort array', () => { test('should use persistedTab sort array if valid and data view is provided', () => { const services = createDiscoverServicesMock(); const appState = getInitialAppState({ + hasGlobalState: false, initialUrlState: undefined, persistedTab: { ...getPersistedTab({ services }), @@ -359,6 +546,7 @@ describe('getInitialAppState', () => { test('should not use persistedTab sort array if invalid and data view is provided', () => { const services = createDiscoverServicesMock(); const appState = getInitialAppState({ + hasGlobalState: false, initialUrlState: undefined, persistedTab: { ...getPersistedTab({ services }), @@ -373,6 +561,7 @@ describe('getInitialAppState', () => { test('should use persistedTab sort array when data view is not provided', () => { const services = createDiscoverServicesMock(); const appState = getInitialAppState({ + hasGlobalState: false, initialUrlState: undefined, persistedTab: { ...getPersistedTab({ services }), @@ -387,6 +576,7 @@ describe('getInitialAppState', () => { test('should use persistedTab sort array when partial data view is provided', () => { const services = createDiscoverServicesMock(); const appState = getInitialAppState({ + hasGlobalState: false, initialUrlState: undefined, persistedTab: { ...getPersistedTab({ services }), @@ -403,6 +593,7 @@ describe('getInitialAppState', () => { test('should use persistedTab columns if provided', () => { const services = createDiscoverServicesMock(); const appState = getInitialAppState({ + hasGlobalState: false, initialUrlState: undefined, persistedTab: { ...getPersistedTab({ services }), @@ -417,6 +608,7 @@ describe('getInitialAppState', () => { test('should use default columns if empty columns are stored for persistedTab', () => { const services = createDiscoverServicesMock(); const appState = getInitialAppState({ + hasGlobalState: false, initialUrlState: undefined, persistedTab: { ...getPersistedTab({ services }), @@ -436,6 +628,7 @@ describe('getInitialAppState', () => { } }); const appState = getInitialAppState({ + hasGlobalState: false, initialUrlState: undefined, persistedTab: { ...getPersistedTab({ services }), @@ -455,6 +648,7 @@ describe('getInitialAppState', () => { } }); const appState = getInitialAppState({ + hasGlobalState: false, initialUrlState: { columns: [] }, persistedTab: undefined, dataView: dataViewWithTimefieldMock, @@ -471,6 +665,7 @@ describe('getInitialAppState', () => { } }); const appState = getInitialAppState({ + hasGlobalState: false, initialUrlState: undefined, persistedTab: undefined, dataView: dataViewWithTimefieldMock, diff --git a/src/platform/plugins/shared/discover/public/application/main/state_management/utils/get_initial_app_state.ts b/src/platform/plugins/shared/discover/public/application/main/state_management/utils/get_initial_app_state.ts index 139bf2d81cfd8..9f525de0366f2 100644 --- a/src/platform/plugins/shared/discover/public/application/main/state_management/utils/get_initial_app_state.ts +++ b/src/platform/plugins/shared/discover/public/application/main/state_management/utils/get_initial_app_state.ts @@ -7,7 +7,8 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import type { DataView } from '@kbn/data-views-plugin/common'; +import { DataView } from '@kbn/data-views-plugin/common'; +import type { AggregateQuery, Query } from '@kbn/es-query'; import { isOfAggregateQueryType } from '@kbn/es-query'; import type { DiscoverSessionTab } from '@kbn/saved-search-plugin/common'; import type { IUiSettingsClient } from '@kbn/core/public'; @@ -20,6 +21,8 @@ import { } from '@kbn/discover-utils'; import { getChartHidden } from '@kbn/unified-histogram'; import { cloneDeep } from 'lodash'; +import { ENABLE_ESQL, getInitialESQLQuery } from '@kbn/esql-utils'; +import { DISCOVER_QUERY_MODE_KEY } from '../../../../../common/constants'; import type { DiscoverServices } from '../../../../build_services'; import type { DiscoverAppState } from '../redux'; import { @@ -32,11 +35,13 @@ import { getValidViewMode } from '../../utils/get_valid_view_mode'; export function getInitialAppState({ initialUrlState, + hasGlobalState = false, persistedTab, dataView, services, }: { initialUrlState: DiscoverAppState | undefined; + hasGlobalState?: boolean; persistedTab: DiscoverSessionTab | undefined; dataView: DataView | Pick | undefined; services: DiscoverServices; @@ -45,6 +50,8 @@ export function getInitialAppState({ persistedTab, dataView, services, + initialUrlState, + hasGlobalState, }); const mergedState = { ...defaultAppState, ...initialUrlState }; @@ -77,18 +84,58 @@ function getDefaultColumns( : undefined; } +function getDefaultQuery({ + initialUrlState, + hasGlobalState, + persistedTab, + services, + dataView, +}: { + persistedTab: DiscoverSessionTab | undefined; + services: DiscoverServices; + dataView: DataView | Pick | undefined; + initialUrlState: DiscoverAppState | undefined; + hasGlobalState: boolean; +}): Query | AggregateQuery | undefined { + if (persistedTab?.serializedSearchSource.query) return persistedTab.serializedSearchSource.query; + + // If there is global or app state (_g or _a) in the URL we should respect it and assume it's a classic query + // This is also useful to reuse the query mode if we are opening a new tab from an existing one + const hasInitialUrlState = Object.keys(initialUrlState || {}).length > 0; + if (hasGlobalState || hasInitialUrlState) + return initialUrlState?.query || services.data.query.queryString.getDefaultQuery(); + + // Lastly fall back to the last selected query mode if available + const hasEsqlEnabled = services.uiSettings.get(ENABLE_ESQL); + + const queryMode = services.storage.get(DISCOVER_QUERY_MODE_KEY); + if (hasEsqlEnabled && queryMode === 'esql' && dataView instanceof DataView) + return { esql: getInitialESQLQuery(dataView, true) }; + + return services.data.query.queryString.getDefaultQuery(); +} + function getDefaultAppState({ persistedTab, dataView, services, + initialUrlState, + hasGlobalState, }: { persistedTab: DiscoverSessionTab | undefined; dataView: DataView | Pick | undefined; services: DiscoverServices; + initialUrlState: DiscoverAppState | undefined; + hasGlobalState: boolean; }) { - const { data, uiSettings, storage } = services; - const query = - persistedTab?.serializedSearchSource.query || data.query.queryString.getDefaultQuery(); + const { uiSettings, storage } = services; + const query = getDefaultQuery({ + persistedTab, + services, + dataView, + initialUrlState, + hasGlobalState, + }); const isEsqlQuery = isOfAggregateQueryType(query); // If the data view doesn't have a getFieldByName method (e.g. if it's a spec or list item), // we assume the sort array is valid since we can't know for sure diff --git a/src/platform/test/functional/apps/discover/context_awareness/_telemetry.ts b/src/platform/test/functional/apps/discover/context_awareness/_telemetry.ts index ae16a0541797a..d9584924f068d 100644 --- a/src/platform/test/functional/apps/discover/context_awareness/_telemetry.ts +++ b/src/platform/test/functional/apps/discover/context_awareness/_telemetry.ts @@ -433,6 +433,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { describe('trackSubmittingQuery telemetry', () => { beforeEach(async () => { + await discover.resetQueryMode(); await common.navigateToApp('discover'); await discover.waitUntilTabIsLoaded(); await ebtUIHelper.setOptIn(true); diff --git a/src/platform/test/functional/apps/discover/group10/_lens_vis.ts b/src/platform/test/functional/apps/discover/group10/_lens_vis.ts index 9d6a668b466c3..19666740a344b 100644 --- a/src/platform/test/functional/apps/discover/group10/_lens_vis.ts +++ b/src/platform/test/functional/apps/discover/group10/_lens_vis.ts @@ -115,6 +115,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await discover.waitUntilSearchingHasFinished(); }); + afterEach(async function () { + await discover.resetQueryMode(); + }); + it('should show histogram by default', async () => { await checkHistogramVis(defaultTimespan, defaultTotalCount); diff --git a/src/platform/test/functional/apps/discover/group2_data_grid2/_data_grid_field_tokens.ts b/src/platform/test/functional/apps/discover/group2_data_grid2/_data_grid_field_tokens.ts index 4d4c1d6743d5b..1b1890e729537 100644 --- a/src/platform/test/functional/apps/discover/group2_data_grid2/_data_grid_field_tokens.ts +++ b/src/platform/test/functional/apps/discover/group2_data_grid2/_data_grid_field_tokens.ts @@ -94,6 +94,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { beforeEach(async function () { await timePicker.setDefaultAbsoluteRangeViaUiSettings(); await kibanaServer.uiSettings.update(defaultSettings); + await discover.resetQueryMode(); await common.navigateToApp('discover'); await discover.waitUntilSearchingHasFinished(); }); diff --git a/src/platform/test/functional/apps/discover/group2_data_grid2/_data_grid_in_table_search.ts b/src/platform/test/functional/apps/discover/group2_data_grid2/_data_grid_in_table_search.ts index f1dcc4df26237..fd5a7548e632b 100644 --- a/src/platform/test/functional/apps/discover/group2_data_grid2/_data_grid_in_table_search.ts +++ b/src/platform/test/functional/apps/discover/group2_data_grid2/_data_grid_in_table_search.ts @@ -59,6 +59,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await discover.waitUntilSearchingHasFinished(); }); + afterEach(async function () { + await discover.resetQueryMode(); + }); + it('should show highlights for in-table search', async () => { expect(await dataGrid.getCurrentPageNumber()).to.be('1'); diff --git a/src/platform/test/functional/apps/discover/group8/_sidenav_link.ts b/src/platform/test/functional/apps/discover/group8/_sidenav_link.ts index ecf9f24802d80..04852a6edda8d 100644 --- a/src/platform/test/functional/apps/discover/group8/_sidenav_link.ts +++ b/src/platform/test/functional/apps/discover/group8/_sidenav_link.ts @@ -51,6 +51,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await kibanaServer.savedObjects.cleanStandardList(); }); + beforeEach(async function () { + await discover.resetQueryMode(); + }); + it('saves the last URL when in data view mode', async function () { await common.navigateToApp('discover'); await header.waitUntilLoadingHasFinished(); diff --git a/src/platform/test/functional/apps/discover/query_mode/_default_query_mode.ts b/src/platform/test/functional/apps/discover/query_mode/_default_query_mode.ts new file mode 100644 index 0000000000000..0d088010b539b --- /dev/null +++ b/src/platform/test/functional/apps/discover/query_mode/_default_query_mode.ts @@ -0,0 +1,67 @@ +/* + * 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ +import expect from '@kbn/expect'; +import type { FtrProviderContext } from '../ftr_provider_context'; + +export default function ({ getPageObjects, getService }: FtrProviderContext) { + const { discover, common, unifiedSearch } = getPageObjects([ + 'discover', + 'common', + 'unifiedSearch', + ]); + + const testSubjects = getService('testSubjects'); + + describe('Default query mode', () => { + afterEach(async () => { + await discover.resetQueryMode(); + }); + + describe('when there is no default query mode set', () => { + it('should open Discover in classic mode', async () => { + // Validate that no default query mode is set + await common.navigateToApp('discover'); + const queryMode = await discover.getQueryMode(); + expect(queryMode).to.be(null); + + // Go to discover and validate classic mode + await testSubjects.existOrFail('discover-dataView-switch-link'); + }); + }); + + describe('when the user clicks ES|QL mode', () => { + it('should set the default mode to ES|QL', async () => { + // Go to discover and select ES|QL mode + await common.navigateToApp('discover'); + await discover.selectTextBaseLang(); + const queryMode = await discover.getQueryMode(); + expect(queryMode).to.contain('esql'); + + // Reload the app and validate ES|QL mode is persisted + await common.navigateToApp('discover', { path: '' }); + await discover.expectSourceViewerToExist(); + }); + }); + + describe('when the user clicks classic', () => { + it('should set the default mode to classic', async () => { + // Go to discover and select classic mode + await common.navigateToApp('discover'); + await discover.selectTextBaseLang(); + await unifiedSearch.switchToDataViewMode(); + const queryMode = await discover.getQueryMode(); + expect(queryMode).to.contain('classic'); + + // Reload the app and validate classic mode is persisted + await common.navigateToApp('discover', { path: '' }); + await testSubjects.existOrFail('discover-dataView-switch-link'); + }); + }); + }); +} diff --git a/src/platform/test/functional/apps/discover/query_mode/config.ts b/src/platform/test/functional/apps/discover/query_mode/config.ts new file mode 100644 index 0000000000000..9d42f45669d4e --- /dev/null +++ b/src/platform/test/functional/apps/discover/query_mode/config.ts @@ -0,0 +1,19 @@ +/* + * 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import type { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../../../config.base.js')); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/src/platform/test/functional/apps/discover/query_mode/index.ts b/src/platform/test/functional/apps/discover/query_mode/index.ts new file mode 100644 index 0000000000000..2f091fb965333 --- /dev/null +++ b/src/platform/test/functional/apps/discover/query_mode/index.ts @@ -0,0 +1,31 @@ +/* + * 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ +import type { FtrProviderContext } from '../ftr_provider_context'; + +export default function ({ getService, loadTestFile }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const browser = getService('browser'); + + describe('discover/query_mode', function () { + before(async function () { + await esArchiver.loadIfNeeded( + 'src/platform/test/functional/fixtures/es_archiver/logstash_functional' + ); + await browser.setWindowSize(1600, 1200); + }); + + after(async function unloadMakelogs() { + await esArchiver.unload( + 'src/platform/test/functional/fixtures/es_archiver/logstash_functional' + ); + }); + + loadTestFile(require.resolve('./_default_query_mode')); + }); +} diff --git a/src/platform/test/functional/apps/discover/tabs/_new_tab.ts b/src/platform/test/functional/apps/discover/tabs/_new_tab.ts index ed7d4640bf544..1d7f14a6ca6c4 100644 --- a/src/platform/test/functional/apps/discover/tabs/_new_tab.ts +++ b/src/platform/test/functional/apps/discover/tabs/_new_tab.ts @@ -28,6 +28,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await browser.setWindowSize(1920, 1080); }); + afterEach(async () => { + await discover.resetQueryMode(); + }); + it('should create a new tab in classic mode', async () => { // tab 0 - with the default data view diff --git a/src/platform/test/functional/apps/discover/tabs/_save_and_load.ts b/src/platform/test/functional/apps/discover/tabs/_save_and_load.ts index e4a03dc971a68..6629e68b35c96 100644 --- a/src/platform/test/functional/apps/discover/tabs/_save_and_load.ts +++ b/src/platform/test/functional/apps/discover/tabs/_save_and_load.ts @@ -25,6 +25,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const browser = getService('browser'); describe('tabs saving and loading', function () { + afterEach(async () => { + await discover.resetQueryMode(); + }); + describe('legacy Discover sessions', () => { const legacySessionName = 'A Saved Search'; const updatedSessionName = 'Updated legacy session'; diff --git a/src/platform/test/functional/apps/discover/tabs2/_recently_closed_tabs.ts b/src/platform/test/functional/apps/discover/tabs2/_recently_closed_tabs.ts index 921305bacedb6..7411553a82db3 100644 --- a/src/platform/test/functional/apps/discover/tabs2/_recently_closed_tabs.ts +++ b/src/platform/test/functional/apps/discover/tabs2/_recently_closed_tabs.ts @@ -29,6 +29,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await unifiedTabs.clearRecentlyClosedTabs(); }); + afterEach(async () => { + await discover.resetQueryMode(); + }); + it('should start with no recently closed tabs', async () => { const recentlyClosedTabs = await unifiedTabs.getRecentlyClosedTabTitles(); expect(recentlyClosedTabs.length).to.be(0); diff --git a/src/platform/test/functional/apps/discover/tabs2/_sharing.ts b/src/platform/test/functional/apps/discover/tabs2/_sharing.ts index 3925fa9659813..e4edeb771c79a 100644 --- a/src/platform/test/functional/apps/discover/tabs2/_sharing.ts +++ b/src/platform/test/functional/apps/discover/tabs2/_sharing.ts @@ -25,6 +25,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); afterEach(async () => { + await discover.resetQueryMode(); await browser.closeCurrentWindow(); await browser.switchTab(0); }); diff --git a/src/platform/test/functional/page_objects/discover_page.ts b/src/platform/test/functional/page_objects/discover_page.ts index 2b37a50b98d0a..9693cd9292e72 100644 --- a/src/platform/test/functional/page_objects/discover_page.ts +++ b/src/platform/test/functional/page_objects/discover_page.ts @@ -11,6 +11,8 @@ import expect from '@kbn/expect'; import type { WebElementWrapper } from '@kbn/ftr-common-functional-ui-services'; import { FtrService } from '../ftr_provider_context'; +const DISCOVER_QUERY_MODE_KEY = 'discover.defaultQueryMode'; + export class DiscoverPageObject extends FtrService { private readonly retry = this.ctx.getService('retry'); private readonly testSubjects = this.ctx.getService('testSubjects'); @@ -1035,4 +1037,16 @@ export class DiscoverPageObject extends FtrService { public async ensureNoUnsavedChangesIndicator() { await this.testSubjects.missingOrFail('split-button-notification-indicator'); } + + public resetQueryMode() { + return this.browser.removeLocalStorageItem(DISCOVER_QUERY_MODE_KEY); + } + + public getQueryMode() { + return this.browser.getLocalStorageItem(DISCOVER_QUERY_MODE_KEY); + } + + public setQueryMode(mode: string) { + return this.browser.setLocalStorageItem(DISCOVER_QUERY_MODE_KEY, JSON.stringify(mode)); + } }