diff --git a/src/plugins/discover/common/app_locator.test.ts b/src/plugins/discover/common/app_locator.test.ts index 78d0d32fd6ed23..9b9c328689ffce 100644 --- a/src/plugins/discover/common/app_locator.test.ts +++ b/src/plugins/discover/common/app_locator.test.ts @@ -15,6 +15,7 @@ import { mockStorage } from '@kbn/kibana-utils-plugin/public/storage/hashed_item import { FilterStateStore } from '@kbn/es-query'; import { DiscoverAppLocatorDefinition } from './app_locator'; import { SerializableRecord } from '@kbn/utility-types'; +import { createDataViewDataSource, createEsqlDataSource } from './data_sources'; const dataViewId: string = 'c367b774-a4c2-11ea-bb37-0242ac130002'; const savedSearchId: string = '571aaf70-4c88-11e8-b3d7-01146121b73d'; @@ -63,7 +64,7 @@ describe('Discover url generator', () => { const { _a, _g } = getStatesFromKbnUrl(path, ['_a', '_g']); expect(_a).toEqual({ - index: dataViewId, + dataSource: createDataViewDataSource({ dataViewId }), }); expect(_g).toEqual(undefined); }); @@ -104,6 +105,25 @@ describe('Discover url generator', () => { expect(_g).toEqual(undefined); }); + test('can specify an ES|QL query', async () => { + const { locator } = await setup(); + const { path } = await locator.getLocation({ + dataViewId, + query: { + esql: 'SELECT * FROM test', + }, + }); + const { _a, _g } = getStatesFromKbnUrl(path, ['_a', '_g']); + + expect(_a).toEqual({ + dataSource: createEsqlDataSource(), + query: { + esql: 'SELECT * FROM test', + }, + }); + expect(_g).toEqual(undefined); + }); + test('can specify local and global filters', async () => { const { locator } = await setup(); const { path } = await locator.getLocation({ diff --git a/src/plugins/discover/common/app_locator.ts b/src/plugins/discover/common/app_locator.ts index 144f052c2a44d3..fe39f92b79d224 100644 --- a/src/plugins/discover/common/app_locator.ts +++ b/src/plugins/discover/common/app_locator.ts @@ -7,13 +7,15 @@ */ import type { SerializableRecord } from '@kbn/utility-types'; -import type { Filter, TimeRange, Query, AggregateQuery } from '@kbn/es-query'; +import { Filter, TimeRange, Query, AggregateQuery, isOfAggregateQueryType } from '@kbn/es-query'; import type { GlobalQueryStateFromUrl, RefreshInterval } from '@kbn/data-plugin/public'; import type { LocatorDefinition, LocatorPublic } from '@kbn/share-plugin/public'; import type { DiscoverGridSettings } from '@kbn/saved-search-plugin/common'; import { DataViewSpec } from '@kbn/data-views-plugin/common'; import { setStateToKbnUrl } from '@kbn/kibana-utils-plugin/common'; import { VIEW_MODE } from './constants'; +import type { DiscoverAppState } from '../public'; +import { createDataViewDataSource, createEsqlDataSource } from './data_sources'; export const DISCOVER_APP_LOCATOR = 'DISCOVER_APP_LOCATOR'; @@ -150,32 +152,21 @@ export class DiscoverAppLocatorDefinition implements LocatorDefinition = {}; const queryState: GlobalQueryStateFromUrl = {}; const { isFilterPinned } = await import('@kbn/es-query'); if (query) appState.query = query; if (filters && filters.length) appState.filters = filters?.filter((f) => !isFilterPinned(f)); - if (indexPatternId) appState.index = indexPatternId; - if (dataViewId) appState.index = dataViewId; + if (indexPatternId) + appState.dataSource = createDataViewDataSource({ dataViewId: indexPatternId }); + if (dataViewId) appState.dataSource = createDataViewDataSource({ dataViewId }); + if (isOfAggregateQueryType(query)) appState.dataSource = createEsqlDataSource(); if (columns) appState.columns = columns; if (grid) appState.grid = grid; if (savedQuery) appState.savedQuery = savedQuery; if (sort) appState.sort = sort; if (interval) appState.interval = interval; - if (timeRange) queryState.time = timeRange; if (filters && filters.length) queryState.filters = filters?.filter((f) => isFilterPinned(f)); if (refreshInterval) queryState.refreshInterval = refreshInterval; diff --git a/src/plugins/discover/common/data_sources/index.ts b/src/plugins/discover/common/data_sources/index.ts new file mode 100644 index 00000000000000..78615e777bc3ea --- /dev/null +++ b/src/plugins/discover/common/data_sources/index.ts @@ -0,0 +1,10 @@ +/* + * 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 * from './utils'; +export * from './types'; diff --git a/src/plugins/discover/common/data_sources/types.ts b/src/plugins/discover/common/data_sources/types.ts new file mode 100644 index 00000000000000..aee1aa4cc68d16 --- /dev/null +++ b/src/plugins/discover/common/data_sources/types.ts @@ -0,0 +1,23 @@ +/* + * 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 enum DataSourceType { + DataView = 'dataView', + Esql = 'esql', +} + +export interface DataViewDataSource { + type: DataSourceType.DataView; + dataViewId: string; +} + +export interface EsqlDataSource { + type: DataSourceType.Esql; +} + +export type DiscoverDataSource = DataViewDataSource | EsqlDataSource; diff --git a/src/plugins/discover/common/data_sources/utils.ts b/src/plugins/discover/common/data_sources/utils.ts new file mode 100644 index 00000000000000..4bf8b0fcf3678a --- /dev/null +++ b/src/plugins/discover/common/data_sources/utils.ts @@ -0,0 +1,27 @@ +/* + * 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 { DataSourceType, DataViewDataSource, DiscoverDataSource, EsqlDataSource } from './types'; + +export const createDataViewDataSource = ({ + dataViewId, +}: { + dataViewId: string; +}): DataViewDataSource => ({ + type: DataSourceType.DataView, + dataViewId, +}); + +export const createEsqlDataSource = (): EsqlDataSource => ({ + type: DataSourceType.Esql, +}); + +export const isDataSourceType = ( + dataSource: DiscoverDataSource | undefined, + type: T +): dataSource is Extract => dataSource?.type === type; diff --git a/src/plugins/discover/public/application/main/components/layout/discover_documents.test.tsx b/src/plugins/discover/public/application/main/components/layout/discover_documents.test.tsx index 0fcd292472145d..bc739d9433e639 100644 --- a/src/plugins/discover/public/application/main/components/layout/discover_documents.test.tsx +++ b/src/plugins/discover/public/application/main/components/layout/discover_documents.test.tsx @@ -25,6 +25,7 @@ import { DiscoverAppState } from '../../state_management/discover_app_state_cont import { DiscoverCustomization, DiscoverCustomizationProvider } from '../../../../customizations'; import { createCustomizationService } from '../../../../customizations/customization_service'; import { DiscoverGrid } from '../../../../components/discover_grid'; +import { createDataViewDataSource } from '../../../../../common/data_sources'; const customisationService = createCustomizationService(); @@ -39,7 +40,9 @@ async function mountComponent(fetchStatus: FetchStatus, hits: EsHitRecord[]) { result: hits.map((hit) => buildDataTableRecord(hit, dataViewMock)), }) as DataDocuments$; const stateContainer = getDiscoverStateMock({}); - stateContainer.appState.update({ index: dataViewMock.id }); + stateContainer.appState.update({ + dataSource: createDataViewDataSource({ dataViewId: dataViewMock.id! }), + }); stateContainer.dataState.data$.documents$ = documents$; const props = { diff --git a/src/plugins/discover/public/application/main/components/layout/discover_documents.tsx b/src/plugins/discover/public/application/main/components/layout/discover_documents.tsx index 8671cc289c281e..57df6e8639eada 100644 --- a/src/plugins/discover/public/application/main/components/layout/discover_documents.tsx +++ b/src/plugins/discover/public/application/main/components/layout/discover_documents.tsx @@ -108,38 +108,20 @@ function DiscoverDocumentsComponent({ const documents$ = stateContainer.dataState.data$.documents$; const savedSearch = useSavedSearchInitial(); const { dataViews, capabilities, uiSettings, uiActions } = services; - const [ - query, - sort, - rowHeight, - headerRowHeight, - rowsPerPage, - grid, - columns, - index, - sampleSizeState, - ] = useAppStateSelector((state) => { - return [ - state.query, - state.sort, - state.rowHeight, - state.headerRowHeight, - state.rowsPerPage, - state.grid, - state.columns, - state.index, - state.sampleSize, - ]; - }); - const setExpandedDoc = useCallback( - (doc: DataTableRecord | undefined) => { - stateContainer.internalState.transitions.setExpandedDoc(doc); - }, - [stateContainer] - ); - + const [query, sort, rowHeight, headerRowHeight, rowsPerPage, grid, columns, sampleSizeState] = + useAppStateSelector((state) => { + return [ + state.query, + state.sort, + state.rowHeight, + state.headerRowHeight, + state.rowsPerPage, + state.grid, + state.columns, + state.sampleSize, + ]; + }); const expandedDoc = useInternalStateSelector((state) => state.expandedDoc); - const isTextBasedQuery = useMemo(() => getRawRecordType(query) === RecordRawType.PLAIN, [query]); const useNewFieldsApi = useMemo(() => !uiSettings.get(SEARCH_FIELDS_FROM_SOURCE), [uiSettings]); const hideAnnouncements = useMemo(() => uiSettings.get(HIDE_ANNOUNCEMENTS), [uiSettings]); @@ -147,7 +129,6 @@ function DiscoverDocumentsComponent({ () => isLegacyTableEnabled({ uiSettings, isTextBasedQueryMode: isTextBasedQuery }), [uiSettings, isTextBasedQuery] ); - const documentState = useDataState(documents$); const isDataLoading = documentState.fetchStatus === FetchStatus.LOADING || @@ -162,7 +143,8 @@ function DiscoverDocumentsComponent({ // 4. since the new sort by field isn't available in currentColumns EuiDataGrid is emitting a 'onSort', which is unsorting the grid // 5. this is propagated to Discover's URL and causes an unwanted change of state to an unsorted state // This solution switches to the loading state in this component when the URL index doesn't match the dataView.id - const isDataViewLoading = !isTextBasedQuery && dataView.id && index !== dataView.id; + const isDataViewLoading = + useInternalStateSelector((state) => state.isDataViewLoading) && !isTextBasedQuery; const isEmptyDataResult = isTextBasedQuery || !documentState.result || documentState.result.length === 0; const rows = useMemo(() => documentState.result || [], [documentState.result]); @@ -189,6 +171,13 @@ function DiscoverDocumentsComponent({ sort, }); + const setExpandedDoc = useCallback( + (doc: DataTableRecord | undefined) => { + stateContainer.internalState.transitions.setExpandedDoc(doc); + }, + [stateContainer] + ); + const onResizeDataGrid = useCallback( (colSettings) => onResize(colSettings, stateContainer), [stateContainer] diff --git a/src/plugins/discover/public/application/main/components/layout/discover_histogram_layout.test.tsx b/src/plugins/discover/public/application/main/components/layout/discover_histogram_layout.test.tsx index a1d450d6aa61b4..7ce8f987e59118 100644 --- a/src/plugins/discover/public/application/main/components/layout/discover_histogram_layout.test.tsx +++ b/src/plugins/discover/public/application/main/components/layout/discover_histogram_layout.test.tsx @@ -34,13 +34,14 @@ import { getDiscoverStateMock } from '../../../../__mocks__/discover_state.mock' import { DiscoverMainProvider } from '../../state_management/discover_state_provider'; import { act } from 'react-dom/test-utils'; import { PanelsToggle } from '../../../../components/panels_toggle'; +import { createDataViewDataSource } from '../../../../../common/data_sources'; function getStateContainer(savedSearch?: SavedSearch) { const stateContainer = getDiscoverStateMock({ isTimeBased: true, savedSearch }); const dataView = savedSearch?.searchSource?.getField('index') as DataView; stateContainer.appState.update({ - index: dataView?.id, + dataSource: createDataViewDataSource({ dataViewId: dataView?.id! }), interval: 'auto', hideChart: false, }); diff --git a/src/plugins/discover/public/application/main/components/layout/discover_layout.test.tsx b/src/plugins/discover/public/application/main/components/layout/discover_layout.test.tsx index 248b5305154510..c1cc257f58cc3e 100644 --- a/src/plugins/discover/public/application/main/components/layout/discover_layout.test.tsx +++ b/src/plugins/discover/public/application/main/components/layout/discover_layout.test.tsx @@ -39,6 +39,7 @@ import { DiscoverMainProvider } from '../../state_management/discover_state_prov import { act } from 'react-dom/test-utils'; import { ErrorCallout } from '../../../../components/common/error_callout'; import { PanelsToggle } from '../../../../components/panels_toggle'; +import { createDataViewDataSource } from '../../../../../common/data_sources'; jest.mock('@elastic/eui', () => ({ ...jest.requireActual('@elastic/eui'), @@ -106,7 +107,11 @@ async function mountComponent( session.getSession$.mockReturnValue(new BehaviorSubject('123')); - stateContainer.appState.update({ index: dataView.id, interval: 'auto', query }); + stateContainer.appState.update({ + dataSource: createDataViewDataSource({ dataViewId: dataView.id! }), + interval: 'auto', + query, + }); stateContainer.internalState.transitions.setDataView(dataView); const props = { diff --git a/src/plugins/discover/public/application/main/components/layout/discover_layout.tsx b/src/plugins/discover/public/application/main/components/layout/discover_layout.tsx index 24c92fd192408d..7065e511951b6c 100644 --- a/src/plugins/discover/public/application/main/components/layout/discover_layout.tsx +++ b/src/plugins/discover/public/application/main/components/layout/discover_layout.tsx @@ -329,7 +329,6 @@ export function DiscoverLayout({ stateContainer }: DiscoverLayoutProps) { void; stateContainer: DiscoverStateContainer; textBasedLanguageModeErrors?: Error; textBasedLanguageModeWarning?: string; @@ -44,7 +39,6 @@ export interface DiscoverTopNavProps { export const DiscoverTopNav = ({ savedQuery, stateContainer, - updateQuery, textBasedLanguageModeErrors, textBasedLanguageModeWarning, onFieldEdited, @@ -241,7 +235,7 @@ export const DiscoverTopNav = ({ {...topNavProps} appName="discover" indexPatterns={[dataView]} - onQuerySubmit={updateQuery} + onQuerySubmit={stateContainer.actions.onUpdateQuery} onCancel={onCancelClick} isLoading={isLoading} onSavedQueryIdChange={updateSavedQueryId} diff --git a/src/plugins/discover/public/application/main/components/top_nav/get_top_nav_links.tsx b/src/plugins/discover/public/application/main/components/top_nav/get_top_nav_links.tsx index 0e05b885af1604..ce1592c1598d3f 100644 --- a/src/plugins/discover/public/application/main/components/top_nav/get_top_nav_links.tsx +++ b/src/plugins/discover/public/application/main/components/top_nav/get_top_nav_links.tsx @@ -10,6 +10,7 @@ import { i18n } from '@kbn/i18n'; import type { DataView } from '@kbn/data-views-plugin/public'; import type { TopNavMenuData } from '@kbn/navigation-plugin/public'; import { setStateToKbnUrl } from '@kbn/kibana-utils-plugin/public'; +import { omit } from 'lodash'; import type { DiscoverAppLocatorParams } from '../../../../../common'; import { showOpenSearchPanel } from './show_open_search_panel'; import { getSharingData, showPublicUrlSwitch } from '../../../../utils/get_sharing_data'; @@ -138,7 +139,7 @@ export const getTopNavLinks = ({ // Share -> Get links -> Snapshot const params: DiscoverAppLocatorParams = { - ...appState, + ...omit(appState, 'dataSource'), ...(savedSearch.id ? { savedSearchId: savedSearch.id } : {}), ...(dataView?.isPersisted() ? { dataViewId: dataView?.id } diff --git a/src/plugins/discover/public/application/main/hooks/use_test_based_query_language.test.tsx b/src/plugins/discover/public/application/main/hooks/use_test_based_query_language.test.tsx index c7d530360ca3e0..8777b357714ba2 100644 --- a/src/plugins/discover/public/application/main/hooks/use_test_based_query_language.test.tsx +++ b/src/plugins/discover/public/application/main/hooks/use_test_based_query_language.test.tsx @@ -269,13 +269,7 @@ describe('useTextBasedQueryLanguage', () => { query: { esql: 'from the-data-view-title | keep field 1 | WHERE field1=1' }, }); - await waitFor(() => expect(replaceUrlState).toHaveBeenCalledTimes(1)); - await waitFor(() => { - expect(replaceUrlState).toHaveBeenCalledWith({ - columns: ['field1', 'field2'], - }); - }); - replaceUrlState.mockReset(); + expect(replaceUrlState).toHaveBeenCalledTimes(0); documents$.next({ recordRawType: RecordRawType.PLAIN, diff --git a/src/plugins/discover/public/application/main/hooks/use_text_based_query_language.ts b/src/plugins/discover/public/application/main/hooks/use_text_based_query_language.ts index 0a5b7c021046f4..063a5c21bfbe3a 100644 --- a/src/plugins/discover/public/application/main/hooks/use_text_based_query_language.ts +++ b/src/plugins/discover/public/application/main/hooks/use_text_based_query_language.ts @@ -64,7 +64,7 @@ export function useTextBasedQueryLanguage({ fetchStatus: FetchStatus.COMPLETE, }); }; - const { index, viewMode } = stateContainer.appState.getState(); + const { viewMode } = stateContainer.appState.getState(); let nextColumns: string[] = []; const isTextBasedQueryLang = recordRawType === 'plain' && isOfAggregateQueryType(query); const hasResults = Boolean(next.result?.length); @@ -83,7 +83,6 @@ export function useTextBasedQueryLanguage({ if (next.fetchStatus !== FetchStatus.PARTIAL) { return; } - const dataViewObj = stateContainer.internalState.getState().dataView; if (hasResults) { // check if state needs to contain column transformation due to a different columns in the resultset @@ -94,22 +93,16 @@ export function useTextBasedQueryLanguage({ initialFetch.current = false; } else { nextColumns = firstRowColumns; - if ( - initialFetch.current && - !prev.current.columns.length && - Boolean(dataViewObj?.id === index) - ) { + if (initialFetch.current && !prev.current.columns.length) { prev.current.columns = firstRowColumns; } } } const addColumnsToState = !isEqual(nextColumns, prev.current.columns); const queryChanged = query[language] !== prev.current.query; - // no need to reset index to state if it hasn't changed - const addDataViewToState = index !== undefined; const changeViewMode = viewMode !== getValidViewMode({ viewMode, isTextBasedQueryMode: true }); - if (!queryChanged || (!addDataViewToState && !addColumnsToState && !changeViewMode)) { + if (!queryChanged || (!addColumnsToState && !changeViewMode)) { sendComplete(); return; } @@ -119,9 +112,8 @@ export function useTextBasedQueryLanguage({ prev.current.columns = nextColumns; } // just change URL state if necessary - if (addDataViewToState || addColumnsToState || changeViewMode) { + if (addColumnsToState || changeViewMode) { const nextState = { - ...(addDataViewToState && { index: undefined }), ...(addColumnsToState && { columns: nextColumns }), ...(changeViewMode && { viewMode: undefined }), }; diff --git a/src/plugins/discover/public/application/main/state_management/discover_app_state_container.test.ts b/src/plugins/discover/public/application/main/state_management/discover_app_state_container.test.ts index 0ddc8c47c27677..75ae6208be8717 100644 --- a/src/plugins/discover/public/application/main/state_management/discover_app_state_container.test.ts +++ b/src/plugins/discover/public/application/main/state_management/discover_app_state_container.test.ts @@ -19,6 +19,7 @@ import { isEqualState, } from './discover_app_state_container'; import { SavedSearch, VIEW_MODE } from '@kbn/saved-search-plugin/common'; +import { createDataViewDataSource } from '../../../../common/data_sources'; let history: History; let state: DiscoverAppStateContainer; @@ -40,16 +41,22 @@ describe('Test discover app state container', () => { }); test('hasChanged returns whether the current state has changed', async () => { - state.set({ index: 'modified' }); + state.set({ + dataSource: createDataViewDataSource({ dataViewId: 'modified' }), + }); expect(state.hasChanged()).toBeTruthy(); state.resetInitialState(); expect(state.hasChanged()).toBeFalsy(); }); test('getPrevious returns the state before the current', async () => { - state.set({ index: 'first' }); + state.set({ + dataSource: createDataViewDataSource({ dataViewId: 'first' }), + }); const stateA = state.getState(); - state.set({ index: 'second' }); + state.set({ + dataSource: createDataViewDataSource({ dataViewId: 'second' }), + }); expect(state.getPrevious()).toEqual(stateA); }); @@ -111,7 +118,7 @@ describe('Test discover app state container', () => { filters: [customFilter], grid: undefined, hideChart: true, - index: 'the-data-view-id', + dataSource: createDataViewDataSource({ dataViewId: 'the-data-view-id' }), interval: 'auto', query: customQuery, rowHeight: undefined, @@ -147,7 +154,7 @@ describe('Test discover app state container', () => { filters: [customFilter], grid: undefined, hideChart: undefined, - index: 'the-data-view-id', + dataSource: createDataViewDataSource({ dataViewId: 'the-data-view-id' }), interval: 'auto', query: defaultQuery, rowHeight: undefined, @@ -217,10 +224,24 @@ describe('Test discover app state container', () => { expect(isEqualState(initialState, { ...initialState, ...param })).toBeFalsy(); }); }); + test('allows to exclude variables from comparison', () => { expect( - isEqualState(initialState, { ...initialState, index: undefined }, ['index']) + isEqualState(initialState, { ...initialState, dataSource: undefined }, ['dataSource']) ).toBeTruthy(); }); }); + + test('should automatically set ES|QL data source when query is ES|QL', () => { + state.update({ + dataSource: createDataViewDataSource({ dataViewId: 'test' }), + }); + expect(state.get().dataSource?.type).toBe('dataView'); + state.update({ + query: { + esql: 'from test', + }, + }); + expect(state.get().dataSource?.type).toBe('esql'); + }); }); diff --git a/src/plugins/discover/public/application/main/state_management/discover_app_state_container.ts b/src/plugins/discover/public/application/main/state_management/discover_app_state_container.ts index ec5d2dc4e84374..5ec9ca4d7215c8 100644 --- a/src/plugins/discover/public/application/main/state_management/discover_app_state_container.ts +++ b/src/plugins/discover/public/application/main/state_management/discover_app_state_container.ts @@ -19,6 +19,7 @@ import { FilterCompareOptions, FilterStateStore, Query, + isOfAggregateQueryType, } from '@kbn/es-query'; import { SavedSearch, VIEW_MODE } from '@kbn/saved-search-plugin/public'; import { IKbnUrlStateStorage, ISyncStateRef, syncState } from '@kbn/kibana-utils-plugin/public'; @@ -30,6 +31,13 @@ import { addLog } from '../../../utils/add_log'; import { cleanupUrlState } from './utils/cleanup_url_state'; import { getStateDefaults } from './utils/get_state_defaults'; import { handleSourceColumnState } from '../../../utils/state_helpers'; +import { + createDataViewDataSource, + createEsqlDataSource, + DataSourceType, + DiscoverDataSource, + isDataSourceType, +} from '../../../../common/data_sources'; export const APP_STATE_URL_KEY = '_a'; export interface DiscoverAppStateContainer extends ReduxLikeStateContainer { @@ -100,9 +108,9 @@ export interface DiscoverAppState { */ hideChart?: boolean; /** - * id of the used data view + * The current data source */ - index?: string; + dataSource?: DiscoverDataSource; /** * Used interval of the histogram */ @@ -174,15 +182,23 @@ export const getDiscoverAppStateContainer = ({ const enhancedAppContainer = { ...appStateContainer, set: (value: DiscoverAppState | null) => { - if (value) { - previousState = appStateContainer.getState(); - appStateContainer.set(value); + if (!value) { + return; + } + + previousState = appStateContainer.getState(); + + // When updating to an ES|QL query, sync the data source + if (isOfAggregateQueryType(value.query)) { + value.dataSource = createEsqlDataSource(); } + + appStateContainer.set(value); }, }; const hasChanged = () => { - return !isEqualState(initialState, appStateContainer.getState()); + return !isEqualState(initialState, enhancedAppContainer.getState()); }; const getAppStateFromSavedSearch = (newSavedSearch: SavedSearch) => { @@ -195,17 +211,17 @@ export const getDiscoverAppStateContainer = ({ const resetToState = (state: DiscoverAppState) => { addLog('[appState] reset state to', state); previousState = state; - appStateContainer.set(state); + enhancedAppContainer.set(state); }; const resetInitialState = () => { addLog('[appState] reset initial state to the current state'); - initialState = appStateContainer.getState(); + initialState = enhancedAppContainer.getState(); }; const replaceUrlState = async (newPartial: DiscoverAppState = {}, merge = true) => { addLog('[appState] replaceUrlState', { newPartial, merge }); - const state = merge ? { ...appStateContainer.getState(), ...newPartial } : newPartial; + const state = merge ? { ...enhancedAppContainer.getState(), ...newPartial } : newPartial; await stateStorage.set(APP_STATE_URL_KEY, state, { replace: true }); }; @@ -220,17 +236,28 @@ export const getDiscoverAppStateContainer = ({ const initializeAndSync = (currentSavedSearch: SavedSearch) => { addLog('[appState] initialize state and sync with URL', currentSavedSearch); + const { data } = services; - const dataView = currentSavedSearch.searchSource.getField('index'); + const savedSearchDataView = currentSavedSearch.searchSource.getField('index'); + const appState = enhancedAppContainer.getState(); + const setDataViewFromSavedSearch = + !appState.dataSource || + (isDataSourceType(appState.dataSource, DataSourceType.DataView) && + appState.dataSource.dataViewId !== savedSearchDataView?.id); - if (appStateContainer.getState().index !== dataView?.id) { + if (setDataViewFromSavedSearch) { // used data view is different from the given by url/state which is invalid - setState(appStateContainer, { index: dataView?.id }); + setState(enhancedAppContainer, { + dataSource: savedSearchDataView?.id + ? createDataViewDataSource({ dataViewId: savedSearchDataView.id }) + : undefined, + }); } + // syncs `_a` portion of url with query services const stopSyncingQueryAppStateWithStateContainer = connectToQueryState( data.query, - appStateContainer, + enhancedAppContainer, { filters: FilterStateStore.APP_STATE, query: true, @@ -244,6 +271,7 @@ export const getDiscoverAppStateContainer = ({ ); const { start, stop } = startAppStateUrlSync(); + // current state need to be pushed to url replaceUrlState({}).then(() => start()); @@ -259,8 +287,8 @@ export const getDiscoverAppStateContainer = ({ if (replace) { return replaceUrlState(newPartial); } else { - previousState = { ...appStateContainer.getState() }; - setState(appStateContainer, newPartial); + previousState = { ...enhancedAppContainer.getState() }; + setState(enhancedAppContainer, newPartial); } }; @@ -291,6 +319,10 @@ export interface AppStateUrl extends Omit { * Necessary to take care of legacy links [fieldName,direction] */ sort?: string[][] | [string, string]; + /** + * Legacy data view ID prop + */ + index?: string; } export function getInitialState( @@ -298,17 +330,17 @@ export function getInitialState( savedSearch: SavedSearch, services: DiscoverServices ) { - const stateStorageURL = stateStorage?.get(APP_STATE_URL_KEY) as AppStateUrl; + const appStateFromUrl = stateStorage?.get(APP_STATE_URL_KEY); const defaultAppState = getStateDefaults({ savedSearch, services, }); return handleSourceColumnState( - stateStorageURL === null + appStateFromUrl == null ? defaultAppState : { ...defaultAppState, - ...cleanupUrlState(stateStorageURL, services.uiSettings), + ...cleanupUrlState(appStateFromUrl, services.uiSettings), }, services.uiSettings ); @@ -353,7 +385,7 @@ export function isEqualFilters( export function isEqualState( stateA: DiscoverAppState, stateB: DiscoverAppState, - exclude: string[] = [] + exclude: Array = [] ) { if (!stateA && !stateB) { return true; @@ -361,8 +393,8 @@ export function isEqualState( return false; } - const { filters: stateAFilters = [], ...stateAPartial } = omit(stateA, exclude); - const { filters: stateBFilters = [], ...stateBPartial } = omit(stateB, exclude); + const { filters: stateAFilters = [], ...stateAPartial } = omit(stateA, exclude as string[]); + const { filters: stateBFilters = [], ...stateBPartial } = omit(stateB, exclude as string[]); return isEqual(stateAPartial, stateBPartial) && isEqualFilters(stateAFilters, stateBFilters); } diff --git a/src/plugins/discover/public/application/main/state_management/discover_state.test.ts b/src/plugins/discover/public/application/main/state_management/discover_state.test.ts index 39aa114673cbba..db8666cf727513 100644 --- a/src/plugins/discover/public/application/main/state_management/discover_state.test.ts +++ b/src/plugins/discover/public/application/main/state_management/discover_state.test.ts @@ -30,6 +30,7 @@ import { dataViewAdHoc, dataViewComplexMock } from '../../../__mocks__/data_view import { copySavedSearch } from './discover_saved_search_container'; import { createKbnUrlStateStorage, IKbnUrlStateStorage } from '@kbn/kibana-utils-plugin/public'; import { mockCustomizationContext } from '../../../customizations/__mocks__/customization_context'; +import { createDataViewDataSource, createEsqlDataSource } from '../../../../common/data_sources'; const startSync = (appState: DiscoverAppStateContainer) => { const { start, stop } = appState.syncState(); @@ -102,40 +103,51 @@ describe('Test discover state', () => { stopSync = () => {}; }); test('setting app state and syncing to URL', async () => { - state.appState.update({ index: 'modified' }); + state.appState.update({ + dataSource: createDataViewDataSource({ dataViewId: 'modified' }), + }); await new Promise(process.nextTick); expect(getCurrentUrl()).toMatchInlineSnapshot( - `"/#?_a=(columns:!(default_column),index:modified,interval:auto,sort:!())"` + `"/#?_a=(columns:!(default_column),dataSource:(dataViewId:modified,type:dataView),interval:auto,sort:!())"` ); }); test('changing URL to be propagated to appState', async () => { - history.push('/#?_a=(index:modified)'); + history.push('/#?_a=(dataSource:(dataViewId:modified,type:dataView))'); expect(state.appState.getState()).toMatchInlineSnapshot(` Object { - "index": "modified", + "dataSource": Object { + "dataViewId": "modified", + "type": "dataView", + }, } `); }); test('URL navigation to url without _a, state should not change', async () => { - history.push('/#?_a=(index:modified)'); + history.push('/#?_a=(dataSource:(dataViewId:modified,type:dataView))'); history.push('/'); expect(state.appState.getState()).toEqual({ - index: 'modified', + dataSource: createDataViewDataSource({ dataViewId: 'modified' }), }); }); test('isAppStateDirty returns whether the current state has changed', async () => { - state.appState.update({ index: 'modified' }); + state.appState.update({ + dataSource: createDataViewDataSource({ dataViewId: 'modified' }), + }); expect(state.appState.hasChanged()).toBeTruthy(); state.appState.resetInitialState(); expect(state.appState.hasChanged()).toBeFalsy(); }); test('getPreviousAppState returns the state before the current', async () => { - state.appState.update({ index: 'first' }); + state.appState.update({ + dataSource: createDataViewDataSource({ dataViewId: 'first' }), + }); const stateA = state.appState.getState(); - state.appState.update({ index: 'second' }); + state.appState.update({ + dataSource: createDataViewDataSource({ dataViewId: 'second' }), + }); expect(state.appState.getPrevious()).toEqual(stateA); }); @@ -189,23 +201,28 @@ describe('Test discover state with overridden state storage', () => { }); test('setting app state and syncing to URL', async () => { - state.appState.update({ index: 'modified' }); + state.appState.update({ + dataSource: createDataViewDataSource({ dataViewId: 'modified' }), + }); await jest.runAllTimersAsync(); expect(history.createHref(history.location)).toMatchInlineSnapshot( - `"/#?_a=(columns:!(default_column),index:modified,interval:auto,sort:!())"` + `"/#?_a=(columns:!(default_column),dataSource:(dataViewId:modified,type:dataView),interval:auto,sort:!())"` ); }); test('changing URL to be propagated to appState', async () => { - history.push('/#?_a=(index:modified)'); + history.push('/#?_a=(dataSource:(dataViewId:modified,type:dataView))'); await jest.runAllTimersAsync(); expect(state.appState.getState()).toMatchInlineSnapshot(` Object { - "index": "modified", + "dataSource": Object { + "dataViewId": "modified", + "type": "dataView", + }, } `); }); @@ -263,7 +280,9 @@ describe('Test createSearchSessionRestorationDataProvider', () => { customizationContext: mockCustomizationContext, }); discoverStateContainer.appState.update({ - index: savedSearchMock.searchSource.getField('index')!.id, + dataSource: createDataViewDataSource({ + dataViewId: savedSearchMock.searchSource.getField('index')!.id!, + }), }); const searchSessionInfoProvider = createSearchSessionRestorationDataProvider({ data: mockDataPlugin, @@ -428,7 +447,7 @@ describe('Test discover state actions', () => { const unsubscribe = state.actions.initializeAndSync(); await new Promise(process.nextTick); expect(getCurrentUrl()).toMatchInlineSnapshot( - `"/#?_g=(refreshInterval:(pause:!t,value:1000),time:(from:now-15d,to:now))&_a=(columns:!(default_column),index:the-data-view-id,interval:auto,sort:!())"` + `"/#?_g=(refreshInterval:(pause:!t,value:1000),time:(from:now-15d,to:now))&_a=(columns:!(default_column),dataSource:(dataViewId:the-data-view-id,type:dataView),interval:auto,sort:!())"` ); expect(state.savedSearchState.getHasChanged$().getValue()).toBe(false); const { searchSource, ...savedSearch } = state.savedSearchState.getState(); @@ -461,7 +480,7 @@ describe('Test discover state actions', () => { const unsubscribe = state.actions.initializeAndSync(); await new Promise(process.nextTick); expect(getCurrentUrl()).toMatchInlineSnapshot( - `"/#?_g=(refreshInterval:(pause:!t,value:1000),time:(from:now-15d,to:now))&_a=(columns:!(default_column),index:the-data-view-id,interval:auto,sort:!())"` + `"/#?_g=(refreshInterval:(pause:!t,value:1000),time:(from:now-15d,to:now))&_a=(columns:!(default_column),dataSource:(dataViewId:the-data-view-id,type:dataView),interval:auto,sort:!())"` ); expect(state.savedSearchState.getHasChanged$().getValue()).toBe(false); unsubscribe(); @@ -476,7 +495,7 @@ describe('Test discover state actions', () => { const unsubscribe = state.actions.initializeAndSync(); await new Promise(process.nextTick); expect(getCurrentUrl()).toMatchInlineSnapshot( - `"/#?_a=(columns:!(bytes),index:the-data-view-id,interval:month,sort:!())&_g=()"` + `"/#?_a=(columns:!(bytes),dataSource:(dataViewId:the-data-view-id,type:dataView),interval:month,sort:!())&_g=()"` ); expect(state.savedSearchState.getHasChanged$().getValue()).toBe(true); unsubscribe(); @@ -491,7 +510,7 @@ describe('Test discover state actions', () => { const unsubscribe = state.actions.initializeAndSync(); await new Promise(process.nextTick); expect(getCurrentUrl()).toMatchInlineSnapshot( - `"/#?_a=(columns:!(bytes),index:the-data-view-id,interval:month,sort:!())&_g=()"` + `"/#?_a=(columns:!(bytes),dataSource:(dataViewId:the-data-view-id,type:dataView),interval:month,sort:!())&_g=()"` ); expect(state.savedSearchState.getHasChanged$().getValue()).toBe(true); unsubscribe(); @@ -505,7 +524,7 @@ describe('Test discover state actions', () => { await new Promise(process.nextTick); expect(newSavedSearch?.id).toBe('the-saved-search-id'); expect(getCurrentUrl()).toMatchInlineSnapshot( - `"/#?_g=(refreshInterval:(pause:!t,value:1000),time:(from:now-15d,to:now))&_a=(columns:!(default_column),index:the-data-view-id,interval:auto,sort:!())"` + `"/#?_g=(refreshInterval:(pause:!t,value:1000),time:(from:now-15d,to:now))&_a=(columns:!(default_column),dataSource:(dataViewId:the-data-view-id,type:dataView),interval:auto,sort:!())"` ); expect(state.savedSearchState.getHasChanged$().getValue()).toBe(false); unsubscribe(); @@ -520,7 +539,7 @@ describe('Test discover state actions', () => { const unsubscribe = state.actions.initializeAndSync(); await new Promise(process.nextTick); expect(getCurrentUrl()).toMatchInlineSnapshot( - `"/#?_a=(columns:!(message),index:the-data-view-id,interval:month,sort:!())&_g=()"` + `"/#?_a=(columns:!(message),dataSource:(dataViewId:the-data-view-id,type:dataView),interval:month,sort:!())&_g=()"` ); expect(state.savedSearchState.getHasChanged$().getValue()).toBe(true); unsubscribe(); @@ -600,7 +619,7 @@ describe('Test discover state actions', () => { }); test('loadSavedSearch without id ignoring invalid index in URL, adding a warning toast', async () => { - const url = '/#?_a=(index:abc)&_g=()'; + const url = '/#?_a=(dataSource:(dataViewId:abc,type:dataView))&_g=()'; const { state } = await getState(url, { savedSearch: savedSearchMock, isEmptyUrl: false }); await state.actions.loadSavedSearch(); expect(state.savedSearchState.getState().searchSource.getField('index')?.id).toBe( @@ -614,14 +633,16 @@ describe('Test discover state actions', () => { }); test('loadSavedSearch without id containing ES|QL, adding no warning toast with an invalid index', async () => { - const url = "/#?_a=(index:abcde,query:(esql:'FROM test'))&_g=()"; + const url = + "/#?_a=(dataSource:(dataViewId:abcde,type:dataView),query:(esql:'FROM test'))&_g=()"; const { state } = await getState(url, { savedSearch: savedSearchMock, isEmptyUrl: false }); await state.actions.loadSavedSearch(); + expect(state.appState.getState().dataSource).toEqual(createEsqlDataSource()); expect(discoverServiceMock.toastNotifications.addWarning).not.toHaveBeenCalled(); }); test('loadSavedSearch with id ignoring invalid index in URL, adding a warning toast', async () => { - const url = '/#?_a=(index:abc)&_g=()'; + const url = '/#?_a=(dataSource:(dataViewId:abc,type:dataView))&_g=()'; const { state } = await getState(url, { savedSearch: savedSearchMock, isEmptyUrl: false }); await state.actions.loadSavedSearch({ savedSearchId: savedSearchMock.id }); expect(state.savedSearchState.getState().searchSource.getField('index')?.id).toBe( @@ -644,7 +665,9 @@ describe('Test discover state actions', () => { state.savedSearchState.load = jest.fn().mockReturnValue(savedSearchMockWithTimeField); // unsetting the previous index else this is considered as update to the persisted saved search - state.appState.set({ index: undefined }); + state.appState.set({ + dataSource: undefined, + }); await state.actions.loadSavedSearch({ savedSearchId: 'the-saved-search-id-with-timefield' }); expect(state.savedSearchState.getState().searchSource.getField('index')?.id).toBe( 'index-pattern-with-timefield-id' @@ -711,14 +734,16 @@ describe('Test discover state actions', () => { const adHocDataViewId = savedSearchAdHoc.searchSource.getField('index')!.id; const { state } = await getState('/', { savedSearch: savedSearchAdHocCopy }); await state.actions.loadSavedSearch({ savedSearchId: savedSearchAdHoc.id }); - expect(state.appState.getState().index).toBe(adHocDataViewId); + expect(state.appState.getState().dataSource).toEqual( + createDataViewDataSource({ dataViewId: adHocDataViewId! }) + ); expect(state.internalState.getState().adHocDataViews[0].id).toBe(adHocDataViewId); }); test('loadSavedSearch with ES|QL, data view index is not overwritten by URL ', async () => { const savedSearchMockWithESQLCopy = copySavedSearch(savedSearchMockWithESQL); const persistedDataViewId = savedSearchMockWithESQLCopy?.searchSource.getField('index')!.id; - const url = "/#?_a=(index:'the-data-view-id')&_g=()"; + const url = "/#?_a=(dataSource:(dataViewId:'the-data-view-id',type:dataView))&_g=()"; const { state } = await getState(url, { savedSearch: savedSearchMockWithESQLCopy, isEmptyUrl: false, @@ -731,7 +756,7 @@ describe('Test discover state actions', () => { test('onChangeDataView', async () => { const { state, getCurrentUrl } = await getState('/', { savedSearch: savedSearchMock }); - const { actions, savedSearchState, dataState, appState } = state; + const { actions, savedSearchState, dataState } = state; await actions.loadSavedSearch({ savedSearchId: savedSearchMock.id }); const unsubscribe = actions.initializeAndSync(); @@ -747,7 +772,9 @@ describe('Test discover state actions', () => { // test changed state, fetch should be called once and URL should be updated expect(dataState.fetch).toHaveBeenCalledTimes(1); - expect(appState.get().index).toBe(dataViewComplexMock.id); + expect(state.appState.getState().dataSource).toEqual( + createDataViewDataSource({ dataViewId: dataViewComplexMock.id! }) + ); expect(savedSearchState.getState().searchSource.getField('index')!.id).toBe( dataViewComplexMock.id ); @@ -763,7 +790,9 @@ describe('Test discover state actions', () => { await waitFor(() => { expect(state.internalState.getState().dataView?.id).toBe(dataViewComplexMock.id); }); - expect(state.appState.get().index).toBe(dataViewComplexMock.id); + expect(state.appState.getState().dataSource).toEqual( + createDataViewDataSource({ dataViewId: dataViewComplexMock.id! }) + ); expect(state.savedSearchState.getState().searchSource.getField('index')!.id).toBe( dataViewComplexMock.id ); @@ -777,7 +806,9 @@ describe('Test discover state actions', () => { await waitFor(() => { expect(state.internalState.getState().dataView?.id).toBe(dataViewAdHoc.id); }); - expect(state.appState.get().index).toBe(dataViewAdHoc.id); + expect(state.appState.getState().dataSource).toEqual( + createDataViewDataSource({ dataViewId: dataViewAdHoc.id! }) + ); expect(state.savedSearchState.getState().searchSource.getField('index')!.id).toBe( dataViewAdHoc.id ); @@ -838,7 +869,9 @@ describe('Test discover state actions', () => { await state.actions.loadSavedSearch({ savedSearchId: savedSearchMock.id }); const unsubscribe = state.actions.initializeAndSync(); await state.actions.createAndAppendAdHocDataView({ title: 'ad-hoc-test' }); - expect(state.appState.getState().index).toBe('ad-hoc-id'); + expect(state.appState.getState().dataSource).toEqual( + createDataViewDataSource({ dataViewId: 'ad-hoc-id' }) + ); expect(state.internalState.getState().adHocDataViews[0].id).toBe('ad-hoc-id'); unsubscribe(); }); @@ -849,7 +882,7 @@ describe('Test discover state actions', () => { const unsubscribe = state.actions.initializeAndSync(); await new Promise(process.nextTick); const initialUrlState = - '/#?_g=(refreshInterval:(pause:!t,value:1000),time:(from:now-15d,to:now))&_a=(columns:!(default_column),index:the-data-view-id,interval:auto,sort:!())'; + '/#?_g=(refreshInterval:(pause:!t,value:1000),time:(from:now-15d,to:now))&_a=(columns:!(default_column),dataSource:(dataViewId:the-data-view-id,type:dataView),interval:auto,sort:!())'; expect(getCurrentUrl()).toBe(initialUrlState); expect(state.internalState.getState().dataView?.id).toBe(dataViewMock.id!); @@ -857,7 +890,7 @@ describe('Test discover state actions', () => { await state.actions.onChangeDataView(dataViewComplexMock.id!); await new Promise(process.nextTick); expect(getCurrentUrl()).toMatchInlineSnapshot( - `"/#?_g=(refreshInterval:(pause:!t,value:1000),time:(from:now-15d,to:now))&_a=(columns:!(),index:data-view-with-various-field-types-id,interval:auto,sort:!(!(data,desc)))"` + `"/#?_g=(refreshInterval:(pause:!t,value:1000),time:(from:now-15d,to:now))&_a=(columns:!(),dataSource:(dataViewId:data-view-with-various-field-types-id,type:dataView),interval:auto,sort:!(!(data,desc)))"` ); await waitFor(() => { expect(state.dataState.fetch).toHaveBeenCalledTimes(1); @@ -925,18 +958,20 @@ describe('Test discover state with embedded mode', () => { }); test('setting app state and syncing to URL', async () => { - state.appState.update({ index: 'modified' }); + state.appState.update({ + dataSource: createDataViewDataSource({ dataViewId: 'modified' }), + }); await new Promise(process.nextTick); expect(getCurrentUrl()).toMatchInlineSnapshot( - `"/?_a=(columns:!(default_column),index:modified,interval:auto,sort:!())"` + `"/?_a=(columns:!(default_column),dataSource:(dataViewId:modified,type:dataView),interval:auto,sort:!())"` ); }); test('changing URL to be propagated to appState', async () => { - history.push('/?_a=(index:modified)'); + history.push('/?_a=(dataSource:(dataViewId:modified,type:dataView))'); expect(state.appState.getState()).toMatchObject( expect.objectContaining({ - index: 'modified', + dataSource: createDataViewDataSource({ dataViewId: 'modified' }), }) ); }); diff --git a/src/plugins/discover/public/application/main/state_management/discover_state.ts b/src/plugins/discover/public/application/main/state_management/discover_state.ts index 4816ac585c1424..169e596a2cf931 100644 --- a/src/plugins/discover/public/application/main/state_management/discover_state.ts +++ b/src/plugins/discover/public/application/main/state_management/discover_state.ts @@ -54,6 +54,11 @@ import { DiscoverGlobalStateContainer, } from './discover_global_state_container'; import type { DiscoverCustomizationContext } from '../../../customizations'; +import { + createDataViewDataSource, + DataSourceType, + isDataSourceType, +} from '../../../../common/data_sources'; export interface DiscoverStateContainerParams { /** @@ -303,21 +308,34 @@ export function getDiscoverStateContainer({ const updateAdHocDataViewId = async () => { const prevDataView = internalStateContainer.getState().dataView; if (!prevDataView || prevDataView.isPersisted()) return; - const newDataView = await services.dataViews.create({ ...prevDataView.toSpec(), id: uuidv4() }); + + const nextDataView = await services.dataViews.create({ + ...prevDataView.toSpec(), + id: uuidv4(), + }); + services.dataViews.clearInstanceCache(prevDataView.id); updateFiltersReferences({ prevDataView, - nextDataView: newDataView, + nextDataView, services, }); - internalStateContainer.transitions.replaceAdHocDataViewWithId(prevDataView.id!, newDataView); - await appStateContainer.replaceUrlState({ index: newDataView.id }); - const trackingEnabled = Boolean(newDataView.isPersisted() || savedSearchContainer.getId()); + internalStateContainer.transitions.replaceAdHocDataViewWithId(prevDataView.id!, nextDataView); + + if (isDataSourceType(appStateContainer.get().dataSource, DataSourceType.DataView)) { + await appStateContainer.replaceUrlState({ + dataSource: nextDataView.id + ? createDataViewDataSource({ dataViewId: nextDataView.id }) + : undefined, + }); + } + + const trackingEnabled = Boolean(nextDataView.isPersisted() || savedSearchContainer.getId()); services.urlTracker.setTrackingEnabled(trackingEnabled); - return newDataView; + return nextDataView; }; const onOpenSavedSearch = async (newSavedSearchId: string) => { @@ -583,7 +601,7 @@ function createUrlGeneratorState({ const dataView = getSavedSearch().searchSource.getField('index'); return { filters: data.query.filterManager.getFilters(), - dataViewId: appState.index, + dataViewId: dataView?.id, query: appState.query, savedSearchId: getSavedSearch().id, timeRange: shouldRestoreSearchSession diff --git a/src/plugins/discover/public/application/main/state_management/utils/build_state_subscribe.test.ts b/src/plugins/discover/public/application/main/state_management/utils/build_state_subscribe.test.ts index 563473c2743225..1a487c5bb3bcb9 100644 --- a/src/plugins/discover/public/application/main/state_management/utils/build_state_subscribe.test.ts +++ b/src/plugins/discover/public/application/main/state_management/utils/build_state_subscribe.test.ts @@ -11,6 +11,7 @@ import { FetchStatus } from '../../../types'; import { dataViewComplexMock } from '../../../../__mocks__/data_view_complex'; import { getDiscoverStateMock } from '../../../../__mocks__/discover_state.mock'; import { discoverServiceMock } from '../../../../__mocks__/services'; +import { createDataViewDataSource } from '../../../../../common/data_sources'; describe('buildStateSubscribe', () => { const savedSearch = savedSearchMock; @@ -35,7 +36,9 @@ describe('buildStateSubscribe', () => { }); it('should set the data view if the index has changed, and refetch should be triggered', async () => { - await getSubscribeFn()({ index: dataViewComplexMock.id }); + await getSubscribeFn()({ + dataSource: createDataViewDataSource({ dataViewId: dataViewComplexMock.id! }), + }); expect(stateContainer.actions.setDataView).toHaveBeenCalledWith(dataViewComplexMock); expect(stateContainer.dataState.reset).toHaveBeenCalled(); @@ -75,18 +78,26 @@ describe('buildStateSubscribe', () => { it('should not execute setState function if initialFetchStatus is UNINITIALIZED', async () => { const stateSubscribeFn = getSubscribeFn(); stateContainer.dataState.getInitialFetchStatus = jest.fn(() => FetchStatus.UNINITIALIZED); - await stateSubscribeFn({ index: dataViewComplexMock.id }); + await stateSubscribeFn({ + dataSource: createDataViewDataSource({ dataViewId: dataViewComplexMock.id! }), + }); expect(stateContainer.dataState.reset).toHaveBeenCalled(); }); it('should not execute setState twice if the identical data view change is propagated twice', async () => { - await getSubscribeFn()({ index: dataViewComplexMock.id }); + await getSubscribeFn()({ + dataSource: createDataViewDataSource({ dataViewId: dataViewComplexMock.id! }), + }); expect(stateContainer.dataState.reset).toBeCalledTimes(1); - stateContainer.appState.getPrevious = jest.fn(() => ({ index: dataViewComplexMock.id })); + stateContainer.appState.getPrevious = jest.fn(() => ({ + dataSource: createDataViewDataSource({ dataViewId: dataViewComplexMock.id! }), + })); - await getSubscribeFn()({ index: dataViewComplexMock.id }); + await getSubscribeFn()({ + dataSource: createDataViewDataSource({ dataViewId: dataViewComplexMock.id! }), + }); expect(stateContainer.dataState.reset).toBeCalledTimes(1); }); }); diff --git a/src/plugins/discover/public/application/main/state_management/utils/build_state_subscribe.ts b/src/plugins/discover/public/application/main/state_management/utils/build_state_subscribe.ts index 6cc9b880089312..fbd324a1a417c7 100644 --- a/src/plugins/discover/public/application/main/state_management/utils/build_state_subscribe.ts +++ b/src/plugins/discover/public/application/main/state_management/utils/build_state_subscribe.ts @@ -20,6 +20,11 @@ import { addLog } from '../../../../utils/add_log'; import { isTextBasedQuery } from '../../utils/is_text_based_query'; import { FetchStatus } from '../../../types'; import { loadAndResolveDataView } from './resolve_data_view'; +import { + createDataViewDataSource, + DataSourceType, + isDataSourceType, +} from '../../../../../common/data_sources'; /** * Builds a subscribe function for the AppStateContainer, that is executed when the AppState changes in URL @@ -52,7 +57,7 @@ export const buildStateSubscribe = if ( isTextBasedQueryLang && - isEqualState(prevState, nextState, ['index', 'viewMode']) && + isEqualState(prevState, nextState, ['dataSource', 'viewMode']) && !queryChanged ) { // When there's a switch from data view to es|ql, this just leads to a cleanup of index and viewMode @@ -60,12 +65,13 @@ export const buildStateSubscribe = addLog('[appstate] subscribe update ignored for es|ql', { prevState, nextState }); return; } + if (isEqualState(prevState, nextState) && !queryChanged) { addLog('[appstate] subscribe update ignored due to no changes', { prevState, nextState }); return; } + addLog('[appstate] subscribe triggered', nextState); - const { hideChart, interval, breakdownField, sampleSize, sort, index } = prevState; if (isTextBasedQueryLang) { const isTextBasedQueryLangPrev = isTextBasedQuery(prevQuery); @@ -74,6 +80,8 @@ export const buildStateSubscribe = dataState.reset(savedSearch); } } + + const { hideChart, interval, breakdownField, sampleSize, sort, dataSource } = prevState; // Cast to boolean to avoid false positives when comparing // undefined and false, which would trigger a refetch const chartDisplayChanged = Boolean(nextState.hideChart) !== Boolean(hideChart); @@ -81,21 +89,36 @@ export const buildStateSubscribe = const breakdownFieldChanged = nextState.breakdownField !== breakdownField; const sampleSizeChanged = nextState.sampleSize !== sampleSize; const docTableSortChanged = !isEqual(nextState.sort, sort) && !isTextBasedQueryLang; - const dataViewChanged = !isEqual(nextState.index, index) && !isTextBasedQueryLang; + const dataSourceChanged = !isEqual(nextState.dataSource, dataSource) && !isTextBasedQueryLang; + let savedSearchDataView; + // NOTE: this is also called when navigating from discover app to context app - if (nextState.index && dataViewChanged) { + if (nextState.dataSource && dataSourceChanged) { + const dataViewId = isDataSourceType(nextState.dataSource, DataSourceType.DataView) + ? nextState.dataSource.dataViewId + : undefined; + const { dataView: nextDataView, fallback } = await loadAndResolveDataView( - { id: nextState.index, savedSearch, isTextBasedQuery: isTextBasedQuery(nextState?.query) }, + { id: dataViewId, savedSearch, isTextBasedQuery: isTextBasedQueryLang }, { internalStateContainer: internalState, services } ); // If the requested data view is not found, don't try to load it, // and instead reset the app state to the fallback data view if (fallback) { - appState.update({ index: nextDataView.id }, true); + appState.update( + { + dataSource: nextDataView.id + ? createDataViewDataSource({ dataViewId: nextDataView.id }) + : undefined, + }, + true + ); + return; } + savedSearch.searchSource.setField('index', nextDataView); dataState.reset(savedSearch); setDataView(nextDataView); @@ -104,7 +127,7 @@ export const buildStateSubscribe = savedSearchState.update({ nextDataView: savedSearchDataView, nextState }); - if (dataViewChanged && dataState.getInitialFetchStatus() === FetchStatus.UNINITIALIZED) { + if (dataSourceChanged && dataState.getInitialFetchStatus() === FetchStatus.UNINITIALIZED) { // stop execution if given data view has changed, and it's not configured to initially start a search in Discover return; } @@ -115,7 +138,7 @@ export const buildStateSubscribe = breakdownFieldChanged || sampleSizeChanged || docTableSortChanged || - dataViewChanged || + dataSourceChanged || queryChanged ) { const logData = { @@ -127,7 +150,7 @@ export const buildStateSubscribe = nextState.breakdownField ), docTableSortChanged: logEntry(docTableSortChanged, sort, nextState.sort), - dataViewChanged: logEntry(dataViewChanged, index, nextState.index), + dataSourceChanged: logEntry(dataSourceChanged, dataSource, nextState.dataSource), queryChanged: logEntry(queryChanged, prevQuery, nextQuery), }; diff --git a/src/plugins/discover/public/application/main/state_management/utils/change_data_view.test.ts b/src/plugins/discover/public/application/main/state_management/utils/change_data_view.test.ts index 62cca6a4199f88..4e486e588b8eba 100644 --- a/src/plugins/discover/public/application/main/state_management/utils/change_data_view.test.ts +++ b/src/plugins/discover/public/application/main/state_management/utils/change_data_view.test.ts @@ -17,6 +17,7 @@ import type { DataView } from '@kbn/data-views-plugin/common'; import { getDiscoverStateMock } from '../../../../__mocks__/discover_state.mock'; import { PureTransitionsToTransitions } from '@kbn/kibana-utils-plugin/common/state_containers'; import { InternalStateTransitions } from '../discover_internal_state_container'; +import { createDataViewDataSource } from '../../../../../common/data_sources'; const setupTestParams = (dataView: DataView | undefined) => { const savedSearch = savedSearchMock; @@ -44,7 +45,7 @@ describe('changeDataView', () => { await changeDataView(dataViewWithDefaultColumnMock.id!, params); expect(params.appState.update).toHaveBeenCalledWith({ columns: ['default_column'], // default_column would be added as dataViewWithDefaultColumn has it as a mapped field - index: 'data-view-with-user-default-column-id', + dataSource: createDataViewDataSource({ dataViewId: 'data-view-with-user-default-column-id' }), sort: [['@timestamp', 'desc']], }); expect(params.internalState.transitions.setIsDataViewLoading).toHaveBeenNthCalledWith(1, true); @@ -56,7 +57,7 @@ describe('changeDataView', () => { await changeDataView(dataViewComplexMock.id!, params); expect(params.appState.update).toHaveBeenCalledWith({ columns: [], // default_column would not be added as dataViewComplexMock does not have it as a mapped field - index: 'data-view-with-various-field-types-id', + dataSource: createDataViewDataSource({ dataViewId: 'data-view-with-various-field-types-id' }), sort: [['data', 'desc']], }); expect(params.internalState.transitions.setIsDataViewLoading).toHaveBeenNthCalledWith(1, true); diff --git a/src/plugins/discover/public/application/main/state_management/utils/cleanup_url_state.test.ts b/src/plugins/discover/public/application/main/state_management/utils/cleanup_url_state.test.ts index 46757b8fcffd85..bf5e30093dbbce 100644 --- a/src/plugins/discover/public/application/main/state_management/utils/cleanup_url_state.test.ts +++ b/src/plugins/discover/public/application/main/state_management/utils/cleanup_url_state.test.ts @@ -9,11 +9,12 @@ import { AppStateUrl } from '../discover_app_state_container'; import { cleanupUrlState } from './cleanup_url_state'; import { createDiscoverServicesMock } from '../../../../__mocks__/services'; +import { DataSourceType } from '../../../../../common/data_sources'; const services = createDiscoverServicesMock(); describe('cleanupUrlState', () => { - test('cleaning up legacy sort', async () => { + test('cleaning up legacy sort', () => { const state = { sort: ['batman', 'desc'] } as AppStateUrl; expect(cleanupUrlState(state, services.uiSettings)).toMatchInlineSnapshot(` Object { @@ -26,11 +27,13 @@ describe('cleanupUrlState', () => { } `); }); - test('not cleaning up broken legacy sort', async () => { + + test('not cleaning up broken legacy sort', () => { const state = { sort: ['batman'] } as unknown as AppStateUrl; expect(cleanupUrlState(state, services.uiSettings)).toMatchInlineSnapshot(`Object {}`); }); - test('not cleaning up regular sort', async () => { + + test('not cleaning up regular sort', () => { const state = { sort: [ ['batman', 'desc'], @@ -52,14 +55,15 @@ describe('cleanupUrlState', () => { } `); }); - test('removing empty sort', async () => { + + test('removing empty sort', () => { const state = { sort: [], } as AppStateUrl; expect(cleanupUrlState(state, services.uiSettings)).toMatchInlineSnapshot(`Object {}`); }); - test('should keep a valid rowsPerPage', async () => { + test('should keep a valid rowsPerPage', () => { const state = { rowsPerPage: 50, } as AppStateUrl; @@ -70,14 +74,14 @@ describe('cleanupUrlState', () => { `); }); - test('should remove a negative rowsPerPage', async () => { + test('should remove a negative rowsPerPage', () => { const state = { rowsPerPage: -50, } as AppStateUrl; expect(cleanupUrlState(state, services.uiSettings)).toMatchInlineSnapshot(`Object {}`); }); - test('should remove an invalid rowsPerPage', async () => { + test('should remove an invalid rowsPerPage', () => { const state = { rowsPerPage: 'test', } as unknown as AppStateUrl; @@ -85,7 +89,7 @@ describe('cleanupUrlState', () => { }); describe('sampleSize', function () { - test('should keep a valid sampleSize', async () => { + test('should keep a valid sampleSize', () => { const state = { sampleSize: 50, } as AppStateUrl; @@ -96,7 +100,7 @@ describe('cleanupUrlState', () => { `); }); - test('should remove for ES|QL', async () => { + test('should remove for ES|QL', () => { const state = { sampleSize: 50, query: { @@ -112,25 +116,78 @@ describe('cleanupUrlState', () => { `); }); - test('should remove a negative sampleSize', async () => { + test('should remove a negative sampleSize', () => { const state = { sampleSize: -50, } as AppStateUrl; expect(cleanupUrlState(state, services.uiSettings)).toMatchInlineSnapshot(`Object {}`); }); - test('should remove an invalid sampleSize', async () => { + test('should remove an invalid sampleSize', () => { const state = { sampleSize: 'test', } as unknown as AppStateUrl; expect(cleanupUrlState(state, services.uiSettings)).toMatchInlineSnapshot(`Object {}`); }); - test('should remove a too large sampleSize', async () => { + test('should remove a too large sampleSize', () => { const state = { sampleSize: 500000, } as AppStateUrl; expect(cleanupUrlState(state, services.uiSettings)).toMatchInlineSnapshot(`Object {}`); }); }); + + describe('index', () => { + it('should convert index to a data view dataSource', () => { + const state: AppStateUrl = { + index: 'test', + }; + expect(cleanupUrlState(state, services.uiSettings)).toMatchInlineSnapshot(` + Object { + "dataSource": Object { + "dataViewId": "test", + "type": "dataView", + }, + } + `); + }); + + it('should not override the dataSource if one is already set', () => { + const state: AppStateUrl = { + index: 'test', + dataSource: { + type: DataSourceType.DataView, + dataViewId: 'test2', + }, + }; + expect(cleanupUrlState(state, services.uiSettings)).toMatchInlineSnapshot(` + Object { + "dataSource": Object { + "dataViewId": "test2", + "type": "dataView", + }, + } + `); + }); + + it('should set an ES|QL dataSource if the query is an ES|QL query', () => { + const state: AppStateUrl = { + index: 'test', + query: { + esql: 'from test', + }, + }; + expect(cleanupUrlState(state, services.uiSettings)).toMatchInlineSnapshot(` + Object { + "dataSource": Object { + "type": "esql", + }, + "query": Object { + "esql": "from test", + }, + } + `); + }); + }); }); diff --git a/src/plugins/discover/public/application/main/state_management/utils/cleanup_url_state.ts b/src/plugins/discover/public/application/main/state_management/utils/cleanup_url_state.ts index 07b939c162f718..ebf5f8dc90bd39 100644 --- a/src/plugins/discover/public/application/main/state_management/utils/cleanup_url_state.ts +++ b/src/plugins/discover/public/application/main/state_management/utils/cleanup_url_state.ts @@ -10,6 +10,7 @@ import type { IUiSettingsClient } from '@kbn/core-ui-settings-browser'; import { DiscoverAppState, AppStateUrl } from '../discover_app_state_container'; import { migrateLegacyQuery } from '../../../../utils/migrate_legacy_query'; import { getMaxAllowedSampleSize } from '../../../../utils/get_allowed_sample_size'; +import { createDataViewDataSource, createEsqlDataSource } from '../../../../../common/data_sources'; /** * Takes care of the given url state, migrates legacy props and cleans up empty props @@ -20,7 +21,6 @@ export function cleanupUrlState( uiSettings: IUiSettingsClient ): DiscoverAppState { if ( - appStateFromUrl && appStateFromUrl.query && !isOfAggregateQueryType(appStateFromUrl.query) && !appStateFromUrl.query.language @@ -28,8 +28,8 @@ export function cleanupUrlState( appStateFromUrl.query = migrateLegacyQuery(appStateFromUrl.query); } - if (typeof appStateFromUrl?.sort?.[0] === 'string') { - if (appStateFromUrl?.sort?.[1] === 'asc' || appStateFromUrl.sort[1] === 'desc') { + if (typeof appStateFromUrl.sort?.[0] === 'string') { + if (appStateFromUrl.sort?.[1] === 'asc' || appStateFromUrl.sort[1] === 'desc') { // handling sort props like this[fieldName,direction] appStateFromUrl.sort = [[appStateFromUrl.sort[0], appStateFromUrl.sort[1]]]; } else { @@ -37,14 +37,14 @@ export function cleanupUrlState( } } - if (appStateFromUrl?.sort && !appStateFromUrl.sort.length) { + if (appStateFromUrl.sort && !appStateFromUrl.sort.length) { // If there's an empty array given in the URL, the sort prop should be removed // This allows the sort prop to be overwritten with the default sorting delete appStateFromUrl.sort; } if ( - appStateFromUrl?.rowsPerPage && + appStateFromUrl.rowsPerPage && !(typeof appStateFromUrl.rowsPerPage === 'number' && appStateFromUrl.rowsPerPage > 0) ) { // remove the param if it's invalid @@ -52,7 +52,7 @@ export function cleanupUrlState( } if ( - appStateFromUrl?.sampleSize && + appStateFromUrl.sampleSize && (isOfAggregateQueryType(appStateFromUrl.query) || // not supported yet for ES|QL !( typeof appStateFromUrl.sampleSize === 'number' && @@ -64,5 +64,16 @@ export function cleanupUrlState( delete appStateFromUrl.sampleSize; } + if (appStateFromUrl.index) { + if (!appStateFromUrl.dataSource) { + // Convert the provided index to a data source + appStateFromUrl.dataSource = isOfAggregateQueryType(appStateFromUrl.query) + ? createEsqlDataSource() + : createDataViewDataSource({ dataViewId: appStateFromUrl.index }); + } + + delete appStateFromUrl.index; + } + return appStateFromUrl as DiscoverAppState; } diff --git a/src/plugins/discover/public/application/main/state_management/utils/get_state_defaults.test.ts b/src/plugins/discover/public/application/main/state_management/utils/get_state_defaults.test.ts index 0a862b712186a8..5e070ef099cde9 100644 --- a/src/plugins/discover/public/application/main/state_management/utils/get_state_defaults.test.ts +++ b/src/plugins/discover/public/application/main/state_management/utils/get_state_defaults.test.ts @@ -13,6 +13,7 @@ import { dataViewWithTimefieldMock } from '../../../../__mocks__/data_view_with_ import { savedSearchMock, savedSearchMockWithESQL } from '../../../../__mocks__/saved_search'; import { dataViewMock } from '@kbn/discover-utils/src/__mocks__'; import { discoverServiceMock } from '../../../../__mocks__/services'; +import { createDataViewDataSource, createEsqlDataSource } from '../../../../../common/data_sources'; describe('getStateDefaults', () => { test('data view with timefield', () => { @@ -27,12 +28,15 @@ describe('getStateDefaults', () => { "columns": Array [ "default_column", ], + "dataSource": Object { + "dataViewId": "index-pattern-with-timefield-id", + "type": "dataView", + }, "filters": undefined, "grid": undefined, "headerRowHeight": undefined, "hideAggregatedPreview": undefined, "hideChart": undefined, - "index": "index-pattern-with-timefield-id", "interval": "auto", "query": undefined, "rowHeight": undefined, @@ -63,12 +67,15 @@ describe('getStateDefaults', () => { "columns": Array [ "default_column", ], + "dataSource": Object { + "dataViewId": "the-data-view-id", + "type": "dataView", + }, "filters": undefined, "grid": undefined, "headerRowHeight": undefined, "hideAggregatedPreview": undefined, "hideChart": undefined, - "index": "the-data-view-id", "interval": "auto", "query": undefined, "rowHeight": undefined, @@ -108,7 +115,7 @@ describe('getStateDefaults', () => { }, }); expect(actualForTextBasedWithValidViewMode.viewMode).toBe(VIEW_MODE.DOCUMENT_LEVEL); - expect(actualForTextBasedWithValidViewMode.index).toBe(undefined); + expect(actualForTextBasedWithValidViewMode.dataSource).toEqual(createEsqlDataSource()); const actualForWithValidViewMode = getStateDefaults({ services: discoverServiceMock, @@ -118,8 +125,32 @@ describe('getStateDefaults', () => { }, }); expect(actualForWithValidViewMode.viewMode).toBe(VIEW_MODE.AGGREGATED_LEVEL); - expect(actualForWithValidViewMode.index).toBe( - savedSearchMock.searchSource.getField('index')?.id + expect(actualForWithValidViewMode.dataSource).toEqual( + createDataViewDataSource({ + dataViewId: savedSearchMock.searchSource.getField('index')?.id!, + }) ); }); + + test('should return expected dataSource', () => { + const actualForTextBased = getStateDefaults({ + services: discoverServiceMock, + savedSearch: savedSearchMockWithESQL, + }); + expect(actualForTextBased.dataSource).toMatchInlineSnapshot(` + Object { + "type": "esql", + } + `); + const actualForDataView = getStateDefaults({ + services: discoverServiceMock, + savedSearch: savedSearchMock, + }); + expect(actualForDataView.dataSource).toMatchInlineSnapshot(` + Object { + "dataViewId": "the-data-view-id", + "type": "dataView", + } + `); + }); }); diff --git a/src/plugins/discover/public/application/main/state_management/utils/get_state_defaults.ts b/src/plugins/discover/public/application/main/state_management/utils/get_state_defaults.ts index 4faf8ffad2990b..7a1f2734aa7c89 100644 --- a/src/plugins/discover/public/application/main/state_management/utils/get_state_defaults.ts +++ b/src/plugins/discover/public/application/main/state_management/utils/get_state_defaults.ts @@ -21,6 +21,11 @@ import { DiscoverServices } from '../../../../build_services'; import { getDefaultSort, getSortArray } from '../../../../utils/sorting'; import { isTextBasedQuery } from '../../utils/is_text_based_query'; import { getValidViewMode } from '../../utils/get_valid_view_mode'; +import { + createDataViewDataSource, + createEsqlDataSource, + DiscoverDataSource, +} from '../../../../../common/data_sources'; function getDefaultColumns(savedSearch: SavedSearch, uiSettings: IUiSettingsClient) { if (savedSearch.columns && savedSearch.columns.length > 0) { @@ -45,12 +50,16 @@ export function getStateDefaults({ const { searchSource } = savedSearch; const { data, uiSettings, storage } = services; const dataView = searchSource.getField('index'); - const query = searchSource.getField('query') || data.query.queryString.getDefaultQuery(); const isTextBasedQueryMode = isTextBasedQuery(query); const sort = getSortArray(savedSearch.sort ?? [], dataView!, isTextBasedQueryMode); const columns = getDefaultColumns(savedSearch, uiSettings); const chartHidden = getChartHidden(storage, 'discover'); + const dataSource: DiscoverDataSource | undefined = isTextBasedQueryMode + ? createEsqlDataSource() + : dataView?.id + ? createDataViewDataSource({ dataViewId: dataView.id }) + : undefined; const defaultState: DiscoverAppState = { query, @@ -63,7 +72,7 @@ export function getStateDefaults({ ) : sort, columns, - index: isTextBasedQueryMode ? undefined : dataView?.id, + dataSource, interval: 'auto', filters: cloneDeep(searchSource.getOwnField('filter')) as DiscoverAppState['filters'], hideChart: typeof chartHidden === 'boolean' ? chartHidden : undefined, diff --git a/src/plugins/discover/public/application/main/state_management/utils/get_switch_data_view_app_state.ts b/src/plugins/discover/public/application/main/state_management/utils/get_switch_data_view_app_state.ts index f94420403f261b..c06cb3b67f2353 100644 --- a/src/plugins/discover/public/application/main/state_management/utils/get_switch_data_view_app_state.ts +++ b/src/plugins/discover/public/application/main/state_management/utils/get_switch_data_view_app_state.ts @@ -10,6 +10,8 @@ import { isOfAggregateQueryType, Query, AggregateQuery } from '@kbn/es-query'; import type { DataView } from '@kbn/data-views-plugin/public'; import type { SortOrder } from '@kbn/saved-search-plugin/public'; import { getSortArray } from '../../../../utils/sorting'; +import { DiscoverAppState } from '../discover_app_state_container'; +import { createDataViewDataSource } from '../../../../../common/data_sources'; /** * Helper function to remove or adapt the currently selected columns/sort to be valid with the next @@ -24,7 +26,7 @@ export function getDataViewAppState( modifyColumns: boolean = true, sortDirection: string = 'desc', query?: Query | AggregateQuery -) { +): Partial { let columns = currentColumns || []; if (modifyColumns) { @@ -66,7 +68,9 @@ export function getDataViewAppState( } return { - index: nextDataView.id, + dataSource: nextDataView.id + ? createDataViewDataSource({ dataViewId: nextDataView.id }) + : undefined, columns, sort: nextSort, }; diff --git a/src/plugins/discover/public/application/main/state_management/utils/load_saved_search.ts b/src/plugins/discover/public/application/main/state_management/utils/load_saved_search.ts index ba7d2a9342c246..0997f0f58b0aa9 100644 --- a/src/plugins/discover/public/application/main/state_management/utils/load_saved_search.ts +++ b/src/plugins/discover/public/application/main/state_management/utils/load_saved_search.ts @@ -24,6 +24,7 @@ import { } from '../discover_app_state_container'; import { DiscoverGlobalStateContainer } from '../discover_global_state_container'; import { DiscoverServices } from '../../../../build_services'; +import { DataSourceType, isDataSourceType } from '../../../../../common/data_sources'; interface LoadSavedSearchDeps { appStateContainer: DiscoverAppStateContainer; @@ -58,11 +59,24 @@ export const loadSavedSearch = async ( const appState = appStateExists ? appStateContainer.getState() : initialAppState; // Loading the saved search or creating a new one - let nextSavedSearch = savedSearchId - ? await savedSearchContainer.load(savedSearchId) - : await savedSearchContainer.new( - await getStateDataView(params, { services, appState, internalStateContainer }) - ); + let nextSavedSearch: SavedSearch; + + if (savedSearchId) { + nextSavedSearch = await savedSearchContainer.load(savedSearchId); + } else { + const dataViewId = isDataSourceType(appState?.dataSource, DataSourceType.DataView) + ? appState?.dataSource.dataViewId + : undefined; + + nextSavedSearch = await savedSearchContainer.new( + await getStateDataView(params, { + dataViewId, + query: appState?.query, + services, + internalStateContainer, + }) + ); + } // Cleaning up the previous state services.filterManager.setAppFilters([]); @@ -86,14 +100,15 @@ export const loadSavedSearch = async ( // Update saved search by a given app state (in URL) if (appState) { - if (savedSearchId && appState.index) { + if (savedSearchId && isDataSourceType(appState.dataSource, DataSourceType.DataView)) { // This is for the case appState is overwriting the loaded saved search data view const savedSearchDataViewId = nextSavedSearch.searchSource.getField('index')?.id; const stateDataView = await getStateDataView(params, { + dataViewId: appState.dataSource.dataViewId, + query: appState.query, + savedSearch: nextSavedSearch, services, - appState, internalStateContainer, - savedSearch: nextSavedSearch, }); const dataViewDifferentToAppState = stateDataView.id !== savedSearchDataViewId; if ( @@ -175,35 +190,39 @@ function updateBySavedSearch(savedSearch: SavedSearch, deps: LoadSavedSearchDeps const getStateDataView = async ( params: LoadParams, { + dataViewId, + query, savedSearch, - appState, services, internalStateContainer, }: { + dataViewId?: string; + query: DiscoverAppState['query']; savedSearch?: SavedSearch; - appState?: DiscoverAppState; services: DiscoverServices; internalStateContainer: DiscoverInternalStateContainer; } ) => { - const { dataView, dataViewSpec } = params ?? {}; + const { dataView, dataViewSpec } = params; + const isTextBased = isTextBasedQuery(query); + if (dataView) { return dataView; } - const query = appState?.query; - if (isTextBasedQuery(query)) { + if (isTextBased) { return await getDataViewByTextBasedQueryLang(query, dataView, services); } const result = await loadAndResolveDataView( { - id: appState?.index, + id: dataViewId, dataViewSpec, savedSearch, - isTextBasedQuery: isTextBasedQuery(appState?.query), + isTextBasedQuery: isTextBased, }, { services, internalStateContainer } ); + return result.dataView; }; diff --git a/test/functional/apps/discover/group5/_url_state.ts b/test/functional/apps/discover/group5/_url_state.ts index e97ac332e8b6ed..099fbed1589a3e 100644 --- a/test/functional/apps/discover/group5/_url_state.ts +++ b/test/functional/apps/discover/group5/_url_state.ts @@ -87,7 +87,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { let discoverLink = await appsMenu.getLink('Discover'); expect(discoverLink?.href).to.contain( '/app/discover#/?_g=(filters:!(),refreshInterval:(pause:!t,value:60000),time:(from:now-15m,to:now))' + - "&_a=(columns:!(),filters:!(),index:'logstash-*',interval:auto,query:(language:kuery,query:''),sort:!(!('@timestamp',desc)))" + "&_a=(columns:!(),dataSource:(dataViewId:'logstash-*',type:dataView),filters:!(),interval:auto,query:(language:kuery,query:''),sort:!(!('@timestamp',desc)))" ); await appsMenu.closeCollapsibleNav(); await PageObjects.timePicker.setDefaultAbsoluteRange(); @@ -107,7 +107,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { 'query:(bool:(minimum_should_match:1,should:!((match_phrase:(extension.raw:jpg)),' + "(match_phrase:(extension.raw:css))))))),query:(language:kuery,query:'')," + "refreshInterval:(pause:!t,value:60000),time:(from:'2015-09-19T06:31:44.000Z'," + - "to:'2015-09-23T18:31:44.000Z'))&_a=(columns:!(),filters:!(),index:'logstash-*'," + + "to:'2015-09-23T18:31:44.000Z'))&_a=(columns:!(),dataSource:(dataViewId:'logstash-*',type:dataView),filters:!()," + "interval:auto,query:(language:kuery,query:''),sort:!(!('@timestamp',desc)))" ); await appsMenu.clickLink('Discover', { category: 'kibana' }); diff --git a/test/functional/page_objects/discover_page.ts b/test/functional/page_objects/discover_page.ts index 9145c5f991ee20..9d02198ea5926d 100644 --- a/test/functional/page_objects/discover_page.ts +++ b/test/functional/page_objects/discover_page.ts @@ -701,11 +701,11 @@ export class DiscoverPageObject extends FtrService { */ public async validateDataViewReffsEquality() { const currentUrl = await this.browser.getCurrentUrl(); - const matches = currentUrl.matchAll(/index:[^,]*/g); + const matches = currentUrl.matchAll(/dataViewId:[^,]*/g); const indexes = []; for (const matchEntry of matches) { const [index] = matchEntry; - indexes.push(decodeURIComponent(index).replace('index:', '').replaceAll("'", '')); + indexes.push(decodeURIComponent(index).replace('dataViewId:', '').replaceAll("'", '')); } const first = indexes[0]; diff --git a/x-pack/plugins/security_solution/public/common/components/discover_in_timeline/use_discover_in_timeline_actions.test.tsx b/x-pack/plugins/security_solution/public/common/components/discover_in_timeline/use_discover_in_timeline_actions.test.tsx index 0be0e7a7b6f5be..6e9936b5f1b416 100644 --- a/x-pack/plugins/security_solution/public/common/components/discover_in_timeline/use_discover_in_timeline_actions.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/discover_in_timeline/use_discover_in_timeline_actions.test.tsx @@ -159,7 +159,10 @@ describe('useDiscoverInTimelineActions', () => { grid: undefined, hideAggregatedPreview: undefined, hideChart: true, - index: 'the-data-view-id', + dataSource: { + type: 'dataView', + dataViewId: 'the-data-view-id', + }, interval: 'auto', query: customQuery, rowHeight: undefined, diff --git a/x-pack/test_serverless/functional/test_suites/common/discover/group5/_url_state.ts b/x-pack/test_serverless/functional/test_suites/common/discover/group5/_url_state.ts index 242dfedde74ef1..76a8f479fa51c4 100644 --- a/x-pack/test_serverless/functional/test_suites/common/discover/group5/_url_state.ts +++ b/x-pack/test_serverless/functional/test_suites/common/discover/group5/_url_state.ts @@ -90,7 +90,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); expect(await discoverLink?.getAttribute('href')).to.contain( '/app/discover#/?_g=(filters:!(),refreshInterval:(pause:!t,value:60000),time:(from:now-15m,to:now))' + - "&_a=(columns:!(),filters:!(),index:'logstash-*',interval:auto,query:(language:kuery,query:''),sort:!(!('@timestamp',desc)))" + "&_a=(columns:!(),dataSource:(dataViewId:'logstash-*',type:dataView),filters:!(),interval:auto,query:(language:kuery,query:''),sort:!(!('@timestamp',desc)))" ); await PageObjects.timePicker.setDefaultAbsoluteRange(); await filterBar.addFilter({ @@ -110,7 +110,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { 'query:(bool:(minimum_should_match:1,should:!((match_phrase:(extension.raw:jpg)),' + "(match_phrase:(extension.raw:css))))))),query:(language:kuery,query:'')," + "refreshInterval:(pause:!t,value:60000),time:(from:'2015-09-19T06:31:44.000Z'," + - "to:'2015-09-23T18:31:44.000Z'))&_a=(columns:!(),filters:!(),index:'logstash-*'," + + "to:'2015-09-23T18:31:44.000Z'))&_a=(columns:!(),dataSource:(dataViewId:'logstash-*',type:dataView),filters:!()," + "interval:auto,query:(language:kuery,query:''),sort:!(!('@timestamp',desc)))" ); await PageObjects.svlCommonNavigation.sidenav.clickLink({ deepLinkId: 'discover' });