Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Data Plugin] combine autocomplete provider and suggestions provider #54451

Merged
merged 27 commits into from
Jan 17, 2020
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
cff60f8
[Data Plugin] combine autocomplete provider and suggestions provider
alexwizp Jan 10, 2020
76e902b
Merge remote-tracking branch 'upstream/master'
alexwizp Jan 10, 2020
ca2bd93
[Data Plugin] combine autocomplete provider and suggestions provider …
alexwizp Jan 10, 2020
595b26d
Merge remote-tracking branch 'upstream/master' into 52843
alexwizp Jan 13, 2020
eb8250f
autocomplete_provider -> autocomplete
alexwizp Jan 13, 2020
6ee7644
value_suggestions.ts - change getSuggestions method
alexwizp Jan 13, 2020
336de38
remove suggestions_provider folder
alexwizp Jan 13, 2020
b90493b
Merge remote-tracking branch 'upstream/master' into 52843
alexwizp Jan 13, 2020
3d6bac1
Merge remote-tracking branch 'upstream/master' into 52843
alexwizp Jan 14, 2020
8e2d6ae
fix PR comments
alexwizp Jan 14, 2020
7862921
Merge remote-tracking branch 'upstream/master' into 52843
alexwizp Jan 14, 2020
5ca7929
Merge remote-tracking branch 'upstream/master' into 52843
alexwizp Jan 15, 2020
ffc07ac
fix PR comments
alexwizp Jan 15, 2020
7c4976d
Merge remote-tracking branch 'upstream/master' into 52843
alexwizp Jan 15, 2020
403e5da
fix CI
alexwizp Jan 15, 2020
6b0ae89
Merge remote-tracking branch 'upstream/master' into 52843
alexwizp Jan 15, 2020
b7b0e90
Merge branch 'master' into 52843
elasticmachine Jan 16, 2020
0fbaf16
fix CI
alexwizp Jan 16, 2020
0293cb2
Merge remote-tracking branch 'upstream/master' into 52843
alexwizp Jan 16, 2020
2179521
Merge branch 'master' into 52843
elasticmachine Jan 16, 2020
165fa68
Merge branch 'master' into 52843
elasticmachine Jan 16, 2020
7b3875b
Merge remote-tracking branch 'upstream/master' into 52843
alexwizp Jan 17, 2020
de397d3
getFieldSuggestions -> getValueSuggestions
alexwizp Jan 17, 2020
e91aa20
Merge remote-tracking branch 'origin/52843' into 52843
alexwizp Jan 17, 2020
e2c8d2f
Merge remote-tracking branch 'upstream/master' into 52843
alexwizp Jan 17, 2020
5bc1213
update Jest snaphots
alexwizp Jan 17, 2020
2024209
Merge branch 'master' into 52843
elasticmachine Jan 17, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 74 additions & 0 deletions src/plugins/data/public/autocomplete/autocomplete_service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/*
* 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 { QuerySuggestionsGet } from './providers/query_suggestion_provider';
import {
setupFieldSuggestionProvider,
FieldSuggestionsGet,
} from './providers/field_suggestion_provider';

export class AutocompleteService {
private readonly querySuggestionProviders: Map<string, QuerySuggestionsGet> = new Map();
private getFieldSuggestions?: FieldSuggestionsGet;

private addQuerySuggestionProvider = (language: string, provider: QuerySuggestionsGet): void => {
if (language && provider) {
this.querySuggestionProviders.set(language, provider);
}
};

private getQuerySuggestions: QuerySuggestionsGet = 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.getFieldSuggestions = setupFieldSuggestionProvider(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,
getFieldSuggestions: this.getFieldSuggestions!,
};
}

/** @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, QuerySuggestionsGet } from './types';
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 { setupFieldSuggestionProvider, FieldSuggestionsGet } from './field_suggestion_provider';
import { IUiSettingsClient, CoreSetup } from 'kibana/public';

describe('getSuggestions', () => {
let getSuggestions: any;
describe('FieldSuggestions', () => {
let getFieldSuggestions: FieldSuggestionsGet;
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() };

getFieldSuggestions = setupFieldSuggestionProvider({ 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 getFieldSuggestions({
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 getFieldSuggestions({
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 getFieldSuggestions({
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 getFieldSuggestions({
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 getFieldSuggestions({
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 getFieldSuggestions(args);
await getFieldSuggestions(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 getFieldSuggestions(args);

Date.now = jest.fn(() => 60 * 1000);
await getSuggestions(index, field, query);
await getFieldSuggestions(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 getFieldSuggestions({
indexPattern: stubIndexPattern,
field: fields[0],
query: '',
});
await getFieldSuggestions({
indexPattern: stubIndexPattern,
field: fields[0],
query: 'query',
});
await getFieldSuggestions({
indexPattern: stubIndexPattern,
field: fields[1],
query: '',
});
await getFieldSuggestions({
indexPattern: stubIndexPattern,
field: fields[1],
query: 'query',
});

const customIndexPattern = {
...stubIndexPattern,
title: 'customIndexPattern',
};

await getFieldSuggestions({
indexPattern: customIndexPattern,
field: fields[0],
query: '',
});
await getFieldSuggestions({
indexPattern: customIndexPattern,
field: fields[0],
query: 'query',
});
await getFieldSuggestions({
indexPattern: customIndexPattern,
field: fields[1],
query: '',
});
await getFieldSuggestions({
indexPattern: customIndexPattern,
field: fields[1],
query: 'query',
});

expect(http.fetch).toHaveBeenCalledTimes(8);
});
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);

return [ttl, query, title, field.name, JSON.stringify(boolFilter)].join('|');
}

export type FieldSuggestionsGet = (args: FieldSuggestionsGetArgs) => Promise<any[]>;

interface FieldSuggestionsGetArgs {
indexPattern: IIndexPattern;
field: IFieldType;
query: string;
boolFilter?: any[];
signal?: AbortSignal;
}

export function getSuggestionsProvider(
uiSettings: IUiSettingsClient,
http: HttpSetup
): IGetSuggestions {
export const setupFieldSuggestionProvider = (core: CoreSetup): FieldSuggestionsGet => {
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,
}: FieldSuggestionsGetArgs): 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);
};
};
Loading