diff --git a/packages/kbn-text-based-editor/src/editor_footer.tsx b/packages/kbn-text-based-editor/src/editor_footer.tsx index 9516979e9b55f..b2acc7fdb4a6f 100644 --- a/packages/kbn-text-based-editor/src/editor_footer.tsx +++ b/packages/kbn-text-based-editor/src/editor_footer.tsx @@ -32,6 +32,7 @@ interface EditorFooterProps { lines: number; containerCSS: Interpolation; errors?: MonacoError[]; + detectTimestamp: boolean; onErrorClick: (error: MonacoError) => void; refreshErrors: () => void; } @@ -40,6 +41,7 @@ export const EditorFooter = memo(function EditorFooter({ lines, containerCSS, errors, + detectTimestamp, onErrorClick, refreshErrors, }: EditorFooterProps) { @@ -54,7 +56,7 @@ export const EditorFooter = memo(function EditorFooter({ > - +

{i18n.translate('textBasedEditor.query.textBasedLanguagesEditor.lineCount', { @@ -64,6 +66,32 @@ export const EditorFooter = memo(function EditorFooter({

+ + + + + + + +

+ {detectTimestamp + ? i18n.translate( + 'textBasedEditor.query.textBasedLanguagesEditor.timestampDetected', + { + defaultMessage: '@timestamp detected', + } + ) + : i18n.translate( + 'textBasedEditor.query.textBasedLanguagesEditor.timestampNotDetected', + { + defaultMessage: '@timestamp not detected', + } + )} +

+
+
+
+
{errors && errors.length > 0 && ( diff --git a/packages/kbn-text-based-editor/src/text_based_languages_editor.test.tsx b/packages/kbn-text-based-editor/src/text_based_languages_editor.test.tsx index 6243150e56b07..69121cf42eb5a 100644 --- a/packages/kbn-text-based-editor/src/text_based_languages_editor.test.tsx +++ b/packages/kbn-text-based-editor/src/text_based_languages_editor.test.tsx @@ -63,6 +63,33 @@ describe('TextBasedLanguagesEditor', () => { }); }); + it('should render the date info with no @timestamp detected', async () => { + const newProps = { + ...props, + isCodeEditorExpanded: true, + }; + await act(async () => { + const component = mount(renderTextBasedLanguagesEditorComponent({ ...newProps })); + expect( + component.find('[data-test-subj="TextBasedLangEditor-date-info"]').at(0).text() + ).toStrictEqual('@timestamp not detected'); + }); + }); + + it('should render the date info with @timestamp detected if detectTimestamp is true', async () => { + const newProps = { + ...props, + isCodeEditorExpanded: true, + detectTimestamp: true, + }; + await act(async () => { + const component = mount(renderTextBasedLanguagesEditorComponent({ ...newProps })); + expect( + component.find('[data-test-subj="TextBasedLangEditor-date-info"]').at(0).text() + ).toStrictEqual('@timestamp detected'); + }); + }); + it('should render the errors badge for the inline mode by default if errors are provides', async () => { const newProps = { ...props, diff --git a/packages/kbn-text-based-editor/src/text_based_languages_editor.tsx b/packages/kbn-text-based-editor/src/text_based_languages_editor.tsx index 9080f634736f5..5f432c090eb8c 100644 --- a/packages/kbn-text-based-editor/src/text_based_languages_editor.tsx +++ b/packages/kbn-text-based-editor/src/text_based_languages_editor.tsx @@ -55,6 +55,7 @@ export interface TextBasedLanguagesEditorProps { onTextLangQuerySubmit: () => void; expandCodeEditor: (status: boolean) => void; isCodeEditorExpanded: boolean; + detectTimestamp?: boolean; errors?: Error[]; isDisabled?: boolean; isDarkMode?: boolean; @@ -87,6 +88,7 @@ export const TextBasedLanguagesEditor = memo(function TextBasedLanguagesEditor({ onTextLangQuerySubmit, expandCodeEditor, isCodeEditorExpanded, + detectTimestamp = false, errors, isDisabled, isDarkMode, @@ -537,6 +539,7 @@ export const TextBasedLanguagesEditor = memo(function TextBasedLanguagesEditor({ errors={editorErrors} onErrorClick={onErrorClick} refreshErrors={onTextLangQuerySubmit} + detectTimestamp={detectTimestamp} /> )} @@ -608,6 +611,7 @@ export const TextBasedLanguagesEditor = memo(function TextBasedLanguagesEditor({ errors={editorErrors} onErrorClick={onErrorClick} refreshErrors={onTextLangQuerySubmit} + detectTimestamp={detectTimestamp} /> )} {isCodeEditorExpanded && ( diff --git a/src/plugins/discover/public/application/main/components/top_nav/discover_topnav.tsx b/src/plugins/discover/public/application/main/components/top_nav/discover_topnav.tsx index 43ed023992330..286b8a6113b27 100644 --- a/src/plugins/discover/public/application/main/components/top_nav/discover_topnav.tsx +++ b/src/plugins/discover/public/application/main/components/top_nav/discover_topnav.tsx @@ -48,10 +48,13 @@ export const DiscoverTopNav = ({ const dataView = useInternalStateSelector((state) => state.dataView!); const savedDataViews = useInternalStateSelector((state) => state.savedDataViews); const savedSearch = useSavedSearchInitial(); - const showDatePicker = useMemo( - () => dataView.isTimeBased() && dataView.type !== DataViewType.ROLLUP, - [dataView] - ); + const showDatePicker = useMemo(() => { + // always show the timepicker for text based languages + return ( + isPlainRecord || + (!isPlainRecord && dataView.isTimeBased() && dataView.type !== DataViewType.ROLLUP) + ); + }, [dataView, isPlainRecord]); const services = useDiscoverServices(); const { dataViewEditor, navigation, dataViewFieldEditor, data, uiSettings, dataViews } = services; 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 edf69444ac5e3..05ef44a03f01b 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 @@ -23,6 +23,7 @@ import { DiscoverMainProvider } from '../services/discover_state_provider'; import { DiscoverAppState } from '../services/discover_app_state_container'; import { DiscoverStateContainer } from '../services/discover_state'; import { VIEW_MODE } from '@kbn/saved-search-plugin/public'; +import { dataViewAdHoc } from '../../../__mocks__/data_view_complex'; function getHookProps( query: AggregateQuery | Query | undefined, @@ -84,6 +85,7 @@ const renderHookWithContext = ( appState?: DiscoverAppState ) => { const props = getHookProps(query, useDataViewsService ? getDataViewsService() : undefined); + props.stateContainer.actions.setDataView(dataViewMock); if (appState) { props.stateContainer.appState.getState = jest.fn(() => { return appState; @@ -98,7 +100,7 @@ const renderHookWithContext = ( describe('useTextBasedQueryLanguage', () => { test('a text based query should change state when loading and finished', async () => { - const { replaceUrlState, stateContainer } = renderHookWithContext(false); + const { replaceUrlState, stateContainer } = renderHookWithContext(true); await waitFor(() => expect(replaceUrlState).toHaveBeenCalledTimes(1)); expect(replaceUrlState).toHaveBeenCalledWith({ index: 'the-data-view-id' }); @@ -191,11 +193,7 @@ describe('useTextBasedQueryLanguage', () => { query: { sql: 'SELECT field1 from the-data-view-title WHERE field1=1' }, }); - await waitFor(() => { - expect(replaceUrlState).toHaveBeenCalledWith({ - index: 'the-data-view-id', - }); - }); + await waitFor(() => expect(replaceUrlState).toHaveBeenCalledTimes(0)); }); test('if its not a text based query coming along, it should be ignored', async () => { const { replaceUrlState, stateContainer } = renderHookWithContext(false); @@ -270,7 +268,7 @@ describe('useTextBasedQueryLanguage', () => { ], query: { sql: 'SELECT field1 from the-data-view-title' }, }); - await waitFor(() => expect(replaceUrlState).toHaveBeenCalledTimes(1)); + await waitFor(() => expect(replaceUrlState).toHaveBeenCalledTimes(2)); expect(replaceUrlState).toHaveBeenCalledWith({ columns: ['field1'], }); @@ -288,7 +286,7 @@ describe('useTextBasedQueryLanguage', () => { fetchStatus: FetchStatus.LOADING, query: { sql: 'SELECT * from the-data-view-title WHERE field1=2' }, }); - await waitFor(() => expect(replaceUrlState).toHaveBeenCalledTimes(0)); + await waitFor(() => expect(replaceUrlState).toHaveBeenCalledTimes(1)); documents$.next({ recordRawType: RecordRawType.PLAIN, fetchStatus: FetchStatus.COMPLETE, @@ -301,7 +299,7 @@ describe('useTextBasedQueryLanguage', () => { ], query: { sql: 'SELECT * from the-data-view-title WHERE field1=2' }, }); - await waitFor(() => expect(replaceUrlState).toHaveBeenCalledTimes(1)); + await waitFor(() => expect(replaceUrlState).toHaveBeenCalledTimes(2)); stateContainer.appState.getState = jest.fn(() => { return { columns: ['field1', 'field2'], index: 'the-data-view-id' }; }); @@ -344,17 +342,10 @@ describe('useTextBasedQueryLanguage', () => { }); test('changing a text based query with an index pattern that not corresponds to a dataview should return results', async () => { - const dataViewsCreateMock = discoverServiceMock.dataViews.create as jest.Mock; - dataViewsCreateMock.mockImplementation(() => ({ - ...dataViewMock, - })); - const dataViewsService = { - ...discoverServiceMock.dataViews, - create: dataViewsCreateMock, - }; - const props = getHookProps(query, dataViewsService); + const props = getHookProps(query, discoverServiceMock.dataViews); const { stateContainer, replaceUrlState } = props; const documents$ = stateContainer.dataState.data$.documents$; + props.stateContainer.actions.setDataView(dataViewMock); renderHook(() => useTextBasedQueryLanguage(props), { wrapper: getHookContext(stateContainer) }); @@ -374,6 +365,7 @@ describe('useTextBasedQueryLanguage', () => { ], query: { sql: 'SELECT field1 from the-data-view-*' }, }); + props.stateContainer.actions.setDataView(dataViewAdHoc); await waitFor(() => expect(replaceUrlState).toHaveBeenCalledTimes(1)); await waitFor(() => { 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 b82b749d2acba..ca1c094594759 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 @@ -37,6 +37,7 @@ export function useTextBasedQueryLanguage({ columns: [], query: undefined, }); + const indexTitle = useRef(''); const savedSearch = useSavedSearchInitial(); const cleanup = useCallback(() => { @@ -80,36 +81,24 @@ export function useTextBasedQueryLanguage({ } } const indexPatternFromQuery = getIndexPatternFromSQLQuery(query.sql); - const internalState = stateContainer.internalState.getState(); - const dataViewList = [...internalState.savedDataViews, ...internalState.adHocDataViews]; - let dataViewObj = dataViewList.find(({ title }) => title === indexPatternFromQuery); - // no dataview found but the index pattern is valid - // create an adhoc instance instead - if (!dataViewObj) { - dataViewObj = await dataViews.create({ - title: indexPatternFromQuery, - }); - stateContainer.internalState.transitions.setAdHocDataViews([dataViewObj]); - - if (dataViewObj.fields.getByName('@timestamp')?.type === 'date') { - dataViewObj.timeFieldName = '@timestamp'; - } else if (dataViewObj.fields.getByType('date')?.length) { - const dateFields = dataViewObj.fields.getByType('date'); - dataViewObj.timeFieldName = dateFields[0].name; - } - } + const dataViewObj = stateContainer.internalState.getState().dataView!; // don't set the columns on initial fetch, to prevent overwriting existing state const addColumnsToState = Boolean( nextColumns.length && (!initialFetch || !stateColumns?.length) ); // no need to reset index to state if it hasn't changed - const addDataViewToState = Boolean(dataViewObj.id !== index); - if (!addColumnsToState && !addDataViewToState) { + const addDataViewToState = Boolean(dataViewObj?.id !== index) || initialFetch; + const queryChanged = indexPatternFromQuery !== indexTitle.current; + if (!addColumnsToState && !queryChanged) { return; } + if (queryChanged) { + indexTitle.current = indexPatternFromQuery; + } + const nextState = { ...(addDataViewToState && { index: dataViewObj.id }), ...(addColumnsToState && { columns: nextColumns }), diff --git a/src/plugins/discover/public/application/main/hooks/utils/build_state_subscribe.ts b/src/plugins/discover/public/application/main/hooks/utils/build_state_subscribe.ts index 1799e5146c803..dac3f4cfc6c62 100644 --- a/src/plugins/discover/public/application/main/hooks/utils/build_state_subscribe.ts +++ b/src/plugins/discover/public/application/main/hooks/utils/build_state_subscribe.ts @@ -17,6 +17,7 @@ import { isEqualState, } from '../../services/discover_app_state_container'; import { addLog } from '../../../../utils/add_log'; +import { isTextBasedQuery } from '../../utils/is_text_based_query'; import { FetchStatus } from '../../../types'; import { loadAndResolveDataView } from '../../utils/resolve_data_view'; @@ -61,7 +62,7 @@ export const buildStateSubscribe = // NOTE: this is also called when navigating from discover app to context app if (nextState.index && dataViewChanged) { const { dataView: nextDataView, fallback } = await loadAndResolveDataView( - { id: nextState.index, savedSearch }, + { id: nextState.index, savedSearch, isTextBasedQuery: isTextBasedQuery(nextState?.query) }, { internalStateContainer: internalState, services } ); diff --git a/src/plugins/discover/public/application/main/services/discover_data_state_container.ts b/src/plugins/discover/public/application/main/services/discover_data_state_container.ts index 07e1f089bcd13..3f3a6427431cc 100644 --- a/src/plugins/discover/public/application/main/services/discover_data_state_container.ts +++ b/src/plugins/discover/public/application/main/services/discover_data_state_container.ts @@ -12,6 +12,9 @@ import { RequestAdapter } from '@kbn/inspector-plugin/common'; import { SavedSearch } from '@kbn/saved-search-plugin/public'; import { AggregateQuery, Query } from '@kbn/es-query'; import type { SearchResponse } from '@elastic/elasticsearch/lib/api/types'; +import { DataView } from '@kbn/data-views-plugin/common'; +import { getDataViewByTextBasedQueryLang } from '../utils/get_data_view_by_text_based_query_lang'; +import { isTextBasedQuery } from '../utils/is_text_based_query'; import { getRawRecordType } from '../utils/get_raw_record_type'; import { DiscoverAppState } from './discover_app_state_container'; import { DiscoverServices } from '../../../build_services'; @@ -129,11 +132,13 @@ export function getDataStateContainer({ searchSessionManager, getAppState, getSavedSearch, + setDataView, }: { services: DiscoverServices; searchSessionManager: DiscoverSearchSessionManager; getAppState: () => DiscoverAppState; getSavedSearch: () => SavedSearch; + setDataView: (dataView: DataView) => void; }): DiscoverDataStateContainer { const { data, uiSettings, toastNotifications } = services; const { timefilter } = data.query.timefilter; @@ -226,7 +231,17 @@ export function getDataStateContainer({ }; } - const fetchQuery = (resetQuery?: boolean) => { + const fetchQuery = async (resetQuery?: boolean) => { + const query = getAppState().query; + const currentDataView = getSavedSearch().searchSource.getField('index'); + + if (isTextBasedQuery(query)) { + const nextDataView = await getDataViewByTextBasedQueryLang(query, currentDataView, services); + if (nextDataView !== currentDataView) { + setDataView(nextDataView); + } + } + if (resetQuery) { refetch$.next('reset'); } else { diff --git a/src/plugins/discover/public/application/main/services/discover_state.test.ts b/src/plugins/discover/public/application/main/services/discover_state.test.ts index 684289b4e4799..928e3d431b65e 100644 --- a/src/plugins/discover/public/application/main/services/discover_state.test.ts +++ b/src/plugins/discover/public/application/main/services/discover_state.test.ts @@ -37,6 +37,14 @@ const startSync = (appState: DiscoverAppStateContainer) => { async function getState(url: string = '/', savedSearch?: SavedSearch) { const nextHistory = createBrowserHistory(); nextHistory.push(url); + + discoverServiceMock.dataViews.create = jest.fn().mockReturnValue({ + ...dataViewMock, + isPersisted: () => false, + id: 'ad-hoc-id', + title: 'test', + }); + const nextState = getDiscoverStateContainer({ services: discoverServiceMock, history: nextHistory, @@ -635,12 +643,6 @@ describe('Test discover state actions', () => { }); test('onCreateDefaultAdHocDataView', async () => { - discoverServiceMock.dataViews.create = jest.fn().mockReturnValue({ - ...dataViewMock, - isPersisted: () => false, - id: 'ad-hoc-id', - title: 'test', - }); const { state } = await getState('/', savedSearchMock); await state.actions.loadSavedSearch({ savedSearchId: savedSearchMock.id }); const unsubscribe = state.actions.initializeAndSync(); diff --git a/src/plugins/discover/public/application/main/services/discover_state.ts b/src/plugins/discover/public/application/main/services/discover_state.ts index 8be2bb344ae3d..24ca0e38a4d34 100644 --- a/src/plugins/discover/public/application/main/services/discover_state.ts +++ b/src/plugins/discover/public/application/main/services/discover_state.ts @@ -228,13 +228,6 @@ export function getDiscoverStateContainer({ */ const internalStateContainer = getInternalStateContainer(); - const dataStateContainer = getDataStateContainer({ - services, - searchSessionManager, - getAppState: appStateContainer.getState, - getSavedSearch: savedSearchContainer.getState, - }); - const pauseAutoRefreshInterval = async (dataView: DataView) => { if (dataView && (!dataView.isTimeBased() || dataView.type === DataViewType.ROLLUP)) { const state = stateStorage.get(GLOBAL_STATE_URL_KEY); @@ -247,13 +240,20 @@ export function getDiscoverStateContainer({ } } }; - const setDataView = (dataView: DataView) => { internalStateContainer.transitions.setDataView(dataView); pauseAutoRefreshInterval(dataView); savedSearchContainer.getState().searchSource.setField('index', dataView); }; + const dataStateContainer = getDataStateContainer({ + services, + searchSessionManager, + getAppState: appStateContainer.getState, + getSavedSearch: savedSearchContainer.getState, + setDataView, + }); + const loadDataViewList = async () => { const dataViewList = await services.dataViews.getIdsWithTitle(true); internalStateContainer.transitions.setSavedDataViews(dataViewList); diff --git a/src/plugins/discover/public/application/main/services/load_saved_search.ts b/src/plugins/discover/public/application/main/services/load_saved_search.ts index 5bae488754322..a4503b0a59e00 100644 --- a/src/plugins/discover/public/application/main/services/load_saved_search.ts +++ b/src/plugins/discover/public/application/main/services/load_saved_search.ts @@ -7,6 +7,7 @@ */ import type { SavedSearch } from '@kbn/saved-search-plugin/public'; import { cloneDeep, isEqual } from 'lodash'; +import { getDataViewByTextBasedQueryLang } from '../utils/get_data_view_by_text_based_query_lang'; import { isTextBasedQuery } from '../utils/is_text_based_query'; import { loadAndResolveDataView } from '../utils/resolve_data_view'; import { DiscoverInternalStateContainer } from './discover_internal_state_container'; @@ -151,6 +152,11 @@ const getStateDataView = async ( if (dataView) { return dataView; } + const query = appState?.query; + + if (isTextBasedQuery(query)) { + return await getDataViewByTextBasedQueryLang(query, dataView, services); + } const result = await loadAndResolveDataView( { diff --git a/src/plugins/discover/public/application/main/utils/get_data_view_by_text_based_query_lang.test.ts b/src/plugins/discover/public/application/main/utils/get_data_view_by_text_based_query_lang.test.ts new file mode 100644 index 0000000000000..3d12a3421e6c4 --- /dev/null +++ b/src/plugins/discover/public/application/main/utils/get_data_view_by_text_based_query_lang.test.ts @@ -0,0 +1,48 @@ +/* + * 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 { getDataViewByTextBasedQueryLang } from './get_data_view_by_text_based_query_lang'; +import { dataViewAdHoc } from '../../../__mocks__/data_view_complex'; +import { dataViewMock } from '../../../__mocks__/data_view'; +import { discoverServiceMock } from '../../../__mocks__/services'; + +describe('getDataViewByTextBasedQueryLang', () => { + discoverServiceMock.dataViews.create = jest.fn().mockReturnValue({ + ...dataViewMock, + isPersisted: () => false, + id: 'ad-hoc-id', + title: 'test', + }); + const services = discoverServiceMock; + it('returns the current dataview if is adhoc and query has not changed', async () => { + const query = { sql: 'Select * from data-view-ad-hoc-title' }; + const dataView = await getDataViewByTextBasedQueryLang(query, dataViewAdHoc, services); + expect(dataView).toStrictEqual(dataViewAdHoc); + }); + + it('creates an adhoc dataview if the current dataview is persistent and query has not changed', async () => { + const query = { sql: 'Select * from the-data-view-title' }; + const dataView = await getDataViewByTextBasedQueryLang(query, dataViewMock, services); + expect(dataView.isPersisted()).toEqual(false); + expect(dataView.timeFieldName).toBe('@timestamp'); + }); + + it('creates an adhoc dataview if the current dataview is ad hoc and query has changed', async () => { + discoverServiceMock.dataViews.create = jest.fn().mockReturnValue({ + ...dataViewAdHoc, + isPersisted: () => false, + id: 'ad-hoc-id-1', + title: 'test-1', + timeFieldName: undefined, + }); + const query = { sql: 'Select * from the-data-view-title' }; + const dataView = await getDataViewByTextBasedQueryLang(query, dataViewAdHoc, services); + expect(dataView.isPersisted()).toEqual(false); + expect(dataView.timeFieldName).toBeUndefined(); + }); +}); diff --git a/src/plugins/discover/public/application/main/utils/get_data_view_by_text_based_query_lang.ts b/src/plugins/discover/public/application/main/utils/get_data_view_by_text_based_query_lang.ts new file mode 100644 index 0000000000000..3b38b95dfceb1 --- /dev/null +++ b/src/plugins/discover/public/application/main/utils/get_data_view_by_text_based_query_lang.ts @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +import { AggregateQuery, getIndexPatternFromSQLQuery } from '@kbn/es-query'; +import { DataView } from '@kbn/data-views-plugin/common'; +import { DiscoverServices } from '../../../build_services'; + +export async function getDataViewByTextBasedQueryLang( + query: AggregateQuery, + currentDataView: DataView | undefined, + services: DiscoverServices +) { + const text = 'sql' in query ? query.sql : undefined; + + const indexPatternFromQuery = getIndexPatternFromSQLQuery(text); + if ( + currentDataView?.isPersisted() || + indexPatternFromQuery !== currentDataView?.getIndexPattern() + ) { + const dataViewObj = await services.dataViews.create({ + title: indexPatternFromQuery, + }); + + if (dataViewObj.fields.getByName('@timestamp')?.type === 'date') { + dataViewObj.timeFieldName = '@timestamp'; + } + return dataViewObj; + } + return currentDataView; +} diff --git a/src/plugins/discover/public/application/main/utils/resolve_data_view.ts b/src/plugins/discover/public/application/main/utils/resolve_data_view.ts index 7a7884e6295ad..d846f7b949513 100644 --- a/src/plugins/discover/public/application/main/utils/resolve_data_view.ts +++ b/src/plugins/discover/public/application/main/utils/resolve_data_view.ts @@ -123,7 +123,8 @@ export function resolveDataView( return ownDataView; } - if (stateVal && !stateValFound) { + // no warnings for text based mode + if (stateVal && !stateValFound && !Boolean(isTextBasedQuery)) { const warningTitle = i18n.translate('discover.valueIsNotConfiguredDataViewIDWarningTitle', { defaultMessage: '{stateVal} is not a configured data view ID', values: { @@ -146,20 +147,18 @@ export function resolveDataView( }); return ownDataView; } - if (!Boolean(isTextBasedQuery)) { - toastNotifications.addWarning({ - title: warningTitle, - text: i18n.translate('discover.showingDefaultDataViewWarningDescription', { - defaultMessage: - 'Showing the default data view: "{loadedDataViewTitle}" ({loadedDataViewId})', - values: { - loadedDataViewTitle: loadedDataView.getIndexPattern(), - loadedDataViewId: loadedDataView.id, - }, - }), - 'data-test-subj': 'dscDataViewNotFoundShowDefaultWarning', - }); - } + toastNotifications.addWarning({ + title: warningTitle, + text: i18n.translate('discover.showingDefaultDataViewWarningDescription', { + defaultMessage: + 'Showing the default data view: "{loadedDataViewTitle}" ({loadedDataViewId})', + values: { + loadedDataViewTitle: loadedDataView.getIndexPattern(), + loadedDataViewId: loadedDataView.id, + }, + }), + 'data-test-subj': 'dscDataViewNotFoundShowDefaultWarning', + }); } return loadedDataView; diff --git a/src/plugins/unified_histogram/public/chart/chart.test.tsx b/src/plugins/unified_histogram/public/chart/chart.test.tsx index 428db01cc3773..39c7fa934a125 100644 --- a/src/plugins/unified_histogram/public/chart/chart.test.tsx +++ b/src/plugins/unified_histogram/public/chart/chart.test.tsx @@ -40,6 +40,7 @@ async function mountComponent({ dataView = dataViewWithTimefieldMock, currentSuggestion, allSuggestions, + isPlainRecord, }: { noChart?: boolean; noHits?: boolean; @@ -49,6 +50,7 @@ async function mountComponent({ dataView?: DataView; currentSuggestion?: Suggestion; allSuggestions?: Suggestion[]; + isPlainRecord?: boolean; } = {}) { (searchSourceInstanceMock.fetch$ as jest.Mock).mockImplementation( jest.fn().mockReturnValue(of({ rawResponse: { hits: { total: noHits ? 0 : 2 } } })) @@ -84,6 +86,7 @@ async function mountComponent({ breakdown: noBreakdown ? undefined : { field: undefined }, currentSuggestion, allSuggestions, + isPlainRecord, appendHistogram, onResetChartHeight: jest.fn(), onChartHiddenChange: jest.fn(), @@ -149,6 +152,14 @@ describe('Chart', () => { expect(component.find('[data-test-subj="unifiedHistogramChart"]').exists()).toBeTruthy(); }); + test('render when is text based and not timebased', async () => { + const component = await mountComponent({ isPlainRecord: true, dataView: dataViewMock }); + expect( + component.find('[data-test-subj="unifiedHistogramChartOptionsToggle"]').exists() + ).toBeTruthy(); + expect(component.find('[data-test-subj="unifiedHistogramChart"]').exists()).toBeTruthy(); + }); + test('triggers onEditVisualization on click', async () => { expect(mockUseEditVisualization).not.toHaveBeenCalled(); const component = await mountComponent(); diff --git a/src/plugins/unified_histogram/public/chart/chart.tsx b/src/plugins/unified_histogram/public/chart/chart.tsx index 1e24de48f55d6..585b5603e1f6c 100644 --- a/src/plugins/unified_histogram/public/chart/chart.tsx +++ b/src/plugins/unified_histogram/public/chart/chart.tsx @@ -136,7 +136,7 @@ export function Chart({ !chart.hidden && dataView.id && dataView.type !== DataViewType.ROLLUP && - dataView.isTimeBased() + (isPlainRecord || (!isPlainRecord && dataView.isTimeBased())) ); const input$ = useMemo( @@ -219,6 +219,7 @@ export function Chart({ dataView, relativeTimeRange: originalRelativeTimeRange ?? relativeTimeRange, lensAttributes: lensAttributesContext.attributes, + isPlainRecord, }); return ( diff --git a/src/plugins/unified_histogram/public/chart/hooks/use_edit_visualization.ts b/src/plugins/unified_histogram/public/chart/hooks/use_edit_visualization.ts index c1b1f899ae756..e681fd34cd91e 100644 --- a/src/plugins/unified_histogram/public/chart/hooks/use_edit_visualization.ts +++ b/src/plugins/unified_histogram/public/chart/hooks/use_edit_visualization.ts @@ -21,16 +21,21 @@ export const useEditVisualization = ({ dataView, relativeTimeRange, lensAttributes, + isPlainRecord, }: { services: UnifiedHistogramServices; dataView: DataView; relativeTimeRange?: TimeRange; lensAttributes: TypedLensByValueInput['attributes']; + isPlainRecord?: boolean; }) => { const [canVisualize, setCanVisualize] = useState(false); const checkCanVisualize = useCallback(async () => { - if (!dataView.id || !dataView.isTimeBased() || !dataView.getTimeField().visualizable) { + if (!dataView.id) { + return false; + } + if (!isPlainRecord && (!dataView.isTimeBased() || !dataView.getTimeField().visualizable)) { return false; } @@ -43,7 +48,7 @@ export const useEditVisualization = ({ ); return Boolean(compatibleActions.length); - }, [dataView, services.uiActions]); + }, [dataView, isPlainRecord, services.uiActions]); const onEditVisualization = useMemo(() => { if (!canVisualize) { diff --git a/src/plugins/unified_search/public/query_string_input/query_bar_top_row.test.tsx b/src/plugins/unified_search/public/query_string_input/query_bar_top_row.test.tsx index afe2022529914..4fd7c3a1a195d 100644 --- a/src/plugins/unified_search/public/query_string_input/query_bar_top_row.test.tsx +++ b/src/plugins/unified_search/public/query_string_input/query_bar_top_row.test.tsx @@ -115,6 +115,7 @@ describe('QueryBarTopRowTopRow', () => { const TIMEPICKER_SELECTOR = 'Memo(EuiSuperDatePicker)'; const REFRESH_BUTTON_SELECTOR = 'EuiSuperUpdateButton'; const TIMEPICKER_DURATION = '[data-shared-timefilter-duration]'; + const TEXT_BASED_EDITOR = '[data-test-subj="unifiedTextLangEditor"]'; beforeEach(() => { jest.clearAllMocks(); @@ -293,7 +294,7 @@ describe('QueryBarTopRowTopRow', () => { }); it('Should NOT render query input bar if on text based languages mode', () => { - const component = shallow( + const component = mount( wrapQueryBarTopRowInContext({ query: sqlQuery, isDirty: false, @@ -307,6 +308,8 @@ describe('QueryBarTopRowTopRow', () => { ); expect(component.find(QUERY_INPUT_SELECTOR).length).toBe(0); + expect(component.find(TEXT_BASED_EDITOR).length).toBe(1); + expect(component.find(TEXT_BASED_EDITOR).prop('detectTimestamp')).toBe(true); }); }); diff --git a/src/plugins/unified_search/public/query_string_input/query_bar_top_row.tsx b/src/plugins/unified_search/public/query_string_input/query_bar_top_row.tsx index 94a342d610399..3037a725f089d 100644 --- a/src/plugins/unified_search/public/query_string_input/query_bar_top_row.tsx +++ b/src/plugins/unified_search/public/query_string_input/query_bar_top_row.tsx @@ -27,6 +27,7 @@ import { OnRefreshProps, useIsWithinBreakpoints, EuiSuperUpdateButton, + EuiToolTip, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { TimeHistoryContract, getQueryLog } from '@kbn/data-plugin/public'; @@ -64,6 +65,30 @@ export const strings = { }), }; +const getWrapperWithTooltip = ( + children: JSX.Element, + enableTooltip: boolean, + query?: Query | AggregateQuery +) => { + if (enableTooltip && query && isOfAggregateQueryType(query)) { + const textBasedLanguage = getAggregateQueryMode(query); + return ( + + {children} + + ); + } else { + return children; + } +}; + const SuperDatePicker = React.memo( EuiSuperDatePicker as any ) as unknown as typeof EuiSuperDatePicker; @@ -394,33 +419,45 @@ export const QueryBarTopRow = React.memo( if (!shouldRenderDatePicker()) { return null; } + let isDisabled = props.isDisabled; + let enableTooltip = false; + // On text based mode the datepicker is always on when the user has unsaved changes. + // When the user doesn't have any changes it should be disabled if dataview doesn't have @timestamp field + if (Boolean(isQueryLangSelected) && !props.isDirty) { + const adHocDataview = props.indexPatterns?.[0]; + if (adHocDataview && typeof adHocDataview !== 'string') { + isDisabled = !Boolean(adHocDataview.timeFieldName); + enableTooltip = !Boolean(adHocDataview.timeFieldName); + } + } const wrapperClasses = classNames('kbnQueryBar__datePickerWrapper'); - return ( - - - + const datePicker = ( + ); + const component = getWrapperWithTooltip(datePicker, enableTooltip, props.query); + + return {component}; } function renderUpdateButton() { @@ -577,6 +614,11 @@ export const QueryBarTopRow = React.memo( } function renderTextLangEditor() { + const adHocDataview = props.indexPatterns?.[0]; + let detectTimestamp = false; + if (adHocDataview && typeof adHocDataview !== 'string') { + detectTimestamp = Boolean(adHocDataview?.timeFieldName); + } return ( isQueryLangSelected && props.query && @@ -587,6 +629,7 @@ export const QueryBarTopRow = React.memo( expandCodeEditor={(status: boolean) => setCodeEditorIsExpanded(status)} isCodeEditorExpanded={codeEditorIsExpanded} errors={props.textBasedLanguageModeErrors} + detectTimestamp={detectTimestamp} onTextLangQuerySubmit={() => onSubmit({ query: queryRef.current, diff --git a/test/functional/apps/discover/group3/_sidebar.ts b/test/functional/apps/discover/group3/_sidebar.ts index 0e0aa51db2c0a..8c681bf321ad6 100644 --- a/test/functional/apps/discover/group3/_sidebar.ts +++ b/test/functional/apps/discover/group3/_sidebar.ts @@ -404,7 +404,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.discover.waitUntilSidebarHasLoaded(); expect(await PageObjects.discover.getSidebarAriaDescription()).to.be( - '1 popular field. 53 available fields. 0 empty fields. 3 meta fields.' + '53 available fields. 0 empty fields. 3 meta fields.' ); }); diff --git a/x-pack/plugins/lens/public/app_plugin/lens_top_nav.tsx b/x-pack/plugins/lens/public/app_plugin/lens_top_nav.tsx index 9128f64783298..41b0fa567d567 100644 --- a/x-pack/plugins/lens/public/app_plugin/lens_top_nav.tsx +++ b/x-pack/plugins/lens/public/app_plugin/lens_top_nav.tsx @@ -1080,6 +1080,8 @@ export const LensTopNavMenu = ({ dataViewPickerComponentProps={dataViewPickerProps} showDatePicker={ indexPatterns.some((ip) => ip.isTimeBased()) || + // always show the timepicker for text based languages + isOnTextBasedMode || Boolean( allLoaded && activeDatasourceId && diff --git a/x-pack/plugins/lens/public/datasources/text_based/utils.test.ts b/x-pack/plugins/lens/public/datasources/text_based/utils.test.ts index 8db05279d593e..c15bf41b6fb8d 100644 --- a/x-pack/plugins/lens/public/datasources/text_based/utils.test.ts +++ b/x-pack/plugins/lens/public/datasources/text_based/utils.test.ts @@ -191,8 +191,9 @@ describe('Text based languages utils', () => { ), create: jest.fn().mockReturnValue( Promise.resolve({ - id: '1', - title: 'my-fake-index-pattern', + id: '4', + title: 'my-adhoc-index-pattern', + name: 'my-adhoc-index-pattern', timeFieldName: 'timeField', isPersisted: () => false, }) @@ -252,6 +253,11 @@ describe('Text based languages utils', () => { timeField: 'timeField', title: 'my-fake-restricted-pattern', }, + { + id: '4', + timeField: 'timeField', + title: 'my-adhoc-index-pattern', + }, ], layers: { first: { @@ -280,7 +286,7 @@ describe('Text based languages utils', () => { ], columns: [], errors: [], - index: '1', + index: '4', query: { sql: 'SELECT * FROM my-fake-index-pattern', }, diff --git a/x-pack/plugins/lens/public/datasources/text_based/utils.ts b/x-pack/plugins/lens/public/datasources/text_based/utils.ts index a521b9f245a34..cd8bb2b8c84fa 100644 --- a/x-pack/plugins/lens/public/datasources/text_based/utils.ts +++ b/x-pack/plugins/lens/public/datasources/text_based/utils.ts @@ -83,29 +83,22 @@ export async function getStateFromAggregateQuery( let allColumns: TextBasedLayerColumn[] = []; let timeFieldName; try { - const dataView = dataViewId - ? await dataViews.get(dataViewId) - : await dataViews.create({ - title: indexPattern, - }); - if (!dataViewId && !dataView.isPersisted()) { - if (dataView && dataView.id) { - if (dataView.fields.getByName('@timestamp')?.type === 'date') { - dataView.timeFieldName = '@timestamp'; - } else if (dataView.fields.getByType('date')?.length) { - const dateFields = dataView.fields.getByType('date'); - dataView.timeFieldName = dateFields[0].name; - } - dataViewId = dataView?.id; - indexPatternRefs = [ - ...indexPatternRefs, - { - id: dataView.id, - title: dataView.name, - timeField: dataView.timeFieldName, - }, - ]; + const dataView = await dataViews.create({ + title: indexPattern, + }); + if (dataView && dataView.id) { + if (dataView?.fields?.getByName('@timestamp')?.type === 'date') { + dataView.timeFieldName = '@timestamp'; } + dataViewId = dataView?.id; + indexPatternRefs = [ + ...indexPatternRefs, + { + id: dataView.id, + title: dataView.name, + timeField: dataView.timeFieldName, + }, + ]; } timeFieldName = dataView.timeFieldName; const table = await fetchDataFromAggregateQuery(query, dataView, data, expressions);