mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
[Discover][Lens] Removes the dataview dependency from the text based mode (#158531)
## Summary
Closes https://github.com/elastic/kibana/issues/154334
This PR:
- make the text based languages to work with adhoc dataviews and not
permanent dataviews. The text based languages will not be related with
dataviews
- enables the timepicker always for text based languages
- the timepicker is disabled if the index pattern doesn't have an
@timestamp field
- the timepicker is enabled if the index pattern has an @timestamp field
- the timepicker is enabled if the index pattern doesn't have an
@timestamp field but there is a dirty state (user is writing the query
and hasn't hit the update button. We do that to give the user the
ability to change the timepicker with the query
- An info text has been added to the editor footer to inform the users
about the @timestamp existence
The timepicker in the disabled state needs to have a disabled status
text (All time) but this is not possible atm. I have created an issue to
eui https://github.com/elastic/eui/issues/6814 to add this property.
This is going to be tackled before the 8.9 FF but we don't want to block
this PR
<img width="1839" alt="image"
src="8fc0a492
-1f00-41b6-a4a6-b0527725931f">
### Checklist
- [x] Any text added follows [EUI's writing
guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses
sentence case text and includes [i18n
support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)
- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
---------
Co-authored-by: Stratoula Kalafateli <efstratia.kalafateli@elastic.co>
Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
58e0fdf096
commit
3ef8b46433
23 changed files with 339 additions and 127 deletions
|
@ -32,6 +32,7 @@ interface EditorFooterProps {
|
|||
lines: number;
|
||||
containerCSS: Interpolation<Theme>;
|
||||
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({
|
|||
>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFlexGroup gutterSize="s" responsive={false} alignItems="center">
|
||||
<EuiFlexItem grow={false} style={{ marginRight: '16px' }}>
|
||||
<EuiFlexItem grow={false} style={{ marginRight: '8px' }}>
|
||||
<EuiText size="xs" color="subdued" data-test-subj="TextBasedLangEditor-footer-lines">
|
||||
<p>
|
||||
{i18n.translate('textBasedEditor.query.textBasedLanguagesEditor.lineCount', {
|
||||
|
@ -64,6 +66,32 @@ export const EditorFooter = memo(function EditorFooter({
|
|||
</p>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false} style={{ marginRight: '16px' }}>
|
||||
<EuiFlexGroup gutterSize="xs" responsive={false} alignItems="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiIcon type="calendar" color="subdued" size="s" />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiText size="xs" color="subdued" data-test-subj="TextBasedLangEditor-date-info">
|
||||
<p>
|
||||
{detectTimestamp
|
||||
? i18n.translate(
|
||||
'textBasedEditor.query.textBasedLanguagesEditor.timestampDetected',
|
||||
{
|
||||
defaultMessage: '@timestamp detected',
|
||||
}
|
||||
)
|
||||
: i18n.translate(
|
||||
'textBasedEditor.query.textBasedLanguagesEditor.timestampNotDetected',
|
||||
{
|
||||
defaultMessage: '@timestamp not detected',
|
||||
}
|
||||
)}
|
||||
</p>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
{errors && errors.length > 0 && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFlexGroup gutterSize="xs" responsive={false} alignItems="center">
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
@ -608,6 +611,7 @@ export const TextBasedLanguagesEditor = memo(function TextBasedLanguagesEditor({
|
|||
errors={editorErrors}
|
||||
onErrorClick={onErrorClick}
|
||||
refreshErrors={onTextLangQuerySubmit}
|
||||
detectTimestamp={detectTimestamp}
|
||||
/>
|
||||
)}
|
||||
{isCodeEditorExpanded && (
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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(() => {
|
||||
|
|
|
@ -37,6 +37,7 @@ export function useTextBasedQueryLanguage({
|
|||
columns: [],
|
||||
query: undefined,
|
||||
});
|
||||
const indexTitle = useRef<string>('');
|
||||
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 }),
|
||||
|
|
|
@ -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 }
|
||||
);
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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<QueryState>(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);
|
||||
|
|
|
@ -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(
|
||||
{
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -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 (
|
||||
<EuiToolTip
|
||||
position="top"
|
||||
content={i18n.translate('unifiedSearch.query.queryBar.textBasedNonTimestampWarning', {
|
||||
defaultMessage:
|
||||
'Date range selection for {language} queries requires the presence of an @timestamp field in the dataset.',
|
||||
values: { language: textBasedLanguage },
|
||||
})}
|
||||
>
|
||||
{children}
|
||||
</EuiToolTip>
|
||||
);
|
||||
} 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 (
|
||||
<EuiFlexItem className={wrapperClasses}>
|
||||
<SuperDatePicker
|
||||
isDisabled={props.isDisabled}
|
||||
start={props.dateRangeFrom}
|
||||
end={props.dateRangeTo}
|
||||
isPaused={props.isRefreshPaused}
|
||||
refreshInterval={props.refreshInterval}
|
||||
onTimeChange={onTimeChange}
|
||||
onRefresh={onRefresh}
|
||||
onRefreshChange={props.onRefreshChange}
|
||||
showUpdateButton={false}
|
||||
recentlyUsedRanges={recentlyUsedRanges}
|
||||
locale={i18n.getLocale()}
|
||||
commonlyUsedRanges={commonlyUsedRanges}
|
||||
dateFormat={uiSettings.get('dateFormat')}
|
||||
isAutoRefreshOnly={showAutoRefreshOnly}
|
||||
className="kbnQueryBar__datePicker"
|
||||
isQuickSelectOnly={isMobile ? false : isQueryInputFocused}
|
||||
width={isMobile ? 'full' : 'auto'}
|
||||
compressed={shouldShowDatePickerAsBadge()}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
const datePicker = (
|
||||
<SuperDatePicker
|
||||
isDisabled={isDisabled}
|
||||
start={props.dateRangeFrom}
|
||||
end={props.dateRangeTo}
|
||||
isPaused={props.isRefreshPaused}
|
||||
refreshInterval={props.refreshInterval}
|
||||
onTimeChange={onTimeChange}
|
||||
onRefresh={onRefresh}
|
||||
onRefreshChange={props.onRefreshChange}
|
||||
showUpdateButton={false}
|
||||
recentlyUsedRanges={recentlyUsedRanges}
|
||||
locale={i18n.getLocale()}
|
||||
commonlyUsedRanges={commonlyUsedRanges}
|
||||
dateFormat={uiSettings.get('dateFormat')}
|
||||
isAutoRefreshOnly={showAutoRefreshOnly}
|
||||
className="kbnQueryBar__datePicker"
|
||||
isQuickSelectOnly={isMobile ? false : isQueryInputFocused}
|
||||
width={isMobile ? 'full' : 'auto'}
|
||||
compressed={shouldShowDatePickerAsBadge()}
|
||||
/>
|
||||
);
|
||||
const component = getWrapperWithTooltip(datePicker, enableTooltip, props.query);
|
||||
|
||||
return <EuiFlexItem className={wrapperClasses}>{component}</EuiFlexItem>;
|
||||
}
|
||||
|
||||
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,
|
||||
|
|
|
@ -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.'
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
@ -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 &&
|
||||
|
|
|
@ -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',
|
||||
},
|
||||
|
|
|
@ -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);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue