Update QueryBarInput to accept index pattern strings (#36916)

For many use cases a consumer of the QueryBarInput (or QueryBar) might only have an index pattern string in hand. Instead of forcing every consumer to reimplement the fetching logic to get a full pattern object, this PR updates the QueryBarInput to do the fetching itself if the indexPatterns array prop contains any strings. If a string does not exactly match the title of any of the saved objects then we return the default index pattern instead.
This commit is contained in:
Matt Bargar 2019-05-24 12:09:25 -05:00 committed by GitHub
parent ec1dc71c37
commit b7b7aa504f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 142 additions and 11 deletions

View file

@ -18,3 +18,4 @@
*/ */
export { QueryBar } from './query_bar'; export { QueryBar } from './query_bar';
export { QueryBarInput } from './query_bar_input';

View file

@ -59,7 +59,7 @@ interface Props {
disableAutoFocus?: boolean; disableAutoFocus?: boolean;
appName: string; appName: string;
screenTitle: string; screenTitle: string;
indexPatterns: IndexPattern[]; indexPatterns: Array<IndexPattern | string>;
store: Storage; store: Storage;
intl: InjectedIntl; intl: InjectedIntl;
prepend?: any; prepend?: any;

View file

@ -20,6 +20,21 @@
import { createKfetch } from 'ui/kfetch/kfetch'; import { createKfetch } from 'ui/kfetch/kfetch';
import { setup } from 'test_utils/http_test_setup'; import { setup } from 'test_utils/http_test_setup';
const mockIndexPattern = {
id: '1234',
title: 'logstash-*',
fields: [
{
name: 'response',
type: 'number',
esTypes: ['integer'],
aggregatable: true,
filterable: true,
searchable: true,
},
],
};
const mockChromeFactory = jest.fn(() => { const mockChromeFactory = jest.fn(() => {
return { return {
getBasePath: () => `foo`, getBasePath: () => `foo`,
@ -52,6 +67,10 @@ const mockAutocompleteProvider = jest.fn(() => mockGetAutocompleteSuggestions);
export const mockGetAutocompleteProvider = jest.fn(() => mockAutocompleteProvider); export const mockGetAutocompleteProvider = jest.fn(() => mockAutocompleteProvider);
const mockKfetch = jest.fn(() => createKfetch(setup().http)); const mockKfetch = jest.fn(() => createKfetch(setup().http));
export const mockFetchIndexPatterns = jest
.fn()
.mockReturnValue(Promise.resolve([mockIndexPattern]));
jest.mock('ui/chrome', () => mockChromeFactory()); jest.mock('ui/chrome', () => mockChromeFactory());
jest.mock('ui/kfetch', () => ({ jest.mock('ui/kfetch', () => ({
kfetch: () => {}, kfetch: () => {},
@ -72,6 +91,10 @@ jest.mock('ui/kfetch', () => ({
kfetch: mockKfetch, kfetch: mockKfetch,
})); }));
jest.mock('../lib/fetch_index_patterns', () => ({
fetchIndexPatterns: mockFetchIndexPatterns,
}));
import _ from 'lodash'; import _ from 'lodash';
// Using doMock to avoid hoisting so that I can override only the debounce method in lodash // Using doMock to avoid hoisting so that I can override only the debounce method in lodash
jest.doMock('lodash', () => ({ jest.doMock('lodash', () => ({

View file

@ -18,6 +18,7 @@
*/ */
import { import {
mockFetchIndexPatterns,
mockGetAutocompleteProvider, mockGetAutocompleteProvider,
mockGetAutocompleteSuggestions, mockGetAutocompleteSuggestions,
mockPersistedLog, mockPersistedLog,
@ -131,6 +132,8 @@ describe('QueryBarInput', () => {
}); });
it('Should create a unique PersistedLog based on the appName and query language', () => { it('Should create a unique PersistedLog based on the appName and query language', () => {
mockPersistedLogFactory.mockClear();
mountWithIntl( mountWithIntl(
<QueryBarInput.WrappedComponent <QueryBarInput.WrappedComponent
query={kqlQuery} query={kqlQuery}
@ -240,4 +243,23 @@ describe('QueryBarInput', () => {
expect(mockGetAutocompleteProvider).toHaveBeenCalledWith('kuery'); expect(mockGetAutocompleteProvider).toHaveBeenCalledWith('kuery');
expect(mockGetAutocompleteSuggestions).toHaveBeenCalled(); expect(mockGetAutocompleteSuggestions).toHaveBeenCalled();
}); });
it('Should accept index pattern strings and fetch the full object', () => {
mockFetchIndexPatterns.mockClear();
mountWithIntl(
<QueryBarInput.WrappedComponent
query={kqlQuery}
onSubmit={noop}
appName={'discover'}
screenTitle={'Another Screen'}
indexPatterns={['logstash-*']}
store={createMockStorage()}
disableAutoFocus={true}
intl={null as any}
/>
);
expect(mockFetchIndexPatterns).toHaveBeenCalledWith(['logstash-*']);
});
}); });

View file

@ -28,8 +28,8 @@ import {
AutocompleteSuggestionType, AutocompleteSuggestionType,
getAutocompleteProvider, getAutocompleteProvider,
} from 'ui/autocomplete_providers'; } from 'ui/autocomplete_providers';
import { debounce, compact } from 'lodash'; import { debounce, compact, isEqual } from 'lodash';
import { IndexPattern } from 'ui/index_patterns'; import { IndexPattern, StaticIndexPattern } from 'ui/index_patterns';
import { PersistedLog } from 'ui/persisted_log'; import { PersistedLog } from 'ui/persisted_log';
import chrome from 'ui/chrome'; import chrome from 'ui/chrome';
import { kfetch } from 'ui/kfetch'; import { kfetch } from 'ui/kfetch';
@ -38,6 +38,7 @@ import { fromUser, matchPairs, toUser } from '../lib';
import { QueryLanguageSwitcher } from './language_switcher'; import { QueryLanguageSwitcher } from './language_switcher';
import { SuggestionsComponent } from './typeahead/suggestions_component'; import { SuggestionsComponent } from './typeahead/suggestions_component';
import { getQueryLog } from '../lib/get_query_log'; import { getQueryLog } from '../lib/get_query_log';
import { fetchIndexPatterns } from '../lib/fetch_index_patterns';
interface Query { interface Query {
query: string; query: string;
@ -45,7 +46,7 @@ interface Query {
} }
interface Props { interface Props {
indexPatterns: IndexPattern[]; indexPatterns: Array<IndexPattern | string>;
intl: InjectedIntl; intl: InjectedIntl;
query: Query; query: Query;
appName: string; appName: string;
@ -65,6 +66,7 @@ interface State {
suggestionLimit: number; suggestionLimit: number;
selectionStart: number | null; selectionStart: number | null;
selectionEnd: number | null; selectionEnd: number | null;
indexPatterns: StaticIndexPattern[];
} }
const KEY_CODES = { const KEY_CODES = {
@ -90,6 +92,7 @@ export class QueryBarInputUI extends Component<Props, State> {
suggestionLimit: 50, suggestionLimit: 50,
selectionStart: null, selectionStart: null,
selectionEnd: null, selectionEnd: null,
indexPatterns: [],
}; };
public inputRef: HTMLInputElement | null = null; public inputRef: HTMLInputElement | null = null;
@ -101,6 +104,21 @@ export class QueryBarInputUI extends Component<Props, State> {
return toUser(this.props.query.query); return toUser(this.props.query.query);
}; };
private fetchIndexPatterns = async () => {
const stringPatterns = this.props.indexPatterns.filter(
indexPattern => typeof indexPattern === 'string'
) as string[];
const objectPatterns = this.props.indexPatterns.filter(
indexPattern => typeof indexPattern !== 'string'
) as IndexPattern[];
const objectPatternsFromStrings = await fetchIndexPatterns(stringPatterns);
this.setState({
indexPatterns: [...objectPatterns, ...objectPatternsFromStrings],
});
};
private getSuggestions = async () => { private getSuggestions = async () => {
if (!this.inputRef) { if (!this.inputRef) {
return; return;
@ -114,13 +132,13 @@ export class QueryBarInputUI extends Component<Props, State> {
const autocompleteProvider = getAutocompleteProvider(language); const autocompleteProvider = getAutocompleteProvider(language);
if ( if (
!autocompleteProvider || !autocompleteProvider ||
!Array.isArray(this.props.indexPatterns) || !Array.isArray(this.state.indexPatterns) ||
compact(this.props.indexPatterns).length === 0 compact(this.state.indexPatterns).length === 0
) { ) {
return recentSearchSuggestions; return recentSearchSuggestions;
} }
const indexPatterns = this.props.indexPatterns; const indexPatterns = this.state.indexPatterns;
const getAutocompleteSuggestions = autocompleteProvider({ config, indexPatterns }); const getAutocompleteSuggestions = autocompleteProvider({ config, indexPatterns });
const { selectionStart, selectionEnd } = this.inputRef; const { selectionStart, selectionEnd } = this.inputRef;
@ -368,14 +386,20 @@ export class QueryBarInputUI extends Component<Props, State> {
this.persistedLog = this.props.persistedLog this.persistedLog = this.props.persistedLog
? this.props.persistedLog ? this.props.persistedLog
: getQueryLog(this.props.appName, this.props.query.language); : getQueryLog(this.props.appName, this.props.query.language);
this.updateSuggestions();
this.fetchIndexPatterns().then(this.updateSuggestions);
} }
public componentDidUpdate(prevProps: Props) { public componentDidUpdate(prevProps: Props) {
this.persistedLog = this.props.persistedLog this.persistedLog = this.props.persistedLog
? this.props.persistedLog ? this.props.persistedLog
: getQueryLog(this.props.appName, this.props.query.language); : getQueryLog(this.props.appName, this.props.query.language);
this.updateSuggestions();
if (!isEqual(prevProps.indexPatterns, this.props.indexPatterns)) {
this.fetchIndexPatterns().then(this.updateSuggestions);
} else {
this.updateSuggestions();
}
if (this.state.selectionStart !== null && this.state.selectionEnd !== null) { if (this.state.selectionStart !== null && this.state.selectionEnd !== null) {
if (this.inputRef) { if (this.inputRef) {

View file

@ -17,7 +17,7 @@
* under the License. * under the License.
*/ */
export { QueryBar } from './components'; export { QueryBar, QueryBarInput } from './components';
export { fromUser } from './lib/from_user'; export { fromUser } from './lib/from_user';
export { toUser } from './lib/to_user'; export { toUser } from './lib/to_user';

View file

@ -0,0 +1,54 @@
/*
* 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 chrome from 'ui/chrome';
import { getFromSavedObject } from 'ui/index_patterns/static_utils';
const config = chrome.getUiSettingsClient();
export async function fetchIndexPatterns(indexPatternStrings: string[]) {
const quotedIndexPatternStrings = indexPatternStrings.map(
indexPatternString => `"${indexPatternString}"`
);
const searchString = quotedIndexPatternStrings.join(' | ');
const indexPatternsFromSavedObjects = await chrome.getSavedObjectsClient().find({
type: 'index-pattern',
fields: ['title', 'fields'],
search: `"${searchString}"`,
searchFields: ['title'],
});
const exactMatches = indexPatternsFromSavedObjects.savedObjects.filter(savedObject => {
return indexPatternStrings.includes(savedObject.attributes.title as string);
});
const allMatches =
exactMatches.length === indexPatternStrings.length
? exactMatches
: [...exactMatches, await fetchDefaultIndexPattern()];
return allMatches.map(getFromSavedObject);
}
const fetchDefaultIndexPattern = async () => {
const savedObjectsClient = chrome.getSavedObjectsClient();
const indexPattern = await savedObjectsClient.get('index-pattern', config.get('defaultIndex'));
return getFromSavedObject(indexPattern);
};

View file

@ -18,7 +18,13 @@
*/ */
import { once } from 'lodash'; import { once } from 'lodash';
import { QueryBar, fromUser, toUser, setupDirective as setupQueryBarDirective } from './query_bar'; import {
QueryBar,
QueryBarInput,
fromUser,
toUser,
setupDirective as setupQueryBarDirective,
} from './query_bar';
/** /**
* Query Service * Query Service
@ -35,6 +41,7 @@ export class QueryService {
}, },
ui: { ui: {
QueryBar, QueryBar,
QueryBarInput,
}, },
}; };
} }