[Autocomplete] Support useTimeFilter option (#81515)

* pass timefilter to autocomplete

* ignoreTimeRange advanced setting

* Show all results in filter bar autocomplete

* Round timerange to minute for autocomplete memoization

* Change useTimeFilter param name
Update autocomplete tests and docs

* Fix maps test
useTimeFilter in uptime

* docs

* useTimeRange in apm

* remove date validation

* Update src/plugins/data/common/constants.ts

Co-authored-by: Lukas Olson <olson.lukas@gmail.com>

* docs

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Lukas Olson <olson.lukas@gmail.com>
This commit is contained in:
Liza Katz 2020-11-10 20:50:09 +02:00 committed by GitHub
parent ccb0b35452
commit 8842e9b7a9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 136 additions and 46 deletions

View file

@ -23,4 +23,5 @@ export interface QuerySuggestionGetFnArgs
| [selectionEnd](./kibana-plugin-plugins-data-public.querysuggestiongetfnargs.selectionend.md) | <code>number</code> | |
| [selectionStart](./kibana-plugin-plugins-data-public.querysuggestiongetfnargs.selectionstart.md) | <code>number</code> | |
| [signal](./kibana-plugin-plugins-data-public.querysuggestiongetfnargs.signal.md) | <code>AbortSignal</code> | |
| [useTimeRange](./kibana-plugin-plugins-data-public.querysuggestiongetfnargs.usetimerange.md) | <code>boolean</code> | |

View file

@ -0,0 +1,11 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) &gt; [QuerySuggestionGetFnArgs](./kibana-plugin-plugins-data-public.querysuggestiongetfnargs.md) &gt; [useTimeRange](./kibana-plugin-plugins-data-public.querysuggestiongetfnargs.usetimerange.md)
## QuerySuggestionGetFnArgs.useTimeRange property
<b>Signature:</b>
```typescript
useTimeRange?: boolean;
```

View file

@ -37,5 +37,6 @@ UI_SETTINGS: {
readonly INDEXPATTERN_PLACEHOLDER: "indexPattern:placeholder";
readonly FILTERS_PINNED_BY_DEFAULT: "filters:pinnedByDefault";
readonly FILTERS_EDITOR_SUGGEST_VALUES: "filterEditor:suggestValues";
readonly AUTOCOMPLETE_USE_TIMERANGE: "autocomplete:useTimeRange";
}
```

View file

@ -37,5 +37,6 @@ UI_SETTINGS: {
readonly INDEXPATTERN_PLACEHOLDER: "indexPattern:placeholder";
readonly FILTERS_PINNED_BY_DEFAULT: "filters:pinnedByDefault";
readonly FILTERS_EDITOR_SUGGEST_VALUES: "filterEditor:suggestValues";
readonly AUTOCOMPLETE_USE_TIMERANGE: "autocomplete:useTimeRange";
}
```

View file

@ -49,4 +49,5 @@ export const UI_SETTINGS = {
INDEXPATTERN_PLACEHOLDER: 'indexPattern:placeholder',
FILTERS_PINNED_BY_DEFAULT: 'filters:pinnedByDefault',
FILTERS_EDITOR_SUGGEST_VALUES: 'filterEditor:suggestValues',
AUTOCOMPLETE_USE_TIMERANGE: 'autocomplete:useTimeRange',
} as const;

View file

@ -39,6 +39,7 @@ export interface QuerySuggestionGetFnArgs {
selectionStart: number;
selectionEnd: number;
signal?: AbortSignal;
useTimeRange?: boolean;
boolFilter?: any;
}

View file

