[ES|QL] Initialize with time named params for dataviews without @timestamp (#189367)

## Summary

Closes https://github.com/elastic/kibana/issues/189032

For indices with no @timestamp, when the users are transitioning from
the dataview mode to the ES|QL mode then we are defaulting to
```
from dataviewIndex | where dataviewTimefield <= ?end and dataviewTimefield >= ?start
```

![meow](https://github.com/user-attachments/assets/d55e17d1-d867-400a-b6eb-b7cf7047d070)

which will :
1. Train the users for the named params
2. Enable the histogram
3. Enable the datepicker

### Checklist

- [ ] [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: Julia Rechkunova <julia.rechkunova@gmail.com>
This commit is contained in:
Stratoula Kalafateli 2024-08-01 17:39:46 +02:00 committed by GitHub
parent 9189208b5e
commit 788e9b34dd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 133 additions and 25 deletions

View file

@ -5,11 +5,98 @@
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import type { DataView } from '@kbn/data-views-plugin/public';
import { getInitialESQLQuery } from './get_initial_esql_query';
const getDataView = (name: string, dataViewFields: DataView['fields'], timeFieldName?: string) => {
dataViewFields.getByName = (fieldName: string) => {
return dataViewFields.find((field) => field.name === fieldName);
};
return {
id: `${name}-id`,
title: name,
metaFields: ['_index', '_score'],
fields: dataViewFields,
type: 'default',
getName: () => name,
getIndexPattern: () => name,
getFieldByName: jest.fn((fieldName: string) => dataViewFields.getByName(fieldName)),
timeFieldName,
isPersisted: () => true,
toSpec: () => ({}),
toMinimalSpec: () => ({}),
} as unknown as DataView;
};
describe('getInitialESQLQuery', () => {
it('should work correctly', () => {
expect(getInitialESQLQuery('logs*')).toBe('FROM logs* | LIMIT 10');
it('should NOT add the where clause if there is @timestamp in the index', () => {
const fields = [
{
name: '@timestamp',
displayName: '@timestamp',
type: 'date',
scripted: false,
filterable: true,
aggregatable: true,
sortable: true,
},
{
name: 'message',
displayName: 'message',
type: 'string',
scripted: false,
filterable: false,
},
] as DataView['fields'];
const dataView = getDataView('logs*', fields, '@timestamp');
expect(getInitialESQLQuery(dataView)).toBe('FROM logs* | LIMIT 10');
});
it('should NOT add the where clause if there is @timestamp in the index although the dataview timefielName is different', () => {
const fields = [
{
name: '@timestamp',
displayName: '@timestamp',
type: 'date',
scripted: false,
filterable: true,
aggregatable: true,
sortable: true,
},
{
name: 'message',
displayName: 'message',
type: 'string',
scripted: false,
filterable: false,
},
] as DataView['fields'];
const dataView = getDataView('logs*', fields, 'timestamp');
expect(getInitialESQLQuery(dataView)).toBe('FROM logs* | LIMIT 10');
});
it('should append a where clause correctly if there is no @timestamp in the index fields', () => {
const fields = [
{
name: '@custom_timestamp',
displayName: '@custom_timestamp',
type: 'date',
scripted: false,
filterable: true,
aggregatable: true,
sortable: true,
},
{
name: 'message',
displayName: 'message',
type: 'string',
scripted: false,
filterable: false,
},
] as DataView['fields'];
const dataView = getDataView('logs*', fields, '@custom_timestamp');
expect(getInitialESQLQuery(dataView)).toBe(
'FROM logs* | WHERE @custom_timestamp >= ?start AND @custom_timestamp <= ?end | LIMIT 10'
);
});
});

View file

@ -5,11 +5,20 @@
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import type { DataView } from '@kbn/data-views-plugin/public';
/**
* Builds an ES|QL query for the provided index or index pattern
* @param indexOrIndexPattern
* Builds an ES|QL query for the provided dataView
* If there is @timestamp field in the index, we don't add the WHERE clause
* If there is no @timestamp and there is a dataView timeFieldName, we add the WHERE clause with the timeFieldName
* @param dataView
*/
export function getInitialESQLQuery(indexOrIndexPattern: string): string {
return `FROM ${indexOrIndexPattern} | LIMIT 10`;
export function getInitialESQLQuery(dataView: DataView): string {
const hasAtTimestampField = dataView?.fields?.getByName?.('@timestamp')?.type === 'date';
const timeFieldName = dataView?.timeFieldName;
const filterByTimeParams =
!hasAtTimestampField && timeFieldName
? ` | WHERE ${timeFieldName} >= ?start AND ${timeFieldName} <= ?end`
: '';
return `FROM ${dataView.getIndexPattern()}${filterByTimeParams} | LIMIT 10`;
}

View file

@ -9,14 +9,14 @@
import { DISCOVER_ESQL_LOCATOR } from '@kbn/deeplinks-analytics';
import { LocatorDefinition, LocatorPublic } from '@kbn/share-plugin/common';
import { SerializableRecord } from '@kbn/utility-types';
import { getIndexForESQLQuery, getInitialESQLQuery } from '@kbn/esql-utils';
import { getIndexForESQLQuery, getInitialESQLQuery, getESQLAdHocDataview } from '@kbn/esql-utils';
import type { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public';
export type DiscoverESQLLocatorParams = SerializableRecord;
export interface DiscoverESQLLocatorDependencies {
discoverAppLocator: LocatorPublic<SerializableRecord>;
getIndices: DataViewsPublicPluginStart['getIndices'];
dataViews: DataViewsPublicPluginStart;
}
export type DiscoverESQLLocator = LocatorPublic<DiscoverESQLLocatorParams>;
@ -27,10 +27,10 @@ export class DiscoverESQLLocatorDefinition implements LocatorDefinition<Discover
constructor(protected readonly deps: DiscoverESQLLocatorDependencies) {}
public readonly getLocation = async () => {
const { discoverAppLocator, getIndices } = this.deps;
const indexName = await getIndexForESQLQuery({ dataViews: { getIndices } });
const esql = getInitialESQLQuery(indexName ?? '*');
const { discoverAppLocator, dataViews } = this.deps;
const indexName = (await getIndexForESQLQuery({ dataViews })) ?? '*';
const dataView = await getESQLAdHocDataview(`from ${indexName}`, dataViews);
const esql = getInitialESQLQuery(dataView);
const params = {
query: { esql },

View file

@ -377,7 +377,7 @@ function getLoadParamsForNewSearch(stateContainer: DiscoverStateContainer): {
? {
// reset to a default ES|QL query
query: {
esql: getInitialESQLQuery(prevDataView.getIndexPattern()),
esql: getInitialESQLQuery(prevDataView),
},
}
: undefined;

View file

@ -50,7 +50,7 @@ export const getESQLSearchProvider: (
const params = {
query: {
esql: getInitialESQLQuery(defaultDataView?.getIndexPattern()),
esql: getInitialESQLQuery(defaultDataView),
},
dataViewSpec: defaultDataView?.toSpec(),
};

View file

@ -278,7 +278,7 @@ export class DiscoverPlugin
plugins.share?.url.locators.create(
new DiscoverESQLLocatorDefinition({
discoverAppLocator: this.locator,
getIndices: plugins.dataViews.getIndices,
dataViews: plugins.dataViews,
})
);
}

View file

@ -92,7 +92,7 @@ export function ChangeDataView({
Boolean(textBasedLanguage)
);
const [isTextLangTransitionModalVisible, setIsTextLangTransitionModalVisible] = useState(false);
const [selectedDataViewId, setSelectedDataViewId] = useState(currentDataViewId);
const [selectedDataView, setSelectedDataView] = useState<DataView | undefined>(undefined);
const kibana = useKibana<IUnifiedSearchPluginServices>();
const { application, data, storage, dataViews, dataViewEditor, appName, usageCollection } =
@ -117,6 +117,10 @@ export function ChangeDataView({
adHocDataViews?.map(mapAdHocDataView) ?? [];
setDataViewsList(savedDataViewRefs.concat(adHocDataViewRefs));
if (currentDataViewId) {
const currentDataview = await data.dataViews.get(currentDataViewId, false);
setSelectedDataView(currentDataview);
}
};
fetchDataViews();
}, [data, currentDataViewId, adHocDataViews, savedDataViews, isTextBasedLangSelected]);
@ -308,7 +312,8 @@ export function ChangeDataView({
isTextBasedLangSelected={isTextBasedLangSelected}
setPopoverIsOpen={setPopoverIsOpen}
onChangeDataView={async (newId) => {
setSelectedDataViewId(newId);
const currentDataview = await data.dataViews.get(newId, false);
setSelectedDataView(currentDataview);
setPopoverIsOpen(false);
if (isTextBasedLangSelected) {
@ -348,7 +353,13 @@ export function ChangeDataView({
color="success"
size="s"
fullWidth
onClick={() => onTextBasedSubmit({ esql: getInitialESQLQuery(trigger.title!) })}
onClick={() => {
if (selectedDataView) {
onTextBasedSubmit({
esql: getInitialESQLQuery(selectedDataView),
});
}
}}
data-test-subj="select-text-based-language-panel"
contentProps={{
css: {
@ -393,8 +404,8 @@ export function ChangeDataView({
language: 'kuery',
query: '',
});
if (selectedDataViewId) {
onChangeDataView(selectedDataViewId);
if (selectedDataView?.id) {
onChangeDataView(selectedDataView?.id);
}
setTriggerLabel(trigger.label);
if (shouldDismissModal) {
@ -405,7 +416,7 @@ export function ChangeDataView({
onChangeDataView,
onTextLangQuerySubmit,
onTransitionModalDismiss,
selectedDataViewId,
selectedDataView?.id,
trigger.label,
]
);

View file

@ -14,6 +14,7 @@ import {
getIndexForESQLQuery,
ENABLE_ESQL,
getESQLQueryColumns,
getInitialESQLQuery,
} from '@kbn/esql-utils';
import type { Datasource, Visualization } from '../../types';
import type { LensPluginStartDependencies } from '../../plugin';
@ -81,10 +82,10 @@ export async function executeCreateAction({
setVisualizationMap(visualizationMap);
}
const defaultIndex = dataView.getIndexPattern();
const esqlQuery = getInitialESQLQuery(dataView);
const defaultEsqlQuery = {
esql: `FROM ${defaultIndex} | LIMIT 10`,
esql: esqlQuery,
};
// For the suggestions api we need only the columns
@ -93,7 +94,7 @@ export async function executeCreateAction({
// all the table
const abortController = new AbortController();
const columns = await getESQLQueryColumns({
esqlQuery: `from ${defaultIndex}`,
esqlQuery,
search: deps.data.search.search,
signal: abortController.signal,
timeRange: deps.data.query.timefilter.timefilter.getAbsoluteTime(),