* [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> Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
parent
587f64fa99
commit
1a3d74f9e0
77
src/plugins/data/public/autocomplete/autocomplete_service.ts
Normal file
77
src/plugins/data/public/autocomplete/autocomplete_service.ts
Normal 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();
|
||||
}
|
||||
}
|
|
@ -17,4 +17,5 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
export { getSuggestionsProvider } from './value_suggestions';
|
||||
export { AutocompleteService } from './autocomplete_service';
|
||||
export { QuerySuggestion, QuerySuggestionType, QuerySuggestionsGetFn } from './types';
|
|
@ -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;
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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);
|
||||
};
|
||||
};
|
|
@ -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';
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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 };
|
||||
|
|
|
@ -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> = {
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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'];
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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],
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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 => {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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<
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 => {
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 => {
|
||||
|
|
|
@ -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 &&
|
||||
|
|
|
@ -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))
|
||||
);
|
||||
};
|
||||
};
|
|
@ -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))
|
||||
);
|
||||
};
|
|
@ -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));
|
||||
};
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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-*',
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 ',
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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>(
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue