Skip to content

Commit

Permalink
[Data Plugin] combine autocomplete provider and suggestions provider (#…
Browse files Browse the repository at this point in the history
…54451) (#55224)

* [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 <[email protected]>

Co-authored-by: Elastic Machine <[email protected]>
  • Loading branch information
alexwizp and elasticmachine authored Jan 18, 2020
1 parent 587f64f commit 1a3d74f
Show file tree
Hide file tree
Showing 59 changed files with 571 additions and 547 deletions.
77 changes: 77 additions & 0 deletions src/plugins/data/public/autocomplete/autocomplete_service.ts
Original file line number Diff line number Diff line change
@@ -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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,5 @@
* under the License.
*/

export { getSuggestionsProvider } from './value_suggestions';
export { AutocompleteService } from './autocomplete_service';
export { QuerySuggestion, QuerySuggestionType, QuerySuggestionsGetFn } from './types';
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};
indexPatterns: IIndexPattern[];
boolFilter?: any;
}) => GetSuggestions;
export type QuerySuggestionsGetFn = (
args: QuerySuggestionsGetFnArgs
) => Promise<QuerySuggestion[]> | undefined;

/** @public **/
export type GetSuggestions = (args: {
interface QuerySuggestionsGetFnArgs {
language: string;
indexPatterns: IIndexPattern[];
query: string;
selectionStart: number;
selectionEnd: number;
signal?: AbortSignal;
}) => Promise<AutocompleteSuggestion[]>;

/** @public **/
export type AutocompleteSuggestionType =
| 'field'
| 'value'
| 'operator'
| 'conjunction'
| 'recentSearch';

// 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;
boolFilter?: any;
}

interface BasicAutocompleteSuggestion {
interface BasicQuerySuggestion {
type: QuerySuggestionType;
description?: string;
end: number;
start: number;
text: string;
type: AutocompleteSuggestionType;
cursorIndex?: number;
}

export type FieldAutocompleteSuggestion = BasicAutocompleteSuggestion & {
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 QuerySuggestion = BasicQuerySuggestion | FieldQuerySuggestion;
Original file line number Diff line number Diff line change
Expand Up @@ -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;

describe('with value suggestions disabled', () => {
beforeEach(() => {
const config = { get: (key: string) => false } as IUiSettingsClient;
http = { fetch: jest.fn() };
getSuggestions = getSuggestionsProvider(config, http);
});
beforeEach(() => {
const uiSettings = { get: (key: string) => shouldSuggestValues } as IUiSettingsClient;
http = { fetch: jest.fn() };

getValueSuggestions = setupValueSuggestionProvider({ http, uiSettings } as CoreSetup);
});

describe('with value suggestions disabled', () => {
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);
Expand All @@ -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);
});
});
Expand Down
Loading

0 comments on commit 1a3d74f

Please sign in to comment.