From 801302e3ce1807ff03fe3a3e6c56c4530ad3a536 Mon Sep 17 00:00:00 2001 From: Alexey Antonov Date: Fri, 17 Jan 2020 22:42:51 +0300 Subject: [PATCH] [Data Plugin] combine autocomplete provider and suggestions provider (#54451) * [Data Plugin] combine autocomplete provider and suggestions provider Closes: #52843 * [Data Plugin] combine autocomplete provider and suggestions provider - add skeleton for SuggestionsProvider * autocomplete_provider -> autocomplete * value_suggestions.ts - change getSuggestions method * remove suggestions_provider folder * fix PR comments * fix PR comments * fix CI * fix CI * getFieldSuggestions -> getValueSuggestions * update Jest snaphots Co-authored-by: Elastic Machine --- .../autocomplete/autocomplete_service.ts | 77 +++++++++ .../index.ts | 3 +- .../providers/query_suggestion_provider.ts} | 64 +++---- .../value_suggestion_provider.test.ts} | 159 ++++++++++++------ .../providers/value_suggestion_provider.ts} | 68 ++++---- .../types.ts | 21 ++- .../public/autocomplete_provider/index.ts | 40 ----- src/plugins/data/public/index.ts | 9 +- src/plugins/data/public/mocks.ts | 6 +- src/plugins/data/public/plugin.ts | 10 +- src/plugins/data/public/types.ts | 11 +- .../filter_editor/phrase_suggestor.tsx | 10 +- .../query_string_input.test.tsx.snap | 36 ++-- .../query_string_input/query_string_input.tsx | 38 ++--- .../typeahead/suggestion_component.test.tsx | 4 +- .../ui/typeahead/suggestion_component.tsx | 6 +- .../typeahead/suggestions_component.test.tsx | 4 +- .../ui/typeahead/suggestions_component.tsx | 6 +- .../components/shared/KueryBar/index.tsx | 46 +---- .../components/autocomplete_field/index.tsx | 4 +- .../autocomplete_field/suggestion_item.tsx | 4 +- .../public/components/table/table.tsx | 4 +- .../containers/with_kuery_autocompletion.tsx | 6 +- .../adapters/elasticsearch/adapter_types.ts | 4 +- .../lib/adapters/elasticsearch/memory.ts | 6 +- .../public/lib/adapters/elasticsearch/rest.ts | 36 ++-- .../public/lib/compose/memory.ts | 4 +- .../public/lib/elasticsearch.ts | 4 +- .../public/components/search_bar.test.tsx | 3 +- .../autocomplete_field/autocomplete_field.tsx | 4 +- .../autocomplete_field/suggestion_item.tsx | 4 +- .../containers/with_kuery_autocompletion.tsx | 37 ++-- .../public/autocomplete_providers/index.js | 45 ----- .../__fixtures__/index_pattern_response.json | 0 .../__tests__/conjunction.js | 0 .../__tests__/escape_kuery.js | 0 .../__tests__/field.js | 0 .../__tests__/operator.js | 0 .../conjunction.js | 0 .../escape_kuery.js | 0 .../field.js | 0 .../public/kql_query_suggestion/index.js | 58 +++++++ .../operator.js | 0 .../sort_prefix_first.test.ts | 0 .../sort_prefix_first.ts | 0 .../value.js | 28 +-- .../value.test.js | 104 ++++++------ .../kuery_autocomplete/public/plugin.ts | 8 +- .../kql_filter_bar/kql_filter_bar.js | 8 +- .../kql_filter_bar/kql_filter_bar.test.js | 2 + .../components/kql_filter_bar/utils.js | 18 +- .../__examples__/index.stories.tsx | 4 +- .../autocomplete_field/index.test.tsx | 4 +- .../components/autocomplete_field/index.tsx | 4 +- .../autocomplete_field/suggestion_item.tsx | 4 +- .../containers/kuery_autocompletion/index.tsx | 38 ++--- .../components/functional/kuery_bar/index.tsx | 55 ++---- .../plugins/uptime/public/pages/overview.tsx | 4 +- .../legacy/plugins/uptime/public/routes.tsx | 4 +- 59 files changed, 575 insertions(+), 551 deletions(-) create mode 100644 src/plugins/data/public/autocomplete/autocomplete_service.ts rename src/plugins/data/public/{suggestions_provider => autocomplete}/index.ts (84%) rename src/plugins/data/public/{autocomplete_provider/types.ts => autocomplete/providers/query_suggestion_provider.ts} (59%) rename src/plugins/data/public/{suggestions_provider/value_suggestions.test.ts => autocomplete/providers/value_suggestion_provider.test.ts} (51%) rename src/plugins/data/public/{suggestions_provider/value_suggestions.ts => autocomplete/providers/value_suggestion_provider.ts} (55%) rename src/plugins/data/public/{suggestions_provider => autocomplete}/types.ts (67%) delete mode 100644 src/plugins/data/public/autocomplete_provider/index.ts delete mode 100644 x-pack/legacy/plugins/kuery_autocomplete/public/autocomplete_providers/index.js rename x-pack/legacy/plugins/kuery_autocomplete/public/{autocomplete_providers => kql_query_suggestion}/__fixtures__/index_pattern_response.json (100%) rename x-pack/legacy/plugins/kuery_autocomplete/public/{autocomplete_providers => kql_query_suggestion}/__tests__/conjunction.js (100%) rename x-pack/legacy/plugins/kuery_autocomplete/public/{autocomplete_providers => kql_query_suggestion}/__tests__/escape_kuery.js (100%) rename x-pack/legacy/plugins/kuery_autocomplete/public/{autocomplete_providers => kql_query_suggestion}/__tests__/field.js (100%) rename x-pack/legacy/plugins/kuery_autocomplete/public/{autocomplete_providers => kql_query_suggestion}/__tests__/operator.js (100%) rename x-pack/legacy/plugins/kuery_autocomplete/public/{autocomplete_providers => kql_query_suggestion}/conjunction.js (100%) rename x-pack/legacy/plugins/kuery_autocomplete/public/{autocomplete_providers => kql_query_suggestion}/escape_kuery.js (100%) rename x-pack/legacy/plugins/kuery_autocomplete/public/{autocomplete_providers => kql_query_suggestion}/field.js (100%) create mode 100644 x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/index.js rename x-pack/legacy/plugins/kuery_autocomplete/public/{autocomplete_providers => kql_query_suggestion}/operator.js (100%) rename x-pack/legacy/plugins/kuery_autocomplete/public/{autocomplete_providers => kql_query_suggestion}/sort_prefix_first.test.ts (100%) rename x-pack/legacy/plugins/kuery_autocomplete/public/{autocomplete_providers => kql_query_suggestion}/sort_prefix_first.ts (100%) rename x-pack/legacy/plugins/kuery_autocomplete/public/{autocomplete_providers => kql_query_suggestion}/value.js (71%) rename x-pack/legacy/plugins/kuery_autocomplete/public/{autocomplete_providers => kql_query_suggestion}/value.test.js (55%) diff --git a/src/plugins/data/public/autocomplete/autocomplete_service.ts b/src/plugins/data/public/autocomplete/autocomplete_service.ts new file mode 100644 index 000000000000..0527f833b0f8 --- /dev/null +++ b/src/plugins/data/public/autocomplete/autocomplete_service.ts @@ -0,0 +1,77 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { CoreSetup } from 'src/core/public'; +import { QuerySuggestionsGetFn } from './providers/query_suggestion_provider'; +import { + setupValueSuggestionProvider, + ValueSuggestionsGetFn, +} from './providers/value_suggestion_provider'; + +export class AutocompleteService { + private readonly querySuggestionProviders: Map = new Map(); + private getValueSuggestions?: ValueSuggestionsGetFn; + + private addQuerySuggestionProvider = ( + language: string, + provider: QuerySuggestionsGetFn + ): void => { + if (language && provider) { + this.querySuggestionProviders.set(language, provider); + } + }; + + private getQuerySuggestions: QuerySuggestionsGetFn = args => { + const { language } = args; + const provider = this.querySuggestionProviders.get(language); + + if (provider) { + return provider(args); + } + }; + + private hasQuerySuggestions = (language: string) => this.querySuggestionProviders.has(language); + + /** @public **/ + public setup(core: CoreSetup) { + this.getValueSuggestions = setupValueSuggestionProvider(core); + + return { + addQuerySuggestionProvider: this.addQuerySuggestionProvider, + + /** @obsolete **/ + /** please use "getProvider" only from the start contract **/ + getQuerySuggestions: this.getQuerySuggestions, + }; + } + + /** @public **/ + public start() { + return { + getQuerySuggestions: this.getQuerySuggestions, + hasQuerySuggestions: this.hasQuerySuggestions, + getValueSuggestions: this.getValueSuggestions!, + }; + } + + /** @internal **/ + public clearProviders(): void { + this.querySuggestionProviders.clear(); + } +} diff --git a/src/plugins/data/public/suggestions_provider/index.ts b/src/plugins/data/public/autocomplete/index.ts similarity index 84% rename from src/plugins/data/public/suggestions_provider/index.ts rename to src/plugins/data/public/autocomplete/index.ts index c83662043b82..5b8f3ae510bf 100644 --- a/src/plugins/data/public/suggestions_provider/index.ts +++ b/src/plugins/data/public/autocomplete/index.ts @@ -17,4 +17,5 @@ * under the License. */ -export { getSuggestionsProvider } from './value_suggestions'; +export { AutocompleteService } from './autocomplete_service'; +export { QuerySuggestion, QuerySuggestionType, QuerySuggestionsGetFn } from './types'; diff --git a/src/plugins/data/public/autocomplete_provider/types.ts b/src/plugins/data/public/autocomplete/providers/query_suggestion_provider.ts similarity index 59% rename from src/plugins/data/public/autocomplete_provider/types.ts rename to src/plugins/data/public/autocomplete/providers/query_suggestion_provider.ts index 389057f94144..53abdd44c0c3 100644 --- a/src/plugins/data/public/autocomplete_provider/types.ts +++ b/src/plugins/data/public/autocomplete/providers/query_suggestion_provider.ts @@ -17,56 +17,40 @@ * under the License. */ -import { AutocompleteProviderRegister } from '.'; -import { IIndexPattern, IFieldType } from '../../common'; +import { IFieldType, IIndexPattern } from '../../../common/index_patterns'; -export type AutocompletePublicPluginSetup = Pick< - AutocompleteProviderRegister, - 'addProvider' | 'getProvider' ->; -export type AutocompletePublicPluginStart = Pick; +export type QuerySuggestionType = 'field' | 'value' | 'operator' | 'conjunction' | 'recentSearch'; -/** @public **/ -export type AutocompleteProvider = (args: { - config: { - get(configKey: string): any; - }; +export type QuerySuggestionsGetFn = ( + args: QuerySuggestionsGetFnArgs +) => Promise | undefined; + +interface QuerySuggestionsGetFnArgs { + language: string; indexPatterns: IIndexPattern[]; - boolFilter?: any; -}) => GetSuggestions; - -/** @public **/ -export type GetSuggestions = (args: { query: string; selectionStart: number; selectionEnd: number; signal?: AbortSignal; -}) => Promise; + boolFilter?: any; +} -/** @public **/ -export type AutocompleteSuggestionType = - | 'field' - | 'value' - | 'operator' - | 'conjunction' - | 'recentSearch'; +interface BasicQuerySuggestion { + type: QuerySuggestionType; + description?: string; + end: number; + start: number; + text: string; + cursorIndex?: number; +} + +interface FieldQuerySuggestion extends BasicQuerySuggestion { + type: 'field'; + field: IFieldType; +} // A union type allows us to do easy type guards in the code. For example, if I want to ensure I'm // working with a FieldAutocompleteSuggestion, I can just do `if ('field' in suggestion)` and the // TypeScript compiler will narrow the type to the parts of the union that have a field prop. /** @public **/ -export type AutocompleteSuggestion = BasicAutocompleteSuggestion | FieldAutocompleteSuggestion; - -interface BasicAutocompleteSuggestion { - description?: string; - end: number; - start: number; - text: string; - type: AutocompleteSuggestionType; - cursorIndex?: number; -} - -export type FieldAutocompleteSuggestion = BasicAutocompleteSuggestion & { - type: 'field'; - field: IFieldType; -}; +export type QuerySuggestion = BasicQuerySuggestion | FieldQuerySuggestion; diff --git a/src/plugins/data/public/suggestions_provider/value_suggestions.test.ts b/src/plugins/data/public/autocomplete/providers/value_suggestion_provider.test.ts similarity index 51% rename from src/plugins/data/public/suggestions_provider/value_suggestions.test.ts rename to src/plugins/data/public/autocomplete/providers/value_suggestion_provider.test.ts index 9089105b4e3a..6b0c0f07cf6c 100644 --- a/src/plugins/data/public/suggestions_provider/value_suggestions.test.ts +++ b/src/plugins/data/public/autocomplete/providers/value_suggestion_provider.test.ts @@ -17,98 +17,121 @@ * under the License. */ -import { stubIndexPattern, stubFields } from '../stubs'; -import { getSuggestionsProvider } from './value_suggestions'; -import { IUiSettingsClient } from 'kibana/public'; +import { stubIndexPattern, stubFields } from '../../stubs'; +import { setupValueSuggestionProvider, ValueSuggestionsGetFn } from './value_suggestion_provider'; +import { IUiSettingsClient, CoreSetup } from 'kibana/public'; -describe('getSuggestions', () => { - let getSuggestions: any; +describe('FieldSuggestions', () => { + let getValueSuggestions: ValueSuggestionsGetFn; let http: any; + let shouldSuggestValues: boolean; + + beforeEach(() => { + const uiSettings = { get: (key: string) => shouldSuggestValues } as IUiSettingsClient; + http = { fetch: jest.fn() }; + + getValueSuggestions = setupValueSuggestionProvider({ http, uiSettings } as CoreSetup); + }); describe('with value suggestions disabled', () => { - beforeEach(() => { - const config = { get: (key: string) => false } as IUiSettingsClient; - http = { fetch: jest.fn() }; - getSuggestions = getSuggestionsProvider(config, http); - }); - it('should return an empty array', async () => { - const index = stubIndexPattern.id; - const [field] = stubFields; - const query = ''; - const suggestions = await getSuggestions(index, field, query); + const suggestions = await getValueSuggestions({ + indexPattern: stubIndexPattern, + field: stubFields[0], + query: '', + }); + expect(suggestions).toEqual([]); expect(http.fetch).not.toHaveBeenCalled(); }); }); describe('with value suggestions enabled', () => { - beforeEach(() => { - const config = { get: (key: string) => true } as IUiSettingsClient; - http = { fetch: jest.fn() }; - getSuggestions = getSuggestionsProvider(config, http); - }); + shouldSuggestValues = true; it('should return true/false for boolean fields', async () => { - const index = stubIndexPattern.id; const [field] = stubFields.filter(({ type }) => type === 'boolean'); - const query = ''; - const suggestions = await getSuggestions(index, field, query); + const suggestions = await getValueSuggestions({ + indexPattern: stubIndexPattern, + field, + query: '', + }); + expect(suggestions).toEqual([true, false]); expect(http.fetch).not.toHaveBeenCalled(); }); it('should return an empty array if the field type is not a string or boolean', async () => { - const index = stubIndexPattern.id; const [field] = stubFields.filter(({ type }) => type !== 'string' && type !== 'boolean'); - const query = ''; - const suggestions = await getSuggestions(index, field, query); + const suggestions = await getValueSuggestions({ + indexPattern: stubIndexPattern, + field, + query: '', + }); + expect(suggestions).toEqual([]); expect(http.fetch).not.toHaveBeenCalled(); }); it('should return an empty array if the field is not aggregatable', async () => { - const index = stubIndexPattern.id; const [field] = stubFields.filter(({ aggregatable }) => !aggregatable); - const query = ''; - const suggestions = await getSuggestions(index, field, query); + const suggestions = await getValueSuggestions({ + indexPattern: stubIndexPattern, + field, + query: '', + }); + expect(suggestions).toEqual([]); expect(http.fetch).not.toHaveBeenCalled(); }); it('should otherwise request suggestions', async () => { - const index = stubIndexPattern.id; const [field] = stubFields.filter( ({ type, aggregatable }) => type === 'string' && aggregatable ); - const query = ''; - await getSuggestions(index, field, query); + + await getValueSuggestions({ + indexPattern: stubIndexPattern, + field, + query: '', + }); + expect(http.fetch).toHaveBeenCalled(); }); it('should cache results if using the same index/field/query/filter', async () => { - const index = stubIndexPattern.id; const [field] = stubFields.filter( ({ type, aggregatable }) => type === 'string' && aggregatable ); - const query = ''; - await getSuggestions(index, field, query); - await getSuggestions(index, field, query); + const args = { + indexPattern: stubIndexPattern, + field, + query: '', + }; + + await getValueSuggestions(args); + await getValueSuggestions(args); + expect(http.fetch).toHaveBeenCalledTimes(1); }); it('should cache results for only one minute', async () => { - const index = stubIndexPattern.id; const [field] = stubFields.filter( ({ type, aggregatable }) => type === 'string' && aggregatable ); - const query = ''; + const args = { + indexPattern: stubIndexPattern, + field, + query: '', + }; const { now } = Date; Date.now = jest.fn(() => 0); - await getSuggestions(index, field, query); + + await getValueSuggestions(args); + Date.now = jest.fn(() => 60 * 1000); - await getSuggestions(index, field, query); + await getValueSuggestions(args); Date.now = now; expect(http.fetch).toHaveBeenCalledTimes(2); @@ -118,14 +141,54 @@ describe('getSuggestions', () => { const fields = stubFields.filter( ({ type, aggregatable }) => type === 'string' && aggregatable ); - await getSuggestions('index', fields[0], ''); - await getSuggestions('index', fields[0], 'query'); - await getSuggestions('index', fields[1], ''); - await getSuggestions('index', fields[1], 'query'); - await getSuggestions('logstash-*', fields[0], ''); - await getSuggestions('logstash-*', fields[0], 'query'); - await getSuggestions('logstash-*', fields[1], ''); - await getSuggestions('logstash-*', fields[1], 'query'); + + await getValueSuggestions({ + indexPattern: stubIndexPattern, + field: fields[0], + query: '', + }); + await getValueSuggestions({ + indexPattern: stubIndexPattern, + field: fields[0], + query: 'query', + }); + await getValueSuggestions({ + indexPattern: stubIndexPattern, + field: fields[1], + query: '', + }); + await getValueSuggestions({ + indexPattern: stubIndexPattern, + field: fields[1], + query: 'query', + }); + + const customIndexPattern = { + ...stubIndexPattern, + title: 'customIndexPattern', + }; + + await getValueSuggestions({ + indexPattern: customIndexPattern, + field: fields[0], + query: '', + }); + await getValueSuggestions({ + indexPattern: customIndexPattern, + field: fields[0], + query: 'query', + }); + await getValueSuggestions({ + indexPattern: customIndexPattern, + field: fields[1], + query: '', + }); + await getValueSuggestions({ + indexPattern: customIndexPattern, + field: fields[1], + query: 'query', + }); + expect(http.fetch).toHaveBeenCalledTimes(8); }); }); diff --git a/src/plugins/data/public/suggestions_provider/value_suggestions.ts b/src/plugins/data/public/autocomplete/providers/value_suggestion_provider.ts similarity index 55% rename from src/plugins/data/public/suggestions_provider/value_suggestions.ts rename to src/plugins/data/public/autocomplete/providers/value_suggestion_provider.ts index e64156c290db..5df88000edbd 100644 --- a/src/plugins/data/public/suggestions_provider/value_suggestions.ts +++ b/src/plugins/data/public/autocomplete/providers/value_suggestion_provider.ts @@ -18,51 +18,53 @@ */ import { memoize } from 'lodash'; +import { CoreSetup } from 'src/core/public'; +import { IIndexPattern, IFieldType } from '../../../common'; -import { IUiSettingsClient, HttpSetup } from 'src/core/public'; -import { IGetSuggestions } from './types'; -import { IFieldType } from '../../common'; +function resolver(title: string, field: IFieldType, query: string, boolFilter: any) { + // Only cache results for a minute + const ttl = Math.floor(Date.now() / 1000 / 60); -export function getSuggestionsProvider( - uiSettings: IUiSettingsClient, - http: HttpSetup -): IGetSuggestions { + return [ttl, query, title, field.name, JSON.stringify(boolFilter)].join('|'); +} + +export type ValueSuggestionsGetFn = (args: ValueSuggestionsGetFnArgs) => Promise; + +interface ValueSuggestionsGetFnArgs { + indexPattern: IIndexPattern; + field: IFieldType; + query: string; + boolFilter?: any[]; + signal?: AbortSignal; +} + +export const setupValueSuggestionProvider = (core: CoreSetup): ValueSuggestionsGetFn => { const requestSuggestions = memoize( - ( - index: string, - field: IFieldType, - query: string, - boolFilter: any = [], - signal?: AbortSignal - ) => { - return http.fetch(`/api/kibana/suggestions/values/${index}`, { + (index: string, field: IFieldType, query: string, boolFilter: any = [], signal?: AbortSignal) => + core.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, - signal?: AbortSignal - ) => { - const shouldSuggestValues = uiSettings.get('filterEditor:suggestValues'); + return async ({ + indexPattern, + field, + query, + boolFilter, + signal, + }: ValueSuggestionsGetFnArgs): Promise => { + const shouldSuggestValues = core!.uiSettings.get('filterEditor:suggestValues'); + const { title } = indexPattern; + if (field.type === 'boolean') { return [true, false]; } else if (!shouldSuggestValues || !field.aggregatable || field.type !== 'string') { return []; } - return await requestSuggestions(index, field, query, boolFilter, signal); - }; -} -function resolver(index: string, field: IFieldType, query: string, boolFilter: any) { - // Only cache results for a minute - const ttl = Math.floor(Date.now() / 1000 / 60); - return [ttl, query, index, field.name, JSON.stringify(boolFilter)].join('|'); -} + return await requestSuggestions(title, field, query, boolFilter, signal); + }; +}; diff --git a/src/plugins/data/public/suggestions_provider/types.ts b/src/plugins/data/public/autocomplete/types.ts similarity index 67% rename from src/plugins/data/public/suggestions_provider/types.ts rename to src/plugins/data/public/autocomplete/types.ts index a13ecfb10143..759e2dd25a5b 100644 --- a/src/plugins/data/public/suggestions_provider/types.ts +++ b/src/plugins/data/public/autocomplete/types.ts @@ -16,11 +16,18 @@ * specific language governing permissions and limitations * under the License. */ -import { IFieldType } from '../../common'; -export type IGetSuggestions = ( - index: string, - field: IFieldType, - query: string, - boolFilter?: any -) => any; +import { AutocompleteService } from './autocomplete_service'; + +/** @public **/ +export type AutocompleteSetup = ReturnType; + +/** @public **/ +export type AutocompleteStart = ReturnType; + +/** @public **/ +export { + QuerySuggestion, + QuerySuggestionsGetFn, + QuerySuggestionType, +} from './providers/query_suggestion_provider'; diff --git a/src/plugins/data/public/autocomplete_provider/index.ts b/src/plugins/data/public/autocomplete_provider/index.ts deleted file mode 100644 index 6758bd7f379c..000000000000 --- a/src/plugins/data/public/autocomplete_provider/index.ts +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -import { AutocompleteProvider } from './types'; - -export class AutocompleteProviderRegister { - private readonly registeredProviders: Map = new Map(); - - /** @public **/ - public addProvider(language: string, provider: AutocompleteProvider): void { - if (language && provider) { - this.registeredProviders.set(language, provider); - } - } - - /** @public **/ - public getProvider(language: string): AutocompleteProvider | undefined { - return this.registeredProviders.get(language); - } - - /** @internal **/ - public clearProviders(): void { - this.registeredProviders.clear(); - } -} diff --git a/src/plugins/data/public/index.ts b/src/plugins/data/public/index.ts index 4b330600417e..19ba246ce02d 100644 --- a/src/plugins/data/public/index.ts +++ b/src/plugins/data/public/index.ts @@ -18,6 +18,8 @@ */ import { PluginInitializerContext } from '../../../core/public'; +import * as autocomplete from './autocomplete'; + export function plugin(initializerContext: PluginInitializerContext) { return new DataPublicPlugin(initializerContext); } @@ -49,11 +51,6 @@ export { TimeRange, } from '../common'; -/** - * Static code to be shared externally - * @public - */ -export * from './autocomplete_provider'; export * from './field_formats_provider'; export * from './index_patterns'; export * from './search'; @@ -97,3 +94,5 @@ export { // Export plugin after all other imports import { DataPublicPlugin } from './plugin'; export { DataPublicPlugin as Plugin }; + +export { autocomplete }; diff --git a/src/plugins/data/public/mocks.ts b/src/plugins/data/public/mocks.ts index 03d3dad61ed0..08a7a3ef1153 100644 --- a/src/plugins/data/public/mocks.ts +++ b/src/plugins/data/public/mocks.ts @@ -30,9 +30,9 @@ export type Setup = jest.Mocked>; export type Start = jest.Mocked>; const autocompleteMock: any = { - addProvider: jest.fn(), - getProvider: jest.fn(), - clearProviders: jest.fn(), + getValueSuggestions: jest.fn(), + getQuerySuggestions: jest.fn(), + hasQuerySuggestions: jest.fn(), }; const fieldFormatsMock: PublicMethodsOf = { diff --git a/src/plugins/data/public/plugin.ts b/src/plugins/data/public/plugin.ts index cd55048ca527..78abcbcfec46 100644 --- a/src/plugins/data/public/plugin.ts +++ b/src/plugins/data/public/plugin.ts @@ -25,8 +25,7 @@ import { DataSetupDependencies, DataStartDependencies, } from './types'; -import { AutocompleteProviderRegister } from './autocomplete_provider'; -import { getSuggestionsProvider } from './suggestions_provider'; +import { AutocompleteService } from './autocomplete'; import { SearchService } from './search/search_service'; import { FieldFormatsService } from './field_formats_provider'; import { QueryService } from './query'; @@ -38,7 +37,7 @@ import { APPLY_FILTER_TRIGGER } from '../../embeddable/public'; import { createSearchBar } from './ui/search_bar/create_search_bar'; export class DataPublicPlugin implements Plugin { - private readonly autocomplete = new AutocompleteProviderRegister(); + private readonly autocomplete = new AutocompleteService(); private readonly searchService: SearchService; private readonly fieldFormatsService: FieldFormatsService; private readonly queryService: QueryService; @@ -62,7 +61,7 @@ export class DataPublicPlugin implements Plugin { appName: string; uiSettings: CoreStart['uiSettings']; diff --git a/src/plugins/data/public/ui/filter_bar/filter_editor/phrase_suggestor.tsx b/src/plugins/data/public/ui/filter_bar/filter_editor/phrase_suggestor.tsx index 61290cc16b8a..2b2d83c9f5a8 100644 --- a/src/plugins/data/public/ui/filter_bar/filter_editor/phrase_suggestor.tsx +++ b/src/plugins/data/public/ui/filter_bar/filter_editor/phrase_suggestor.tsx @@ -63,13 +63,19 @@ export class PhraseSuggestorUI extends Component this.updateSuggestions(`${value}`); }; - protected updateSuggestions = debounce(async (value: string = '') => { + protected updateSuggestions = debounce(async (query: string = '') => { const { indexPattern, field } = this.props as PhraseSuggestorProps; if (!field || !this.isSuggestingValues()) { return; } this.setState({ isLoading: true }); - const suggestions = await this.services.data.getSuggestions(indexPattern.title, field, value); + + const suggestions = await this.services.data.autocomplete.getValueSuggestions({ + indexPattern, + field, + query, + }); + this.setState({ suggestions, isLoading: false }); }, 500); } diff --git a/src/plugins/data/public/ui/query_string_input/__snapshots__/query_string_input.test.tsx.snap b/src/plugins/data/public/ui/query_string_input/__snapshots__/query_string_input.test.tsx.snap index 1fb39710f675..bc08c87304fc 100644 --- a/src/plugins/data/public/ui/query_string_input/__snapshots__/query_string_input.test.tsx.snap +++ b/src/plugins/data/public/ui/query_string_input/__snapshots__/query_string_input.test.tsx.snap @@ -151,9 +151,9 @@ exports[`QueryStringInput Should disable autoFocus on EuiFieldText when disableA }, "data": Object { "autocomplete": Object { - "addProvider": [MockFunction], - "clearProviders": [MockFunction], - "getProvider": [MockFunction], + "getQuerySuggestions": [MockFunction], + "getValueSuggestions": [MockFunction], + "hasQuerySuggestions": [MockFunction], }, "fieldFormats": Object { "getByFieldType": [MockFunction], @@ -777,9 +777,9 @@ exports[`QueryStringInput Should disable autoFocus on EuiFieldText when disableA }, "data": Object { "autocomplete": Object { - "addProvider": [MockFunction], - "clearProviders": [MockFunction], - "getProvider": [MockFunction], + "getQuerySuggestions": [MockFunction], + "getValueSuggestions": [MockFunction], + "hasQuerySuggestions": [MockFunction], }, "fieldFormats": Object { "getByFieldType": [MockFunction], @@ -1385,9 +1385,9 @@ exports[`QueryStringInput Should pass the query language to the language switche }, "data": Object { "autocomplete": Object { - "addProvider": [MockFunction], - "clearProviders": [MockFunction], - "getProvider": [MockFunction], + "getQuerySuggestions": [MockFunction], + "getValueSuggestions": [MockFunction], + "hasQuerySuggestions": [MockFunction], }, "fieldFormats": Object { "getByFieldType": [MockFunction], @@ -2008,9 +2008,9 @@ exports[`QueryStringInput Should pass the query language to the language switche }, "data": Object { "autocomplete": Object { - "addProvider": [MockFunction], - "clearProviders": [MockFunction], - "getProvider": [MockFunction], + "getQuerySuggestions": [MockFunction], + "getValueSuggestions": [MockFunction], + "hasQuerySuggestions": [MockFunction], }, "fieldFormats": Object { "getByFieldType": [MockFunction], @@ -2616,9 +2616,9 @@ exports[`QueryStringInput Should render the given query 1`] = ` }, "data": Object { "autocomplete": Object { - "addProvider": [MockFunction], - "clearProviders": [MockFunction], - "getProvider": [MockFunction], + "getQuerySuggestions": [MockFunction], + "getValueSuggestions": [MockFunction], + "hasQuerySuggestions": [MockFunction], }, "fieldFormats": Object { "getByFieldType": [MockFunction], @@ -3239,9 +3239,9 @@ exports[`QueryStringInput Should render the given query 1`] = ` }, "data": Object { "autocomplete": Object { - "addProvider": [MockFunction], - "clearProviders": [MockFunction], - "getProvider": [MockFunction], + "getQuerySuggestions": [MockFunction], + "getValueSuggestions": [MockFunction], + "hasQuerySuggestions": [MockFunction], }, "fieldFormats": Object { "getByFieldType": [MockFunction], diff --git a/src/plugins/data/public/ui/query_string_input/query_string_input.tsx b/src/plugins/data/public/ui/query_string_input/query_string_input.tsx index 960a843f98ab..cf219c35bcce 100644 --- a/src/plugins/data/public/ui/query_string_input/query_string_input.tsx +++ b/src/plugins/data/public/ui/query_string_input/query_string_input.tsx @@ -35,8 +35,7 @@ import { InjectedIntl, injectI18n, FormattedMessage } from '@kbn/i18n/react'; import { debounce, compact, isEqual } from 'lodash'; import { Toast } from 'src/core/public'; import { - AutocompleteSuggestion, - AutocompleteSuggestionType, + autocomplete, IDataPluginServices, IIndexPattern, PersistedLog, @@ -71,7 +70,7 @@ interface Props { interface State { isSuggestionsVisible: boolean; index: number | null; - suggestions: AutocompleteSuggestion[]; + suggestions: autocomplete.QuerySuggestion[]; suggestionLimit: number; selectionStart: number | null; selectionEnd: number | null; @@ -90,7 +89,7 @@ const KEY_CODES = { END: 35, }; -const recentSearchType: AutocompleteSuggestionType = 'recentSearch'; +const recentSearchType: autocomplete.QuerySuggestionType = 'recentSearch'; export class QueryStringInputUI extends Component { public state: State = { @@ -138,15 +137,14 @@ export class QueryStringInputUI extends Component { return; } - const uiSettings = this.services.uiSettings; const language = this.props.query.language; const queryString = this.getQueryString(); const recentSearchSuggestions = this.getRecentSearchSuggestions(queryString); - const autocompleteProvider = this.services.data.autocomplete.getProvider(language); + const hasQuerySuggestions = this.services.data.autocomplete.hasQuerySuggestions(language); if ( - !autocompleteProvider || + !hasQuerySuggestions || !Array.isArray(this.state.indexPatterns) || compact(this.state.indexPatterns).length === 0 ) { @@ -154,10 +152,6 @@ export class QueryStringInputUI extends Component { } const indexPatterns = this.state.indexPatterns; - const getAutocompleteSuggestions = autocompleteProvider({ - config: uiSettings, - indexPatterns, - }); const { selectionStart, selectionEnd } = this.inputRef; if (selectionStart === null || selectionEnd === null) { @@ -167,12 +161,16 @@ export class QueryStringInputUI extends Component { try { if (this.abortController) this.abortController.abort(); this.abortController = new AbortController(); - const suggestions: AutocompleteSuggestion[] = await getAutocompleteSuggestions({ - query: queryString, - selectionStart, - selectionEnd, - signal: this.abortController.signal, - }); + const suggestions = + (await this.services.data.autocomplete.getQuerySuggestions({ + language, + indexPatterns, + 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 @@ -321,7 +319,7 @@ export class QueryStringInputUI extends Component { } }; - private selectSuggestion = (suggestion: AutocompleteSuggestion) => { + private selectSuggestion = (suggestion: autocomplete.QuerySuggestion) => { if (!this.inputRef) { return; } @@ -351,7 +349,7 @@ export class QueryStringInputUI extends Component { } }; - private handleNestedFieldSyntaxNotification = (suggestion: AutocompleteSuggestion) => { + private handleNestedFieldSyntaxNotification = (suggestion: autocomplete.QuerySuggestion) => { if ( 'field' in suggestion && suggestion.field.subType && @@ -453,7 +451,7 @@ export class QueryStringInputUI extends Component { } }; - private onClickSuggestion = (suggestion: AutocompleteSuggestion) => { + private onClickSuggestion = (suggestion: autocomplete.QuerySuggestion) => { if (!this.inputRef) { return; } diff --git a/src/plugins/data/public/ui/typeahead/suggestion_component.test.tsx b/src/plugins/data/public/ui/typeahead/suggestion_component.test.tsx index 591176bf133f..0c5c70164275 100644 --- a/src/plugins/data/public/ui/typeahead/suggestion_component.test.tsx +++ b/src/plugins/data/public/ui/typeahead/suggestion_component.test.tsx @@ -19,14 +19,14 @@ import { mount, shallow } from 'enzyme'; import React from 'react'; -import { AutocompleteSuggestion } from '../..'; +import { autocomplete } from '../..'; import { SuggestionComponent } from './suggestion_component'; const noop = () => { return; }; -const mockSuggestion: AutocompleteSuggestion = { +const mockSuggestion: autocomplete.QuerySuggestion = { description: 'This is not a helpful suggestion', end: 0, start: 42, diff --git a/src/plugins/data/public/ui/typeahead/suggestion_component.tsx b/src/plugins/data/public/ui/typeahead/suggestion_component.tsx index fd29de4573ff..1d2ac8dee1a8 100644 --- a/src/plugins/data/public/ui/typeahead/suggestion_component.tsx +++ b/src/plugins/data/public/ui/typeahead/suggestion_component.tsx @@ -20,7 +20,7 @@ import { EuiIcon } from '@elastic/eui'; import classNames from 'classnames'; import React, { FunctionComponent } from 'react'; -import { AutocompleteSuggestion } from '../..'; +import { autocomplete } from '../..'; function getEuiIconType(type: string) { switch (type) { @@ -40,10 +40,10 @@ function getEuiIconType(type: string) { } interface Props { - onClick: (suggestion: AutocompleteSuggestion) => void; + onClick: (suggestion: autocomplete.QuerySuggestion) => void; onMouseEnter: () => void; selected: boolean; - suggestion: AutocompleteSuggestion; + suggestion: autocomplete.QuerySuggestion; innerRef: (node: HTMLDivElement) => void; ariaId: string; } diff --git a/src/plugins/data/public/ui/typeahead/suggestions_component.test.tsx b/src/plugins/data/public/ui/typeahead/suggestions_component.test.tsx index 7fb2fdf25104..b84f612b6d13 100644 --- a/src/plugins/data/public/ui/typeahead/suggestions_component.test.tsx +++ b/src/plugins/data/public/ui/typeahead/suggestions_component.test.tsx @@ -19,7 +19,7 @@ import { mount, shallow } from 'enzyme'; import React from 'react'; -import { AutocompleteSuggestion } from '../..'; +import { autocomplete } from '../..'; import { SuggestionComponent } from './suggestion_component'; import { SuggestionsComponent } from './suggestions_component'; @@ -27,7 +27,7 @@ const noop = () => { return; }; -const mockSuggestions: AutocompleteSuggestion[] = [ +const mockSuggestions: autocomplete.QuerySuggestion[] = [ { description: 'This is not a helpful suggestion', end: 0, diff --git a/src/plugins/data/public/ui/typeahead/suggestions_component.tsx b/src/plugins/data/public/ui/typeahead/suggestions_component.tsx index e4cccbcde4fb..b37a2e479e87 100644 --- a/src/plugins/data/public/ui/typeahead/suggestions_component.tsx +++ b/src/plugins/data/public/ui/typeahead/suggestions_component.tsx @@ -19,15 +19,15 @@ import { isEmpty } from 'lodash'; import React, { Component } from 'react'; -import { AutocompleteSuggestion } from '../..'; +import { autocomplete } from '../..'; import { SuggestionComponent } from './suggestion_component'; interface Props { index: number | null; - onClick: (suggestion: AutocompleteSuggestion) => void; + onClick: (suggestion: autocomplete.QuerySuggestion) => void; onMouseEnter: (index: number) => void; show: boolean; - suggestions: AutocompleteSuggestion[]; + suggestions: autocomplete.QuerySuggestion[]; loadMore: () => void; } diff --git a/x-pack/legacy/plugins/apm/public/components/shared/KueryBar/index.tsx b/x-pack/legacy/plugins/apm/public/components/shared/KueryBar/index.tsx index 67bff86c8ccd..32432b7b85ef 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/KueryBar/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/KueryBar/index.tsx @@ -18,8 +18,7 @@ import { history } from '../../../utils/history'; import { useApmPluginContext } from '../../../hooks/useApmPluginContext'; import { useDynamicIndexPattern } from '../../../hooks/useDynamicIndexPattern'; import { - AutocompleteProvider, - AutocompleteSuggestion, + autocomplete, esKuery, IIndexPattern } from '../../../../../../../../src/plugins/data/public'; @@ -29,7 +28,7 @@ const Container = styled.div` `; interface State { - suggestions: AutocompleteSuggestion[]; + suggestions: autocomplete.QuerySuggestion[]; isLoadingSuggestions: boolean; } @@ -38,32 +37,6 @@ function convertKueryToEsQuery(kuery: string, indexPattern: IIndexPattern) { return esKuery.toElasticsearchQuery(ast, indexPattern); } -function getSuggestions( - query: string, - selectionStart: number, - indexPattern: IIndexPattern, - boolFilter: unknown, - autocompleteProvider?: AutocompleteProvider -) { - if (!autocompleteProvider) { - return []; - } - const config = { - get: () => true - }; - - const getAutocompleteSuggestions = autocompleteProvider({ - config, - indexPatterns: [indexPattern], - boolFilter - }); - return getAutocompleteSuggestions({ - query, - selectionStart, - selectionEnd: selectionStart - }); -} - export function KueryBar() { const [state, setState] = useState({ suggestions: [], @@ -72,7 +45,6 @@ export function KueryBar() { const { urlParams } = useUrlParams(); const location = useLocation(); const { data } = useApmPluginContext().plugins; - const autocompleteProvider = data.autocomplete.getProvider('kuery'); let currentRequestCheck; @@ -100,16 +72,16 @@ export function KueryBar() { const currentRequest = uniqueId(); currentRequestCheck = currentRequest; - const boolFilter = getBoolFilter(urlParams); try { const suggestions = ( - await getSuggestions( - inputValue, + (await data.autocomplete.getQuerySuggestions({ + language: 'kuery', + indexPatterns: [indexPattern], + boolFilter: getBoolFilter(urlParams), + query: inputValue, selectionStart, - indexPattern, - boolFilter, - autocompleteProvider - ) + selectionEnd: selectionStart + })) || [] ) .filter(suggestion => !startsWith(suggestion.text, 'span.')) .slice(0, 15); diff --git a/x-pack/legacy/plugins/beats_management/public/components/autocomplete_field/index.tsx b/x-pack/legacy/plugins/beats_management/public/components/autocomplete_field/index.tsx index 1f49711ae5e4..f3e0f3dfbdae 100644 --- a/x-pack/legacy/plugins/beats_management/public/components/autocomplete_field/index.tsx +++ b/x-pack/legacy/plugins/beats_management/public/components/autocomplete_field/index.tsx @@ -13,7 +13,7 @@ import { import React from 'react'; import styled from 'styled-components'; -import { AutocompleteSuggestion } from '../../../../../../../src/plugins/data/public'; +import { autocomplete } from '../../../../../../../src/plugins/data/public'; import { composeStateUpdaters } from '../../utils/typed_react'; import { SuggestionItem } from './suggestion_item'; @@ -25,7 +25,7 @@ interface AutocompleteFieldProps { onSubmit?: (value: string) => void; onChange?: (value: string) => void; placeholder?: string; - suggestions: AutocompleteSuggestion[]; + suggestions: autocomplete.QuerySuggestion[]; value: string; } diff --git a/x-pack/legacy/plugins/beats_management/public/components/autocomplete_field/suggestion_item.tsx b/x-pack/legacy/plugins/beats_management/public/components/autocomplete_field/suggestion_item.tsx index a753a944a2ec..0132667b9e51 100644 --- a/x-pack/legacy/plugins/beats_management/public/components/autocomplete_field/suggestion_item.tsx +++ b/x-pack/legacy/plugins/beats_management/public/components/autocomplete_field/suggestion_item.tsx @@ -9,13 +9,13 @@ import { tint } from 'polished'; import React from 'react'; import styled from 'styled-components'; -import { AutocompleteSuggestion } from '../../../../../../../src/plugins/data/public'; +import { autocomplete } from '../../../../../../../src/plugins/data/public'; interface SuggestionItemProps { isSelected?: boolean; onClick?: React.MouseEventHandler; onMouseEnter?: React.MouseEventHandler; - suggestion: AutocompleteSuggestion; + suggestion: autocomplete.QuerySuggestion; } export const SuggestionItem: React.FC = props => { diff --git a/x-pack/legacy/plugins/beats_management/public/components/table/table.tsx b/x-pack/legacy/plugins/beats_management/public/components/table/table.tsx index 26ddd682405c..d1cbc0888dca 100644 --- a/x-pack/legacy/plugins/beats_management/public/components/table/table.tsx +++ b/x-pack/legacy/plugins/beats_management/public/components/table/table.tsx @@ -8,7 +8,7 @@ import { EuiBasicTable, EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eu import { i18n } from '@kbn/i18n'; import React from 'react'; import styled from 'styled-components'; -import { AutocompleteSuggestion } from '../../../../../../../src/plugins/data/public'; +import { autocomplete } from '../../../../../../../src/plugins/data/public'; import { TABLE_CONFIG } from '../../../common/constants'; import { AutocompleteField } from '../autocomplete_field/index'; import { ControlSchema } from './action_schema'; @@ -31,7 +31,7 @@ export interface KueryBarProps { loadSuggestions: (value: string, cursorPosition: number, maxCount?: number) => void; onChange?: (value: string) => void; onSubmit?: (value: string) => void; - suggestions: AutocompleteSuggestion[]; + suggestions: autocomplete.QuerySuggestion[]; value: string; } diff --git a/x-pack/legacy/plugins/beats_management/public/containers/with_kuery_autocompletion.tsx b/x-pack/legacy/plugins/beats_management/public/containers/with_kuery_autocompletion.tsx index 2ac20438536c..db73a7cb38c1 100644 --- a/x-pack/legacy/plugins/beats_management/public/containers/with_kuery_autocompletion.tsx +++ b/x-pack/legacy/plugins/beats_management/public/containers/with_kuery_autocompletion.tsx @@ -6,7 +6,7 @@ import React from 'react'; -import { AutocompleteSuggestion } from '../../../../../../src/plugins/data/public'; +import { autocomplete } from '../../../../../../src/plugins/data/public'; import { FrontendLibs } from '../lib/types'; import { RendererFunction } from '../utils/typed_react'; @@ -17,7 +17,7 @@ interface WithKueryAutocompletionLifecycleProps { children: RendererFunction<{ isLoadingSuggestions: boolean; loadSuggestions: (expression: string, cursorPosition: number, maxSuggestions?: number) => void; - suggestions: AutocompleteSuggestion[]; + suggestions: autocomplete.QuerySuggestion[]; }>; } @@ -28,7 +28,7 @@ interface WithKueryAutocompletionLifecycleState { expression: string; cursorPosition: number; } | null; - suggestions: AutocompleteSuggestion[]; + suggestions: autocomplete.QuerySuggestion[]; } export class WithKueryAutocompletion extends React.Component< diff --git a/x-pack/legacy/plugins/beats_management/public/lib/adapters/elasticsearch/adapter_types.ts b/x-pack/legacy/plugins/beats_management/public/lib/adapters/elasticsearch/adapter_types.ts index 4f4ce70e817c..12898027d5fb 100644 --- a/x-pack/legacy/plugins/beats_management/public/lib/adapters/elasticsearch/adapter_types.ts +++ b/x-pack/legacy/plugins/beats_management/public/lib/adapters/elasticsearch/adapter_types.ts @@ -3,10 +3,10 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { AutocompleteSuggestion } from '../../../../../../../../src/plugins/data/public'; +import { autocomplete } from '../../../../../../../../src/plugins/data/public'; export interface ElasticsearchAdapter { convertKueryToEsQuery: (kuery: string) => Promise; - getSuggestions: (kuery: string, selectionStart: any) => Promise; + getSuggestions: (kuery: string, selectionStart: any) => Promise; isKueryValid(kuery: string): boolean; } diff --git a/x-pack/legacy/plugins/beats_management/public/lib/adapters/elasticsearch/memory.ts b/x-pack/legacy/plugins/beats_management/public/lib/adapters/elasticsearch/memory.ts index e001bf6c6e84..111255b55c99 100644 --- a/x-pack/legacy/plugins/beats_management/public/lib/adapters/elasticsearch/memory.ts +++ b/x-pack/legacy/plugins/beats_management/public/lib/adapters/elasticsearch/memory.ts @@ -4,14 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AutocompleteSuggestion } from '../../../../../../../../src/plugins/data/public'; +import { autocomplete } from '../../../../../../../../src/plugins/data/public'; import { ElasticsearchAdapter } from './adapter_types'; export class MemoryElasticsearchAdapter implements ElasticsearchAdapter { constructor( private readonly mockIsKueryValid: (kuery: string) => boolean, private readonly mockKueryToEsQuery: (kuery: string) => string, - private readonly suggestions: AutocompleteSuggestion[] + private readonly suggestions: autocomplete.QuerySuggestion[] ) {} public isKueryValid(kuery: string): boolean { @@ -23,7 +23,7 @@ export class MemoryElasticsearchAdapter implements ElasticsearchAdapter { public async getSuggestions( kuery: string, selectionStart: any - ): Promise { + ): Promise { return this.suggestions; } } diff --git a/x-pack/legacy/plugins/beats_management/public/lib/adapters/elasticsearch/rest.ts b/x-pack/legacy/plugins/beats_management/public/lib/adapters/elasticsearch/rest.ts index 8771181639f4..fc400c600e57 100644 --- a/x-pack/legacy/plugins/beats_management/public/lib/adapters/elasticsearch/rest.ts +++ b/x-pack/legacy/plugins/beats_management/public/lib/adapters/elasticsearch/rest.ts @@ -7,10 +7,7 @@ import { isEmpty } from 'lodash'; import { npStart } from 'ui/new_platform'; import { ElasticsearchAdapter } from './adapter_types'; -import { AutocompleteSuggestion, esKuery } from '../../../../../../../../src/plugins/data/public'; - -const getAutocompleteProvider = (language: string) => - npStart.plugins.data.autocomplete.getProvider(language); +import { autocomplete, esKuery } from '../../../../../../../../src/plugins/data/public'; export class RestElasticsearchAdapter implements ElasticsearchAdapter { private cachedIndexPattern: any = null; @@ -33,30 +30,23 @@ export class RestElasticsearchAdapter implements ElasticsearchAdapter { const indexPattern = await this.getIndexPattern(); return JSON.stringify(esKuery.toElasticsearchQuery(ast, indexPattern)); } + public async getSuggestions( kuery: string, selectionStart: any - ): Promise { - const autocompleteProvider = getAutocompleteProvider('kuery'); - if (!autocompleteProvider) { - return []; - } - const config = { - get: () => true, - }; + ): Promise { const indexPattern = await this.getIndexPattern(); - const getAutocompleteSuggestions = autocompleteProvider({ - config, - indexPatterns: [indexPattern], - boolFilter: null, - }); - const results = getAutocompleteSuggestions({ - query: kuery || '', - selectionStart, - selectionEnd: selectionStart, - }); - return results; + return ( + (await npStart.plugins.data.autocomplete.getQuerySuggestions({ + language: 'kuery', + indexPatterns: [indexPattern], + boolFilter: [], + query: kuery || '', + selectionStart, + selectionEnd: selectionStart, + })) || [] + ); } private async getIndexPattern() { diff --git a/x-pack/legacy/plugins/beats_management/public/lib/compose/memory.ts b/x-pack/legacy/plugins/beats_management/public/lib/compose/memory.ts index f357e698afc3..47df51dea862 100644 --- a/x-pack/legacy/plugins/beats_management/public/lib/compose/memory.ts +++ b/x-pack/legacy/plugins/beats_management/public/lib/compose/memory.ts @@ -24,14 +24,14 @@ import { TagsLib } from '../tags'; import { FrontendLibs } from '../types'; import { MemoryElasticsearchAdapter } from './../adapters/elasticsearch/memory'; import { ElasticsearchLib } from './../elasticsearch'; -import { AutocompleteSuggestion } from '../../../../../../../src/plugins/data/public'; +import { autocomplete } from '../../../../../../../src/plugins/data/public'; const onKibanaReady = uiModules.get('kibana').run; export function compose( mockIsKueryValid: (kuery: string) => boolean, mockKueryToEsQuery: (kuery: string) => string, - suggestions: AutocompleteSuggestion[] + suggestions: autocomplete.QuerySuggestion[] ): FrontendLibs { const esAdapter = new MemoryElasticsearchAdapter( mockIsKueryValid, diff --git a/x-pack/legacy/plugins/beats_management/public/lib/elasticsearch.ts b/x-pack/legacy/plugins/beats_management/public/lib/elasticsearch.ts index 0897dfd9c139..d71512e80d3d 100644 --- a/x-pack/legacy/plugins/beats_management/public/lib/elasticsearch.ts +++ b/x-pack/legacy/plugins/beats_management/public/lib/elasticsearch.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AutocompleteSuggestion } from '../../../../../../src/plugins/data/public'; +import { autocomplete } from '../../../../../../src/plugins/data/public'; import { ElasticsearchAdapter } from './adapters/elasticsearch/adapter_types'; interface HiddenFields { @@ -35,7 +35,7 @@ export class ElasticsearchLib { kuery: string, selectionStart: any, fieldPrefix?: string - ): Promise { + ): Promise { const suggestions = await this.adapter.getSuggestions(kuery, selectionStart); const filteredSuggestions = suggestions.filter(suggestion => { diff --git a/x-pack/legacy/plugins/graph/public/components/search_bar.test.tsx b/x-pack/legacy/plugins/graph/public/components/search_bar.test.tsx index 360561df7195..95b7dd22e9fc 100644 --- a/x-pack/legacy/plugins/graph/public/components/search_bar.test.tsx +++ b/x-pack/legacy/plugins/graph/public/components/search_bar.test.tsx @@ -21,6 +21,7 @@ import { ReactWrapper } from 'enzyme'; import { createMockGraphStore } from '../state_management/mocks'; import { Provider } from 'react-redux'; +jest.mock('ui/new_platform'); jest.mock('../services/source_modal', () => ({ openSourceModal: jest.fn() })); const waitForIndexPatternFetch = () => new Promise(r => setTimeout(r)); @@ -51,7 +52,7 @@ function wrapSearchBarInContext(testProps: OuterSearchBarProps) { savedQueries: {}, }, autocomplete: { - getProvider: () => undefined, + hasQuerySuggestions: () => false, }, }, }; diff --git a/x-pack/legacy/plugins/infra/public/components/autocomplete_field/autocomplete_field.tsx b/x-pack/legacy/plugins/infra/public/components/autocomplete_field/autocomplete_field.tsx index 4c215835ca24..dc6eabb325d1 100644 --- a/x-pack/legacy/plugins/infra/public/components/autocomplete_field/autocomplete_field.tsx +++ b/x-pack/legacy/plugins/infra/public/components/autocomplete_field/autocomplete_field.tsx @@ -12,7 +12,7 @@ import { } from '@elastic/eui'; import React from 'react'; -import { AutocompleteSuggestion } from '../../../../../../../src/plugins/data/public'; +import { autocomplete } from '../../../../../../../src/plugins/data/public'; import euiStyled from '../../../../../common/eui_styled_components'; import { composeStateUpdaters } from '../../utils/typed_react'; @@ -25,7 +25,7 @@ interface AutocompleteFieldProps { onSubmit?: (value: string) => void; onChange?: (value: string) => void; placeholder?: string; - suggestions: AutocompleteSuggestion[]; + suggestions: autocomplete.QuerySuggestion[]; value: string; autoFocus?: boolean; 'aria-label'?: string; diff --git a/x-pack/legacy/plugins/infra/public/components/autocomplete_field/suggestion_item.tsx b/x-pack/legacy/plugins/infra/public/components/autocomplete_field/suggestion_item.tsx index 0c29b1f51b07..79b18f5888bd 100644 --- a/x-pack/legacy/plugins/infra/public/components/autocomplete_field/suggestion_item.tsx +++ b/x-pack/legacy/plugins/infra/public/components/autocomplete_field/suggestion_item.tsx @@ -8,14 +8,14 @@ import { EuiIcon } from '@elastic/eui'; import { transparentize } from 'polished'; import React from 'react'; -import { AutocompleteSuggestion } from '../../../../../../../src/plugins/data/public'; +import { autocomplete } from '../../../../../../../src/plugins/data/public'; import euiStyled from '../../../../../common/eui_styled_components'; interface Props { isSelected?: boolean; onClick?: React.MouseEventHandler; onMouseEnter?: React.MouseEventHandler; - suggestion: AutocompleteSuggestion; + suggestion: autocomplete.QuerySuggestion; } export const SuggestionItem: React.FC = props => { diff --git a/x-pack/legacy/plugins/infra/public/containers/with_kuery_autocompletion.tsx b/x-pack/legacy/plugins/infra/public/containers/with_kuery_autocompletion.tsx index ba072b754b95..c92e2ecec926 100644 --- a/x-pack/legacy/plugins/infra/public/containers/with_kuery_autocompletion.tsx +++ b/x-pack/legacy/plugins/infra/public/containers/with_kuery_autocompletion.tsx @@ -6,17 +6,14 @@ import React from 'react'; import { npStart } from 'ui/new_platform'; -import { AutocompleteSuggestion, IIndexPattern } from 'src/plugins/data/public'; +import { autocomplete, IIndexPattern } from 'src/plugins/data/public'; import { RendererFunction } from '../utils/typed_react'; -const getAutocompleteProvider = (language: string) => - npStart.plugins.data.autocomplete.getProvider(language); - interface WithKueryAutocompletionLifecycleProps { children: RendererFunction<{ isLoadingSuggestions: boolean; loadSuggestions: (expression: string, cursorPosition: number, maxSuggestions?: number) => void; - suggestions: AutocompleteSuggestion[]; + suggestions: autocomplete.QuerySuggestion[]; }>; indexPattern: IIndexPattern; } @@ -28,7 +25,7 @@ interface WithKueryAutocompletionLifecycleState { expression: string; cursorPosition: number; } | null; - suggestions: AutocompleteSuggestion[]; + suggestions: autocomplete.QuerySuggestion[]; } export class WithKueryAutocompletion extends React.Component< @@ -56,21 +53,13 @@ export class WithKueryAutocompletion extends React.Component< maxSuggestions?: number ) => { const { indexPattern } = this.props; - const autocompletionProvider = getAutocompleteProvider('kuery'); - const config = { - get: () => true, - }; + const language = 'kuery'; + const hasQuerySuggestions = npStart.plugins.data.autocomplete.hasQuerySuggestions(language); - if (!autocompletionProvider) { + if (!hasQuerySuggestions) { return; } - const getSuggestions = autocompletionProvider({ - config, - indexPatterns: [indexPattern], - boolFilter: [], - }); - this.setState({ currentRequest: { expression, @@ -79,11 +68,15 @@ export class WithKueryAutocompletion extends React.Component< suggestions: [], }); - const suggestions = await getSuggestions({ - query: expression, - selectionStart: cursorPosition, - selectionEnd: cursorPosition, - }); + const suggestions = + (await npStart.plugins.data.autocomplete.getQuerySuggestions({ + language, + query: expression, + selectionStart: cursorPosition, + selectionEnd: cursorPosition, + indexPatterns: [indexPattern], + boolFilter: [], + })) || []; this.setState(state => state.currentRequest && diff --git a/x-pack/legacy/plugins/kuery_autocomplete/public/autocomplete_providers/index.js b/x-pack/legacy/plugins/kuery_autocomplete/public/autocomplete_providers/index.js deleted file mode 100644 index a5fb4eff388f..000000000000 --- a/x-pack/legacy/plugins/kuery_autocomplete/public/autocomplete_providers/index.js +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { flatten, mapValues, uniq } from 'lodash'; -import { getSuggestionsProvider as field } from './field'; -import { getSuggestionsProvider as value } from './value'; -import { getSuggestionsProvider as operator } from './operator'; -import { getSuggestionsProvider as conjunction } from './conjunction'; -import { esKuery } from '../../../../../../src/plugins/data/public'; - -const cursorSymbol = '@kuery-cursor@'; - -function dedup(suggestions) { - return uniq(suggestions, ({ type, text, start, end }) => [type, text, start, end].join('|')); -} - -export const kueryProvider = ({ config, indexPatterns, boolFilter }) => { - const getSuggestionsByType = mapValues({ field, value, operator, conjunction }, provider => { - return provider({ config, indexPatterns, boolFilter }); - }); - - return function getSuggestions({ query, selectionStart, selectionEnd, signal }) { - const cursoredQuery = `${query.substr(0, selectionStart)}${cursorSymbol}${query.substr( - selectionEnd - )}`; - - let cursorNode; - try { - cursorNode = esKuery.fromKueryExpression(cursoredQuery, { cursorSymbol, parseCursor: true }); - } catch (e) { - cursorNode = {}; - } - - const { suggestionTypes = [] } = cursorNode; - const suggestionsByType = suggestionTypes.map(type => { - return getSuggestionsByType[type](cursorNode, signal); - }); - return Promise.all(suggestionsByType).then(suggestionsByType => - dedup(flatten(suggestionsByType)) - ); - }; -}; diff --git a/x-pack/legacy/plugins/kuery_autocomplete/public/autocomplete_providers/__fixtures__/index_pattern_response.json b/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/__fixtures__/index_pattern_response.json similarity index 100% rename from x-pack/legacy/plugins/kuery_autocomplete/public/autocomplete_providers/__fixtures__/index_pattern_response.json rename to x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/__fixtures__/index_pattern_response.json diff --git a/x-pack/legacy/plugins/kuery_autocomplete/public/autocomplete_providers/__tests__/conjunction.js b/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/__tests__/conjunction.js similarity index 100% rename from x-pack/legacy/plugins/kuery_autocomplete/public/autocomplete_providers/__tests__/conjunction.js rename to x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/__tests__/conjunction.js diff --git a/x-pack/legacy/plugins/kuery_autocomplete/public/autocomplete_providers/__tests__/escape_kuery.js b/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/__tests__/escape_kuery.js similarity index 100% rename from x-pack/legacy/plugins/kuery_autocomplete/public/autocomplete_providers/__tests__/escape_kuery.js rename to x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/__tests__/escape_kuery.js diff --git a/x-pack/legacy/plugins/kuery_autocomplete/public/autocomplete_providers/__tests__/field.js b/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/__tests__/field.js similarity index 100% rename from x-pack/legacy/plugins/kuery_autocomplete/public/autocomplete_providers/__tests__/field.js rename to x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/__tests__/field.js diff --git a/x-pack/legacy/plugins/kuery_autocomplete/public/autocomplete_providers/__tests__/operator.js b/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/__tests__/operator.js similarity index 100% rename from x-pack/legacy/plugins/kuery_autocomplete/public/autocomplete_providers/__tests__/operator.js rename to x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/__tests__/operator.js diff --git a/x-pack/legacy/plugins/kuery_autocomplete/public/autocomplete_providers/conjunction.js b/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/conjunction.js similarity index 100% rename from x-pack/legacy/plugins/kuery_autocomplete/public/autocomplete_providers/conjunction.js rename to x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/conjunction.js diff --git a/x-pack/legacy/plugins/kuery_autocomplete/public/autocomplete_providers/escape_kuery.js b/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/escape_kuery.js similarity index 100% rename from x-pack/legacy/plugins/kuery_autocomplete/public/autocomplete_providers/escape_kuery.js rename to x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/escape_kuery.js diff --git a/x-pack/legacy/plugins/kuery_autocomplete/public/autocomplete_providers/field.js b/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/field.js similarity index 100% rename from x-pack/legacy/plugins/kuery_autocomplete/public/autocomplete_providers/field.js rename to x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/field.js diff --git a/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/index.js b/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/index.js new file mode 100644 index 000000000000..b877f9eb852d --- /dev/null +++ b/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/index.js @@ -0,0 +1,58 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { flatten, uniq } from 'lodash'; +import { getSuggestionsProvider as field } from './field'; +import { getSuggestionsProvider as value } from './value'; +import { getSuggestionsProvider as operator } from './operator'; +import { getSuggestionsProvider as conjunction } from './conjunction'; +import { esKuery } from '../../../../../../src/plugins/data/public'; + +const cursorSymbol = '@kuery-cursor@'; +const providers = { + field, + value, + operator, + conjunction, +}; + +function dedup(suggestions) { + return uniq(suggestions, ({ type, text, start, end }) => [type, text, start, end].join('|')); +} + +const getProviderByType = (type, args) => providers[type](args); + +export const setupKqlQuerySuggestionProvider = ({ uiSettings }) => ({ + indexPatterns, + boolFilter, + query, + selectionStart, + selectionEnd, + signal, +}) => { + const cursoredQuery = `${query.substr(0, selectionStart)}${cursorSymbol}${query.substr( + selectionEnd + )}`; + + let cursorNode; + try { + cursorNode = esKuery.fromKueryExpression(cursoredQuery, { cursorSymbol, parseCursor: true }); + } catch (e) { + cursorNode = {}; + } + + const { suggestionTypes = [] } = cursorNode; + const suggestionsByType = suggestionTypes.map(type => + getProviderByType(type, { + config: uiSettings, + indexPatterns, + boolFilter, + })(cursorNode, signal) + ); + return Promise.all(suggestionsByType).then(suggestionsByType => + dedup(flatten(suggestionsByType)) + ); +}; diff --git a/x-pack/legacy/plugins/kuery_autocomplete/public/autocomplete_providers/operator.js b/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/operator.js similarity index 100% rename from x-pack/legacy/plugins/kuery_autocomplete/public/autocomplete_providers/operator.js rename to x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/operator.js diff --git a/x-pack/legacy/plugins/kuery_autocomplete/public/autocomplete_providers/sort_prefix_first.test.ts b/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/sort_prefix_first.test.ts similarity index 100% rename from x-pack/legacy/plugins/kuery_autocomplete/public/autocomplete_providers/sort_prefix_first.test.ts rename to x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/sort_prefix_first.test.ts diff --git a/x-pack/legacy/plugins/kuery_autocomplete/public/autocomplete_providers/sort_prefix_first.ts b/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/sort_prefix_first.ts similarity index 100% rename from x-pack/legacy/plugins/kuery_autocomplete/public/autocomplete_providers/sort_prefix_first.ts rename to x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/sort_prefix_first.ts diff --git a/x-pack/legacy/plugins/kuery_autocomplete/public/autocomplete_providers/value.js b/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/value.js similarity index 71% rename from x-pack/legacy/plugins/kuery_autocomplete/public/autocomplete_providers/value.js rename to x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/value.js index f44a3d9d658f..9d0d70fd9574 100644 --- a/x-pack/legacy/plugins/kuery_autocomplete/public/autocomplete_providers/value.js +++ b/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/value.js @@ -15,7 +15,7 @@ export function getSuggestionsProvider({ indexPatterns, boolFilter }) { indexPatterns.map(indexPattern => { return indexPattern.fields.map(field => ({ ...field, - indexPatternTitle: indexPattern.title, + indexPattern, })); }) ); @@ -27,18 +27,22 @@ export function getSuggestionsProvider({ indexPatterns, boolFilter }) { 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 { getValueSuggestions } = npStart.plugins.data.autocomplete; - const suggestionsByField = fields.map(field => { - 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); - } - ); - }); + const suggestionsByField = fields.map(field => + getValueSuggestions({ + indexPattern: field.indexPattern, + field, + query, + boolFilter, + signal, + }).then(data => { + const quotedValues = data.map(value => + typeof value === 'string' ? `"${escapeQuotes(value)}"` : `${value}` + ); + return wrapAsSuggestions(start, end, query, quotedValues); + }) + ); return Promise.all(suggestionsByField).then(suggestions => flatten(suggestions)); }; diff --git a/x-pack/legacy/plugins/kuery_autocomplete/public/autocomplete_providers/value.test.js b/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/value.test.js similarity index 55% rename from x-pack/legacy/plugins/kuery_autocomplete/public/autocomplete_providers/value.test.js rename to x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/value.test.js index d989fd9046a4..f5b652d2e216 100644 --- a/x-pack/legacy/plugins/kuery_autocomplete/public/autocomplete_providers/value.test.js +++ b/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/value.test.js @@ -6,25 +6,26 @@ import { getSuggestionsProvider } from './value'; import indexPatternResponse from './__fixtures__/index_pattern_response.json'; - import { npStart } from 'ui/new_platform'; jest.mock('ui/new_platform', () => ({ npStart: { plugins: { data: { - getSuggestions: (_, field) => { - let res; - if (field.type === 'boolean') { - res = [true, false]; - } else if (field.name === 'machine.os') { - res = ['Windo"ws', "Mac'", 'Linux']; - } else if (field.name === 'nestedField.child') { - res = ['foo']; - } else { - res = []; - } - return Promise.resolve(res); + autocomplete: { + getValueSuggestions: jest.fn(({ field }) => { + let res; + if (field.type === 'boolean') { + res = [true, false]; + } else if (field.name === 'machine.os') { + res = ['Windo"ws', "Mac'", 'Linux']; + } else if (field.name === 'nestedField.child') { + res = ['foo']; + } else { + res = []; + } + return Promise.resolve(res); + }), }, }, }, @@ -49,19 +50,24 @@ describe('Kuery value suggestions', function() { const fieldName = 'i_dont_exist'; const prefix = ''; const suffix = ''; - const spy = jest.spyOn(npStart.plugins.data, 'getSuggestions'); + const suggestions = await getSuggestions({ fieldName, prefix, suffix }); expect(suggestions.map(({ text }) => text)).toEqual([]); - expect(spy).toHaveBeenCalledTimes(0); + + expect(npStart.plugins.data.autocomplete.getValueSuggestions).toHaveBeenCalledTimes(0); }); test('should format suggestions', async () => { - const fieldName = 'ssl'; // Has results with quotes in mock - const prefix = ''; - const suffix = ''; const start = 1; const end = 5; - const suggestions = await getSuggestions({ fieldName, prefix, suffix, start, end }); + const suggestions = await getSuggestions({ + fieldName: 'ssl', + prefix: '', + suffix: '', + start, + end, + }); + expect(suggestions[0].type).toEqual('value'); expect(suggestions[0].start).toEqual(start); expect(suggestions[0].end).toEqual(end); @@ -80,64 +86,60 @@ describe('Kuery value suggestions', function() { describe('Boolean suggestions', function() { test('should stringify boolean fields', async () => { - const fieldName = 'ssl'; - const prefix = ''; - const suffix = ''; - const spy = jest.spyOn(npStart.plugins.data, 'getSuggestions'); - const suggestions = await getSuggestions({ fieldName, prefix, suffix }); + const suggestions = await getSuggestions({ fieldName: 'ssl', prefix: '', suffix: '' }); + expect(suggestions.map(({ text }) => text)).toEqual(['true ', 'false ']); - expect(spy).toHaveBeenCalledTimes(1); + expect(npStart.plugins.data.autocomplete.getValueSuggestions).toHaveBeenCalledTimes(1); }); test('should filter out boolean suggestions', async () => { - const fieldName = 'ssl'; // Has results with quotes in mock - const prefix = 'fa'; - const suffix = ''; - const suggestions = await getSuggestions({ fieldName, prefix, suffix }); + const suggestions = await getSuggestions({ fieldName: 'ssl', prefix: 'fa', suffix: '' }); + expect(suggestions.length).toEqual(1); }); }); describe('String suggestions', function() { test('should merge prefix and suffix', async () => { - const fieldName = 'machine.os.raw'; const prefix = 'he'; const suffix = 'llo'; - 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, - undefined + + await getSuggestions({ fieldName: 'machine.os.raw', prefix, suffix }); + + expect(npStart.plugins.data.autocomplete.getValueSuggestions).toHaveBeenCalledTimes(1); + expect(npStart.plugins.data.autocomplete.getValueSuggestions).toBeCalledWith( + expect.objectContaining({ + field: expect.any(Object), + query: prefix + suffix, + }) ); }); test('should escape quotes in suggestions', async () => { - const fieldName = 'machine.os'; // Has results with quotes in mock - const prefix = ''; - const suffix = ''; - const suggestions = await getSuggestions({ fieldName, prefix, suffix }); + const suggestions = await getSuggestions({ fieldName: 'machine.os', prefix: '', suffix: '' }); + expect(suggestions[0].text).toEqual('"Windo\\"ws" '); expect(suggestions[1].text).toEqual('"Mac\'" '); expect(suggestions[2].text).toEqual('"Linux" '); }); test('should filter out string suggestions', async () => { - const fieldName = 'machine.os'; // Has results with quotes in mock - const prefix = 'banana'; - const suffix = ''; - const suggestions = await getSuggestions({ fieldName, prefix, suffix }); + const suggestions = await getSuggestions({ + fieldName: 'machine.os', + prefix: 'banana', + suffix: '', + }); + expect(suggestions.length).toEqual(0); }); test('should partially filter out string suggestions - case insensitive', async () => { - const fieldName = 'machine.os'; // Has results with quotes in mock - const prefix = 'ma'; - const suffix = ''; - const suggestions = await getSuggestions({ fieldName, prefix, suffix }); + const suggestions = await getSuggestions({ + fieldName: 'machine.os', + prefix: 'ma', + suffix: '', + }); + expect(suggestions.length).toEqual(1); }); }); diff --git a/x-pack/legacy/plugins/kuery_autocomplete/public/plugin.ts b/x-pack/legacy/plugins/kuery_autocomplete/public/plugin.ts index ded66a7c6e8f..216e0f49ccd3 100644 --- a/x-pack/legacy/plugins/kuery_autocomplete/public/plugin.ts +++ b/x-pack/legacy/plugins/kuery_autocomplete/public/plugin.ts @@ -8,7 +8,7 @@ import { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from 'src/core import { Plugin as DataPublicPlugin } from '../../../../../src/plugins/data/public'; // @ts-ignore -import { kueryProvider } from './autocomplete_providers'; +import { setupKqlQuerySuggestionProvider } from './kql_query_suggestion'; /** @internal */ export interface KueryAutocompletePluginSetupDependencies { @@ -25,8 +25,10 @@ export class KueryAutocompletePlugin implements Plugin, void> { this.initializerContext = initializerContext; } - public async setup(core: CoreSetup, { data }: KueryAutocompletePluginSetupDependencies) { - data.autocomplete.addProvider(KUERY_LANGUAGE_NAME, kueryProvider); + public async setup(core: CoreSetup, plugins: KueryAutocompletePluginSetupDependencies) { + const kueryProvider = setupKqlQuerySuggestionProvider(core, plugins); + + plugins.data.autocomplete.addQuerySuggestionProvider(KUERY_LANGUAGE_NAME, kueryProvider); } public start(core: CoreStart) { diff --git a/x-pack/legacy/plugins/ml/public/application/components/kql_filter_bar/kql_filter_bar.js b/x-pack/legacy/plugins/ml/public/application/components/kql_filter_bar/kql_filter_bar.js index 3591b0907ebb..e604c101a999 100644 --- a/x-pack/legacy/plugins/ml/public/application/components/kql_filter_bar/kql_filter_bar.js +++ b/x-pack/legacy/plugins/ml/public/application/components/kql_filter_bar/kql_filter_bar.js @@ -46,12 +46,8 @@ export class KqlFilterBar extends Component { const boolFilter = []; try { - const suggestions = await getSuggestions( - inputValue, - selectionStart, - indexPattern, - boolFilter - ); + const suggestions = + (await getSuggestions(inputValue, selectionStart, indexPattern, boolFilter)) || []; if (currentRequest !== this.currentRequest) { return; diff --git a/x-pack/legacy/plugins/ml/public/application/components/kql_filter_bar/kql_filter_bar.test.js b/x-pack/legacy/plugins/ml/public/application/components/kql_filter_bar/kql_filter_bar.test.js index b1e79cbf5592..4e74a4bd545a 100644 --- a/x-pack/legacy/plugins/ml/public/application/components/kql_filter_bar/kql_filter_bar.test.js +++ b/x-pack/legacy/plugins/ml/public/application/components/kql_filter_bar/kql_filter_bar.test.js @@ -8,6 +8,8 @@ import React from 'react'; import { shallow } from 'enzyme'; import { KqlFilterBar } from './kql_filter_bar'; +jest.mock('ui/new_platform'); + const defaultProps = { indexPattern: { title: '.ml-anomalies-*', diff --git a/x-pack/legacy/plugins/ml/public/application/components/kql_filter_bar/utils.js b/x-pack/legacy/plugins/ml/public/application/components/kql_filter_bar/utils.js index c007e8bd05c5..bb7b143c948d 100644 --- a/x-pack/legacy/plugins/ml/public/application/components/kql_filter_bar/utils.js +++ b/x-pack/legacy/plugins/ml/public/application/components/kql_filter_bar/utils.js @@ -6,23 +6,11 @@ import { npStart } from 'ui/new_platform'; import { esKuery } from '../../../../../../../../src/plugins/data/public'; -const getAutocompleteProvider = language => npStart.plugins.data.autocomplete.getProvider(language); - -export async function getSuggestions(query, selectionStart, indexPattern, boolFilter) { - const autocompleteProvider = getAutocompleteProvider('kuery'); - if (!autocompleteProvider) { - return []; - } - const config = { - get: () => true, - }; - - const getAutocompleteSuggestions = autocompleteProvider({ - config, +export function getSuggestions(query, selectionStart, indexPattern, boolFilter) { + return npStart.plugins.data.autocomplete.getQuerySuggestions({ + language: 'kuery', indexPatterns: [indexPattern], boolFilter, - }); - return getAutocompleteSuggestions({ query, selectionStart, selectionEnd: selectionStart, diff --git a/x-pack/legacy/plugins/siem/public/components/autocomplete_field/__examples__/index.stories.tsx b/x-pack/legacy/plugins/siem/public/components/autocomplete_field/__examples__/index.stories.tsx index 86d003bf577f..4d92e8cb1335 100644 --- a/x-pack/legacy/plugins/siem/public/components/autocomplete_field/__examples__/index.stories.tsx +++ b/x-pack/legacy/plugins/siem/public/components/autocomplete_field/__examples__/index.stories.tsx @@ -8,10 +8,10 @@ import * as React from 'react'; import { storiesOf } from '@storybook/react'; import { ThemeProvider } from 'styled-components'; import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; -import { AutocompleteSuggestion } from '../../../../../../../../src/plugins/data/public'; +import { autocomplete } from '../../../../../../../../src/plugins/data/public'; import { SuggestionItem } from '../suggestion_item'; -const suggestion: AutocompleteSuggestion = { +const suggestion: autocomplete.QuerySuggestion = { description: 'Description...', end: 3, start: 1, diff --git a/x-pack/legacy/plugins/siem/public/components/autocomplete_field/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/autocomplete_field/index.test.tsx index 27e87d25e286..ef16f79a4b83 100644 --- a/x-pack/legacy/plugins/siem/public/components/autocomplete_field/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/autocomplete_field/index.test.tsx @@ -10,13 +10,13 @@ import { mount, shallow } from 'enzyme'; import { noop } from 'lodash/fp'; import React from 'react'; import { ThemeProvider } from 'styled-components'; -import { AutocompleteSuggestion } from '../../../../../../../src/plugins/data/public'; +import { autocomplete } from '../../../../../../../src/plugins/data/public'; import { TestProviders } from '../../mock'; import { AutocompleteField } from '.'; -const mockAutoCompleteData: AutocompleteSuggestion[] = [ +const mockAutoCompleteData: autocomplete.QuerySuggestion[] = [ { type: 'field', text: 'agent.ephemeral_id ', diff --git a/x-pack/legacy/plugins/siem/public/components/autocomplete_field/index.tsx b/x-pack/legacy/plugins/siem/public/components/autocomplete_field/index.tsx index 124ef26602f3..2f76ae21944b 100644 --- a/x-pack/legacy/plugins/siem/public/components/autocomplete_field/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/autocomplete_field/index.tsx @@ -11,7 +11,7 @@ import { EuiPanel, } from '@elastic/eui'; import React from 'react'; -import { AutocompleteSuggestion } from '../../../../../../../src/plugins/data/public'; +import { autocomplete } from '../../../../../../../src/plugins/data/public'; import euiStyled from '../../../../../common/eui_styled_components'; @@ -25,7 +25,7 @@ interface AutocompleteFieldProps { onSubmit?: (value: string) => void; onChange?: (value: string) => void; placeholder?: string; - suggestions: AutocompleteSuggestion[]; + suggestions: autocomplete.QuerySuggestion[]; value: string; } diff --git a/x-pack/legacy/plugins/siem/public/components/autocomplete_field/suggestion_item.tsx b/x-pack/legacy/plugins/siem/public/components/autocomplete_field/suggestion_item.tsx index aaf7be2f7f5a..44bc65bb0dc1 100644 --- a/x-pack/legacy/plugins/siem/public/components/autocomplete_field/suggestion_item.tsx +++ b/x-pack/legacy/plugins/siem/public/components/autocomplete_field/suggestion_item.tsx @@ -9,13 +9,13 @@ import { transparentize } from 'polished'; import React from 'react'; import styled from 'styled-components'; import euiStyled from '../../../../../common/eui_styled_components'; -import { AutocompleteSuggestion } from '../../../../../../../src/plugins/data/public'; +import { autocomplete } from '../../../../../../../src/plugins/data/public'; interface SuggestionItemProps { isSelected?: boolean; onClick?: React.MouseEventHandler; onMouseEnter?: React.MouseEventHandler; - suggestion: AutocompleteSuggestion; + suggestion: autocomplete.QuerySuggestion; } export const SuggestionItem = React.memo( diff --git a/x-pack/legacy/plugins/siem/public/containers/kuery_autocompletion/index.tsx b/x-pack/legacy/plugins/siem/public/containers/kuery_autocompletion/index.tsx index 6361f7abcf97..4eb51dfe6407 100644 --- a/x-pack/legacy/plugins/siem/public/containers/kuery_autocompletion/index.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/kuery_autocompletion/index.tsx @@ -5,10 +5,7 @@ */ import React, { useState } from 'react'; -import { - AutocompleteSuggestion, - IIndexPattern, -} from '../../../../../../../src/plugins/data/public'; +import { autocomplete, IIndexPattern } from '../../../../../../../src/plugins/data/public'; import { useKibana } from '../../lib/kibana'; type RendererResult = React.ReactElement | null; @@ -18,7 +15,7 @@ interface KueryAutocompletionLifecycleProps { children: RendererFunction<{ isLoadingSuggestions: boolean; loadSuggestions: (expression: string, cursorPosition: number, maxSuggestions?: number) => void; - suggestions: AutocompleteSuggestion[]; + suggestions: autocomplete.QuerySuggestion[]; }>; indexPattern: IIndexPattern; } @@ -33,26 +30,19 @@ export const KueryAutocompletion = React.memo const [currentRequest, setCurrentRequest] = useState( null ); - const [suggestions, setSuggestions] = useState([]); + const [suggestions, setSuggestions] = useState([]); const kibana = useKibana(); const loadSuggestions = async ( expression: string, cursorPosition: number, maxSuggestions?: number ) => { - const autocompletionProvider = kibana.services.data.autocomplete.getProvider('kuery'); - const config = { - get: () => true, - }; - if (!autocompletionProvider) { + const language = 'kuery'; + + if (!kibana.services.data.autocomplete.hasQuerySuggestions(language)) { return; } - const getSuggestions = autocompletionProvider({ - config, - indexPatterns: [indexPattern], - boolFilter: [], - }); const futureRequest = { expression, cursorPosition, @@ -62,16 +52,22 @@ export const KueryAutocompletion = React.memo cursorPosition, }); setSuggestions([]); - const newSuggestions = await getSuggestions({ - query: expression, - selectionStart: cursorPosition, - selectionEnd: cursorPosition, - }); + if ( futureRequest && futureRequest.expression !== (currentRequest && currentRequest.expression) && futureRequest.cursorPosition !== (currentRequest && currentRequest.cursorPosition) ) { + const newSuggestions = + (await kibana.services.data.autocomplete.getQuerySuggestions({ + language: 'kuery', + indexPatterns: [indexPattern], + boolFilter: [], + query: expression, + selectionStart: cursorPosition, + selectionEnd: cursorPosition, + })) || []; + setCurrentRequest(null); setSuggestions(maxSuggestions ? newSuggestions.slice(0, maxSuggestions) : newSuggestions); } diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/kuery_bar/index.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/kuery_bar/index.tsx index 731f560d315d..679106f7e19b 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/kuery_bar/index.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/functional/kuery_bar/index.tsx @@ -13,10 +13,10 @@ import { Typeahead } from './typeahead'; import { useUrlParams } from '../../../hooks'; import { toStaticIndexPattern } from '../../../lib/helper'; import { - AutocompleteProviderRegister, - AutocompleteSuggestion, esKuery, IIndexPattern, + autocomplete, + DataPublicPluginStart, } from '../../../../../../../../src/plugins/data/public'; import { useIndexPattern } from '../../../hooks'; @@ -25,7 +25,7 @@ const Container = styled.div` `; interface State { - suggestions: AutocompleteSuggestion[]; + suggestions: autocomplete.QuerySuggestion[]; isLoadingIndexPattern: boolean; } @@ -34,38 +34,11 @@ function convertKueryToEsQuery(kuery: string, indexPattern: IIndexPattern) { return esKuery.toElasticsearchQuery(ast, indexPattern); } -function getSuggestions( - query: string, - selectionStart: number, - apmIndexPattern: IIndexPattern, - autocomplete: Pick -) { - const autocompleteProvider = autocomplete.getProvider('kuery'); - if (!autocompleteProvider) { - return []; - } - const config = { - get: () => true, - }; - - const getAutocompleteSuggestions = autocompleteProvider({ - config, - indexPatterns: [apmIndexPattern], - }); - - const suggestions = getAutocompleteSuggestions({ - query, - selectionStart, - selectionEnd: selectionStart, - }); - return suggestions; -} - interface Props { - autocomplete: Pick; + autocomplete: DataPublicPluginStart['autocomplete']; } -export function KueryBar({ autocomplete }: Props) { +export function KueryBar({ autocomplete: autocompleteService }: Props) { const [state, setState] = useState({ suggestions: [], isLoadingIndexPattern: true, @@ -99,14 +72,16 @@ export function KueryBar({ autocomplete }: Props) { currentRequestCheck = currentRequest; try { - let suggestions = await getSuggestions( - inputValue, - selectionStart, - indexPattern, - autocomplete - ); - suggestions = suggestions - .filter((suggestion: AutocompleteSuggestion) => !startsWith(suggestion.text, 'span.')) + const suggestions = ( + (await autocompleteService.getQuerySuggestions({ + language: 'kuery', + indexPatterns: [indexPattern], + query: inputValue, + selectionStart, + selectionEnd: selectionStart, + })) || [] + ) + .filter(suggestion => !startsWith(suggestion.text, 'span.')) .slice(0, 15); if (currentRequest !== currentRequestCheck) { diff --git a/x-pack/legacy/plugins/uptime/public/pages/overview.tsx b/x-pack/legacy/plugins/uptime/public/pages/overview.tsx index fbfbfc06e3c5..1c14d971120b 100644 --- a/x-pack/legacy/plugins/uptime/public/pages/overview.tsx +++ b/x-pack/legacy/plugins/uptime/public/pages/overview.tsx @@ -20,14 +20,14 @@ import { useIndexPattern, useUrlParams, useUptimeTelemetry, UptimePage } from '. import { stringifyUrlParams } from '../lib/helper/stringify_url_params'; import { useTrackPageview } from '../../../infra/public'; import { combineFiltersAndUserSearch, stringifyKueries, toStaticIndexPattern } from '../lib/helper'; -import { AutocompleteProviderRegister, esKuery } from '../../../../../../src/plugins/data/public'; import { store } from '../state'; import { setEsKueryString } from '../state/actions'; import { PageHeader } from './page_header'; +import { esKuery, DataPublicPluginStart } from '../../../../../../src/plugins/data/public'; import { UptimeThemeContext } from '../contexts/uptime_theme_context'; interface OverviewPageProps { - autocomplete: Pick; + autocomplete: DataPublicPluginStart['autocomplete']; setBreadcrumbs: UMUpdateBreadcrumbs; } diff --git a/x-pack/legacy/plugins/uptime/public/routes.tsx b/x-pack/legacy/plugins/uptime/public/routes.tsx index 08d752f5b32a..028f2d595832 100644 --- a/x-pack/legacy/plugins/uptime/public/routes.tsx +++ b/x-pack/legacy/plugins/uptime/public/routes.tsx @@ -7,14 +7,14 @@ import React, { FC } from 'react'; import { Route, Switch } from 'react-router-dom'; import { MonitorPage, OverviewPage, NotFoundPage } from './pages'; -import { AutocompleteProviderRegister } from '../../../../../src/plugins/data/public'; +import { DataPublicPluginStart } from '../../../../../src/plugins/data/public'; import { UMUpdateBreadcrumbs } from './lib/lib'; export const MONITOR_ROUTE = '/monitor/:monitorId/:location?'; export const OVERVIEW_ROUTE = '/'; interface RouterProps { - autocomplete: Pick; + autocomplete: DataPublicPluginStart['autocomplete']; basePath: string; setBreadcrumbs: UMUpdateBreadcrumbs; }