@ -21,6 +21,26 @@ import { stubIndexPattern, stubFields } from '../../stubs';
import { setupValueSuggestionProvider, ValueSuggestionsGetFn } from './value_suggestion_provider';
import { IUiSettingsClient, CoreSetup } from 'kibana/public';
jest.mock('../../services', () => ({
getQueryService: () => ({
timefilter: {
timefilter: {
createFilter: () => {
return {
time: 'fake',
};
},
getTime: () => {
return {
to: 'now',
from: 'now-15m',
};
},
},
},
}),
}));
describe('FieldSuggestions', () => {
let getValueSuggestions: ValueSuggestionsGetFn;
let http: any;
@ -94,6 +114,7 @@ describe('FieldSuggestions', () => {
indexPattern: stubIndexPattern,
field,
query: '',
useTimeRange: false,
});
expect(http.fetch).toHaveBeenCalled();
@ -107,6 +128,7 @@ describe('FieldSuggestions', () => {
indexPattern: stubIndexPattern,
field,
query: '',
useTimeRange: false,
};
await getValueSuggestions(args);
@ -123,6 +145,7 @@ describe('FieldSuggestions', () => {
indexPattern: stubIndexPattern,
field,
query: '',
useTimeRange: false,
};
const { now } = Date;
@ -146,50 +169,76 @@ describe('FieldSuggestions', () => {
indexPattern: stubIndexPattern,
field: fields[0],
query: '',
useTimeRange: false,
});
await getValueSuggestions({
indexPattern: stubIndexPattern,
field: fields[0],
query: 'query',
useTimeRange: false,
});
await getValueSuggestions({
indexPattern: stubIndexPattern,
field: fields[1],
query: '',
useTimeRange: false,
});
await getValueSuggestions({
indexPattern: stubIndexPattern,
field: fields[1],
query: 'query',
useTimeRange: false,
});
const customIndexPattern = {
...stubIndexPattern,
title: 'customIndexPattern',
useTimeRange: false,
};
await getValueSuggestions({
indexPattern: customIndexPattern,
field: fields[0],
query: '',
useTimeRange: false,
});
await getValueSuggestions({
indexPattern: customIndexPattern,
field: fields[0],
query: 'query',
useTimeRange: false,
});
await getValueSuggestions({
indexPattern: customIndexPattern,
field: fields[1],
query: '',
useTimeRange: false,
});
await getValueSuggestions({
indexPattern: customIndexPattern,
field: fields[1],
query: 'query',
useTimeRange: false,
});
expect(http.fetch).toHaveBeenCalledTimes(8);
});
it('should apply timefilter', async () => {
const [field] = stubFields.filter(
({ type, aggregatable }) => type === 'string' && aggregatable
);
await getValueSuggestions({
indexPattern: stubIndexPattern,
field,
query: '',
useTimeRange: true,
});
const callParams = http.fetch.mock.calls[0][1];
expect(JSON.parse(callParams.body).filters).toHaveLength(1);
expect(http.fetch).toHaveBeenCalled();
});
});
});

View file

@ -17,15 +17,16 @@
* under the License.
*/
import dateMath from '@elastic/datemath';
import { memoize } from 'lodash';
import { CoreSetup } from 'src/core/public';
import { IIndexPattern, IFieldType, UI_SETTINGS } from '../../../common';
import { IIndexPattern, IFieldType, UI_SETTINGS, buildQueryFromFilters } from '../../../common';
import { getQueryService } from '../../services';
function resolver(title: string, field: IFieldType, query: string, boolFilter: any) {
function resolver(title: string, field: IFieldType, query: string, filters: any[]) {
// Only cache results for a minute
const ttl = Math.floor(Date.now() / 1000 / 60);
return [ttl, query, title, field.name, JSON.stringify(boolFilter)].join('|');
return [ttl, query, title, field.name, JSON.stringify(filters)].join('|');
}
export type ValueSuggestionsGetFn = (args: ValueSuggestionsGetFnArgs) => Promise<any[]>;
@ -34,18 +35,31 @@ interface ValueSuggestionsGetFnArgs {
indexPattern: IIndexPattern;
field: IFieldType;
query: string;
useTimeRange?: boolean;
boolFilter?: any[];
signal?: AbortSignal;
}
const getAutocompleteTimefilter = (indexPattern: IIndexPattern) => {
const { timefilter } = getQueryService().timefilter;
const timeRange = timefilter.getTime();
// Use a rounded timerange so that memoizing works properly
const roundedTimerange = {
from: dateMath.parse(timeRange.from)!.startOf('minute').toISOString(),
to: dateMath.parse(timeRange.to)!.endOf('minute').toISOString(),
};
return timefilter.createFilter(indexPattern, roundedTimerange);
};
export const getEmptyValueSuggestions = (() => Promise.resolve([])) as ValueSuggestionsGetFn;
export const setupValueSuggestionProvider = (core: CoreSetup): ValueSuggestionsGetFn => {
const requestSuggestions = memoize(
(index: string, field: IFieldType, query: string, boolFilter: any = [], signal?: AbortSignal) =>
(index: string, field: IFieldType, query: string, filters: any = [], signal?: AbortSignal) =>
core.http.fetch(`/api/kibana/suggestions/values/${index}`, {
method: 'POST',
body: JSON.stringify({ query, field: field.name, boolFilter }),
body: JSON.stringify({ query, field: field.name, filters }),
signal,
}),
resolver
@ -55,12 +69,15 @@ export const setupValueSuggestionProvider = (core: CoreSetup): ValueSuggestionsG
indexPattern,
field,
query,
useTimeRange,
boolFilter,
signal,
}: ValueSuggestionsGetFnArgs): Promise<any[]> => {
const shouldSuggestValues = core!.uiSettings.get<boolean>(
UI_SETTINGS.FILTERS_EDITOR_SUGGEST_VALUES
);
useTimeRange =
useTimeRange ?? core!.uiSettings.get<boolean>(UI_SETTINGS.AUTOCOMPLETE_USE_TIMERANGE);
const { title } = indexPattern;
if (field.type === 'boolean') {
@ -69,6 +86,9 @@ export const setupValueSuggestionProvider = (core: CoreSetup): ValueSuggestionsG
return [];
}
return await requestSuggestions(title, field, query, boolFilter, signal);
const timeFilter = useTimeRange ? getAutocompleteTimefilter(indexPattern) : undefined;
const filterQuery = timeFilter ? buildQueryFromFilters([timeFilter], indexPattern).filter : [];
const filters = [...(boolFilter ? boolFilter : []), ...filterQuery];
return await requestSuggestions(title, field, query, filters, signal);
};
};

