mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
Multi-line kql bar (#70140)
* Multiline kql bar * fix id * use visibility rather than display to hide stuff, cross fingers for tests * another vis trick for tests * quasi fix tests, still some failures * caroline feedback * fun! * fix for mouse * fix test * check api * fix unit test on query_string_input * Fix cypress test * handle the resize of the height of the textarea when the window have been resize Co-authored-by: Xavier Mouligneau <189600+XavierM@users.noreply.github.com> Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com> Co-authored-by: Liza K <liza.katz@elastic.co>
This commit is contained in:
parent
93ac059cac
commit
c815c96937
15 changed files with 237 additions and 123 deletions
|
@ -7,5 +7,5 @@
|
|||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
QueryStringInput: React.FC<Pick<Props, "query" | "prepend" | "placeholder" | "onChange" | "onBlur" | "onSubmit" | "indexPatterns" | "dataTestSubj" | "screenTitle" | "disableAutoFocus" | "persistedLog" | "bubbleSubmitEvent" | "languageSwitcherPopoverAnchorPosition">>
|
||||
QueryStringInput: React.FC<Pick<Props, "query" | "prepend" | "placeholder" | "onChange" | "onBlur" | "onSubmit" | "indexPatterns" | "dataTestSubj" | "screenTitle" | "disableAutoFocus" | "persistedLog" | "bubbleSubmitEvent" | "languageSwitcherPopoverAnchorPosition" | "onChangeQueryInputFocus">>
|
||||
```
|
||||
|
|
|
@ -51,7 +51,6 @@ import { ErrorToastOptions } from 'src/core/public/notifications';
|
|||
import { EuiButtonEmptyProps } from '@elastic/eui';
|
||||
import { EuiComboBoxProps } from '@elastic/eui';
|
||||
import { EuiConfirmModalProps } from '@elastic/eui';
|
||||
import { EuiFieldText } from '@elastic/eui';
|
||||
import { EuiGlobalToastListToast } from '@elastic/eui';
|
||||
import { ExclusiveUnion } from '@elastic/eui';
|
||||
import { ExistsParams } from 'elasticsearch';
|
||||
|
@ -1482,7 +1481,7 @@ export interface QueryState {
|
|||
// Warning: (ae-missing-release-tag) "QueryStringInput" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
export const QueryStringInput: React.FC<Pick<Props_3, "query" | "prepend" | "placeholder" | "onChange" | "onBlur" | "onSubmit" | "indexPatterns" | "dataTestSubj" | "screenTitle" | "disableAutoFocus" | "persistedLog" | "bubbleSubmitEvent" | "languageSwitcherPopoverAnchorPosition">>;
|
||||
export const QueryStringInput: React.FC<Pick<Props_3, "query" | "prepend" | "placeholder" | "onChange" | "onBlur" | "onSubmit" | "indexPatterns" | "dataTestSubj" | "screenTitle" | "disableAutoFocus" | "persistedLog" | "bubbleSubmitEvent" | "languageSwitcherPopoverAnchorPosition" | "onChangeQueryInputFocus">>;
|
||||
|
||||
// @public (undocumented)
|
||||
export type QuerySuggestion = QuerySuggestionBasic | QuerySuggestionField;
|
||||
|
|
|
@ -1,3 +1,41 @@
|
|||
.kbnQueryBar__wrap {
|
||||
max-width: 100%;
|
||||
z-index: $euiZContentMenu;
|
||||
}
|
||||
|
||||
// Uses the append style, but no bordering
|
||||
.kqlQueryBar__languageSwitcherButton {
|
||||
border-right: none !important;
|
||||
}
|
||||
|
||||
.kbnQueryBar__textarea {
|
||||
z-index: $euiZContentMenu;
|
||||
resize: none !important; // When in the group, it will autosize
|
||||
height: $euiSizeXXL;
|
||||
// Unlike most inputs within layout control groups, the text area still needs a border.
|
||||
// These adjusts help it sit above the control groups shadow to line up correctly.
|
||||
padding-top: $euiSizeS + 3px !important;
|
||||
transform: translateY(-2px);
|
||||
padding: $euiSizeS - 1px;
|
||||
|
||||
&:not(:focus) {
|
||||
@include euiYScrollWithShadows;
|
||||
white-space: nowrap;
|
||||
overflow-y: hidden;
|
||||
overflow-x: hidden;
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
// When focused, let it scroll
|
||||
&:focus {
|
||||
overflow-x: auto;
|
||||
overflow-y: auto;
|
||||
width: calc(100% + 1px); // To overtake the group's fake border
|
||||
white-space: normal;
|
||||
}
|
||||
}
|
||||
|
||||
@include euiBreakpoint('xs', 's') {
|
||||
.kbnQueryBar--withDatePicker {
|
||||
> :first-child {
|
||||
|
@ -16,5 +54,11 @@
|
|||
// sass-lint:disable-block no-important
|
||||
flex-grow: 0 !important;
|
||||
flex-basis: auto !important;
|
||||
margin-right: -$euiSizeXS !important;
|
||||
|
||||
&.kbnQueryBar__datePickerWrapper-isHidden {
|
||||
width: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -60,7 +60,7 @@ export function QueryLanguageSwitcher(props: Props) {
|
|||
<EuiButtonEmpty
|
||||
size="xs"
|
||||
onClick={() => setIsPopoverOpen(!isPopoverOpen)}
|
||||
className="euiFormControlLayout__append"
|
||||
className="euiFormControlLayout__append kqlQueryBar__languageSwitcherButton"
|
||||
data-test-subj={'switchQueryLanguageButton'}
|
||||
>
|
||||
{props.language === 'lucene' ? luceneLabel : kqlLabel}
|
||||
|
|
|
@ -69,6 +69,7 @@ interface Props {
|
|||
|
||||
export function QueryBarTopRow(props: Props) {
|
||||
const [isDateRangeInvalid, setIsDateRangeInvalid] = useState(false);
|
||||
const [isQueryInputFocused, setIsQueryInputFocused] = useState(false);
|
||||
|
||||
const kibana = useKibana<IDataPluginServices>();
|
||||
const { uiSettings, notifications, storage, appName, docLinks } = kibana.services;
|
||||
|
@ -107,6 +108,10 @@ export function QueryBarTopRow(props: Props) {
|
|||
});
|
||||
}
|
||||
|
||||
function onChangeQueryInputFocus(isFocused: boolean) {
|
||||
setIsQueryInputFocused(isFocused);
|
||||
}
|
||||
|
||||
function onTimeChange({
|
||||
start,
|
||||
end,
|
||||
|
@ -182,6 +187,7 @@ export function QueryBarTopRow(props: Props) {
|
|||
query={props.query!}
|
||||
screenTitle={props.screenTitle}
|
||||
onChange={onQueryChange}
|
||||
onChangeQueryInputFocus={onChangeQueryInputFocus}
|
||||
onSubmit={onInputSubmit}
|
||||
persistedLog={persistedLog}
|
||||
dataTestSubj={props.dataTestSubj}
|
||||
|
@ -268,8 +274,12 @@ export function QueryBarTopRow(props: Props) {
|
|||
};
|
||||
});
|
||||
|
||||
const wrapperClasses = classNames('kbnQueryBar__datePickerWrapper', {
|
||||
'kbnQueryBar__datePickerWrapper-isHidden': isQueryInputFocused,
|
||||
});
|
||||
|
||||
return (
|
||||
<EuiFlexItem className="kbnQueryBar__datePickerWrapper">
|
||||
<EuiFlexItem className={wrapperClasses}>
|
||||
<EuiSuperDatePicker
|
||||
start={props.dateRangeFrom}
|
||||
end={props.dateRangeTo}
|
||||
|
@ -283,6 +293,7 @@ export function QueryBarTopRow(props: Props) {
|
|||
commonlyUsedRanges={commonlyUsedRanges}
|
||||
dateFormat={uiSettings!.get('dateFormat')}
|
||||
isAutoRefreshOnly={props.showAutoRefreshOnly}
|
||||
className="kbnQueryBar__datePicker"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
);
|
||||
|
|
|
@ -23,7 +23,7 @@ import {
|
|||
mockPersistedLogFactory,
|
||||
} from './query_string_input.test.mocks';
|
||||
|
||||
import { EuiFieldText } from '@elastic/eui';
|
||||
import { EuiTextArea } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import { QueryLanguageSwitcher } from './language_switcher';
|
||||
import { QueryStringInput, QueryStringInputUI } from './query_string_input';
|
||||
|
@ -102,7 +102,7 @@ describe('QueryStringInput', () => {
|
|||
indexPatterns: [stubIndexPatternWithFields],
|
||||
})
|
||||
);
|
||||
expect(component.find(EuiFieldText).props().value).toBe(kqlQuery.query);
|
||||
expect(component.find(EuiTextArea).props().value).toBe(kqlQuery.query);
|
||||
expect(component.find(QueryLanguageSwitcher).prop('language')).toBe(kqlQuery.language);
|
||||
});
|
||||
|
||||
|
@ -117,7 +117,7 @@ describe('QueryStringInput', () => {
|
|||
expect(component.find(QueryLanguageSwitcher).prop('language')).toBe(luceneQuery.language);
|
||||
});
|
||||
|
||||
it('Should disable autoFocus on EuiFieldText when disableAutoFocus prop is true', () => {
|
||||
it('Should disable autoFocus on EuiTextArea when disableAutoFocus prop is true', () => {
|
||||
const component = mount(
|
||||
wrapQueryStringInputInContext({
|
||||
query: kqlQuery,
|
||||
|
@ -126,7 +126,7 @@ describe('QueryStringInput', () => {
|
|||
disableAutoFocus: true,
|
||||
})
|
||||
);
|
||||
expect(component.find(EuiFieldText).prop('autoFocus')).toBeFalsy();
|
||||
expect(component.find(EuiTextArea).prop('autoFocus')).toBeFalsy();
|
||||
});
|
||||
|
||||
it('Should create a unique PersistedLog based on the appName and query language', () => {
|
||||
|
@ -179,7 +179,7 @@ describe('QueryStringInput', () => {
|
|||
|
||||
const instance = component.find('QueryStringInputUI').instance() as QueryStringInputUI;
|
||||
const input = instance.inputRef;
|
||||
const inputWrapper = component.find(EuiFieldText).find('input');
|
||||
const inputWrapper = component.find(EuiTextArea).find('textarea');
|
||||
inputWrapper.simulate('keyDown', { target: input, keyCode: 13, key: 'Enter', metaKey: true });
|
||||
|
||||
expect(mockCallback).toHaveBeenCalledTimes(1);
|
||||
|
@ -199,7 +199,7 @@ describe('QueryStringInput', () => {
|
|||
|
||||
const instance = component.find('QueryStringInputUI').instance() as QueryStringInputUI;
|
||||
const input = instance.inputRef;
|
||||
const inputWrapper = component.find(EuiFieldText).find('input');
|
||||
const inputWrapper = component.find(EuiTextArea).find('textarea');
|
||||
inputWrapper.simulate('keyDown', { target: input, keyCode: 13, key: 'Enter', metaKey: true });
|
||||
|
||||
expect(mockPersistedLog.add).toHaveBeenCalledWith('response:200');
|
||||
|
|
|
@ -22,13 +22,14 @@ import React from 'react';
|
|||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import {
|
||||
EuiFieldText,
|
||||
EuiTextArea,
|
||||
EuiOutsideClickDetector,
|
||||
PopoverAnchorPosition,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiButton,
|
||||
EuiLink,
|
||||
htmlIdGenerator,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
|
@ -49,13 +50,14 @@ interface Props {
|
|||
query: Query;
|
||||
disableAutoFocus?: boolean;
|
||||
screenTitle?: string;
|
||||
prepend?: React.ComponentProps<typeof EuiFieldText>['prepend'];
|
||||
prepend?: any;
|
||||
persistedLog?: PersistedLog;
|
||||
bubbleSubmitEvent?: boolean;
|
||||
placeholder?: string;
|
||||
languageSwitcherPopoverAnchorPosition?: PopoverAnchorPosition;
|
||||
onBlur?: () => void;
|
||||
onChange?: (query: Query) => void;
|
||||
onChangeQueryInputFocus?: (isFocused: boolean) => void;
|
||||
onSubmit?: (query: Query) => void;
|
||||
dataTestSubj?: string;
|
||||
}
|
||||
|
@ -93,7 +95,7 @@ export class QueryStringInputUI extends Component<Props, State> {
|
|||
indexPatterns: [],
|
||||
};
|
||||
|
||||
public inputRef: HTMLInputElement | null = null;
|
||||
public inputRef: HTMLTextAreaElement | null = null;
|
||||
|
||||
private persistedLog: PersistedLog | undefined;
|
||||
private abortController?: AbortController;
|
||||
|
@ -223,27 +225,32 @@ export class QueryStringInputUI extends Component<Props, State> {
|
|||
this.onChange({ query: value, language: this.props.query.language });
|
||||
};
|
||||
|
||||
private onInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
private onInputChange = (event: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
this.onQueryStringChange(event.target.value);
|
||||
if (event.target.value === '') {
|
||||
this.handleRemoveHeight();
|
||||
} else {
|
||||
this.handleAutoHeight();
|
||||
}
|
||||
};
|
||||
|
||||
private onClickInput = (event: React.MouseEvent<HTMLInputElement>) => {
|
||||
if (event.target instanceof HTMLInputElement) {
|
||||
private onClickInput = (event: React.MouseEvent<HTMLTextAreaElement>) => {
|
||||
if (event.target instanceof HTMLTextAreaElement) {
|
||||
this.onQueryStringChange(event.target.value);
|
||||
}
|
||||
};
|
||||
|
||||
private onKeyUp = (event: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
private onKeyUp = (event: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
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) {
|
||||
if (event.target instanceof HTMLTextAreaElement) {
|
||||
this.onQueryStringChange(event.target.value);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private onKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (event.target instanceof HTMLInputElement) {
|
||||
private onKeyDown = (event: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
if (event.target instanceof HTMLTextAreaElement) {
|
||||
const { isSuggestionsVisible, index } = this.state;
|
||||
const preventDefault = event.preventDefault.bind(event);
|
||||
const { target, key, metaKey } = event;
|
||||
|
@ -258,16 +265,19 @@ export class QueryStringInputUI extends Component<Props, State> {
|
|||
|
||||
switch (event.keyCode) {
|
||||
case KEY_CODES.DOWN:
|
||||
event.preventDefault();
|
||||
if (isSuggestionsVisible && index !== null) {
|
||||
event.preventDefault();
|
||||
this.incrementIndex(index);
|
||||
} else {
|
||||
// Note to engineers. `isSuggestionVisible` does not mean the suggestions are visible.
|
||||
// This should likely be fixed, it's more that suggestions can be shown.
|
||||
} else if ((isSuggestionsVisible && index == null) || this.getQueryString() === '') {
|
||||
event.preventDefault();
|
||||
this.setState({ isSuggestionsVisible: true, index: 0 });
|
||||
}
|
||||
break;
|
||||
case KEY_CODES.UP:
|
||||
event.preventDefault();
|
||||
if (isSuggestionsVisible && index !== null) {
|
||||
event.preventDefault();
|
||||
this.decrementIndex(index);
|
||||
}
|
||||
break;
|
||||
|
@ -439,6 +449,17 @@ export class QueryStringInputUI extends Component<Props, State> {
|
|||
if (this.state.isSuggestionsVisible) {
|
||||
this.setState({ isSuggestionsVisible: false, index: null });
|
||||
}
|
||||
this.handleBlurHeight();
|
||||
if (this.props.onChangeQueryInputFocus) {
|
||||
this.props.onChangeQueryInputFocus(false);
|
||||
}
|
||||
};
|
||||
|
||||
private onInputBlur = () => {
|
||||
this.handleBlurHeight();
|
||||
if (this.props.onChangeQueryInputFocus) {
|
||||
this.props.onChangeQueryInputFocus(false);
|
||||
}
|
||||
};
|
||||
|
||||
private onClickSuggestion = (suggestion: QuerySuggestion) => {
|
||||
|
@ -460,6 +481,8 @@ export class QueryStringInputUI extends Component<Props, State> {
|
|||
this.setState({ index });
|
||||
};
|
||||
|
||||
textareaId = htmlIdGenerator()();
|
||||
|
||||
public componentDidMount() {
|
||||
const parsedQuery = fromUser(toUser(this.props.query.query));
|
||||
if (!isEqual(this.props.query.query, parsedQuery)) {
|
||||
|
@ -468,6 +491,8 @@ export class QueryStringInputUI extends Component<Props, State> {
|
|||
|
||||
this.initPersistedLog();
|
||||
this.fetchIndexPatterns().then(this.updateSuggestions);
|
||||
|
||||
window.addEventListener('resize', this.handleAutoHeight);
|
||||
}
|
||||
|
||||
public componentDidUpdate(prevProps: Props) {
|
||||
|
@ -485,15 +510,18 @@ export class QueryStringInputUI extends Component<Props, State> {
|
|||
}
|
||||
|
||||
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
|
||||
if (this.inputRef != null) {
|
||||
this.inputRef.setSelectionRange(this.state.selectionStart, this.state.selectionEnd);
|
||||
}
|
||||
this.setState({
|
||||
selectionStart: null,
|
||||
selectionEnd: null,
|
||||
});
|
||||
if (document.activeElement !== null && document.activeElement.id === this.textareaId) {
|
||||
this.handleAutoHeight();
|
||||
} else {
|
||||
this.handleRemoveHeight();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -501,8 +529,37 @@ export class QueryStringInputUI extends Component<Props, State> {
|
|||
if (this.abortController) this.abortController.abort();
|
||||
this.updateSuggestions.cancel();
|
||||
this.componentIsUnmounting = true;
|
||||
window.removeEventListener('resize', this.handleAutoHeight);
|
||||
}
|
||||
|
||||
handleAutoHeight = () => {
|
||||
if (this.inputRef !== null && document.activeElement === this.inputRef) {
|
||||
this.inputRef.style.setProperty('height', `${this.inputRef.scrollHeight}px`, 'important');
|
||||
}
|
||||
};
|
||||
|
||||
handleRemoveHeight = () => {
|
||||
if (this.inputRef !== null) {
|
||||
this.inputRef.style.removeProperty('height');
|
||||
}
|
||||
};
|
||||
|
||||
handleBlurHeight = () => {
|
||||
if (this.inputRef !== null) {
|
||||
this.handleRemoveHeight();
|
||||
this.inputRef.scrollTop = 0;
|
||||
}
|
||||
};
|
||||
|
||||
handleOnFocus = () => {
|
||||
if (this.props.onChangeQueryInputFocus) {
|
||||
this.props.onChangeQueryInputFocus(true);
|
||||
}
|
||||
requestAnimationFrame(() => {
|
||||
this.handleAutoHeight();
|
||||
});
|
||||
};
|
||||
|
||||
public render() {
|
||||
const isSuggestionsVisible = this.state.isSuggestionsVisible && {
|
||||
'aria-controls': 'kbnTypeahead__items',
|
||||
|
@ -511,20 +568,24 @@ export class QueryStringInputUI extends Component<Props, State> {
|
|||
const ariaCombobox = { ...isSuggestionsVisible, role: 'combobox' };
|
||||
|
||||
return (
|
||||
<EuiOutsideClickDetector onOutsideClick={this.onOutsideClick}>
|
||||
<div
|
||||
{...ariaCombobox}
|
||||
style={{ position: 'relative' }}
|
||||
aria-label={i18n.translate('data.query.queryBar.comboboxAriaLabel', {
|
||||
defaultMessage: 'Search and filter the {pageType} page',
|
||||
values: { pageType: this.services.appName },
|
||||
})}
|
||||
aria-haspopup="true"
|
||||
aria-expanded={this.state.isSuggestionsVisible}
|
||||
>
|
||||
<div role="search">
|
||||
<div className="kuiLocalSearchAssistedInput">
|
||||
<EuiFieldText
|
||||
<div className="euiFormControlLayout euiFormControlLayout--group kbnQueryBar__wrap">
|
||||
{this.props.prepend}
|
||||
<EuiOutsideClickDetector onOutsideClick={this.onOutsideClick}>
|
||||
<div
|
||||
{...ariaCombobox}
|
||||
style={{ position: 'relative', width: '100%' }}
|
||||
aria-label={i18n.translate('data.query.queryBar.comboboxAriaLabel', {
|
||||
defaultMessage: 'Search and filter the {pageType} page',
|
||||
values: { pageType: this.services.appName },
|
||||
})}
|
||||
aria-haspopup="true"
|
||||
aria-expanded={this.state.isSuggestionsVisible}
|
||||
>
|
||||
<div
|
||||
role="search"
|
||||
className="euiFormControlLayout__childrenWrapper kuiLocalSearchAssistedInput"
|
||||
>
|
||||
<EuiTextArea
|
||||
placeholder={
|
||||
this.props.placeholder ||
|
||||
i18n.translate('data.query.queryBar.searchInputPlaceholder', {
|
||||
|
@ -536,10 +597,16 @@ export class QueryStringInputUI extends Component<Props, State> {
|
|||
onKeyUp={this.onKeyUp}
|
||||
onChange={this.onInputChange}
|
||||
onClick={this.onClickInput}
|
||||
onBlur={this.props.onBlur}
|
||||
onBlur={this.onInputBlur}
|
||||
onFocus={this.handleOnFocus}
|
||||
className="kbnQueryBar__textarea"
|
||||
fullWidth
|
||||
autoFocus={!this.props.disableAutoFocus}
|
||||
inputRef={(node) => {
|
||||
rows={1}
|
||||
id={this.textareaId}
|
||||
autoFocus={
|
||||
this.props.onChangeQueryInputFocus ? false : !this.props.disableAutoFocus
|
||||
}
|
||||
inputRef={(node: any) => {
|
||||
if (node) {
|
||||
this.inputRef = node;
|
||||
}
|
||||
|
@ -550,7 +617,6 @@ export class QueryStringInputUI extends Component<Props, State> {
|
|||
defaultMessage: 'Start typing to search and filter the {pageType} page',
|
||||
values: { pageType: this.services.appName },
|
||||
})}
|
||||
type="text"
|
||||
aria-autocomplete="list"
|
||||
aria-controls={this.state.isSuggestionsVisible ? 'kbnTypeahead__items' : undefined}
|
||||
aria-activedescendant={
|
||||
|
@ -559,29 +625,29 @@ export class QueryStringInputUI extends Component<Props, State> {
|
|||
: undefined
|
||||
}
|
||||
role="textbox"
|
||||
prepend={this.props.prepend}
|
||||
append={
|
||||
<QueryLanguageSwitcher
|
||||
language={this.props.query.language}
|
||||
anchorPosition={this.props.languageSwitcherPopoverAnchorPosition}
|
||||
onSelectLanguage={this.onSelectLanguage}
|
||||
/>
|
||||
}
|
||||
data-test-subj={this.props.dataTestSubj || 'queryInput'}
|
||||
/>
|
||||
>
|
||||
{this.getQueryString()}
|
||||
</EuiTextArea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
<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>
|
||||
|
||||
<QueryLanguageSwitcher
|
||||
language={this.props.query.language}
|
||||
anchorPosition={this.props.languageSwitcherPopoverAnchorPosition}
|
||||
onSelectLanguage={this.onSelectLanguage}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,7 +16,7 @@ $kbnTypeaheadTypes: (
|
|||
color: $euiTextColor;
|
||||
background-color: $euiColorEmptyShade;
|
||||
position: absolute;
|
||||
top: -1px;
|
||||
top: -2px;
|
||||
z-index: $euiZContentMenu;
|
||||
width: 100%;
|
||||
border-bottom-left-radius: $euiBorderRadius;
|
||||
|
@ -56,7 +56,6 @@ $kbnTypeaheadTypes: (
|
|||
.kbnTypeahead__item.active {
|
||||
background-color: $euiColorLightestShade;
|
||||
|
||||
|
||||
.kbnSuggestionItem__callout {
|
||||
background: $euiColorEmptyShade;
|
||||
}
|
||||
|
@ -130,7 +129,6 @@ $kbnTypeaheadTypes: (
|
|||
align-items: center;
|
||||
}
|
||||
|
||||
|
||||
.kbnSuggestionItem__text {
|
||||
flex-grow: 0; /* 2 */
|
||||
flex-basis: auto; /* 2 */
|
||||
|
@ -142,16 +140,15 @@ $kbnTypeaheadTypes: (
|
|||
color: $euiTextColor;
|
||||
}
|
||||
|
||||
|
||||
.kbnSuggestionItem__description {
|
||||
color: $euiColorDarkShade;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
margin-left: $euiSizeXL;
|
||||
|
||||
|
||||
&:empty {
|
||||
flex-grow: 0;
|
||||
margin-left:0;
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -218,6 +218,8 @@ export default function ({ getService, getPageObjects }) {
|
|||
await PageObjects.common.navigateToApp('discover');
|
||||
await PageObjects.header.awaitKibanaChrome();
|
||||
await queryBar.setQuery('');
|
||||
// To remove focus of the of the search bar so date/time picker can show
|
||||
await PageObjects.discover.selectIndexPattern(defaultSettings.defaultIndex);
|
||||
await PageObjects.timePicker.setDefaultAbsoluteRange();
|
||||
|
||||
log.debug(
|
||||
|
|
|
@ -99,6 +99,6 @@ describe('Cases', () => {
|
|||
|
||||
cy.get(TIMELINE_TITLE).should('have.attr', 'value', case1.timeline.title);
|
||||
cy.get(TIMELINE_DESCRIPTION).should('have.attr', 'value', case1.timeline.description);
|
||||
cy.get(TIMELINE_QUERY).should('have.attr', 'value', case1.timeline.query);
|
||||
cy.get(TIMELINE_QUERY).invoke('text').should('eq', case1.timeline.query);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -27,74 +27,67 @@ import {
|
|||
describe('ml conditional links', () => {
|
||||
it('sets the KQL from a single IP with a value for the query', () => {
|
||||
loginAndWaitForPageWithoutDateRange(mlNetworkSingleIpKqlQuery);
|
||||
cy.get(KQL_INPUT).should(
|
||||
'have.attr',
|
||||
'value',
|
||||
'(process.name: "conhost.exe" or process.name: "sc.exe")'
|
||||
);
|
||||
cy.get(KQL_INPUT)
|
||||
.invoke('text')
|
||||
.should('eq', '(process.name: "conhost.exe" or process.name: "sc.exe")');
|
||||
});
|
||||
|
||||
it('sets the KQL from a multiple IPs with a null for the query', () => {
|
||||
loginAndWaitForPageWithoutDateRange(mlNetworkMultipleIpNullKqlQuery);
|
||||
cy.get(KQL_INPUT).should(
|
||||
'have.attr',
|
||||
'value',
|
||||
'((source.ip: "127.0.0.1" or destination.ip: "127.0.0.1") or (source.ip: "127.0.0.2" or destination.ip: "127.0.0.2"))'
|
||||
);
|
||||
cy.get(KQL_INPUT)
|
||||
.invoke('text')
|
||||
.should(
|
||||
'eq',
|
||||
'((source.ip: "127.0.0.1" or destination.ip: "127.0.0.1") or (source.ip: "127.0.0.2" or destination.ip: "127.0.0.2"))'
|
||||
);
|
||||
});
|
||||
|
||||
it('sets the KQL from a multiple IPs with a value for the query', () => {
|
||||
loginAndWaitForPageWithoutDateRange(mlNetworkMultipleIpKqlQuery);
|
||||
cy.get(KQL_INPUT).should(
|
||||
'have.attr',
|
||||
'value',
|
||||
'((source.ip: "127.0.0.1" or destination.ip: "127.0.0.1") or (source.ip: "127.0.0.2" or destination.ip: "127.0.0.2")) and ((process.name: "conhost.exe" or process.name: "sc.exe"))'
|
||||
);
|
||||
cy.get(KQL_INPUT)
|
||||
.invoke('text')
|
||||
.should(
|
||||
'eq',
|
||||
'((source.ip: "127.0.0.1" or destination.ip: "127.0.0.1") or (source.ip: "127.0.0.2" or destination.ip: "127.0.0.2")) and ((process.name: "conhost.exe" or process.name: "sc.exe"))'
|
||||
);
|
||||
});
|
||||
|
||||
it('sets the KQL from a $ip$ with a value for the query', () => {
|
||||
loginAndWaitForPageWithoutDateRange(mlNetworkKqlQuery);
|
||||
cy.get(KQL_INPUT).should(
|
||||
'have.attr',
|
||||
'value',
|
||||
'(process.name: "conhost.exe" or process.name: "sc.exe")'
|
||||
);
|
||||
cy.get(KQL_INPUT)
|
||||
.invoke('text')
|
||||
.should('eq', '(process.name: "conhost.exe" or process.name: "sc.exe")');
|
||||
});
|
||||
|
||||
it('sets the KQL from a single host name with a value for query', () => {
|
||||
loginAndWaitForPageWithoutDateRange(mlHostSingleHostKqlQuery);
|
||||
cy.get(KQL_INPUT).should(
|
||||
'have.attr',
|
||||
'value',
|
||||
'(process.name: "conhost.exe" or process.name: "sc.exe")'
|
||||
);
|
||||
cy.get(KQL_INPUT)
|
||||
.invoke('text')
|
||||
.should('eq', '(process.name: "conhost.exe" or process.name: "sc.exe")');
|
||||
});
|
||||
|
||||
it('sets the KQL from a multiple host names with null for query', () => {
|
||||
loginAndWaitForPageWithoutDateRange(mlHostMultiHostNullKqlQuery);
|
||||
cy.get(KQL_INPUT).should(
|
||||
'have.attr',
|
||||
'value',
|
||||
'(host.name: "siem-windows" or host.name: "siem-suricata")'
|
||||
);
|
||||
cy.get(KQL_INPUT)
|
||||
.invoke('text')
|
||||
.should('eq', '(host.name: "siem-windows" or host.name: "siem-suricata")');
|
||||
});
|
||||
|
||||
it('sets the KQL from a multiple host names with a value for query', () => {
|
||||
loginAndWaitForPageWithoutDateRange(mlHostMultiHostKqlQuery);
|
||||
cy.get(KQL_INPUT).should(
|
||||
'have.attr',
|
||||
'value',
|
||||
'(host.name: "siem-windows" or host.name: "siem-suricata") and ((process.name: "conhost.exe" or process.name: "sc.exe"))'
|
||||
);
|
||||
cy.get(KQL_INPUT)
|
||||
.invoke('text')
|
||||
.should(
|
||||
'eq',
|
||||
'(host.name: "siem-windows" or host.name: "siem-suricata") and ((process.name: "conhost.exe" or process.name: "sc.exe"))'
|
||||
);
|
||||
});
|
||||
|
||||
it('sets the KQL from a undefined/null host name but with a value for query', () => {
|
||||
loginAndWaitForPageWithoutDateRange(mlHostVariableHostKqlQuery);
|
||||
cy.get(KQL_INPUT).should(
|
||||
'have.attr',
|
||||
'value',
|
||||
'(process.name: "conhost.exe" or process.name: "sc.exe")'
|
||||
);
|
||||
cy.get(KQL_INPUT)
|
||||
.invoke('text')
|
||||
.should('eq', '(process.name: "conhost.exe" or process.name: "sc.exe")');
|
||||
});
|
||||
|
||||
it('redirects from a single IP with a null for the query', () => {
|
||||
|
|
|
@ -154,12 +154,12 @@ describe('url state', () => {
|
|||
|
||||
it('sets kql on network page', () => {
|
||||
loginAndWaitForPageWithoutDateRange(ABSOLUTE_DATE_RANGE.urlKqlNetworkNetwork);
|
||||
cy.get(KQL_INPUT).should('have.attr', 'value', 'source.ip: "10.142.0.9"');
|
||||
cy.get(KQL_INPUT).invoke('text').should('eq', 'source.ip: "10.142.0.9"');
|
||||
});
|
||||
|
||||
it('sets kql on hosts page', () => {
|
||||
loginAndWaitForPageWithoutDateRange(ABSOLUTE_DATE_RANGE.urlKqlHostsHosts);
|
||||
cy.get(KQL_INPUT).should('have.attr', 'value', 'source.ip: "10.142.0.9"');
|
||||
cy.get(KQL_INPUT).invoke('text').should('eq', 'source.ip: "10.142.0.9"');
|
||||
});
|
||||
|
||||
it('sets the url state when kql is set', () => {
|
||||
|
@ -230,8 +230,7 @@ describe('url state', () => {
|
|||
it('Do not clears kql when navigating to a new page', () => {
|
||||
loginAndWaitForPageWithoutDateRange(ABSOLUTE_DATE_RANGE.urlKqlHostsHosts);
|
||||
navigateFromHeaderTo(NETWORK);
|
||||
|
||||
cy.get(KQL_INPUT).should('have.attr', 'value', 'source.ip: "10.142.0.9"');
|
||||
cy.get(KQL_INPUT).invoke('text').should('eq', 'source.ip: "10.142.0.9"');
|
||||
});
|
||||
|
||||
it.skip('sets and reads the url state for timeline by id', () => {
|
||||
|
|
|
@ -82,7 +82,7 @@ export const fillAboutRuleAndContinue = (rule: CustomRule | MachineLearningRule)
|
|||
|
||||
export const fillDefineCustomRuleAndContinue = (rule: CustomRule) => {
|
||||
cy.get(CUSTOM_QUERY_INPUT).type(rule.customQuery);
|
||||
cy.get(CUSTOM_QUERY_INPUT).should('have.attr', 'value', rule.customQuery);
|
||||
cy.get(CUSTOM_QUERY_INPUT).invoke('text').should('eq', rule.customQuery);
|
||||
cy.get(DEFINE_CONTINUE_BUTTON).should('exist').click({ force: true });
|
||||
|
||||
cy.get(CUSTOM_QUERY_INPUT).should('not.exist');
|
||||
|
@ -91,7 +91,7 @@ export const fillDefineCustomRuleAndContinue = (rule: CustomRule) => {
|
|||
export const fillDefineCustomRuleWithImportedQueryAndContinue = (rule: CustomRule) => {
|
||||
cy.get(IMPORT_QUERY_FROM_SAVED_TIMELINE_LINK).click();
|
||||
cy.get(TIMELINE(rule.timelineId)).click();
|
||||
cy.get(CUSTOM_QUERY_INPUT).should('have.attr', 'value', rule.customQuery);
|
||||
cy.get(CUSTOM_QUERY_INPUT).invoke('text').should('eq', rule.customQuery);
|
||||
cy.get(DEFINE_CONTINUE_BUTTON).should('exist').click({ force: true });
|
||||
|
||||
cy.get(CUSTOM_QUERY_INPUT).should('not.exist');
|
||||
|
|
|
@ -58,7 +58,7 @@ export const createNewTimeline = () => {
|
|||
};
|
||||
|
||||
export const executeTimelineKQL = (query: string) => {
|
||||
cy.get(`${SEARCH_OR_FILTER_CONTAINER} input`).type(`${query} {enter}`);
|
||||
cy.get(`${SEARCH_OR_FILTER_CONTAINER} textarea`).type(`${query} {enter}`);
|
||||
};
|
||||
|
||||
export const expandFirstTimelineEventDetails = () => {
|
||||
|
|
|
@ -214,15 +214,18 @@ describe('QueryBar ', () => {
|
|||
/>
|
||||
);
|
||||
|
||||
const queryInput = wrapper.find(QueryBar).find('input[data-test-subj="queryInput"]');
|
||||
let queryInput = wrapper.find(QueryBar).find('textarea[data-test-subj="queryInput"]');
|
||||
queryInput.simulate('change', { target: { value: 'host.name:*' } });
|
||||
|
||||
expect(queryInput.html()).toContain('value="host.name:*"');
|
||||
wrapper.update();
|
||||
queryInput = wrapper.find(QueryBar).find('textarea[data-test-subj="queryInput"]');
|
||||
expect(queryInput.props().children).toBe('host.name:*');
|
||||
|
||||
wrapper.setProps({ filterQueryDraft: null });
|
||||
wrapper.update();
|
||||
queryInput = wrapper.find(QueryBar).find('textarea[data-test-subj="queryInput"]');
|
||||
|
||||
expect(queryInput.html()).toContain('value=""');
|
||||
expect(queryInput.props().children).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -258,7 +261,7 @@ describe('QueryBar ', () => {
|
|||
const onSubmitQueryRef = searchBarProps.onQuerySubmit;
|
||||
const onSavedQueryRef = searchBarProps.onSavedQueryUpdated;
|
||||
|
||||
const queryInput = wrapper.find(QueryBar).find('input[data-test-subj="queryInput"]');
|
||||
const queryInput = wrapper.find(QueryBar).find('textarea[data-test-subj="queryInput"]');
|
||||
queryInput.simulate('change', { target: { value: 'hello: world' } });
|
||||
wrapper.update();
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue