Extract QueryBarInput component from QueryBar (#35827) (#36383)

Solution for use cases that need a query bar without a submit button or date picker that still want KQL and autocomplete. Necessary for the KQL in TSVB and KQL in filters aggregation efforts.
This commit is contained in:
Matt Bargar 2019-05-09 17:49:16 -05:00 committed by GitHub
parent 66054e8208
commit d6100f4f36
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 1894 additions and 821 deletions

View file

@ -1,179 +1,5 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`QueryBar Should disable autoFocus on EuiFieldText when disableAutoFocus prop is true 1`] = `
<EuiFlexGroup
className="kbnQueryBar"
gutterSize="s"
responsive={false}
>
<EuiFlexItem>
<EuiOutsideClickDetector
onOutsideClick={[Function]}
>
<div
aria-controls="kbnTypeahead__items"
aria-expanded={false}
aria-haspopup="true"
aria-owns="kbnTypeahead__items"
role="combobox"
style={
Object {
"position": "relative",
}
}
>
<form
name="queryBarForm"
>
<div
role="search"
>
<div
className="kuiLocalSearchAssistedInput"
>
<EuiFieldText
append={
<QueryLanguageSwitcher
language="kuery"
onSelectLanguage={[Function]}
/>
}
aria-activedescendant=""
aria-autocomplete="list"
aria-controls="kbnTypeahead__items"
aria-label="You are on search box of Another Screen page. Start typing to search and filter the discover"
autoComplete="off"
autoFocus={false}
compressed={false}
data-test-subj="queryInput"
fullWidth={true}
inputRef={[Function]}
isLoading={false}
onChange={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
onKeyUp={[Function]}
placeholder="Search"
role="textbox"
spellCheck={false}
type="text"
value="response:200"
/>
</div>
</div>
</form>
<SuggestionsComponent
index={null}
loadMore={[Function]}
onClick={[Function]}
onMouseEnter={[Function]}
show={false}
suggestions={Array []}
/>
</div>
</EuiOutsideClickDetector>
</EuiFlexItem>
<EuiFlexItem
grow={false}
>
<EuiSuperUpdateButton
data-test-subj="querySubmitButton"
isDisabled={false}
isLoading={false}
needsUpdate={false}
onClick={[Function]}
/>
</EuiFlexItem>
</EuiFlexGroup>
`;
exports[`QueryBar Should pass the query language to the language switcher 1`] = `
<EuiFlexGroup
className="kbnQueryBar"
gutterSize="s"
responsive={false}
>
<EuiFlexItem>
<EuiOutsideClickDetector
onOutsideClick={[Function]}
>
<div
aria-controls="kbnTypeahead__items"
aria-expanded={false}
aria-haspopup="true"
aria-owns="kbnTypeahead__items"
role="combobox"
style={
Object {
"position": "relative",
}
}
>
<form
name="queryBarForm"
>
<div
role="search"
>
<div
className="kuiLocalSearchAssistedInput"
>
<EuiFieldText
append={
<QueryLanguageSwitcher
language="lucene"
onSelectLanguage={[Function]}
/>
}
aria-activedescendant=""
aria-autocomplete="list"
aria-controls="kbnTypeahead__items"
aria-label="You are on search box of Another Screen page. Start typing to search and filter the discover"
autoComplete="off"
autoFocus={true}
compressed={false}
data-test-subj="queryInput"
fullWidth={true}
inputRef={[Function]}
isLoading={false}
onChange={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
onKeyUp={[Function]}
placeholder="Search"
role="textbox"
spellCheck={false}
type="text"
value="response:200"
/>
</div>
</div>
</form>
<SuggestionsComponent
index={null}
loadMore={[Function]}
onClick={[Function]}
onMouseEnter={[Function]}
show={false}
suggestions={Array []}
/>
</div>
</EuiOutsideClickDetector>
</EuiFlexItem>
<EuiFlexItem
grow={false}
>
<EuiSuperUpdateButton
data-test-subj="querySubmitButton"
isDisabled={false}
isLoading={false}
needsUpdate={false}
onClick={[Function]}
/>
</EuiFlexItem>
</EuiFlexGroup>
`;
exports[`QueryBar Should render the given query 1`] = `
<EuiFlexGroup
className="kbnQueryBar"
@ -181,71 +7,54 @@ exports[`QueryBar Should render the given query 1`] = `
responsive={false}
>
<EuiFlexItem>
<EuiOutsideClickDetector
onOutsideClick={[Function]}
>
<div
aria-controls="kbnTypeahead__items"
aria-expanded={false}
aria-haspopup="true"
aria-owns="kbnTypeahead__items"
role="combobox"
style={
<InjectIntl(QueryBarInputUI)
appName="discover"
indexPatterns={
Array [
Object {
"position": "relative",
}
"fields": Array [
Object {
"aggregatable": true,
"esTypes": Array [
"integer",
],
"filterable": true,
"name": "response",
"searchable": true,
"type": "number",
},
],
"id": "1234",
"title": "logstash-*",
},
]
}
onChange={[Function]}
onSubmit={[Function]}
query={
Object {
"language": "kuery",
"query": "response:200",
}
>
<form
name="queryBarForm"
>
<div
role="search"
>
<div
className="kuiLocalSearchAssistedInput"
>
<EuiFieldText
append={
<QueryLanguageSwitcher
language="kuery"
onSelectLanguage={[Function]}
/>
}
aria-activedescendant=""
aria-autocomplete="list"
aria-controls="kbnTypeahead__items"
aria-label="You are on search box of Another Screen page. Start typing to search and filter the discover"
autoComplete="off"
autoFocus={true}
compressed={false}
data-test-subj="queryInput"
fullWidth={true}
inputRef={[Function]}
isLoading={false}
onChange={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
onKeyUp={[Function]}
placeholder="Search"
role="textbox"
spellCheck={false}
type="text"
value="response:200"
/>
</div>
</div>
</form>
<SuggestionsComponent
index={null}
loadMore={[Function]}
onClick={[Function]}
onMouseEnter={[Function]}
show={false}
suggestions={Array []}
/>
</div>
</EuiOutsideClickDetector>
}
screenTitle="Another Screen"
store={
Object {
"clear": [MockFunction],
"get": [MockFunction],
"remove": [MockFunction],
"set": [MockFunction],
"store": Object {
"clear": [MockFunction],
"getItem": [MockFunction],
"key": [MockFunction],
"length": 0,
"removeItem": [MockFunction],
"setItem": [MockFunction],
},
}
}
/>
</EuiFlexItem>
<EuiFlexItem
grow={false}

View file

@ -17,19 +17,11 @@
* under the License.
*/
import {
mockGetAutocompleteProvider,
mockGetAutocompleteSuggestions,
mockPersistedLog,
mockPersistedLogFactory,
} from './query_bar.test.mocks';
import { mockPersistedLogFactory } from './query_bar_input.test.mocks';
import { EuiFieldText } from '@elastic/eui';
import React from 'react';
import { mountWithIntl, shallowWithIntl } from 'test_utils/enzyme_helpers';
import { shallowWithIntl } from 'test_utils/enzyme_helpers';
import { QueryBar } from './query_bar';
import { QueryLanguageSwitcher } from './language_switcher';
import { QueryBarUI } from './query_bar';
const noop = () => {
return;
@ -40,11 +32,6 @@ const kqlQuery = {
language: 'kuery',
};
const luceneQuery = {
query: 'response:200',
language: 'lucene',
};
const createMockWebStorage = () => ({
clear: jest.fn(),
getItem: jest.fn(),
@ -98,39 +85,6 @@ describe('QueryBar', () => {
expect(component).toMatchSnapshot();
});
it('Should pass the query language to the language switcher', () => {
const component = shallowWithIntl(
<QueryBar.WrappedComponent
query={luceneQuery}
onSubmit={noop}
appName={'discover'}
screenTitle={'Another Screen'}
indexPatterns={[mockIndexPattern]}
store={createMockStorage()}
intl={null as any}
/>
);
expect(component).toMatchSnapshot();
});
it('Should disable autoFocus on EuiFieldText when disableAutoFocus prop is true', () => {
const component = shallowWithIntl(
<QueryBar.WrappedComponent
query={kqlQuery}
onSubmit={noop}
appName={'discover'}
screenTitle={'Another Screen'}
indexPatterns={[mockIndexPattern]}
store={createMockStorage()}
disableAutoFocus={true}
intl={null as any}
/>
);
expect(component).toMatchSnapshot();
});
it('Should create a unique PersistedLog based on the appName and query language', () => {
shallowWithIntl(
<QueryBar.WrappedComponent
@ -147,118 +101,4 @@ describe('QueryBar', () => {
expect(mockPersistedLogFactory.mock.calls[0][0]).toBe('typeahead:discover-kuery');
});
it("On language selection, should store the user's preference in localstorage and reset the query", () => {
const mockStorage = createMockStorage();
const mockCallback = jest.fn();
const component = mountWithIntl(
<QueryBar.WrappedComponent
query={kqlQuery}
onSubmit={mockCallback}
appName={'discover'}
screenTitle={'Another Screen'}
indexPatterns={[mockIndexPattern]}
store={mockStorage}
disableAutoFocus={true}
intl={null as any}
/>
);
component
.find(QueryLanguageSwitcher)
.props()
.onSelectLanguage('lucene');
expect(mockStorage.set).toHaveBeenCalledWith('kibana.userQueryLanguage', 'lucene');
expect(mockCallback).toHaveBeenCalledWith({
dateRange: {
from: 'now-15m',
to: 'now',
},
query: {
query: '',
language: 'lucene',
},
});
});
it('Should call onSubmit with the current query when the user hits enter inside the query bar', () => {
const mockCallback = jest.fn();
const component = mountWithIntl(
<QueryBar.WrappedComponent
query={kqlQuery}
onSubmit={mockCallback}
appName={'discover'}
screenTitle={'Another Screen'}
indexPatterns={[mockIndexPattern]}
store={createMockStorage()}
disableAutoFocus={true}
intl={null as any}
/>
);
const instance = component.instance() as QueryBarUI;
const input = instance.inputRef;
const inputWrapper = component.find(EuiFieldText).find('input');
inputWrapper.simulate('change', { target: { value: 'extension:jpg' } });
inputWrapper.simulate('keyDown', { target: input, keyCode: 13, key: 'Enter', metaKey: true });
expect(mockCallback).toHaveBeenCalledTimes(1);
expect(mockCallback).toHaveBeenCalledWith({
dateRange: {
from: 'now-15m',
to: 'now',
},
query: {
query: 'extension:jpg',
language: 'kuery',
},
});
});
it('Should use PersistedLog for recent search suggestions', async () => {
const component = mountWithIntl(
<QueryBar.WrappedComponent
query={kqlQuery}
onSubmit={noop}
appName={'discover'}
screenTitle={'Another Screen'}
indexPatterns={[mockIndexPattern]}
store={createMockStorage()}
disableAutoFocus={true}
intl={null as any}
/>
);
const instance = component.instance() as QueryBarUI;
const input = instance.inputRef;
const inputWrapper = component.find(EuiFieldText).find('input');
inputWrapper.simulate('change', { target: { value: 'extension:jpg' } });
inputWrapper.simulate('keyDown', { target: input, keyCode: 13, key: 'Enter', metaKey: true });
expect(mockPersistedLog.add).toHaveBeenCalledWith('extension:jpg');
mockPersistedLog.get.mockClear();
inputWrapper.simulate('change', { target: { value: 'extensi' } });
expect(mockPersistedLog.get).toHaveBeenCalledTimes(1);
});
it('Should get suggestions from the autocomplete provider for the current language', () => {
mountWithIntl(
<QueryBar.WrappedComponent
query={kqlQuery}
onSubmit={noop}
appName={'discover'}
screenTitle={'Another Screen'}
indexPatterns={[mockIndexPattern]}
store={createMockStorage()}
disableAutoFocus={true}
intl={null as any}
/>
);
expect(mockGetAutocompleteProvider).toHaveBeenCalledWith('kuery');
expect(mockGetAutocompleteSuggestions).toHaveBeenCalled();
});
});

View file

@ -22,22 +22,12 @@ import { IndexPattern } from 'ui/index_patterns';
import classNames from 'classnames';
import _ from 'lodash';
import { compact, debounce, get, isEqual } from 'lodash';
import { get, isEqual } from 'lodash';
import React, { Component } from 'react';
import { kfetch } from 'ui/kfetch';
import { PersistedLog } from 'ui/persisted_log';
import { Storage } from 'ui/storage';
import { timeHistory } from 'ui/timefilter/time_history';
import {
EuiButton,
EuiFieldText,
EuiFlexGroup,
EuiFlexItem,
EuiLink,
EuiOutsideClickDetector,
EuiSuperDatePicker,
} from '@elastic/eui';
import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiLink, EuiSuperDatePicker } from '@elastic/eui';
// @ts-ignore
import { EuiSuperUpdateButton } from '@elastic/eui';
@ -45,31 +35,13 @@ import { EuiSuperUpdateButton } from '@elastic/eui';
import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react';
import { documentationLinks } from 'ui/documentation_links';
import { Toast, toastNotifications } from 'ui/notify';
import {
AutocompleteSuggestion,
AutocompleteSuggestionType,
getAutocompleteProvider,
} from 'ui/autocomplete_providers';
import chrome from 'ui/chrome';
import { PersistedLog } from 'ui/persisted_log';
import { QueryBarInput } from './query_bar_input';
import { fromUser, matchPairs, toUser } from '../lib';
import { QueryLanguageSwitcher } from './language_switcher';
import { SuggestionsComponent } from './typeahead/suggestions_component';
const KEY_CODES = {
LEFT: 37,
UP: 38,
RIGHT: 39,
DOWN: 40,
ENTER: 13,
ESC: 27,
TAB: 9,
HOME: 36,
END: 35,
};
import { getQueryLog } from '../lib/get_query_log';
const config = chrome.getUiSettingsClient();
const recentSearchType: AutocompleteSuggestionType = 'recentSearch';
interface Query {
query: string;
@ -104,10 +76,6 @@ interface Props {
interface State {
query: Query;
inputIsPristine: boolean;
isSuggestionsVisible: boolean;
index: number | null;
suggestions: AutocompleteSuggestion[];
suggestionLimit: number;
currentProps?: Props;
dateRangeFrom: string;
dateRangeTo: string;
@ -123,7 +91,7 @@ export class QueryBarUI extends Component<Props, State> {
let nextQuery = null;
if (nextProps.query.query !== prevState.query.query) {
nextQuery = {
query: toUser(nextProps.query.query),
query: nextProps.query.query,
language: nextProps.query.language,
};
} else if (nextProps.query.language !== prevState.query.language) {
@ -171,31 +139,19 @@ export class QueryBarUI extends Component<Props, State> {
*/
public state = {
query: {
query: toUser(this.props.query.query),
query: this.props.query.query,
language: this.props.query.language,
},
inputIsPristine: true,
isSuggestionsVisible: false,
currentProps: this.props,
index: null,
suggestions: [],
suggestionLimit: 50,
dateRangeFrom: _.get(this.props, 'dateRangeFrom', 'now-15m'),
dateRangeTo: _.get(this.props, 'dateRangeTo', 'now'),
isDateRangeInvalid: false,
};
public updateSuggestions = debounce(async () => {
const suggestions = (await this.getSuggestions()) || [];
if (!this.componentIsUnmounting) {
this.setState({ suggestions });
}
}, 100);
public inputRef: HTMLInputElement | null = null;
private componentIsUnmounting = false;
private persistedLog: PersistedLog | null = null;
private persistedLog: PersistedLog | undefined;
public isDirty = () => {
if (!this.props.showDatePicker) {
@ -209,176 +165,20 @@ export class QueryBarUI extends Component<Props, State> {
);
};
public increaseLimit = () => {
this.setState({
suggestionLimit: this.state.suggestionLimit + 50,
});
};
public incrementIndex = (currentIndex: number) => {
let nextIndex = currentIndex + 1;
if (currentIndex === null || nextIndex >= this.state.suggestions.length) {
nextIndex = 0;
}
this.setState({ index: nextIndex });
};
public decrementIndex = (currentIndex: number) => {
const previousIndex = currentIndex - 1;
if (previousIndex < 0) {
this.setState({ index: this.state.suggestions.length - 1 });
} else {
this.setState({ index: previousIndex });
}
};
public getSuggestions = async () => {
if (!this.inputRef) {
return;
}
const {
query: { query, language },
} = this.state;
const recentSearchSuggestions = this.getRecentSearchSuggestions(query);
const autocompleteProvider = getAutocompleteProvider(language);
if (
!autocompleteProvider ||
!Array.isArray(this.props.indexPatterns) ||
compact(this.props.indexPatterns).length === 0
) {
return recentSearchSuggestions;
}
const indexPatterns = this.props.indexPatterns;
const getAutocompleteSuggestions = autocompleteProvider({ config, indexPatterns });
const { selectionStart, selectionEnd } = this.inputRef;
if (selectionStart === null || selectionEnd === null) {
return;
}
const suggestions: AutocompleteSuggestion[] = await getAutocompleteSuggestions({
query,
selectionStart,
selectionEnd,
});
return [...suggestions, ...recentSearchSuggestions];
};
public selectSuggestion = ({
type,
text,
start,
end,
}: {
type: AutocompleteSuggestionType;
text: string;
start: number;
end: number;
}) => {
if (!this.inputRef) {
return;
}
const query = this.state.query.query;
const { selectionStart, selectionEnd } = this.inputRef;
if (selectionStart === null || selectionEnd === null) {
return;
}
const value = query.substr(0, selectionStart) + query.substr(selectionEnd);
this.setState(
{
query: {
...this.state.query,
query: value.substr(0, start) + text + value.substr(end),
},
index: null,
},
() => {
if (!this.inputRef) {
return;
}
this.inputRef.setSelectionRange(start + text.length, start + text.length);
if (type === recentSearchType) {
this.onSubmit();
} else {
this.updateSuggestions();
}
}
);
};
public getRecentSearchSuggestions = (query: string) => {
if (!this.persistedLog) {
return [];
}
const recentSearches = this.persistedLog.get();
const matchingRecentSearches = recentSearches.filter(recentQuery => {
const recentQueryString = typeof recentQuery === 'object' ? toUser(recentQuery) : recentQuery;
return recentQueryString.includes(query);
});
return matchingRecentSearches.map(recentSearch => {
const text = recentSearch;
const start = 0;
const end = query.length;
return { type: recentSearchType, text, start, end };
});
};
public onOutsideClick = () => {
if (this.state.isSuggestionsVisible) {
this.setState({ isSuggestionsVisible: false, index: null });
}
};
public onClickInput = (event: React.MouseEvent<HTMLInputElement>) => {
if (event.target instanceof HTMLInputElement) {
this.onInputChange(event.target.value);
}
};
public onClickSubmitButton = (event: React.MouseEvent<HTMLButtonElement>) => {
if (this.persistedLog) {
this.persistedLog.add(this.state.query.query);
}
this.onSubmit(() => event.preventDefault());
};
public onClickSuggestion = (suggestion: AutocompleteSuggestion) => {
if (!this.inputRef) {
return;
}
this.selectSuggestion(suggestion);
this.inputRef.focus();
};
public onMouseEnterSuggestion = (index: number) => {
this.setState({ index });
};
public onInputChange = (value: string) => {
const hasValue = Boolean(value.trim());
public onChange = (query: Query) => {
this.setState({
query: {
query: value,
language: this.state.query.language,
},
query,
inputIsPristine: false,
isSuggestionsVisible: hasValue,
index: null,
suggestionLimit: 50,
});
};
public onChange = (event: React.ChangeEvent<HTMLInputElement>) => {
this.updateSuggestions();
this.onInputChange(event.target.value);
};
public onTimeChange = ({
start,
end,
@ -400,83 +200,6 @@ export class QueryBarUI extends Component<Props, State> {
);
};
public onKeyUp = (event: React.KeyboardEvent<HTMLInputElement>) => {
if ([KEY_CODES.LEFT, KEY_CODES.RIGHT, KEY_CODES.HOME, KEY_CODES.END].includes(event.keyCode)) {
this.setState({ isSuggestionsVisible: true });
if (event.target instanceof HTMLInputElement) {
this.onInputChange(event.target.value);
}
}
};
public onKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
if (event.target instanceof HTMLInputElement) {
const { isSuggestionsVisible, index } = this.state;
const preventDefault = event.preventDefault.bind(event);
const { target, key, metaKey } = event;
const { value, selectionStart, selectionEnd } = target;
const updateQuery = (query: string, newSelectionStart: number, newSelectionEnd: number) => {
this.setState(
{
query: {
...this.state.query,
query,
},
},
() => {
target.setSelectionRange(newSelectionStart, newSelectionEnd);
}
);
};
switch (event.keyCode) {
case KEY_CODES.DOWN:
event.preventDefault();
if (isSuggestionsVisible && index !== null) {
this.incrementIndex(index);
} else {
this.setState({ isSuggestionsVisible: true, index: 0 });
}
break;
case KEY_CODES.UP:
event.preventDefault();
if (isSuggestionsVisible && index !== null) {
this.decrementIndex(index);
}
break;
case KEY_CODES.ENTER:
event.preventDefault();
if (isSuggestionsVisible && index !== null && this.state.suggestions[index]) {
this.selectSuggestion(this.state.suggestions[index]);
} else {
this.onSubmit(() => event.preventDefault());
}
break;
case KEY_CODES.ESC:
event.preventDefault();
this.setState({ isSuggestionsVisible: false, index: null });
break;
case KEY_CODES.TAB:
this.setState({ isSuggestionsVisible: false, index: null });
break;
default:
if (selectionStart !== null && selectionEnd !== null) {
matchPairs({
value,
selectionStart,
selectionEnd,
key,
metaKey,
updateQuery,
preventDefault,
});
}
break;
}
}
};
public onSubmit = (preventDefault?: () => void) => {
if (preventDefault) {
preventDefault();
@ -484,10 +207,6 @@ export class QueryBarUI extends Component<Props, State> {
this.handleLuceneSyntaxWarning();
if (this.persistedLog) {
this.persistedLog.add(this.state.query.query);
}
timeHistory.add({
from: this.state.dateRangeFrom,
to: this.state.dateRangeTo,
@ -495,7 +214,7 @@ export class QueryBarUI extends Component<Props, State> {
this.props.onSubmit({
query: {
query: fromUser(this.state.query.query),
query: this.state.query.query,
language: this.state.query.language,
},
dateRange: {
@ -503,146 +222,44 @@ export class QueryBarUI extends Component<Props, State> {
to: this.state.dateRangeTo,
},
});
this.setState({ isSuggestionsVisible: false });
};
public onSelectLanguage = (language: string) => {
// Send telemetry info every time the user opts in or out of kuery
// As a result it is important this function only ever gets called in the
// UI component's change handler.
kfetch({
pathname: '/api/kibana/kql_opt_in_telemetry',
method: 'POST',
body: JSON.stringify({ opt_in: language === 'kuery' }),
});
this.props.store.set('kibana.userQueryLanguage', language);
this.props.onSubmit({
query: {
query: '',
language,
},
dateRange: {
from: this.state.dateRangeFrom,
to: this.state.dateRangeTo,
},
private onInputSubmit = (query: Query) => {
this.setState({ query }, () => {
this.onSubmit();
});
};
public componentDidMount() {
this.persistedLog = new PersistedLog(
`typeahead:${this.props.appName}-${this.state.query.language}`,
{
maxLength: config.get('history:limit'),
filterDuplicates: true,
}
);
this.updateSuggestions();
this.persistedLog = getQueryLog(this.props.appName, this.props.query.language);
}
public componentDidUpdate(prevProps: Props) {
if (prevProps.query.language !== this.props.query.language) {
this.persistedLog = new PersistedLog(
`typeahead:${this.props.appName}-${this.state.query.language}`,
{
maxLength: config.get('history:limit'),
filterDuplicates: true,
}
);
this.updateSuggestions();
this.persistedLog = getQueryLog(this.props.appName, this.props.query.language);
}
}
public componentWillUnmount() {
this.updateSuggestions.cancel();
this.componentIsUnmounting = true;
}
public render() {
const classes = classNames('kbnQueryBar', {
'kbnQueryBar--withDatePicker': this.props.showDatePicker,
});
return (
<EuiFlexGroup
className={classes}
responsive={this.props.showDatePicker ? true : false}
gutterSize="s"
>
<EuiFlexGroup className={classes} responsive={!!this.props.showDatePicker} gutterSize="s">
<EuiFlexItem>
<EuiOutsideClickDetector onOutsideClick={this.onOutsideClick}>
{/* position:relative required on container so the suggestions appear under the query bar*/}
<div
style={{ position: 'relative' }}
role="combobox"
aria-haspopup="true"
aria-expanded={this.state.isSuggestionsVisible}
aria-owns="kbnTypeahead__items"
aria-controls="kbnTypeahead__items"
>
<form name="queryBarForm">
<div role="search">
<div className="kuiLocalSearchAssistedInput">
<EuiFieldText
placeholder={this.props.intl.formatMessage({
id: 'data.query.queryBar.searchInputPlaceholder',
defaultMessage: 'Search',
})}
value={this.state.query.query}
onKeyDown={this.onKeyDown}
onKeyUp={this.onKeyUp}
onChange={this.onChange}
onClick={this.onClickInput}
fullWidth
autoFocus={!this.props.disableAutoFocus}
inputRef={node => {
if (node) {
this.inputRef = node;
}
}}
autoComplete="off"
spellCheck={false}
aria-label={this.props.intl.formatMessage(
{
id: 'data.query.queryBar.searchInputAriaLabel',
defaultMessage:
'You are on search box of {previouslyTranslatedPageTitle} page. Start typing to search and filter the {pageType}',
},
{
previouslyTranslatedPageTitle: this.props.screenTitle,
pageType: this.props.appName,
}
)}
type="text"
data-test-subj="queryInput"
aria-autocomplete="list"
aria-controls="kbnTypeahead__items"
aria-activedescendant={
this.state.isSuggestionsVisible ? 'suggestion-' + this.state.index : ''
}
role="textbox"
prepend={this.props.prepend}
append={
<QueryLanguageSwitcher
language={this.state.query.language}
onSelectLanguage={this.onSelectLanguage}
/>
}
/>
</div>
</div>
</form>
<SuggestionsComponent
show={this.state.isSuggestionsVisible}
suggestions={this.state.suggestions.slice(0, this.state.suggestionLimit)}
index={this.state.index}
onClick={this.onClickSuggestion}
onMouseEnter={this.onMouseEnterSuggestion}
loadMore={this.increaseLimit}
/>
</div>
</EuiOutsideClickDetector>
<QueryBarInput
appName={this.props.appName}
disableAutoFocus={this.props.disableAutoFocus}
indexPatterns={this.props.indexPatterns}
prepend={this.props.prepend}
query={this.state.query}
screenTitle={this.props.screenTitle}
store={this.props.store}
onChange={this.onChange}
onSubmit={this.onInputSubmit}
persistedLog={this.persistedLog}
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>{this.renderUpdateButton()}</EuiFlexItem>
</EuiFlexGroup>

View file

@ -0,0 +1,243 @@
/*
* 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 {
mockGetAutocompleteProvider,
mockGetAutocompleteSuggestions,
mockPersistedLog,
mockPersistedLogFactory,
} from './query_bar_input.test.mocks';
import { EuiFieldText } from '@elastic/eui';
import React from 'react';
import { mountWithIntl } from 'test_utils/enzyme_helpers';
import { QueryLanguageSwitcher } from './language_switcher';
import { QueryBarInput, QueryBarInputUI } from './query_bar_input';
const noop = () => {
return;
};
const kqlQuery = {
query: 'response:200',
language: 'kuery',
};
const luceneQuery = {
query: 'response:200',
language: 'lucene',
};
const createMockWebStorage = () => ({
clear: jest.fn(),
getItem: jest.fn(),
key: jest.fn(),
removeItem: jest.fn(),
setItem: jest.fn(),
length: 0,
});
const createMockStorage = () => ({
store: createMockWebStorage(),
get: jest.fn(),
set: jest.fn(),
remove: jest.fn(),
clear: jest.fn(),
});
const mockIndexPattern = {
id: '1234',
title: 'logstash-*',
fields: [
{
name: 'response',
type: 'number',
esTypes: ['integer'],
aggregatable: true,
filterable: true,
searchable: true,
},
],
};
describe('QueryBarInput', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('Should render the given query', () => {
const component = mountWithIntl(
<QueryBarInput.WrappedComponent
query={kqlQuery}
onSubmit={noop}
appName={'discover'}
screenTitle={'Another Screen'}
indexPatterns={[mockIndexPattern]}
store={createMockStorage()}
intl={null as any}
/>
);
expect(component).toMatchSnapshot();
});
it('Should pass the query language to the language switcher', () => {
const component = mountWithIntl(
<QueryBarInput.WrappedComponent
query={luceneQuery}
onSubmit={noop}
appName={'discover'}
screenTitle={'Another Screen'}
indexPatterns={[mockIndexPattern]}
store={createMockStorage()}
intl={null as any}
/>
);
expect(component).toMatchSnapshot();
});
it('Should disable autoFocus on EuiFieldText when disableAutoFocus prop is true', () => {
const component = mountWithIntl(
<QueryBarInput.WrappedComponent
query={kqlQuery}
onSubmit={noop}
appName={'discover'}
screenTitle={'Another Screen'}
indexPatterns={[mockIndexPattern]}
store={createMockStorage()}
disableAutoFocus={true}
intl={null as any}
/>
);
expect(component).toMatchSnapshot();
});
it('Should create a unique PersistedLog based on the appName and query language', () => {
mountWithIntl(
<QueryBarInput.WrappedComponent
query={kqlQuery}
onSubmit={noop}
appName={'discover'}
screenTitle={'Another Screen'}
indexPatterns={[mockIndexPattern]}
store={createMockStorage()}
disableAutoFocus={true}
intl={null as any}
/>
);
expect(mockPersistedLogFactory.mock.calls[0][0]).toBe('typeahead:discover-kuery');
});
it("On language selection, should store the user's preference in localstorage and reset the query", () => {
const mockStorage = createMockStorage();
const mockCallback = jest.fn();
const component = mountWithIntl(
<QueryBarInput.WrappedComponent
query={kqlQuery}
onSubmit={mockCallback}
appName={'discover'}
screenTitle={'Another Screen'}
indexPatterns={[mockIndexPattern]}
store={mockStorage}
disableAutoFocus={true}
intl={null as any}
/>
);
component
.find(QueryLanguageSwitcher)
.props()
.onSelectLanguage('lucene');
expect(mockStorage.set).toHaveBeenCalledWith('kibana.userQueryLanguage', 'lucene');
expect(mockCallback).toHaveBeenCalledWith({ query: '', language: 'lucene' });
});
it('Should call onSubmit when the user hits enter inside the query bar', () => {
const mockCallback = jest.fn();
const component = mountWithIntl(
<QueryBarInput.WrappedComponent
query={kqlQuery}
onSubmit={mockCallback}
appName={'discover'}
screenTitle={'Another Screen'}
indexPatterns={[mockIndexPattern]}
store={createMockStorage()}
disableAutoFocus={true}
intl={null as any}
/>
);
const instance = component.instance() as QueryBarInputUI;
const input = instance.inputRef;
const inputWrapper = component.find(EuiFieldText).find('input');
inputWrapper.simulate('keyDown', { target: input, keyCode: 13, key: 'Enter', metaKey: true });
expect(mockCallback).toHaveBeenCalledTimes(1);
expect(mockCallback).toHaveBeenCalledWith({ query: 'response:200', language: 'kuery' });
});
it('Should use PersistedLog for recent search suggestions', async () => {
const component = mountWithIntl(
<QueryBarInput.WrappedComponent
query={kqlQuery}
onSubmit={noop}
appName={'discover'}
screenTitle={'Another Screen'}
indexPatterns={[mockIndexPattern]}
store={createMockStorage()}
disableAutoFocus={true}
intl={null as any}
/>
);
const instance = component.instance() as QueryBarInputUI;
const input = instance.inputRef;
const inputWrapper = component.find(EuiFieldText).find('input');
inputWrapper.simulate('keyDown', { target: input, keyCode: 13, key: 'Enter', metaKey: true });
expect(mockPersistedLog.add).toHaveBeenCalledWith('response:200');
mockPersistedLog.get.mockClear();
inputWrapper.simulate('change', { target: { value: 'extensi' } });
expect(mockPersistedLog.get).toHaveBeenCalled();
});
it('Should get suggestions from the autocomplete provider for the current language', () => {
mountWithIntl(
<QueryBarInput.WrappedComponent
query={kqlQuery}
onSubmit={noop}
appName={'discover'}
screenTitle={'Another Screen'}
indexPatterns={[mockIndexPattern]}
store={createMockStorage()}
disableAutoFocus={true}
intl={null as any}
/>
);
expect(mockGetAutocompleteProvider).toHaveBeenCalledWith('kuery');
expect(mockGetAutocompleteSuggestions).toHaveBeenCalled();
});
});

View file

@ -0,0 +1,476 @@
/*
* 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 { Component } from 'react';
import React from 'react';
import { EuiFieldText, EuiOutsideClickDetector } from '@elastic/eui';
import { InjectedIntl, injectI18n } from '@kbn/i18n/react';
import {
AutocompleteSuggestion,
AutocompleteSuggestionType,
getAutocompleteProvider,
} from 'ui/autocomplete_providers';
import { debounce, compact } from 'lodash';
import { IndexPattern } from 'ui/index_patterns';
import { PersistedLog } from 'ui/persisted_log';
import chrome from 'ui/chrome';
import { kfetch } from 'ui/kfetch';
import { Storage } from 'ui/storage';
import { fromUser, matchPairs, toUser } from '../lib';
import { QueryLanguageSwitcher } from './language_switcher';
import { SuggestionsComponent } from './typeahead/suggestions_component';
import { getQueryLog } from '../lib/get_query_log';
interface Query {
query: string;
language: string;
}
interface Props {
indexPatterns: IndexPattern[];
intl: InjectedIntl;
query: Query;
appName: string;
disableAutoFocus?: boolean;
screenTitle: string;
prepend?: any;
store: Storage;
persistedLog?: PersistedLog;
onChange?: (query: Query) => void;
onSubmit?: (query: Query) => void;
}
interface State {
isSuggestionsVisible: boolean;
index: number | null;
suggestions: AutocompleteSuggestion[];
suggestionLimit: number;
selectionStart: number | null;
selectionEnd: number | null;
}
const KEY_CODES = {
LEFT: 37,
UP: 38,
RIGHT: 39,
DOWN: 40,
ENTER: 13,
ESC: 27,
TAB: 9,
HOME: 36,
END: 35,
};
const config = chrome.getUiSettingsClient();
const recentSearchType: AutocompleteSuggestionType = 'recentSearch';
export class QueryBarInputUI extends Component<Props, State> {
public state = {
isSuggestionsVisible: false,
index: null,
suggestions: [],
suggestionLimit: 50,
selectionStart: null,
selectionEnd: null,
};
public inputRef: HTMLInputElement | null = null;
private persistedLog: PersistedLog | undefined;
private componentIsUnmounting = false;
private getQueryString = () => {
return toUser(this.props.query.query);
};
private getSuggestions = async () => {
if (!this.inputRef) {
return;
}
const {
query: { query, language },
} = this.props;
const recentSearchSuggestions = this.getRecentSearchSuggestions(query);
const autocompleteProvider = getAutocompleteProvider(language);
if (
!autocompleteProvider ||
!Array.isArray(this.props.indexPatterns) ||
compact(this.props.indexPatterns).length === 0
) {
return recentSearchSuggestions;
}
const indexPatterns = this.props.indexPatterns;
const getAutocompleteSuggestions = autocompleteProvider({ config, indexPatterns });
const { selectionStart, selectionEnd } = this.inputRef;
if (selectionStart === null || selectionEnd === null) {
return;
}
const suggestions: AutocompleteSuggestion[] = await getAutocompleteSuggestions({
query,
selectionStart,
selectionEnd,
});
return [...suggestions, ...recentSearchSuggestions];
};
private getRecentSearchSuggestions = (query: string) => {
if (!this.persistedLog) {
return [];
}
const recentSearches = this.persistedLog.get();
const matchingRecentSearches = recentSearches.filter(recentQuery => {
const recentQueryString = typeof recentQuery === 'object' ? toUser(recentQuery) : recentQuery;
return recentQueryString.includes(query);
});
return matchingRecentSearches.map(recentSearch => {
const text = toUser(recentSearch);
const start = 0;
const end = query.length;
return { type: recentSearchType, text, start, end };
});
};
private updateSuggestions = debounce(async () => {
const suggestions = (await this.getSuggestions()) || [];
if (!this.componentIsUnmounting) {
this.setState({ suggestions });
}
}, 100);
private onSubmit = (query: Query) => {
if (this.props.onSubmit) {
if (this.persistedLog) {
this.persistedLog.add(query.query);
}
this.props.onSubmit({ query: fromUser(query.query), language: query.language });
}
};
private onChange = (query: Query) => {
this.updateSuggestions();
if (this.props.onChange) {
this.props.onChange({ query: fromUser(query.query), language: query.language });
}
};
private onQueryStringChange = (value: string) => {
const hasValue = Boolean(value.trim());
this.setState({
isSuggestionsVisible: hasValue,
index: null,
suggestionLimit: 50,
});
this.onChange({ query: value, language: this.props.query.language });
};
private onInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
this.onQueryStringChange(event.target.value);
};
private onClickInput = (event: React.MouseEvent<HTMLInputElement>) => {
if (event.target instanceof HTMLInputElement) {
this.onQueryStringChange(event.target.value);
}
};
private onKeyUp = (event: React.KeyboardEvent<HTMLInputElement>) => {
if ([KEY_CODES.LEFT, KEY_CODES.RIGHT, KEY_CODES.HOME, KEY_CODES.END].includes(event.keyCode)) {
this.setState({ isSuggestionsVisible: true });
if (event.target instanceof HTMLInputElement) {
this.onQueryStringChange(event.target.value);
}
}
};
private onKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
if (event.target instanceof HTMLInputElement) {
const { isSuggestionsVisible, index } = this.state;
const preventDefault = event.preventDefault.bind(event);
const { target, key, metaKey } = event;
const { value, selectionStart, selectionEnd } = target;
const updateQuery = (query: string, newSelectionStart: number, newSelectionEnd: number) => {
this.onQueryStringChange(query);
this.setState({
selectionStart: newSelectionStart,
selectionEnd: newSelectionEnd,
});
};
switch (event.keyCode) {
case KEY_CODES.DOWN:
event.preventDefault();
if (isSuggestionsVisible && index !== null) {
this.incrementIndex(index);
} else {
this.setState({ isSuggestionsVisible: true, index: 0 });
}
break;
case KEY_CODES.UP:
event.preventDefault();
if (isSuggestionsVisible && index !== null) {
this.decrementIndex(index);
}
break;
case KEY_CODES.ENTER:
event.preventDefault();
if (isSuggestionsVisible && index !== null && this.state.suggestions[index]) {
this.selectSuggestion(this.state.suggestions[index]);
} else {
this.onSubmit(this.props.query);
this.setState({
isSuggestionsVisible: false,
});
}
break;
case KEY_CODES.ESC:
event.preventDefault();
this.setState({ isSuggestionsVisible: false, index: null });
break;
case KEY_CODES.TAB:
this.setState({ isSuggestionsVisible: false, index: null });
break;
default:
if (selectionStart !== null && selectionEnd !== null) {
matchPairs({
value,
selectionStart,
selectionEnd,
key,
metaKey,
updateQuery,
preventDefault,
});
}
break;
}
}
};
private selectSuggestion = ({
type,
text,
start,
end,
}: {
type: AutocompleteSuggestionType;
text: string;
start: number;
end: number;
}) => {
if (!this.inputRef) {
return;
}
const query = this.getQueryString();
const { selectionStart, selectionEnd } = this.inputRef;
if (selectionStart === null || selectionEnd === null) {
return;
}
const value = query.substr(0, selectionStart) + query.substr(selectionEnd);
const newQueryString = value.substr(0, start) + text + value.substr(end);
this.onQueryStringChange(newQueryString);
if (type === recentSearchType) {
this.setState({ isSuggestionsVisible: false, index: null });
this.onSubmit({ query: newQueryString, language: this.props.query.language });
}
};
private increaseLimit = () => {
this.setState({
suggestionLimit: this.state.suggestionLimit + 50,
});
};
private incrementIndex = (currentIndex: number) => {
let nextIndex = currentIndex + 1;
if (currentIndex === null || nextIndex >= this.state.suggestions.length) {
nextIndex = 0;
}
this.setState({ index: nextIndex });
};
private decrementIndex = (currentIndex: number) => {
const previousIndex = currentIndex - 1;
if (previousIndex < 0) {
this.setState({ index: this.state.suggestions.length - 1 });
} else {
this.setState({ index: previousIndex });
}
};
private onSelectLanguage = (language: string) => {
// Send telemetry info every time the user opts in or out of kuery
// As a result it is important this function only ever gets called in the
// UI component's change handler.
kfetch({
pathname: '/api/kibana/kql_opt_in_telemetry',
method: 'POST',
body: JSON.stringify({ opt_in: language === 'kuery' }),
});
this.props.store.set('kibana.userQueryLanguage', language);
const newQuery = { query: '', language };
this.onChange(newQuery);
this.onSubmit(newQuery);
};
private onOutsideClick = () => {
if (this.state.isSuggestionsVisible) {
this.setState({ isSuggestionsVisible: false, index: null });
}
};
private onClickSuggestion = (suggestion: AutocompleteSuggestion) => {
if (!this.inputRef) {
return;
}
this.selectSuggestion(suggestion);
this.inputRef.focus();
};
public onMouseEnterSuggestion = (index: number) => {
this.setState({ index });
};
public componentDidMount() {
this.persistedLog = this.props.persistedLog
? this.props.persistedLog
: getQueryLog(this.props.appName, this.props.query.language);
this.updateSuggestions();
}
public componentDidUpdate(prevProps: Props) {
this.persistedLog = this.props.persistedLog
? this.props.persistedLog
: getQueryLog(this.props.appName, this.props.query.language);
this.updateSuggestions();
if (this.state.selectionStart !== null && this.state.selectionEnd !== null) {
if (this.inputRef) {
// For some reason the type guard above does not make the compiler happy
// @ts-ignore
this.inputRef.setSelectionRange(this.state.selectionStart, this.state.selectionEnd);
}
this.setState({
selectionStart: null,
selectionEnd: null,
});
}
}
public componentWillUnmount() {
this.updateSuggestions.cancel();
this.componentIsUnmounting = true;
}
public render() {
return (
<EuiOutsideClickDetector onOutsideClick={this.onOutsideClick}>
<div
style={{ position: 'relative' }}
role="combobox"
aria-haspopup="true"
aria-expanded={this.state.isSuggestionsVisible}
aria-owns="kbnTypeahead__items"
aria-controls="kbnTypeahead__items"
>
<form name="queryBarForm">
<div role="search">
<div className="kuiLocalSearchAssistedInput">
<EuiFieldText
placeholder={this.props.intl.formatMessage({
id: 'data.query.queryBar.searchInputPlaceholder',
defaultMessage: 'Search',
})}
value={this.getQueryString()}
onKeyDown={this.onKeyDown}
onKeyUp={this.onKeyUp}
onChange={this.onInputChange}
onClick={this.onClickInput}
fullWidth
autoFocus={!this.props.disableAutoFocus}
inputRef={node => {
if (node) {
this.inputRef = node;
}
}}
autoComplete="off"
spellCheck={false}
aria-label={this.props.intl.formatMessage(
{
id: 'data.query.queryBar.searchInputAriaLabel',
defaultMessage:
'You are on search box of {previouslyTranslatedPageTitle} page. Start typing to search and filter the {pageType}',
},
{
previouslyTranslatedPageTitle: this.props.screenTitle,
pageType: this.props.appName,
}
)}
type="text"
data-test-subj="queryInput"
aria-autocomplete="list"
aria-controls="kbnTypeahead__items"
aria-activedescendant={
this.state.isSuggestionsVisible ? 'suggestion-' + this.state.index : ''
}
role="textbox"
prepend={this.props.prepend}
append={
<QueryLanguageSwitcher
language={this.props.query.language}
onSelectLanguage={this.onSelectLanguage}
/>
}
/>
</div>
</div>
</form>
<SuggestionsComponent
show={this.state.isSuggestionsVisible}
suggestions={this.state.suggestions.slice(0, this.state.suggestionLimit)}
index={this.state.index}
onClick={this.onClickSuggestion}
onMouseEnter={this.onMouseEnterSuggestion}
loadMore={this.increaseLimit}
/>
</div>
</EuiOutsideClickDetector>
);
}
}
export const QueryBarInput = injectI18n(QueryBarInputUI);

View file

@ -0,0 +1,30 @@
/*
* 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 { PersistedLog } from 'ui/persisted_log';
const config = chrome.getUiSettingsClient();
export function getQueryLog(appName: string, language: string) {
return new PersistedLog(`typeahead:${appName}-${language}`, {
maxLength: config.get('history:limit'),
filterDuplicates: true,
});
}

View file

@ -81,7 +81,7 @@ export default function ({ getService, getPageObjects }) {
it('when the query is edited and applied', async function () {
const originalQuery = await queryBar.getQueryString();
await queryBar.setQuery(`${originalQuery} and extra stuff`);
await queryBar.setQuery(`${originalQuery}and extra stuff`);
await queryBar.submitQuery();
await PageObjects.dashboard.clickCancelOutOfEditMode();
@ -209,7 +209,7 @@ export default function ({ getService, getPageObjects }) {
await PageObjects.dashboard.gotoDashboardEditMode(dashboardName);
const originalQuery = await queryBar.getQueryString();
await queryBar.setQuery(`${originalQuery} extra stuff`);
await queryBar.setQuery(`${originalQuery}extra stuff`);
await PageObjects.dashboard.clickCancelOutOfEditMode();

View file

@ -165,10 +165,9 @@ export class WebElementWrapper {
await delay(100);
}
} else {
const selectionKey = this.Keys[process.platform === 'darwin' ? 'COMMAND' : 'CONTROL'];
await this.pressKeys([selectionKey, 'a']);
await this.pressKeys(this.Keys.NULL); // Release modifier keys
await this.pressKeys(this.Keys.BACK_SPACE); // Delete all content
// https://bugs.chromium.org/p/chromedriver/issues/detail?id=30
await this.driver.executeScript(`arguments[0].select();`, this._webElement);
await this.pressKeys(this.Keys.BACK_SPACE);
}
}

View file

@ -22,6 +22,7 @@ export function QueryBarProvider({ getService, getPageObjects }) {
const retry = getService('retry');
const log = getService('log');
const PageObjects = getPageObjects(['header', 'common']);
const find = getService('find');
class QueryBar {
@ -34,7 +35,13 @@ export function QueryBarProvider({ getService, getPageObjects }) {
// Extra caution used because of flaky test here: https://github.com/elastic/kibana/issues/16978 doesn't seem
// to be actually setting the query in the query input based off
await retry.try(async () => {
await testSubjects.setValue('queryInput', query);
await testSubjects.click('queryInput');
// testSubjects.setValue uses input.clearValue which wasn't working, but input.clearValueWithKeyboard does.
// So the following lines do the same thing as input.setValue but with input.clearValueWithKeyboard instead.
const input = await find.activeElement();
await input.clearValueWithKeyboard();
await input.type(query);
const currentQuery = await this.getQueryString();
if (currentQuery !== query) {
throw new Error(`Failed to set query input to ${query}, instead query is ${currentQuery}`);