View file

@ -1840,6 +1840,8 @@ export interface QuerySuggestionGetFnArgs {
selectionStart: number;
// (undocumented)
signal?: AbortSignal;
// (undocumented)
useTimeRange?: boolean;
}
// Warning: (ae-missing-release-tag) "QuerySuggestionTypes" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
@ -2309,6 +2311,7 @@ export const UI_SETTINGS: {
readonly INDEXPATTERN_PLACEHOLDER: "indexPattern:placeholder";
readonly FILTERS_PINNED_BY_DEFAULT: "filters:pinnedByDefault";
readonly FILTERS_EDITOR_SUGGEST_VALUES: "filterEditor:suggestValues";
readonly AUTOCOMPLETE_USE_TIMERANGE: "autocomplete:useTimeRange";
};

View file

@ -24,9 +24,14 @@ import { PublicMethodsOf } from '@kbn/utility-types';
import { areRefreshIntervalsDifferent, areTimeRangesDifferent } from './lib/diff_time_picker_vals';
import { getForceNow } from './lib/get_force_now';
import { TimefilterConfig, InputTimeRange, TimeRangeBounds } from './types';
import { calculateBounds, getTime, RefreshInterval, TimeRange } from '../../../common';
import {
calculateBounds,
getTime,
IIndexPattern,
RefreshInterval,
TimeRange,
} from '../../../common';
import { TimeHistoryContract } from './time_history';
import { IndexPattern } from '../../index_patterns';
// TODO: remove!
@ -170,7 +175,7 @@ export class Timefilter {
}
};
public createFilter = (indexPattern: IndexPattern, timeRange?: TimeRange) => {
public createFilter = (indexPattern: IIndexPattern, timeRange?: TimeRange) => {
return getTime(indexPattern, timeRange ? timeRange : this._time, {
forceNow: this.getForceNow(),
});

View file

@ -85,6 +85,8 @@ export class PhraseSuggestorUI<T extends PhraseSuggestorProps> extends React.Com
field,
query,
signal: this.abortController.signal,
// Show all results in filter bar autocomplete
useTimeRange: false,
});
this.setState({ suggestions, isLoading: false });

View file

