[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 <elasticmachine@users.noreply.github.com>
This commit is contained in:
Alexey Antonov 2020-01-17 22:42:51 +03:00 committed by GitHub
parent 2234210369
commit 801302e3ce
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
59 changed files with 575 additions and 551 deletions

View file

@ -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<string, QuerySuggestionsGetFn> = 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();
}
}

View file

@ -17,4 +17,5 @@
* under the License.
*/
export { getSuggestionsProvider } from './value_suggestions';
export { AutocompleteService } from './autocomplete_service';
export { QuerySuggestion, QuerySuggestionType, QuerySuggestionsGetFn } from './types';

View file

@ -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<AutocompleteProviderRegister, 'getProvider'>;
export type QuerySuggestionType = 'field' | 'value' | 'operator' | 'conjunction' | 'recentSearch';
/** @public **/
export type AutocompleteProvider = (args: {
config: {
get(configKey: string): any;
};
export type QuerySuggestionsGetFn = (
args: QuerySuggestionsGetFnArgs
) => Promise<QuerySuggestion[]> | undefined;
interface QuerySuggestionsGetFnArgs {
language: string;
indexPatterns: IIndexPattern[];
boolFilter?: any;
}) => GetSuggestions;
/** @public **/
export type GetSuggestions = (args: {
query: string;
selectionStart: number;
selectionEnd: number;
signal?: AbortSignal;
}) => Promise<AutocompleteSuggestion[]>;
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;

View file

@ -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);
});
});

View file

@ -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<any[]>;
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<any[]> => {
const shouldSuggestValues = core!.uiSettings.get<boolean>('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);
};
};

View file

@ -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<AutocompleteService['setup']>;
/** @public **/
export type AutocompleteStart = ReturnType<AutocompleteService['start']>;
/** @public **/
export {
QuerySuggestion,
QuerySuggestionsGetFn,
QuerySuggestionType,
} from './providers/query_suggestion_provider';

View file

@ -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<string, AutocompleteProvider> = 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();
}
}

View file

@ -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 };

View file

