Cancel discarded KQL value suggestion requests (#51411)

* Fix filter matches index for filters with partial meta

* Abort discarded KQL value suggestion requests

* Abort server-side connection to ES

* Fix failing test
This commit is contained in:
Lukas Olson 2019-12-09 12:43:52 -07:00 committed by GitHub
parent 4bbe3cf85b
commit 25c750b225
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 43 additions and 17 deletions

View file

@ -18,6 +18,7 @@
*/
import { get, map } from 'lodash';
import { abortableRequestHandler } from '../../../../../elasticsearch/lib/abortable_request_handler';
export function registerValueSuggestions(server) {
const serverConfig = server.config();
@ -26,7 +27,7 @@ export function registerValueSuggestions(server) {
server.route({
path: '/api/kibana/suggestions/values/{index}',
method: ['POST'],
handler: async function (req) {
handler: abortableRequestHandler(async function (signal, req) {
const { index } = req.params;
const { field: fieldName, query, boolFilter } = req.payload;
const { callWithRequest } = server.plugins.elasticsearch.getCluster('data');
@ -46,7 +47,7 @@ export function registerValueSuggestions(server) {
);
try {
const response = await callWithRequest(req, 'search', { index, body });
const response = await callWithRequest(req, 'search', { index, body }, { signal });
const buckets = get(response, 'aggregations.suggestions.buckets')
|| get(response, 'aggregations.nestedSuggestions.suggestions.buckets')
|| [];
@ -55,7 +56,7 @@ export function registerValueSuggestions(server) {
} catch (error) {
throw server.plugins.elasticsearch.handleESError(error);
}
},
}),
});
}

View file

@ -40,6 +40,7 @@ export type GetSuggestions = (args: {
query: string;
selectionStart: number;
selectionEnd: number;
signal?: AbortSignal;
}) => Promise<AutocompleteSuggestion[]>;
/** @public **/

View file

@ -28,23 +28,36 @@ export function getSuggestionsProvider(
http: HttpServiceBase
): IGetSuggestions {
const requestSuggestions = memoize(
(index: string, field: IFieldType, query: string, boolFilter: any = []) => {
(
index: string,
field: IFieldType,
query: string,
boolFilter: any = [],
signal?: AbortSignal
) => {
return http.fetch(`/api/kibana/suggestions/values/${index}`, {
method: 'POST',
body: JSON.stringify({ query, field: field.name, boolFilter }),
signal,
});
},
resolver
);
return async (index: string, field: IFieldType, query: string, boolFilter?: any) => {
return async (
index: string,
field: IFieldType,
query: string,
boolFilter?: any,
signal?: AbortSignal
) => {
const shouldSuggestValues = uiSettings.get('filterEditor:suggestValues');
if (field.type === 'boolean') {
return [true, false];
} else if (!shouldSuggestValues || !field.aggregatable || field.type !== 'string') {
return [];
}
return await requestSuggestions(index, field, query, boolFilter);
return await requestSuggestions(index, field, query, boolFilter, signal);
};
}

View file

@ -106,6 +106,7 @@ export class QueryStringInputUI extends Component<Props, State> {
public inputRef: HTMLInputElement | null = null;
private persistedLog: PersistedLog | undefined;
private abortController: AbortController | undefined;
private services = this.props.kibana.services;
private componentIsUnmounting = false;
@ -163,12 +164,22 @@ export class QueryStringInputUI extends Component<Props, State> {
return;
}
const suggestions: AutocompleteSuggestion[] = await getAutocompleteSuggestions({
query: queryString,
selectionStart,
selectionEnd,
});
return [...suggestions, ...recentSearchSuggestions];
try {
if (this.abortController) this.abortController.abort();
this.abortController = new AbortController();
const suggestions: AutocompleteSuggestion[] = await getAutocompleteSuggestions({
query: queryString,
selectionStart,
selectionEnd,
signal: this.abortController.signal,
});
return [...suggestions, ...recentSearchSuggestions];
} catch (e) {
// TODO: Waiting on https://github.com/elastic/kibana/issues/51406 for a properly typed error
// Ignore aborted requests
if (e.message === 'The user aborted a request.') return;
throw e;
}
};
private getRecentSearchSuggestions = (query: string) => {

View file

@ -22,7 +22,7 @@ export const kueryProvider = ({ config, indexPatterns, boolFilter }) => {
return provider({ config, indexPatterns, boolFilter });
});
return function getSuggestions({ query, selectionStart, selectionEnd }) {
return function getSuggestions({ query, selectionStart, selectionEnd, signal }) {
const cursoredQuery = `${query.substr(0, selectionStart)}${cursorSymbol}${query.substr(selectionEnd)}`;
let cursorNode;
@ -34,7 +34,7 @@ export const kueryProvider = ({ config, indexPatterns, boolFilter }) => {
const { suggestionTypes = [] } = cursorNode;
const suggestionsByType = suggestionTypes.map(type => {
return getSuggestionsByType[type](cursorNode);
return getSuggestionsByType[type](cursorNode, signal);
});
return Promise.all(suggestionsByType)
.then(suggestionsByType => dedup(flatten(suggestionsByType)));

View file

@ -27,14 +27,14 @@ export function getSuggestionsProvider({ indexPatterns, boolFilter }) {
suffix,
fieldName,
nestedPath,
}) {
}, signal) {
const fullFieldName = nestedPath ? `${nestedPath}.${fieldName}` : fieldName;
const fields = allFields.filter(field => field.name === fullFieldName);
const query = `${prefix}${suffix}`.trim();
const { getSuggestions } = npStart.plugins.data;
const suggestionsByField = fields.map(field => {
return getSuggestions(field.indexPatternTitle, field, query, boolFilter).then(data => {
return getSuggestions(field.indexPatternTitle, field, query, boolFilter, signal).then(data => {
const quotedValues = data.map(value => typeof value === 'string' ? `"${escapeQuotes(value)}"` : `${value}`);
return wrapAsSuggestions(start, end, query, quotedValues);
});

View file

@ -112,7 +112,7 @@ describe('Kuery value suggestions', function () {
const spy = jest.spyOn(npStart.plugins.data, 'getSuggestions');
await getSuggestions({ fieldName, prefix, suffix });
expect(spy).toHaveBeenCalledTimes(1);
expect(spy).toBeCalledWith(expect.any(String), expect.any(Object), prefix + suffix, undefined);
expect(spy).toBeCalledWith(expect.any(String), expect.any(Object), prefix + suffix, undefined, undefined);
});
test('should escape quotes in suggestions', async () => {