From d3a2d9fde26a417d27f93c687960e2abdfa688cd Mon Sep 17 00:00:00 2001 From: Alexey Antonov Date: Fri, 31 Jan 2020 12:57:30 +0300 Subject: [PATCH 20/23] kuery_autocomplete -> convert remaining items to TS/Jest (#56316) (#56471) * kuery_autocomplete -> convert remaining items to TS/Jest Closes #55487 * QuerySuggestionsTypes rename values * remove ref to npStart * conjunction.test.ts it -> test * remove ts-ignore --- .../data/common/es_query/kuery/types.ts | 2 + .../autocomplete/autocomplete_service.ts | 6 + src/plugins/data/public/autocomplete/index.ts | 5 +- .../providers/query_suggestion_provider.ts | 26 ++- .../autocomplete/{types.ts => static.ts} | 14 +- src/plugins/data/public/index.ts | 5 +- src/plugins/data/public/types.ts | 2 +- .../query_string_input/query_string_input.tsx | 6 +- .../typeahead/suggestion_component.test.tsx | 2 +- .../typeahead/suggestions_component.test.tsx | 4 +- .../__tests__/conjunction.js | 48 ---- .../kql_query_suggestion/__tests__/field.js | 146 ------------ .../__tests__/operator.js | 64 ------ .../kql_query_suggestion/conjunction.test.ts | 63 +++++ .../{conjunction.js => conjunction.tsx} | 35 +-- .../public/kql_query_suggestion/field.test.ts | 216 ++++++++++++++++++ .../{field.js => field.tsx} | 71 +++--- .../public/kql_query_suggestion/index.js | 58 ----- .../public/kql_query_suggestion/index.ts | 59 +++++ .../escape_kuery.test.ts} | 45 ++-- .../{escape_kuery.js => lib/escape_kuery.ts} | 20 +- .../kql_query_suggestion/operator.test.ts | 96 ++++++++ .../{operator.js => operator.tsx} | 57 +++-- .../kql_query_suggestion/sort_prefix_first.ts | 2 + .../public/kql_query_suggestion/types.ts | 15 ++ .../public/kql_query_suggestion/value.js | 58 ----- .../public/kql_query_suggestion/value.test.js | 146 ------------ .../public/kql_query_suggestion/value.test.ts | 192 ++++++++++++++++ .../public/kql_query_suggestion/value.ts | 62 +++++ .../kuery_autocomplete/public/legacy.ts | 15 +- .../kuery_autocomplete/public/plugin.ts | 14 +- .../kuery_autocomplete/public/services.ts | 12 + .../__examples__/index.stories.tsx | 2 +- .../autocomplete_field/index.test.tsx | 20 +- 34 files changed, 915 insertions(+), 673 deletions(-) rename src/plugins/data/public/autocomplete/{types.ts => static.ts} (76%) delete mode 100644 x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/__tests__/conjunction.js delete mode 100644 x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/__tests__/field.js delete mode 100644 x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/__tests__/operator.js create mode 100644 x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/conjunction.test.ts rename x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/{conjunction.js => conjunction.tsx} (72%) create mode 100644 x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/field.test.ts rename x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/{field.js => field.tsx} (66%) delete mode 100644 x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/index.js create mode 100644 x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/index.ts rename x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/{__tests__/escape_kuery.js => lib/escape_kuery.test.ts} (55%) rename x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/{escape_kuery.js => lib/escape_kuery.ts} (57%) create mode 100644 x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/operator.test.ts rename x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/{operator.js => operator.tsx} (82%) create mode 100644 x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/types.ts delete mode 100644 x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/value.js delete mode 100644 x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/value.test.js create mode 100644 x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/value.test.ts create mode 100644 x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/value.ts create mode 100644 x-pack/legacy/plugins/kuery_autocomplete/public/services.ts diff --git a/src/plugins/data/common/es_query/kuery/types.ts b/src/plugins/data/common/es_query/kuery/types.ts index 86cb7e08a767c..63c52bb64dc65 100644 --- a/src/plugins/data/common/es_query/kuery/types.ts +++ b/src/plugins/data/common/es_query/kuery/types.ts @@ -33,6 +33,8 @@ export interface KueryParseOptions { startRule: string; allowLeadingWildcards: boolean; errorOnLuceneSyntax: boolean; + cursorSymbol?: string; + parseCursor?: boolean; } export { nodeTypes } from './node_types'; diff --git a/src/plugins/data/public/autocomplete/autocomplete_service.ts b/src/plugins/data/public/autocomplete/autocomplete_service.ts index 0527f833b0f8c..78bd2ec85f477 100644 --- a/src/plugins/data/public/autocomplete/autocomplete_service.ts +++ b/src/plugins/data/public/autocomplete/autocomplete_service.ts @@ -75,3 +75,9 @@ export class AutocompleteService { this.querySuggestionProviders.clear(); } } + +/** @public **/ +export type AutocompleteSetup = ReturnType; + +/** @public **/ +export type AutocompleteStart = ReturnType; diff --git a/src/plugins/data/public/autocomplete/index.ts b/src/plugins/data/public/autocomplete/index.ts index 5b8f3ae510bfd..c2b21e84b7a38 100644 --- a/src/plugins/data/public/autocomplete/index.ts +++ b/src/plugins/data/public/autocomplete/index.ts @@ -16,6 +16,7 @@ * specific language governing permissions and limitations * under the License. */ +import * as autocomplete from './static'; +export { AutocompleteService, AutocompleteSetup, AutocompleteStart } from './autocomplete_service'; -export { AutocompleteService } from './autocomplete_service'; -export { QuerySuggestion, QuerySuggestionType, QuerySuggestionsGetFn } from './types'; +export { autocomplete }; diff --git a/src/plugins/data/public/autocomplete/providers/query_suggestion_provider.ts b/src/plugins/data/public/autocomplete/providers/query_suggestion_provider.ts index 53abdd44c0c3f..94054ed56f42a 100644 --- a/src/plugins/data/public/autocomplete/providers/query_suggestion_provider.ts +++ b/src/plugins/data/public/autocomplete/providers/query_suggestion_provider.ts @@ -19,13 +19,20 @@ import { IFieldType, IIndexPattern } from '../../../common/index_patterns'; -export type QuerySuggestionType = 'field' | 'value' | 'operator' | 'conjunction' | 'recentSearch'; +export enum QuerySuggestionsTypes { + Field = 'field', + Value = 'value', + Operator = 'operator', + Conjunction = 'conjunction', + RecentSearch = 'recentSearch', +} export type QuerySuggestionsGetFn = ( args: QuerySuggestionsGetFnArgs ) => Promise | undefined; -interface QuerySuggestionsGetFnArgs { +/** @public **/ +export interface QuerySuggestionsGetFnArgs { language: string; indexPatterns: IIndexPattern[]; query: string; @@ -35,22 +42,21 @@ interface QuerySuggestionsGetFnArgs { boolFilter?: any; } -interface BasicQuerySuggestion { - type: QuerySuggestionType; - description?: string; +/** @public **/ +export interface BasicQuerySuggestion { + type: QuerySuggestionsTypes; + description?: string | JSX.Element; end: number; start: number; text: string; cursorIndex?: number; } -interface FieldQuerySuggestion extends BasicQuerySuggestion { - type: 'field'; +/** @public **/ +export interface FieldQuerySuggestion extends BasicQuerySuggestion { + type: QuerySuggestionsTypes.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 QuerySuggestion = BasicQuerySuggestion | FieldQuerySuggestion; diff --git a/src/plugins/data/public/autocomplete/types.ts b/src/plugins/data/public/autocomplete/static.ts similarity index 76% rename from src/plugins/data/public/autocomplete/types.ts rename to src/plugins/data/public/autocomplete/static.ts index 759e2dd25a5bc..7d627486c6d65 100644 --- a/src/plugins/data/public/autocomplete/types.ts +++ b/src/plugins/data/public/autocomplete/static.ts @@ -17,17 +17,11 @@ * under the License. */ -import { AutocompleteService } from './autocomplete_service'; - -/** @public **/ -export type AutocompleteSetup = ReturnType; - -/** @public **/ -export type AutocompleteStart = ReturnType; - -/** @public **/ export { QuerySuggestion, + QuerySuggestionsTypes, QuerySuggestionsGetFn, - QuerySuggestionType, + QuerySuggestionsGetFnArgs, + BasicQuerySuggestion, + FieldQuerySuggestion, } from './providers/query_suggestion_provider'; diff --git a/src/plugins/data/public/index.ts b/src/plugins/data/public/index.ts index bc25c64f0e96e..2fa6b8deae69d 100644 --- a/src/plugins/data/public/index.ts +++ b/src/plugins/data/public/index.ts @@ -18,7 +18,6 @@ */ import { PluginInitializerContext } from '../../../core/public'; -import * as autocomplete from './autocomplete'; export function plugin(initializerContext: PluginInitializerContext) { return new DataPublicPlugin(initializerContext); @@ -44,7 +43,7 @@ export { RefreshInterval, TimeRange, } from '../common'; - +export { autocomplete } from './autocomplete'; export * from './field_formats'; export * from './index_patterns'; export * from './search'; @@ -70,5 +69,3 @@ export { // Export plugin after all other imports import { DataPublicPlugin } from './plugin'; export { DataPublicPlugin as Plugin }; - -export { autocomplete }; diff --git a/src/plugins/data/public/types.ts b/src/plugins/data/public/types.ts index 6b6ff5e62e63f..e62aba5f2713d 100644 --- a/src/plugins/data/public/types.ts +++ b/src/plugins/data/public/types.ts @@ -20,7 +20,7 @@ import { CoreStart } from 'src/core/public'; import { IStorageWrapper } from 'src/plugins/kibana_utils/public'; import { IUiActionsSetup, IUiActionsStart } from 'src/plugins/ui_actions/public'; -import { AutocompleteSetup, AutocompleteStart } from './autocomplete/types'; +import { AutocompleteSetup, AutocompleteStart } from './autocomplete'; import { FieldFormatsSetup, FieldFormatsStart } from './field_formats'; import { ISearchSetup, ISearchStart } from './search'; import { QuerySetup, QueryStart } from './query'; 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 cf219c35bcced..7a8c0f7269fa1 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 @@ -89,8 +89,6 @@ const KEY_CODES = { END: 35, }; -const recentSearchType: autocomplete.QuerySuggestionType = 'recentSearch'; - export class QueryStringInputUI extends Component { public state: State = { isSuggestionsVisible: false, @@ -193,7 +191,7 @@ export class QueryStringInputUI extends Component { const text = toUser(recentSearch); const start = 0; const end = query.length; - return { type: recentSearchType, text, start, end }; + return { type: autocomplete.QuerySuggestionsTypes.RecentSearch, text, start, end }; }); }; @@ -343,7 +341,7 @@ export class QueryStringInputUI extends Component { selectionEnd: start + (cursorIndex ? cursorIndex : text.length), }); - if (type === recentSearchType) { + if (type === autocomplete.QuerySuggestionsTypes.RecentSearch) { this.setState({ isSuggestionsVisible: false, index: null }); this.onSubmit({ query: newQueryString, language: this.props.query.language }); } 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 0c5c701642757..ba92be8947ea5 100644 --- a/src/plugins/data/public/ui/typeahead/suggestion_component.test.tsx +++ b/src/plugins/data/public/ui/typeahead/suggestion_component.test.tsx @@ -31,7 +31,7 @@ const mockSuggestion: autocomplete.QuerySuggestion = { end: 0, start: 42, text: 'as promised, not helpful', - type: 'value', + type: autocomplete.QuerySuggestionsTypes.Value, }; describe('SuggestionComponent', () => { 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 b84f612b6d13a..eebe438025949 100644 --- a/src/plugins/data/public/ui/typeahead/suggestions_component.test.tsx +++ b/src/plugins/data/public/ui/typeahead/suggestions_component.test.tsx @@ -33,14 +33,14 @@ const mockSuggestions: autocomplete.QuerySuggestion[] = [ end: 0, start: 42, text: 'as promised, not helpful', - type: 'value', + type: autocomplete.QuerySuggestionsTypes.Value, }, { description: 'This is another unhelpful suggestion', end: 0, start: 42, text: 'yep', - type: 'field', + type: autocomplete.QuerySuggestionsTypes.Field, }, ]; diff --git a/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/__tests__/conjunction.js b/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/__tests__/conjunction.js deleted file mode 100644 index 94990edef5e82..0000000000000 --- a/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/__tests__/conjunction.js +++ /dev/null @@ -1,48 +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 expect from '@kbn/expect'; -import { getSuggestionsProvider } from '../conjunction'; - -describe('Kuery conjunction suggestions', function() { - const getSuggestions = getSuggestionsProvider(); - - it('should return a function', function() { - expect(typeof getSuggestions).to.be('function'); - }); - - it('should not suggest anything for phrases not ending in whitespace', function() { - const text = 'foo'; - const suggestions = getSuggestions({ text }); - expect(suggestions).to.eql([]); - }); - - it('should suggest and/or for phrases ending in whitespace', function() { - const text = 'foo '; - const suggestions = getSuggestions({ text }); - expect(suggestions.length).to.be(2); - expect(suggestions.map(suggestion => suggestion.text)).to.eql(['and ', 'or ']); - }); - - it('should suggest to insert the suggestion at the end of the string', function() { - const text = 'bar '; - const end = text.length; - const suggestions = getSuggestions({ text, end }); - expect(suggestions.length).to.be(2); - expect(suggestions.map(suggestion => suggestion.start)).to.eql([end, end]); - expect(suggestions.map(suggestion => suggestion.end)).to.eql([end, end]); - }); - it('should have descriptions', function() { - const text = ' '; - const suggestions = getSuggestions({ text }); - expect(typeof suggestions).to.be('object'); - expect(Object.keys(suggestions).length).to.be(2); - suggestions.forEach(suggestion => { - expect(typeof suggestion).to.be('object'); - expect(suggestion).to.have.property('description'); - }); - }); -}); diff --git a/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/__tests__/field.js b/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/__tests__/field.js deleted file mode 100644 index 6dc07da68a5ea..0000000000000 --- a/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/__tests__/field.js +++ /dev/null @@ -1,146 +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 expect from '@kbn/expect'; -import { getSuggestionsProvider } from '../field'; -import indexPatternResponse from '../__fixtures__/index_pattern_response.json'; -import { isFilterable } from '../../../../../../../src/plugins/data/public'; - -describe('Kuery field suggestions', function() { - let indexPattern; - let indexPatterns; - let getSuggestions; - - beforeEach(() => { - indexPattern = indexPatternResponse; - indexPatterns = [indexPattern]; - getSuggestions = getSuggestionsProvider({ indexPatterns }); - }); - - it('should return a function', function() { - expect(typeof getSuggestions).to.be('function'); - }); - - it('should return filterable fields', function() { - const prefix = ''; - const suffix = ''; - const suggestions = getSuggestions({ prefix, suffix }); - const filterableFields = indexPattern.fields.filter(isFilterable); - expect(suggestions.length).to.be(filterableFields.length); - }); - - it('should filter suggestions based on the query', () => { - const prefix = 'machine'; - const suffix = ''; - const suggestions = getSuggestions({ prefix, suffix }); - expect(suggestions.find(({ text }) => text === 'machine.os ')).to.be.ok(); - }); - - it('should filter suggestions case insensitively', () => { - const prefix = 'MACHINE'; - const suffix = ''; - const suggestions = getSuggestions({ prefix, suffix }); - expect(suggestions.find(({ text }) => text === 'machine.os ')).to.be.ok(); - }); - - it('should return suggestions where the query matches somewhere in the middle', () => { - const prefix = '.'; - const suffix = ''; - const suggestions = getSuggestions({ prefix, suffix }); - expect(suggestions.find(({ text }) => text === 'machine.os ')).to.be.ok(); - }); - - it('should return field names that start with the query first', () => { - const prefix = 'e'; - const suffix = ''; - const suggestions = getSuggestions({ prefix, suffix }); - const extensionIndex = suggestions.findIndex(({ text }) => text === 'extension '); - const bytesIndex = suggestions.findIndex(({ text }) => text === 'bytes '); - expect(extensionIndex).to.be.lessThan(bytesIndex); - }); - - it('should sort keyword fields before analyzed versions', () => { - const prefix = ''; - const suffix = ''; - const suggestions = getSuggestions({ prefix, suffix }); - const analyzedIndex = suggestions.findIndex(({ text }) => text === 'machine.os '); - const keywordIndex = suggestions.findIndex(({ text }) => text === 'machine.os.raw '); - expect(keywordIndex).to.be.lessThan(analyzedIndex); - }); - - it('should have descriptions', function() { - const prefix = ''; - const suffix = ''; - const suggestions = getSuggestions({ prefix, suffix }); - expect(suggestions.length).to.be.greaterThan(0); - suggestions.forEach(suggestion => { - expect(suggestion).to.have.property('description'); - }); - }); - - describe('nested fields', function() { - it("should automatically wrap nested fields in KQL's nested syntax", () => { - const prefix = 'ch'; - const suffix = ''; - const suggestions = getSuggestions({ prefix, suffix }); - - const suggestion = suggestions.find(({ field }) => field.name === 'nestedField.child'); - expect(suggestion.text).to.be('nestedField:{ child }'); - - // For most suggestions the cursor can be placed at the end of the suggestion text, but - // for the nested field syntax we want to place the cursor inside the curly braces - expect(suggestion.cursorIndex).to.be(20); - }); - - it('should narrow suggestions to children of a nested path if provided', () => { - const prefix = 'ch'; - const suffix = ''; - - const allSuggestions = getSuggestions({ prefix, suffix }); - expect(allSuggestions.length).to.be.greaterThan(2); - - const nestedSuggestions = getSuggestions({ prefix, suffix, nestedPath: 'nestedField' }); - expect(nestedSuggestions).to.have.length(2); - }); - - it("should not wrap the suggestion in KQL's nested syntax if the correct nested path is already provided", () => { - const prefix = 'ch'; - const suffix = ''; - - const suggestions = getSuggestions({ prefix, suffix, nestedPath: 'nestedField' }); - const suggestion = suggestions.find(({ field }) => field.name === 'nestedField.child'); - expect(suggestion.text).to.be('child '); - }); - - it('should handle fields nested multiple levels deep', () => { - const prefix = 'doubly'; - const suffix = ''; - - const suggestionsWithNoPath = getSuggestions({ prefix, suffix }); - expect(suggestionsWithNoPath).to.have.length(1); - const [noPathSuggestion] = suggestionsWithNoPath; - expect(noPathSuggestion.text).to.be('nestedField.nestedChild:{ doublyNestedChild }'); - - const suggestionsWithPartialPath = getSuggestions({ - prefix, - suffix, - nestedPath: 'nestedField', - }); - expect(suggestionsWithPartialPath).to.have.length(1); - const [partialPathSuggestion] = suggestionsWithPartialPath; - expect(partialPathSuggestion.text).to.be('nestedChild:{ doublyNestedChild }'); - - const suggestionsWithFullPath = getSuggestions({ - prefix, - suffix, - nestedPath: 'nestedField.nestedChild', - }); - expect(suggestionsWithFullPath).to.have.length(1); - const [fullPathSuggestion] = suggestionsWithFullPath; - expect(fullPathSuggestion.text).to.be('doublyNestedChild '); - }); - }); -}); diff --git a/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/__tests__/operator.js b/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/__tests__/operator.js deleted file mode 100644 index c248e3e8366a9..0000000000000 --- a/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/__tests__/operator.js +++ /dev/null @@ -1,64 +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 expect from '@kbn/expect'; -import { getSuggestionsProvider } from '../operator'; -import indexPatternResponse from '../__fixtures__/index_pattern_response.json'; - -describe('Kuery operator suggestions', function() { - let indexPatterns; - let getSuggestions; - - beforeEach(() => { - indexPatterns = [indexPatternResponse]; - getSuggestions = getSuggestionsProvider({ indexPatterns }); - }); - - it('should return a function', function() { - expect(typeof getSuggestions).to.be('function'); - }); - - it('should not return suggestions for non-fields', () => { - const fieldName = 'foo'; - const suggestions = getSuggestions({ fieldName }); - expect(suggestions.length).to.eql([]); - }); - - it('should return exists for every field', () => { - const fieldName = 'custom_user_field'; - const suggestions = getSuggestions({ fieldName }); - expect(suggestions.length).to.eql(1); - expect(suggestions[0].text).to.be(': * '); - }); - - it('should return equals for string fields', () => { - const fieldName = 'machine.os'; - const suggestions = getSuggestions({ fieldName }); - expect(suggestions.find(({ text }) => text === ': ')).to.be.ok(); - expect(suggestions.find(({ text }) => text === '< ')).to.not.be.ok(); - }); - - it('should return numeric operators for numeric fields', () => { - const fieldName = 'bytes'; - const suggestions = getSuggestions({ fieldName }); - expect(suggestions.find(({ text }) => text === ': ')).to.be.ok(); - expect(suggestions.find(({ text }) => text === '< ')).to.be.ok(); - }); - - it('should have descriptions', function() { - const fieldName = 'bytes'; - const suggestions = getSuggestions({ fieldName }); - expect(suggestions.length).to.be.greaterThan(0); - suggestions.forEach(suggestion => { - expect(suggestion).to.have.property('description'); - }); - }); - - it('should handle nested paths', () => { - const suggestions = getSuggestions({ fieldName: 'child', nestedPath: 'nestedField' }); - expect(suggestions.length).to.be.greaterThan(0); - }); -}); diff --git a/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/conjunction.test.ts b/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/conjunction.test.ts new file mode 100644 index 0000000000000..e8aec0deec6d7 --- /dev/null +++ b/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/conjunction.test.ts @@ -0,0 +1,63 @@ +/* + * 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 { setupGetConjunctionSuggestions } from './conjunction'; +import { autocomplete, esKuery } from '../../../../../../src/plugins/data/public'; +import { coreMock } from '../../../../../../src/core/public/mocks'; + +const mockKueryNode = (kueryNode: Partial) => + (kueryNode as unknown) as esKuery.KueryNode; + +describe('Kuery conjunction suggestions', () => { + const querySuggestionsArgs = (null as unknown) as autocomplete.QuerySuggestionsGetFnArgs; + let getSuggestions: ReturnType; + + beforeEach(() => { + getSuggestions = setupGetConjunctionSuggestions(coreMock.createSetup()); + }); + + test('should return a function', () => { + expect(typeof getSuggestions).toBe('function'); + }); + + test('should not suggest anything for phrases not ending in whitespace', async () => { + const text = 'foo'; + const suggestions = await getSuggestions(querySuggestionsArgs, mockKueryNode({ text })); + + expect(suggestions).toEqual([]); + }); + + test('should suggest and/or for phrases ending in whitespace', async () => { + const text = 'foo '; + const suggestions = await getSuggestions(querySuggestionsArgs, mockKueryNode({ text })); + + expect(suggestions.length).toBe(2); + expect(suggestions.map(suggestion => suggestion.text)).toEqual(['and ', 'or ']); + }); + + test('should suggest to insert the suggestion at the end of the string', async () => { + const text = 'bar '; + const end = text.length; + const suggestions = await getSuggestions(querySuggestionsArgs, mockKueryNode({ text, end })); + + expect(suggestions.length).toBe(2); + expect(suggestions.map(suggestion => suggestion.start)).toEqual([end, end]); + expect(suggestions.map(suggestion => suggestion.end)).toEqual([end, end]); + }); + + test('should have descriptions', async () => { + const text = ' '; + const suggestions = await getSuggestions(querySuggestionsArgs, mockKueryNode({ text })); + + expect(typeof suggestions).toBe('object'); + expect(Object.keys(suggestions).length).toBe(2); + + suggestions.forEach(suggestion => { + expect(typeof suggestion).toBe('object'); + expect(suggestion).toHaveProperty('description'); + }); + }); +}); diff --git a/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/conjunction.js b/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/conjunction.tsx similarity index 72% rename from x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/conjunction.js rename to x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/conjunction.tsx index 66f4e6f8eb341..f570586274fdd 100644 --- a/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/conjunction.js +++ b/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/conjunction.tsx @@ -4,9 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ import React from 'react'; +import { $Keys } from 'utility-types'; import { FormattedMessage } from '@kbn/i18n/react'; - -const type = 'conjunction'; +import { KqlQuerySuggestionProvider } from './types'; +import { autocomplete } from '../../../../../../src/plugins/data/public'; const bothArgumentsText = ( ); -const conjunctions = { +const conjunctions: Record = { and: (

{ + return (querySuggestionsArgs, { text, end }) => { + let suggestions: autocomplete.QuerySuggestion[] | [] = []; + + if (text.endsWith(' ')) { + suggestions = Object.keys(conjunctions).map((key: $Keys) => ({ + type: autocomplete.QuerySuggestionsTypes.Conjunction, + text: `${key} `, + description: conjunctions[key], + start: end, + end, + })); + } -export function getSuggestionsProvider() { - return function getConjunctionSuggestions({ text, end }) { - if (!text.endsWith(' ')) return []; - const suggestions = Object.keys(conjunctions).map(conjunction => { - const text = `${conjunction} `; - const description = getDescription(conjunction); - return { type, text, description, start: end, end }; - }); - return suggestions; + return Promise.resolve(suggestions); }; -} +}; diff --git a/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/field.test.ts b/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/field.test.ts new file mode 100644 index 0000000000000..2fd5cfd17eb69 --- /dev/null +++ b/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/field.test.ts @@ -0,0 +1,216 @@ +/* + * 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 indexPatternResponse from './__fixtures__/index_pattern_response.json'; + +import { setupGetFieldSuggestions } from './field'; +import { isFilterable, autocomplete, esKuery } from '../../../../../../src/plugins/data/public'; +import { coreMock } from '../../../../../../src/core/public/mocks'; + +const mockKueryNode = (kueryNode: Partial) => + (kueryNode as unknown) as esKuery.KueryNode; + +describe('Kuery field suggestions', () => { + let querySuggestionsArgs: autocomplete.QuerySuggestionsGetFnArgs; + let getSuggestions: ReturnType; + + beforeEach(() => { + querySuggestionsArgs = ({ + indexPatterns: [indexPatternResponse], + } as unknown) as autocomplete.QuerySuggestionsGetFnArgs; + + getSuggestions = setupGetFieldSuggestions(coreMock.createSetup()); + }); + + test('should return a function', () => { + expect(typeof getSuggestions).toBe('function'); + }); + + test('should return filterable fields', async () => { + const prefix = ''; + const suffix = ''; + const suggestions = await getSuggestions( + querySuggestionsArgs, + mockKueryNode({ prefix, suffix }) + ); + const filterableFields = indexPatternResponse.fields.filter(isFilterable); + + expect(suggestions.length).toBe(filterableFields.length); + }); + + test('should filter suggestions based on the query', async () => { + const prefix = 'machine'; + const suffix = ''; + const suggestions = await getSuggestions( + querySuggestionsArgs, + mockKueryNode({ prefix, suffix }) + ); + + expect(suggestions.find(({ text }) => text === 'machine.os ')).toBeDefined(); + }); + + test('should filter suggestions case insensitively', async () => { + const prefix = 'MACHINE'; + const suffix = ''; + const suggestions = await getSuggestions( + querySuggestionsArgs, + mockKueryNode({ prefix, suffix }) + ); + + expect(suggestions.find(({ text }) => text === 'machine.os ')).toBeDefined(); + }); + + test('should return suggestions where the query matches somewhere in the middle', async () => { + const prefix = '.'; + const suffix = ''; + const suggestions = await getSuggestions( + querySuggestionsArgs, + mockKueryNode({ prefix, suffix }) + ); + + expect(suggestions.find(({ text }) => text === 'machine.os ')).toBeDefined(); + }); + + test('should return field names that start with the query first', async () => { + const prefix = 'e'; + const suffix = ''; + const suggestions = await getSuggestions( + querySuggestionsArgs, + mockKueryNode({ prefix, suffix }) + ); + const extensionIndex = suggestions.findIndex(({ text }) => text === 'extension '); + const bytesIndex = suggestions.findIndex(({ text }) => text === 'bytes '); + + expect(extensionIndex).toBeLessThan(bytesIndex); + }); + + test('should sort keyword fields before analyzed versions', async () => { + const prefix = ''; + const suffix = ''; + const suggestions = await getSuggestions( + querySuggestionsArgs, + mockKueryNode({ prefix, suffix }) + ); + const analyzedIndex = suggestions.findIndex(({ text }) => text === 'machine.os '); + const keywordIndex = suggestions.findIndex(({ text }) => text === 'machine.os.raw '); + + expect(keywordIndex).toBeLessThan(analyzedIndex); + }); + + test('should have descriptions', async () => { + const prefix = ''; + const suffix = ''; + const suggestions = await getSuggestions( + querySuggestionsArgs, + mockKueryNode({ prefix, suffix }) + ); + expect(suggestions.length).toBeGreaterThan(0); + suggestions.forEach(suggestion => { + expect(suggestion).toHaveProperty('description'); + }); + }); + + describe('nested fields', () => { + test("should automatically wrap nested fields in KQL's nested syntax", async () => { + const prefix = 'ch'; + const suffix = ''; + const suggestions = await getSuggestions( + querySuggestionsArgs, + mockKueryNode({ prefix, suffix }) + ); + + const suggestion = suggestions.find(({ field }) => field.name === 'nestedField.child'); + + expect(suggestion).toBeDefined(); + + if (suggestion) { + expect(suggestion.text).toBe('nestedField:{ child }'); + + // For most suggestions the cursor can be placed at the end of the suggestion text, but + // for the nested field syntax we want to place the cursor inside the curly braces + expect(suggestion.cursorIndex).toBe(20); + } + }); + + test('should narrow suggestions to children of a nested path if provided', async () => { + const prefix = 'ch'; + const suffix = ''; + + const allSuggestions = await getSuggestions( + querySuggestionsArgs, + mockKueryNode({ prefix, suffix }) + ); + expect(allSuggestions.length).toBeGreaterThan(2); + + const nestedSuggestions = await getSuggestions( + querySuggestionsArgs, + mockKueryNode({ + prefix, + suffix, + nestedPath: 'nestedField', + }) + ); + expect(nestedSuggestions).toHaveLength(2); + }); + + test("should not wrap the suggestion in KQL's nested syntax if the correct nested path is already provided", async () => { + const prefix = 'ch'; + const suffix = ''; + + const suggestions = await getSuggestions( + querySuggestionsArgs, + mockKueryNode({ + prefix, + suffix, + nestedPath: 'nestedField', + }) + ); + const suggestion = suggestions.find(({ field }) => field.name === 'nestedField.child'); + + expect(suggestion).toBeDefined(); + + if (suggestion) { + expect(suggestion.text).toBe('child '); + } + }); + + test('should handle fields nested multiple levels deep', async () => { + const prefix = 'doubly'; + const suffix = ''; + + const suggestionsWithNoPath = await getSuggestions( + querySuggestionsArgs, + mockKueryNode({ prefix, suffix }) + ); + expect(suggestionsWithNoPath).toHaveLength(1); + const [noPathSuggestion] = suggestionsWithNoPath; + expect(noPathSuggestion.text).toBe('nestedField.nestedChild:{ doublyNestedChild }'); + + const suggestionsWithPartialPath = await getSuggestions( + querySuggestionsArgs, + mockKueryNode({ + prefix, + suffix, + nestedPath: 'nestedField', + }) + ); + expect(suggestionsWithPartialPath).toHaveLength(1); + const [partialPathSuggestion] = suggestionsWithPartialPath; + expect(partialPathSuggestion.text).toBe('nestedChild:{ doublyNestedChild }'); + + const suggestionsWithFullPath = await getSuggestions( + querySuggestionsArgs, + mockKueryNode({ + prefix, + suffix, + nestedPath: 'nestedField.nestedChild', + }) + ); + expect(suggestionsWithFullPath).toHaveLength(1); + const [fullPathSuggestion] = suggestionsWithFullPath; + expect(fullPathSuggestion.text).toBe('doublyNestedChild '); + }); + }); +}); diff --git a/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/field.js b/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/field.tsx similarity index 66% rename from x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/field.js rename to x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/field.tsx index 3e5c92dfc007f..a8af884c24fc3 100644 --- a/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/field.js +++ b/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/field.tsx @@ -5,32 +5,42 @@ */ import React from 'react'; import { flatten } from 'lodash'; -import { escapeKuery } from './escape_kuery'; -import { sortPrefixFirst } from './sort_prefix_first'; -import { isFilterable } from '../../../../../../src/plugins/data/public'; import { FormattedMessage } from '@kbn/i18n/react'; +import { escapeKuery } from './lib/escape_kuery'; +import { sortPrefixFirst } from './sort_prefix_first'; +import { IFieldType, isFilterable, autocomplete } from '../../../../../../src/plugins/data/public'; +import { KqlQuerySuggestionProvider } from './types'; -const type = 'field'; - -function getDescription(fieldName) { +const getDescription = (field: IFieldType) => { return (

{fieldName} }} + values={{ fieldName: {field.name} }} />

); -} +}; -export function getSuggestionsProvider({ indexPatterns }) { - const allFields = flatten( - indexPatterns.map(indexPattern => { - return indexPattern.fields.filter(isFilterable); - }) - ); - return function getFieldSuggestions({ start, end, prefix, suffix, nestedPath = '' }) { +const keywordComparator = (first: IFieldType, second: IFieldType) => { + const extensions = ['raw', 'keyword']; + if (extensions.map(ext => `${first.name}.${ext}`).includes(second.name)) { + return 1; + } else if (extensions.map(ext => `${second.name}.${ext}`).includes(first.name)) { + return -1; + } + + return first.name.localeCompare(second.name); +}; + +export const setupGetFieldSuggestions: KqlQuerySuggestionProvider = core => { + return ({ indexPatterns }, { start, end, prefix, suffix, nestedPath = '' }) => { + const allFields = flatten( + indexPatterns.map(indexPattern => { + return indexPattern.fields.filter(isFilterable); + }) + ); const search = `${prefix}${suffix}`.trim().toLowerCase(); const matchingFields = allFields.filter(field => { return ( @@ -44,7 +54,8 @@ export function getSuggestionsProvider({ indexPatterns }) { ); }); const sortedFields = sortPrefixFirst(matchingFields.sort(keywordComparator), search, 'name'); - const suggestions = sortedFields.map(field => { + + const suggestions: autocomplete.FieldQuerySuggestion[] = sortedFields.map(field => { const remainingPath = field.subType && field.subType.nested ? field.subType.nested.path.slice(nestedPath ? nestedPath.length + 1 : 0) @@ -55,23 +66,23 @@ export function getSuggestionsProvider({ indexPatterns }) { field.name.slice(field.subType.nested.path.length + 1) )} }` : `${escapeKuery(field.name.slice(nestedPath ? nestedPath.length + 1 : 0))} `; - const description = getDescription(field.name); + const description = getDescription(field); const cursorIndex = field.subType && field.subType.nested && remainingPath.length > 0 ? text.length - 2 : text.length; - return { type, text, description, start, end, cursorIndex, field }; + + return { + type: autocomplete.QuerySuggestionsTypes.Field, + text, + description, + start, + end, + cursorIndex, + field, + }; }); - return suggestions; - }; -} -function keywordComparator(first, second) { - const extensions = ['raw', 'keyword']; - if (extensions.map(ext => `${first.name}.${ext}`).includes(second.name)) { - return 1; - } else if (extensions.map(ext => `${second.name}.${ext}`).includes(first.name)) { - return -1; - } - return first.name.localeCompare(second.name); -} + return Promise.resolve(suggestions); + }; +}; 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 deleted file mode 100644 index b877f9eb852d5..0000000000000 --- a/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/index.js +++ /dev/null @@ -1,58 +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, 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/kql_query_suggestion/index.ts b/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/index.ts new file mode 100644 index 0000000000000..2cc15fe4c9280 --- /dev/null +++ b/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/index.ts @@ -0,0 +1,59 @@ +/* + * 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 { CoreSetup } from 'kibana/public'; +import { $Keys } from 'utility-types'; +import { flatten, uniq } from 'lodash'; +import { setupGetFieldSuggestions } from './field'; +import { setupGetValueSuggestions } from './value'; +import { setupGetOperatorSuggestions } from './operator'; +import { setupGetConjunctionSuggestions } from './conjunction'; +import { esKuery, autocomplete } from '../../../../../../src/plugins/data/public'; + +const cursorSymbol = '@kuery-cursor@'; + +const dedup = (suggestions: autocomplete.QuerySuggestion[]): autocomplete.QuerySuggestion[] => + uniq(suggestions, ({ type, text, start, end }) => [type, text, start, end].join('|')); + +export const setupKqlQuerySuggestionProvider = ( + core: CoreSetup +): autocomplete.QuerySuggestionsGetFn => { + const providers = { + field: setupGetFieldSuggestions(core), + value: setupGetValueSuggestions(core), + operator: setupGetOperatorSuggestions(core), + conjunction: setupGetConjunctionSuggestions(core), + }; + + const getSuggestionsByType = ( + cursoredQuery: string, + querySuggestionsArgs: autocomplete.QuerySuggestionsGetFnArgs + ): Array> | [] => { + try { + const cursorNode = esKuery.fromKueryExpression(cursoredQuery, { + cursorSymbol, + parseCursor: true, + }); + + return cursorNode.suggestionTypes.map((type: $Keys) => + providers[type](querySuggestionsArgs, cursorNode) + ); + } catch (e) { + return []; + } + }; + + return querySuggestionsArgs => { + const { query, selectionStart, selectionEnd } = querySuggestionsArgs; + const cursoredQuery = `${query.substr(0, selectionStart)}${cursorSymbol}${query.substr( + selectionEnd + )}`; + + return Promise.all( + getSuggestionsByType(cursoredQuery, querySuggestionsArgs) + ).then(suggestionsByType => dedup(flatten(suggestionsByType))); + }; +}; diff --git a/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/__tests__/escape_kuery.js b/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/lib/escape_kuery.test.ts similarity index 55% rename from x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/__tests__/escape_kuery.js rename to x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/lib/escape_kuery.test.ts index 2127194c9a890..a4a1d977a207f 100644 --- a/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/__tests__/escape_kuery.js +++ b/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/lib/escape_kuery.test.ts @@ -4,55 +4,62 @@ * you may not use this file except in compliance with the Elastic License. */ -import expect from '@kbn/expect'; -import { escapeQuotes, escapeKuery } from '../escape_kuery'; +import { escapeQuotes, escapeKuery } from './escape_kuery'; -describe('Kuery escape', function() { - it('should escape quotes', function() { +describe('Kuery escape', () => { + test('should escape quotes', () => { const value = 'I said, "Hello."'; const expected = 'I said, \\"Hello.\\"'; - expect(escapeQuotes(value)).to.be(expected); + + expect(escapeQuotes(value)).toBe(expected); }); - it('should escape special characters', function() { + test('should escape special characters', () => { const value = `This \\ has (a lot of) characters, don't you *think*? "Yes."`; const expected = `This \\\\ has \\(a lot of\\) \\ characters, don't you \\*think\\*? \\"Yes.\\"`; - expect(escapeKuery(value)).to.be(expected); + + expect(escapeKuery(value)).toBe(expected); }); - it('should escape keywords', function() { + test('should escape keywords', () => { const value = 'foo and bar or baz not qux'; const expected = 'foo \\and bar \\or baz \\not qux'; - expect(escapeKuery(value)).to.be(expected); + + expect(escapeKuery(value)).toBe(expected); }); - it('should escape keywords next to each other', function() { + test('should escape keywords next to each other', () => { const value = 'foo and bar or not baz'; const expected = 'foo \\and bar \\or \\not baz'; - expect(escapeKuery(value)).to.be(expected); + + expect(escapeKuery(value)).toBe(expected); }); - it('should not escape keywords without surrounding spaces', function() { + test('should not escape keywords without surrounding spaces', () => { const value = 'And this has keywords, or does it not?'; const expected = 'And this has keywords, \\or does it not?'; - expect(escapeKuery(value)).to.be(expected); + + expect(escapeKuery(value)).toBe(expected); }); - it('should escape uppercase keywords', function() { + test('should escape uppercase keywords', () => { const value = 'foo AND bar'; const expected = 'foo \\AND bar'; - expect(escapeKuery(value)).to.be(expected); + + expect(escapeKuery(value)).toBe(expected); }); - it('should escape both keywords and special characters', function() { + test('should escape both keywords and special characters', () => { const value = 'Hello, world, and to meet you!'; const expected = 'Hello, world, \\and \\ to meet you!'; - expect(escapeKuery(value)).to.be(expected); + + expect(escapeKuery(value)).toBe(expected); }); - it('should escape newlines and tabs', () => { + test('should escape newlines and tabs', () => { const value = 'This\nhas\tnewlines\r\nwith\ttabs'; const expected = 'This\\nhas\\tnewlines\\r\\nwith\\ttabs'; - expect(escapeKuery(value)).to.be(expected); + + expect(escapeKuery(value)).toBe(expected); }); }); diff --git a/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/escape_kuery.js b/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/lib/escape_kuery.ts similarity index 57% rename from x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/escape_kuery.js rename to x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/lib/escape_kuery.ts index 5d9bfe6143c22..a00082f8c7d7c 100644 --- a/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/escape_kuery.js +++ b/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/lib/escape_kuery.ts @@ -6,29 +6,29 @@ import { flow } from 'lodash'; -export function escapeQuotes(string) { - return string.replace(/"/g, '\\"'); +export function escapeQuotes(str: string) { + return str.replace(/"/g, '\\"'); } export const escapeKuery = flow(escapeSpecialCharacters, escapeAndOr, escapeNot, escapeWhitespace); // See the SpecialCharacter rule in kuery.peg -function escapeSpecialCharacters(string) { - return string.replace(/[\\():<>"*]/g, '\\$&'); // $& means the whole matched string +function escapeSpecialCharacters(str: string) { + return str.replace(/[\\():<>"*]/g, '\\$&'); // $& means the whole matched string } // See the Keyword rule in kuery.peg -function escapeAndOr(string) { - return string.replace(/(\s+)(and|or)(\s+)/gi, '$1\\$2$3'); +function escapeAndOr(str: string) { + return str.replace(/(\s+)(and|or)(\s+)/gi, '$1\\$2$3'); } -function escapeNot(string) { - return string.replace(/not(\s+)/gi, '\\$&'); +function escapeNot(str: string) { + return str.replace(/not(\s+)/gi, '\\$&'); } // See the Space rule in kuery.peg -function escapeWhitespace(string) { - return string +function escapeWhitespace(str: string) { + return str .replace(/\t/g, '\\t') .replace(/\r/g, '\\r') .replace(/\n/g, '\\n'); diff --git a/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/operator.test.ts b/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/operator.test.ts new file mode 100644 index 0000000000000..acafc4e169c8f --- /dev/null +++ b/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/operator.test.ts @@ -0,0 +1,96 @@ +/* + * 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 indexPatternResponse from './__fixtures__/index_pattern_response.json'; + +import { setupGetOperatorSuggestions } from './operator'; +import { autocomplete, esKuery } from '../../../../../../src/plugins/data/public'; +import { coreMock } from '../../../../../../src/core/public/mocks'; + +const mockKueryNode = (kueryNode: Partial) => + (kueryNode as unknown) as esKuery.KueryNode; + +describe('Kuery operator suggestions', () => { + let getSuggestions: ReturnType; + let querySuggestionsArgs: autocomplete.QuerySuggestionsGetFnArgs; + + beforeEach(() => { + querySuggestionsArgs = ({ + indexPatterns: [indexPatternResponse], + } as unknown) as autocomplete.QuerySuggestionsGetFnArgs; + + getSuggestions = setupGetOperatorSuggestions(coreMock.createSetup()); + }); + + test('should return a function', () => { + expect(typeof getSuggestions).toBe('function'); + }); + + test('should not return suggestions for non-fields', async () => { + const suggestions = await getSuggestions( + querySuggestionsArgs, + mockKueryNode({ fieldName: 'foo' }) + ); + + expect(suggestions).toEqual([]); + }); + + test('should return exists for every field', async () => { + const suggestions = await getSuggestions( + querySuggestionsArgs, + mockKueryNode({ + fieldName: 'custom_user_field', + }) + ); + + expect(suggestions.length).toEqual(1); + expect(suggestions[0].text).toBe(': * '); + }); + + test('should return equals for string fields', async () => { + const suggestions = await getSuggestions( + querySuggestionsArgs, + mockKueryNode({ fieldName: 'machine.os' }) + ); + + expect(suggestions.find(({ text }) => text === ': ')).toBeDefined(); + expect(suggestions.find(({ text }) => text === '< ')).not.toBeDefined(); + }); + + test('should return numeric operators for numeric fields', async () => { + const suggestions = await getSuggestions( + querySuggestionsArgs, + mockKueryNode({ fieldName: 'bytes' }) + ); + + expect(suggestions.find(({ text }) => text === ': ')).toBeDefined(); + expect(suggestions.find(({ text }) => text === '< ')).toBeDefined(); + }); + + test('should have descriptions', async () => { + const suggestions = await getSuggestions( + querySuggestionsArgs, + mockKueryNode({ fieldName: 'bytes' }) + ); + + expect(suggestions.length).toBeGreaterThan(0); + + suggestions.forEach(suggestion => { + expect(suggestion).toHaveProperty('description'); + }); + }); + + test('should handle nested paths', async () => { + const suggestions = await getSuggestions( + querySuggestionsArgs, + mockKueryNode({ + fieldName: 'child', + nestedPath: 'nestedField', + }) + ); + + expect(suggestions.length).toBeGreaterThan(0); + }); +}); diff --git a/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/operator.js b/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/operator.tsx similarity index 82% rename from x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/operator.js rename to x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/operator.tsx index 173a24b3f5f1e..6e9010c4310fb 100644 --- a/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/operator.js +++ b/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/operator.tsx @@ -6,8 +6,11 @@ import React from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; +import { $Keys } from 'utility-types'; import { flatten } from 'lodash'; -const type = 'operator'; + +import { KqlQuerySuggestionProvider } from './types'; +import { autocomplete } from '../../../../../../src/plugins/data/public'; const equalsText = ( ), + fieldTypes: undefined, }, }; -function getDescription(operator) { - const { description } = operators[operator]; - return

{description}

; -} +type Operators = $Keys; + +const getOperatorByName = (operator: string) => operators[operator as Operators]; +const getDescription = (operator: string) =>

{getOperatorByName(operator).description}

; -export function getSuggestionsProvider({ indexPatterns }) { - const allFields = flatten( - indexPatterns.map(indexPattern => { - return indexPattern.fields.slice(); - }) - ); - return function getOperatorSuggestions({ end, fieldName, nestedPath }) { +export const setupGetOperatorSuggestions: KqlQuerySuggestionProvider = () => { + return ({ indexPatterns }, { end, fieldName, nestedPath }) => { + const allFields = flatten( + indexPatterns.map(indexPattern => { + return indexPattern.fields.slice(); + }) + ); const fullFieldName = nestedPath ? `${nestedPath}.${fieldName}` : fieldName; - const fields = allFields.filter(field => field.name === fullFieldName); - return flatten( - fields.map(field => { + const fields = allFields + .filter(field => field.name === fullFieldName) + .map(field => { const matchingOperators = Object.keys(operators).filter(operator => { - const { fieldTypes } = operators[operator]; + const { fieldTypes } = getOperatorByName(operator); + return !fieldTypes || fieldTypes.includes(field.type); }); - const suggestions = matchingOperators.map(operator => { - const text = operator + ' '; - const description = getDescription(operator); - return { type, text, description, start: end, end }; - }); + + const suggestions = matchingOperators.map(operator => ({ + type: autocomplete.QuerySuggestionsTypes.Operator, + text: operator + ' ', + description: getDescription(operator), + start: end, + end, + })); return suggestions; - }) - ); + }); + + return Promise.resolve(flatten(fields)); }; -} +}; diff --git a/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/sort_prefix_first.ts b/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/sort_prefix_first.ts index 123e440b75231..03e1a9099f1ab 100644 --- a/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/sort_prefix_first.ts +++ b/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/sort_prefix_first.ts @@ -14,7 +14,9 @@ export function sortPrefixFirst(array: any[], prefix?: string | number, property const partitions = partition(array, entry => { const value = ('' + (property ? entry[property] : entry)).toLowerCase(); + return value.startsWith(lowerCasePrefix); }); + return [...partitions[0], ...partitions[1]]; } diff --git a/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/types.ts b/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/types.ts new file mode 100644 index 0000000000000..c51b75e001b9f --- /dev/null +++ b/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/types.ts @@ -0,0 +1,15 @@ +/* + * 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 { CoreSetup } from 'kibana/public'; +import { esKuery, autocomplete } from '../../../../../../src/plugins/data/public'; + +export type KqlQuerySuggestionProvider = ( + core: CoreSetup +) => ( + querySuggestionsGetFnArgs: autocomplete.QuerySuggestionsGetFnArgs, + kueryNode: esKuery.KueryNode +) => Promise; diff --git a/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/value.js b/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/value.js deleted file mode 100644 index 9d0d70fd95747..0000000000000 --- a/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/value.js +++ /dev/null @@ -1,58 +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 } from 'lodash'; -import { escapeQuotes } from './escape_kuery'; -import { npStart } from 'ui/new_platform'; - -const type = 'value'; - -export function getSuggestionsProvider({ indexPatterns, boolFilter }) { - const allFields = flatten( - indexPatterns.map(indexPattern => { - return indexPattern.fields.map(field => ({ - ...field, - indexPattern, - })); - }) - ); - - return function getValueSuggestions( - { start, end, prefix, suffix, fieldName, nestedPath }, - signal - ) { - const fullFieldName = nestedPath ? `${nestedPath}.${fieldName}` : fieldName; - const fields = allFields.filter(field => field.name === fullFieldName); - const query = `${prefix}${suffix}`.trim(); - const { getValueSuggestions } = npStart.plugins.data.autocomplete; - - 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)); - }; -} - -function wrapAsSuggestions(start, end, query, values) { - return values - .filter(value => value.toLowerCase().includes(query.toLowerCase())) - .map(value => { - const text = `${value} `; - return { type, text, start, end }; - }); -} diff --git a/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/value.test.js b/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/value.test.js deleted file mode 100644 index f5b652d2e2164..0000000000000 --- a/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/value.test.js +++ /dev/null @@ -1,146 +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 { 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: { - 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); - }), - }, - }, - }, - }, -})); - -describe('Kuery value suggestions', function() { - let indexPatterns; - let getSuggestions; - - beforeEach(() => { - indexPatterns = [indexPatternResponse]; - getSuggestions = getSuggestionsProvider({ indexPatterns }); - jest.clearAllMocks(); - }); - - test('should return a function', function() { - expect(typeof getSuggestions).toBe('function'); - }); - - test('should not search for non existing field', async () => { - const fieldName = 'i_dont_exist'; - const prefix = ''; - const suffix = ''; - - const suggestions = await getSuggestions({ fieldName, prefix, suffix }); - expect(suggestions.map(({ text }) => text)).toEqual([]); - - expect(npStart.plugins.data.autocomplete.getValueSuggestions).toHaveBeenCalledTimes(0); - }); - - test('should format suggestions', async () => { - const start = 1; - const end = 5; - 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); - }); - - test('should handle nested paths', async () => { - const suggestions = await getSuggestions({ - fieldName: 'child', - nestedPath: 'nestedField', - prefix: '', - suffix: '', - }); - expect(suggestions.length).toEqual(1); - expect(suggestions[0].text).toEqual('"foo" '); - }); - - describe('Boolean suggestions', function() { - test('should stringify boolean fields', async () => { - const suggestions = await getSuggestions({ fieldName: 'ssl', prefix: '', suffix: '' }); - - expect(suggestions.map(({ text }) => text)).toEqual(['true ', 'false ']); - expect(npStart.plugins.data.autocomplete.getValueSuggestions).toHaveBeenCalledTimes(1); - }); - - test('should filter out boolean suggestions', async () => { - 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 prefix = 'he'; - const suffix = 'llo'; - - 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 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 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 suggestions = await getSuggestions({ - fieldName: 'machine.os', - prefix: 'ma', - suffix: '', - }); - - expect(suggestions.length).toEqual(1); - }); - }); -}); diff --git a/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/value.test.ts b/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/value.test.ts new file mode 100644 index 0000000000000..5ffe30c877868 --- /dev/null +++ b/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/value.test.ts @@ -0,0 +1,192 @@ +/* + * 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 { setupGetValueSuggestions } from './value'; +import indexPatternResponse from './__fixtures__/index_pattern_response.json'; +import { coreMock } from '../../../../../../src/core/public/mocks'; +import { autocomplete, esKuery } from '../../../../../../src/plugins/data/public'; +import { setAutocompleteService } from '../services'; + +const mockKueryNode = (kueryNode: Partial) => + (kueryNode as unknown) as esKuery.KueryNode; + +describe('Kuery value suggestions', () => { + let getSuggestions: ReturnType; + let querySuggestionsArgs: autocomplete.QuerySuggestionsGetFnArgs; + let autocompleteServiceMock: any; + + beforeEach(() => { + getSuggestions = setupGetValueSuggestions(coreMock.createSetup()); + querySuggestionsArgs = ({ + indexPatterns: [indexPatternResponse], + } as unknown) as autocomplete.QuerySuggestionsGetFnArgs; + + autocompleteServiceMock = { + getValueSuggestions: jest.fn(({ field }) => { + let res: any[]; + + 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); + }), + }; + setAutocompleteService(autocompleteServiceMock); + + jest.clearAllMocks(); + }); + + test('should return a function', () => { + expect(typeof getSuggestions).toBe('function'); + }); + + test('should not search for non existing field', async () => { + const fieldName = 'i_dont_exist'; + const prefix = ''; + const suffix = ''; + + const suggestions = await getSuggestions( + querySuggestionsArgs, + mockKueryNode({ fieldName, prefix, suffix }) + ); + + expect(suggestions.map(({ text }) => text)).toEqual([]); + expect(autocompleteServiceMock.getValueSuggestions).toHaveBeenCalledTimes(0); + }); + + test('should format suggestions', async () => { + const start = 1; + const end = 5; + const suggestions = await getSuggestions( + querySuggestionsArgs, + mockKueryNode({ + fieldName: 'ssl', + prefix: '', + suffix: '', + start, + end, + }) + ); + + expect(suggestions[0].type).toEqual('value'); + expect(suggestions[0].start).toEqual(start); + expect(suggestions[0].end).toEqual(end); + }); + + test('should handle nested paths', async () => { + const suggestions = await getSuggestions( + querySuggestionsArgs, + mockKueryNode({ + fieldName: 'child', + nestedPath: 'nestedField', + prefix: '', + suffix: '', + }) + ); + + expect(suggestions.length).toEqual(1); + expect(suggestions[0].text).toEqual('"foo" '); + }); + + describe('Boolean suggestions', () => { + test('should stringify boolean fields', async () => { + const suggestions = await getSuggestions( + querySuggestionsArgs, + mockKueryNode({ + fieldName: 'ssl', + prefix: '', + suffix: '', + }) + ); + + expect(suggestions.map(({ text }) => text)).toEqual(['true ', 'false ']); + expect(autocompleteServiceMock.getValueSuggestions).toHaveBeenCalledTimes(1); + }); + + test('should filter out boolean suggestions', async () => { + const suggestions = await getSuggestions( + querySuggestionsArgs, + mockKueryNode({ + fieldName: 'ssl', + prefix: 'fa', + suffix: '', + }) + ); + + expect(suggestions.length).toEqual(1); + }); + }); + + describe('String suggestions', () => { + test('should merge prefix and suffix', async () => { + const prefix = 'he'; + const suffix = 'llo'; + + await getSuggestions( + querySuggestionsArgs, + mockKueryNode({ + fieldName: 'machine.os.raw', + prefix, + suffix, + }) + ); + + expect(autocompleteServiceMock.getValueSuggestions).toHaveBeenCalledTimes(1); + expect(autocompleteServiceMock.getValueSuggestions).toBeCalledWith( + expect.objectContaining({ + field: expect.any(Object), + query: prefix + suffix, + }) + ); + }); + + test('should escape quotes in suggestions', async () => { + const suggestions = await getSuggestions( + querySuggestionsArgs, + mockKueryNode({ + 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 suggestions = await getSuggestions( + querySuggestionsArgs, + mockKueryNode({ + fieldName: 'machine.os', + prefix: 'banana', + suffix: '', + }) + ); + + expect(suggestions.length).toEqual(0); + }); + + test('should partially filter out string suggestions - case insensitive', async () => { + const suggestions = await getSuggestions( + querySuggestionsArgs, + mockKueryNode({ + fieldName: 'machine.os', + prefix: 'ma', + suffix: '', + }) + ); + + expect(suggestions.length).toEqual(1); + }); + }); +}); diff --git a/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/value.ts b/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/value.ts new file mode 100644 index 0000000000000..242b9ccba3508 --- /dev/null +++ b/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/value.ts @@ -0,0 +1,62 @@ +/* + * 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 } from 'lodash'; +import { escapeQuotes } from './lib/escape_kuery'; +import { KqlQuerySuggestionProvider } from './types'; +import { getAutocompleteService } from '../services'; +import { autocomplete } from '../../../../../../src/plugins/data/public'; + +const wrapAsSuggestions = (start: number, end: number, query: string, values: string[]) => + values + .filter(value => value.toLowerCase().includes(query.toLowerCase())) + .map(value => ({ + type: autocomplete.QuerySuggestionsTypes.Value, + text: `${value} `, + start, + end, + })); + +export const setupGetValueSuggestions: KqlQuerySuggestionProvider = core => { + return async ( + { indexPatterns, boolFilter, signal }, + { start, end, prefix, suffix, fieldName, nestedPath } + ): Promise => { + const allFields = flatten( + indexPatterns.map(indexPattern => + indexPattern.fields.map(field => ({ + ...field, + indexPattern, + })) + ) + ); + + const fullFieldName = nestedPath ? `${nestedPath}.${fieldName}` : fieldName; + const fields = allFields.filter(field => field.name === fullFieldName); + const query = `${prefix}${suffix}`.trim(); + const { getValueSuggestions } = getAutocompleteService(); + + const data = await Promise.all( + fields.map(field => + getValueSuggestions({ + indexPattern: field.indexPattern, + field, + query, + boolFilter, + signal, + }).then(valueSuggestions => { + const quotedValues = valueSuggestions.map(value => + typeof value === 'string' ? `"${escapeQuotes(value)}"` : `${value}` + ); + + return wrapAsSuggestions(start, end, query, quotedValues); + }) + ) + ); + + return flatten(data); + }; +}; diff --git a/x-pack/legacy/plugins/kuery_autocomplete/public/legacy.ts b/x-pack/legacy/plugins/kuery_autocomplete/public/legacy.ts index 52abe21c055ee..303fe8c557fbd 100644 --- a/x-pack/legacy/plugins/kuery_autocomplete/public/legacy.ts +++ b/x-pack/legacy/plugins/kuery_autocomplete/public/legacy.ts @@ -8,13 +8,20 @@ import { PluginInitializerContext } from 'src/core/public'; import { npSetup, npStart } from 'ui/new_platform'; import { plugin } from './index'; -import { KueryAutocompletePluginSetupDependencies } from './plugin'; +import { + KueryAutocompletePluginSetupDependencies, + KueryAutocompletePluginStartDependencies, +} from './plugin'; -const plugins: Readonly = { +const pluginsSetup: Readonly = { data: npSetup.plugins.data, }; +const pluginsStart: Readonly = { + data: npStart.plugins.data, +}; + const pluginInstance = plugin({} as PluginInitializerContext); -export const setup = pluginInstance.setup(npSetup.core, plugins); -export const start = pluginInstance.start(npStart.core); +export const setup = pluginInstance.setup(npSetup.core, pluginsSetup); +export const start = pluginInstance.start(npStart.core, pluginsStart); diff --git a/x-pack/legacy/plugins/kuery_autocomplete/public/plugin.ts b/x-pack/legacy/plugins/kuery_autocomplete/public/plugin.ts index 216e0f49ccd34..81737c4636532 100644 --- a/x-pack/legacy/plugins/kuery_autocomplete/public/plugin.ts +++ b/x-pack/legacy/plugins/kuery_autocomplete/public/plugin.ts @@ -6,8 +6,7 @@ import { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from 'src/core/public'; import { Plugin as DataPublicPlugin } from '../../../../../src/plugins/data/public'; - -// @ts-ignore +import { setAutocompleteService } from './services'; import { setupKqlQuerySuggestionProvider } from './kql_query_suggestion'; /** @internal */ @@ -15,6 +14,11 @@ export interface KueryAutocompletePluginSetupDependencies { data: ReturnType; } +/** @internal */ +export interface KueryAutocompletePluginStartDependencies { + data: ReturnType; +} + const KUERY_LANGUAGE_NAME = 'kuery'; /** @internal */ @@ -26,12 +30,12 @@ export class KueryAutocompletePlugin implements Plugin, void> { } public async setup(core: CoreSetup, plugins: KueryAutocompletePluginSetupDependencies) { - const kueryProvider = setupKqlQuerySuggestionProvider(core, plugins); + const kueryProvider = setupKqlQuerySuggestionProvider(core); plugins.data.autocomplete.addQuerySuggestionProvider(KUERY_LANGUAGE_NAME, kueryProvider); } - public start(core: CoreStart) { - // nothing to do here yet + public start(core: CoreStart, plugins: KueryAutocompletePluginStartDependencies) { + setAutocompleteService(plugins.data.autocomplete); } } diff --git a/x-pack/legacy/plugins/kuery_autocomplete/public/services.ts b/x-pack/legacy/plugins/kuery_autocomplete/public/services.ts new file mode 100644 index 0000000000000..1ec48e597f636 --- /dev/null +++ b/x-pack/legacy/plugins/kuery_autocomplete/public/services.ts @@ -0,0 +1,12 @@ +/* + * 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 { createGetterSetter } from '../../../../../src/plugins/kibana_utils/public'; +import { DataPublicPluginStart } from '../../../../../src/plugins/data/public'; + +export const [getAutocompleteService, setAutocompleteService] = createGetterSetter< + DataPublicPluginStart['autocomplete'] +>('Autocomplete'); 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 4d92e8cb1335d..85e2b3b3fe384 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 @@ -16,7 +16,7 @@ const suggestion: autocomplete.QuerySuggestion = { end: 3, start: 1, text: 'Text...', - type: 'value', + type: autocomplete.QuerySuggestionsTypes.Value, }; storiesOf('components/SuggestionItem', module).add('example', () => ( 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 ef16f79a4b83c..552aaa5889719 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 @@ -18,7 +18,7 @@ import { AutocompleteField } from '.'; const mockAutoCompleteData: autocomplete.QuerySuggestion[] = [ { - type: 'field', + type: autocomplete.QuerySuggestionsTypes.Field, text: 'agent.ephemeral_id ', description: '

Filter results that contain agent.ephemeral_id

', @@ -26,7 +26,7 @@ const mockAutoCompleteData: autocomplete.QuerySuggestion[] = [ end: 1, }, { - type: 'field', + type: autocomplete.QuerySuggestionsTypes.Field, text: 'agent.hostname ', description: '

Filter results that contain agent.hostname

', @@ -34,7 +34,7 @@ const mockAutoCompleteData: autocomplete.QuerySuggestion[] = [ end: 1, }, { - type: 'field', + type: autocomplete.QuerySuggestionsTypes.Field, text: 'agent.id ', description: '

Filter results that contain agent.id

', @@ -42,7 +42,7 @@ const mockAutoCompleteData: autocomplete.QuerySuggestion[] = [ end: 1, }, { - type: 'field', + type: autocomplete.QuerySuggestionsTypes.Field, text: 'agent.name ', description: '

Filter results that contain agent.name

', @@ -50,7 +50,7 @@ const mockAutoCompleteData: autocomplete.QuerySuggestion[] = [ end: 1, }, { - type: 'field', + type: autocomplete.QuerySuggestionsTypes.Field, text: 'agent.type ', description: '

Filter results that contain agent.type

', @@ -58,7 +58,7 @@ const mockAutoCompleteData: autocomplete.QuerySuggestion[] = [ end: 1, }, { - type: 'field', + type: autocomplete.QuerySuggestionsTypes.Field, text: 'agent.version ', description: '

Filter results that contain agent.version

', @@ -66,7 +66,7 @@ const mockAutoCompleteData: autocomplete.QuerySuggestion[] = [ end: 1, }, { - type: 'field', + type: autocomplete.QuerySuggestionsTypes.Field, text: 'agent.test1 ', description: '

Filter results that contain agent.test1

', @@ -74,7 +74,7 @@ const mockAutoCompleteData: autocomplete.QuerySuggestion[] = [ end: 1, }, { - type: 'field', + type: autocomplete.QuerySuggestionsTypes.Field, text: 'agent.test2 ', description: '

Filter results that contain agent.test2

', @@ -82,7 +82,7 @@ const mockAutoCompleteData: autocomplete.QuerySuggestion[] = [ end: 1, }, { - type: 'field', + type: autocomplete.QuerySuggestionsTypes.Field, text: 'agent.test3 ', description: '

Filter results that contain agent.test3

', @@ -90,7 +90,7 @@ const mockAutoCompleteData: autocomplete.QuerySuggestion[] = [ end: 1, }, { - type: 'field', + type: autocomplete.QuerySuggestionsTypes.Field, text: 'agent.test4 ', description: '

Filter results that contain agent.test4

', From 3f5149f57bc8aedf376b41d1e0f55a315d9a3eb4 Mon Sep 17 00:00:00 2001 From: Dima Arnautov Date: Fri, 31 Jan 2020 11:10:25 +0100 Subject: [PATCH 21/23] [ML] conditional rison encoding for query params (#56380) (#56469) --- .../public/application/util/url_state.test.ts | 2 +- .../ml/public/application/util/url_state.ts | 22 ++++++++++++++++--- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/x-pack/legacy/plugins/ml/public/application/util/url_state.test.ts b/x-pack/legacy/plugins/ml/public/application/util/url_state.test.ts index 91bbef2dba6c2..0813f2e3da97f 100644 --- a/x-pack/legacy/plugins/ml/public/application/util/url_state.test.ts +++ b/x-pack/legacy/plugins/ml/public/application/util/url_state.test.ts @@ -75,7 +75,7 @@ describe('useUrlState', () => { expect(mockHistoryPush).toHaveBeenCalledWith({ search: - '_a=%28mlExplorerFilter%3A%28%29%2CmlExplorerSwimlane%3A%28viewByFieldName%3Aaction%29%2Cquery%3A%28%29%29&_g=%28ml%3A%28jobIds%3A%21%28dec-2%29%29%2CrefreshInterval%3A%28display%3AOff%2Cpause%3A%21f%2Cvalue%3A0%29%2Ctime%3A%28from%3A%272019-01-01T00%3A03%3A40.000Z%27%2Cmode%3Aabsolute%2Cto%3A%272019-08-30T11%3A55%3A07.000Z%27%29%29&savedSearchId=%27571aaf70-4c88-11e8-b3d7-01146121b73d%27', + '_a=%28mlExplorerFilter%3A%28%29%2CmlExplorerSwimlane%3A%28viewByFieldName%3Aaction%29%2Cquery%3A%28%29%29&_g=%28ml%3A%28jobIds%3A%21%28dec-2%29%29%2CrefreshInterval%3A%28display%3AOff%2Cpause%3A%21f%2Cvalue%3A0%29%2Ctime%3A%28from%3A%272019-01-01T00%3A03%3A40.000Z%27%2Cmode%3Aabsolute%2Cto%3A%272019-08-30T11%3A55%3A07.000Z%27%29%29&savedSearchId=571aaf70-4c88-11e8-b3d7-01146121b73d', }); }); }); diff --git a/x-pack/legacy/plugins/ml/public/application/util/url_state.ts b/x-pack/legacy/plugins/ml/public/application/util/url_state.ts index 546944b1a33bf..e7d5a94e2694f 100644 --- a/x-pack/legacy/plugins/ml/public/application/util/url_state.ts +++ b/x-pack/legacy/plugins/ml/public/application/util/url_state.ts @@ -18,14 +18,26 @@ import { getNestedProperty } from './object_utils'; export type SetUrlState = (attribute: string | Dictionary, value?: any) => void; export type UrlState = [Dictionary, SetUrlState]; -const decodedParams = new Set(['_a', '_g']); +/** + * Set of URL query parameters that require the rison serialization. + */ +const risonSerializedParams = new Set(['_a', '_g']); + +/** + * Checks if the URL query parameter requires rison serialization. + * @param queryParam + */ +function isRisonSerializationRequired(queryParam: string): boolean { + return risonSerializedParams.has(queryParam); +} + export function getUrlState(search: string): Dictionary { const urlState: Dictionary = {}; const parsedQueryString = queryString.parse(search); try { Object.keys(parsedQueryString).forEach(a => { - if (decodedParams.has(a)) { + if (isRisonSerializationRequired(a)) { urlState[a] = decode(parsedQueryString[a]) as Dictionary; } else { urlState[a] = parsedQueryString[a]; @@ -75,7 +87,11 @@ export const useUrlState = (accessor: string): UrlState => { const oldLocationSearch = queryString.stringify(parsedQueryString, { encode: false }); Object.keys(urlState).forEach(a => { - parsedQueryString[a] = encode(urlState[a]); + if (isRisonSerializationRequired(a)) { + parsedQueryString[a] = encode(urlState[a]); + } else { + parsedQueryString[a] = urlState[a]; + } }); const newLocationSearch = queryString.stringify(parsedQueryString, { encode: false }); From 5df9c86fedb58649adb8e27b25ad8e8fb9ac5b5c Mon Sep 17 00:00:00 2001 From: Dima Arnautov Date: Fri, 31 Jan 2020 11:10:46 +0100 Subject: [PATCH 22/23] [ML] Fix Data Visualizer responsive layout (#56372) (#56472) * [ML] replace Example component with EuiListGroup * [ML] center alignment --- .../field_data_card/examples_list/example.tsx | 31 ------------------- .../examples_list/examples_list.tsx | 17 +++++++--- 2 files changed, 12 insertions(+), 36 deletions(-) delete mode 100644 x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/examples_list/example.tsx diff --git a/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/examples_list/example.tsx b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/examples_list/example.tsx deleted file mode 100644 index 29fe690f4a43b..0000000000000 --- a/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/examples_list/example.tsx +++ /dev/null @@ -1,31 +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 React, { FC } from 'react'; - -import { EuiFlexGroup, EuiFlexItem, EuiText, EuiToolTip } from '@elastic/eui'; - -interface Props { - example: string | object; -} - -export const Example: FC = ({ example }) => { - const exampleStr = typeof example === 'string' ? example : JSON.stringify(example); - - // Use 95% width for each example so that the truncation ellipses show up when - // wrapped inside a tooltip. - return ( - - - - - {exampleStr} - - - - - ); -}; diff --git a/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/examples_list/examples_list.tsx b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/examples_list/examples_list.tsx index 0bf911c1edf86..c8eb810115401 100644 --- a/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/examples_list/examples_list.tsx +++ b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/examples_list/examples_list.tsx @@ -6,12 +6,10 @@ import React, { FC } from 'react'; -import { EuiSpacer, EuiText } from '@elastic/eui'; +import { EuiListGroup, EuiListGroupItem, EuiSpacer, EuiText } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { Example } from './example'; - interface Props { examples: Array; } @@ -22,7 +20,14 @@ export const ExamplesList: FC = ({ examples }) => { } const examplesContent = examples.map((example, i) => { - return ; + return ( + + ); }); return ( @@ -39,7 +44,9 @@ export const ExamplesList: FC = ({ examples }) => { - {examplesContent} + + {examplesContent} +