@ -30,9 +30,9 @@ export type Setup = jest.Mocked<ReturnType<Plugin['setup']>>;
export type Start = jest.Mocked<ReturnType<Plugin['start']>>;
const autocompleteMock: any = {
addProvider: jest.fn(),
getProvider: jest.fn(),
clearProviders: jest.fn(),
getValueSuggestions: jest.fn(),
getQuerySuggestions: jest.fn(),
hasQuerySuggestions: jest.fn(),
};
const fieldFormatsMock: PublicMethodsOf<FieldFormatRegisty> = {

View file

@ -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<DataPublicPluginSetup, DataPublicPluginStart> {
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<DataPublicPluginSetup, DataPubli
);
return {
autocomplete: this.autocomplete,
autocomplete: this.autocomplete.setup(core),
search: this.searchService.setup(core),
fieldFormats: this.fieldFormatsService.setup(core),
query: queryService,
@ -82,8 +81,7 @@ export class DataPublicPlugin implements Plugin<DataPublicPluginSetup, DataPubli
uiActions.attachAction(APPLY_FILTER_TRIGGER, GLOBAL_APPLY_FILTER_ACTION);
const dataServices = {
autocomplete: this.autocomplete,
getSuggestions: getSuggestionsProvider(core.uiSettings, core.http),
autocomplete: this.autocomplete.start(),
search: this.searchService.start(core),
fieldFormats,
query: this.queryService.start(core.savedObjects),

View file

@ -20,10 +20,9 @@
import { CoreStart } from 'src/core/public';
import { IStorageWrapper } from 'src/plugins/kibana_utils/public';
import { IUiActionsSetup, IUiActionsStart } from 'src/plugins/ui_actions/public';
import { AutocompletePublicPluginSetup, AutocompletePublicPluginStart } from '.';
import { AutocompleteSetup, AutocompleteStart } from './autocomplete/types';
import { FieldFormatsSetup, FieldFormatsStart } from './field_formats_provider';
import { ISearchSetup, ISearchStart } from './search';
import { IGetSuggestions } from './suggestions_provider/types';
import { QuerySetup, QueryStart } from './query';
import { IndexPatternSelectProps } from './ui/index_pattern_select';
import { IndexPatternsContract } from './index_patterns';
@ -38,15 +37,14 @@ export interface DataStartDependencies {
}
export interface DataPublicPluginSetup {
autocomplete: AutocompletePublicPluginSetup;
autocomplete: AutocompleteSetup;
search: ISearchSetup;
fieldFormats: FieldFormatsSetup;
query: QuerySetup;
}
export interface DataPublicPluginStart {
autocomplete: AutocompletePublicPluginStart;
getSuggestions: IGetSuggestions;
autocomplete: AutocompleteStart;
indexPatterns: IndexPatternsContract;
search: ISearchStart;
fieldFormats: FieldFormatsStart;
@ -57,9 +55,6 @@ export interface DataPublicPluginStart {
};
}
export * from './autocomplete_provider/types';
export { IGetSuggestions } from './suggestions_provider/types';
export interface IDataPluginServices extends Partial<CoreStart> {
appName: string;
uiSettings: CoreStart['uiSettings'];

View file

@ -63,13 +63,19 @@ export class PhraseSuggestorUI<T extends PhraseSuggestorProps> 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);
}

View file

@ -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],

View file

@ -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<Props, State> {
public state: State = {
@ -138,15 +137,14 @@ export class QueryStringInputUI extends Component<Props, State> {
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<Props, State> {
}
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<Props, State> {
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<Props, State> {
}
};
private selectSuggestion = (suggestion: AutocompleteSuggestion) => {
private selectSuggestion = (suggestion: autocomplete.QuerySuggestion) => {
if (!this.inputRef) {
return;
}
@ -351,7 +349,7 @@ export class QueryStringInputUI extends Component<Props, State> {
}
};
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<Props, State> {
}
};
private onClickSuggestion = (suggestion: AutocompleteSuggestion) => {
private onClickSuggestion = (suggestion: autocomplete.QuerySuggestion) => {
if (!this.inputRef) {
return;
}

View file

@ -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,

View file

@ -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;
}

View file

@ -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,

View file

@ -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;
}

View file

@ -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<State>({
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);

View file

@ -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;
}

View file

@ -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<HTMLDivElement>;
onMouseEnter?: React.MouseEventHandler<HTMLDivElement>;
suggestion: AutocompleteSuggestion;
suggestion: autocomplete.QuerySuggestion;
}
export const SuggestionItem: React.FC<SuggestionItemProps> = props => {

View file

@ -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;
}

View file

@ -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<

View file

@ -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<string>;
getSuggestions: (kuery: string, selectionStart: any) => Promise<AutocompleteSuggestion[]>;
getSuggestions: (kuery: string, selectionStart: any) => Promise<autocomplete.QuerySuggestion[]>;
isKueryValid(kuery: string): boolean;
}

View file

@ -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<AutocompleteSuggestion[]> {
): Promise<autocomplete.QuerySuggestion[]> {
return this.suggestions;
}
}

View file

@ -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<AutocompleteSuggestion[]> {
const autocompleteProvider = getAutocompleteProvider('kuery');
if (!autocompleteProvider) {
return [];
}
const config = {
get: () => true,
};
): Promise<autocomplete.QuerySuggestion[]> {
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() {

View file

@ -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,

View file

@ -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<AutocompleteSuggestion[]> {
): Promise<autocomplete.QuerySuggestion[]> {
const suggestions = await this.adapter.getSuggestions(kuery, selectionStart);
const filteredSuggestions = suggestions.filter(suggestion => {

View file

@ -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,
},
},
};

View file

@ -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;

View file

@ -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<HTMLDivElement>;
onMouseEnter?: React.MouseEventHandler<HTMLDivElement>;
suggestion: AutocompleteSuggestion;
suggestion: autocomplete.QuerySuggestion;
}
export const SuggestionItem: React.FC<Props> = props => {

View file

@ -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 &&

View file

@ -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))
);
};
};

View file

@ -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))
);
};

View file

@ -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));
};

View file

@ -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);
});
});

View file

@ -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<Promise<void>, 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) {

View file

@ -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;

View file

@ -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-*',

View file

@ -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,

View file

@ -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,

View file

@ -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 ',

View file

@ -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;
}

View file

@ -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<HTMLDivElement>;
onMouseEnter?: React.MouseEventHandler<HTMLDivElement>;
suggestion: AutocompleteSuggestion;
suggestion: autocomplete.QuerySuggestion;
}
export const SuggestionItem = React.memo<SuggestionItemProps>(

View file

@ -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<JSX.Element> | 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<KueryAutocompletionLifecycleProps>
const [currentRequest, setCurrentRequest] = useState<KueryAutocompletionCurrentRequest | null>(
null
);
const [suggestions, setSuggestions] = useState<AutocompleteSuggestion[]>([]);
const [suggestions, setSuggestions] = useState<autocomplete.QuerySuggestion[]>([]);
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<KueryAutocompletionLifecycleProps>
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);
}

View file

@ -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<AutocompleteProviderRegister, 'getProvider'>
) {
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<AutocompleteProviderRegister, 'getProvider'>;
autocomplete: DataPublicPluginStart['autocomplete'];
}
export function KueryBar({ autocomplete }: Props) {
export function KueryBar({ autocomplete: autocompleteService }: Props) {
const [state, setState] = useState<State>({
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) {

View file

@ -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<AutocompleteProviderRegister, 'getProvider'>;
autocomplete: DataPublicPluginStart['autocomplete'];
setBreadcrumbs: UMUpdateBreadcrumbs;
}

View file

@ -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<AutocompleteProviderRegister, 'getProvider'>;
autocomplete: DataPublicPluginStart['autocomplete'];
basePath: string;
setBreadcrumbs: UMUpdateBreadcrumbs;
}