@ -45,7 +45,7 @@ export function registerValueSuggestionsRoute(
{
field: schema.string(),
query: schema.string(),
boolFilter: schema.maybe(schema.any()),
filters: schema.maybe(schema.any()),
},
{ unknowns: 'allow' }
),
@ -53,7 +53,7 @@ export function registerValueSuggestionsRoute(
},
async (context, request, response) => {
const config = await config$.pipe(first()).toPromise();
const { field: fieldName, query, boolFilter } = request.body;
const { field: fieldName, query, filters } = request.body;
const { index } = request.params;
const { client } = context.core.elasticsearch.legacy;
const signal = getRequestAbortedSignal(request.events.aborted$);
@ -66,7 +66,7 @@ export function registerValueSuggestionsRoute(
const indexPattern = await findIndexPatternById(context.core.savedObjects.client, index);
const field = indexPattern && getFieldByName(fieldName, indexPattern);
const body = await getBody(autocompleteSearchOptions, field || fieldName, query, boolFilter);
const body = await getBody(autocompleteSearchOptions, field || fieldName, query, filters);
try {
const result = await client.callAsCurrentUser('search', { index, body }, { signal });
@ -88,7 +88,7 @@ async function getBody(
{ timeout, terminate_after }: Record<string, any>,
field: IFieldType | string,
query: string,
boolFilter: Filter[] = []
filters: Filter[] = []
) {
const isFieldObject = (f: any): f is IFieldType => Boolean(f && f.name);
@ -108,7 +108,7 @@ async function getBody(
terminate_after,
query: {
bool: {
filter: boolFilter,
filter: filters,
},
},
aggs: {

View file

@ -1181,6 +1181,7 @@ export const UI_SETTINGS: {
readonly INDEXPATTERN_PLACEHOLDER: "indexPattern:placeholder";
readonly FILTERS_PINNED_BY_DEFAULT: "filters:pinnedByDefault";
readonly FILTERS_EDITOR_SUGGEST_VALUES: "filterEditor:suggestValues";
readonly AUTOCOMPLETE_USE_TIMERANGE: "autocomplete:useTimeRange";
};
// Warning: (ae-missing-release-tag) "usageProvider" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)

View file

@ -684,5 +684,17 @@ export function getUiSettings(): Record<string, UiSettingsParams<unknown>> {
}),
schema: schema.boolean(),
},
[UI_SETTINGS.AUTOCOMPLETE_USE_TIMERANGE]: {
name: i18n.translate('data.advancedSettings.autocompleteIgnoreTimerange', {
defaultMessage: 'Use time range',
description: 'Restrict autocomplete results to the current time range',
}),
value: true,
description: i18n.translate('data.advancedSettings.autocompleteIgnoreTimerangeText', {
defaultMessage:
'Disable this property to get autocomplete suggestions from your full dataset, rather than from the current time range.',
}),
schema: schema.boolean(),
},
};
}

View file

@ -26,23 +26,7 @@ export function getBoolFilter({
serviceName?: string;
urlParams: IUrlParams;
}) {
const { start, end } = urlParams;
if (!start || !end) {
throw new Error('Date range was not defined');
}
const boolFilter: ESFilter[] = [
{
range: {
'@timestamp': {
gte: new Date(start).getTime(),
lte: new Date(end).getTime(),
format: 'epoch_millis',
},
},
},
];
const boolFilter: ESFilter[] = [];
if (serviceName) {
boolFilter.push({

View file

@ -111,6 +111,7 @@ export function KueryBar() {
query: inputValue,
selectionStart,
selectionEnd: selectionStart,
useTimeRange: true,
})) || []
)
.filter((suggestion) => !startsWith(suggestion.text, 'span.'))

View file

@ -27,7 +27,7 @@ const wrapAsSuggestions = (start: number, end: number, query: string, values: st
export const setupGetValueSuggestions: KqlQuerySuggestionProvider = () => {
return async (
{ indexPatterns, boolFilter, signal },
{ indexPatterns, boolFilter, useTimeRange, signal },
{ start, end, prefix, suffix, fieldName, nestedPath }
): Promise<QuerySuggestion[]> => {
const fullFieldName = nestedPath ? `${nestedPath}.${fieldName}` : fieldName;
@ -49,6 +49,7 @@ export const setupGetValueSuggestions: KqlQuerySuggestionProvider = () => {
field,
query,
boolFilter,
useTimeRange,
signal,
}).then((valueSuggestions) => {
const quotedValues = valueSuggestions.map((value) =>

View file

@ -67,7 +67,7 @@ export function KueryBar({
let currentRequestCheck: string;
const [getUrlParams, updateUrlParams] = useUrlParams();
const { search: kuery, dateRangeStart, dateRangeEnd } = getUrlParams();
const { search: kuery } = getUrlParams();
useEffect(() => {
updateSearchText(kuery);
@ -105,16 +105,7 @@ export function KueryBar({
query: inputValue,
selectionStart: selectionStart || 0,
selectionEnd: selectionStart || 0,
boolFilter: [
{
range: {
'@timestamp': {
gte: dateRangeStart,
lte: dateRangeEnd,
},
},
},
],
useTimeRange: true,
})) || []
).filter((suggestion: QuerySuggestion) => !suggestion.text.startsWith('span.'));
if (currentRequest !== currentRequestCheck) {

View file

@ -10,13 +10,14 @@ import { FtrProviderContext } from '../../ftr_provider_context';
export default function ({ getService, getPageObjects }: FtrProviderContext) {
const esArchiver = getService('esArchiver');
const queryBar = getService('queryBar');
const PageObjects = getPageObjects(['common']);
const PageObjects = getPageObjects(['common', 'timePicker']);
describe('value suggestions', function describeIndexTests() {
before(async function () {
await esArchiver.loadIfNeeded('logstash_functional');
await esArchiver.load('dashboard/drilldowns');
await PageObjects.common.navigateToApp('discover');
await PageObjects.timePicker.setDefaultAbsoluteRange();
});
after(async () => {

View file

@ -7,13 +7,17 @@
import expect from '@kbn/expect';
export default function ({ getService, getPageObjects }) {
const PageObjects = getPageObjects(['maps']);
const PageObjects = getPageObjects(['maps', 'timePicker']);
const security = getService('security');
describe('vector styling', () => {
before(async () => {
await security.testUser.setRoles(['test_logstash_reader', 'global_maps_all']);
await PageObjects.maps.loadSavedMap('document example');
await PageObjects.timePicker.setAbsoluteRange(
'Mar 1, 2015 @ 00:00:00.000',
'Mar 1, 2016 @ 00:00:00.000'
);
});
after(async () => {
await PageObjects.maps.refreshAndClearUnsavedChangesWarning();