diff --git a/x-pack/plugins/ml/common/constants/search.ts b/x-pack/plugins/ml/common/constants/search.ts index e17f6b3098421..da65748668a4f 100644 --- a/x-pack/plugins/ml/common/constants/search.ts +++ b/x-pack/plugins/ml/common/constants/search.ts @@ -11,3 +11,8 @@ export enum SEARCH_QUERY_LANGUAGE { KUERY = 'kuery', LUCENE = 'lucene', } + +export interface ErrorMessage { + query: string; + message: string; +} diff --git a/x-pack/plugins/ml/public/application/components/anomalies_table/links_menu.js b/x-pack/plugins/ml/public/application/components/anomalies_table/links_menu.js index 7da49a378ec96..2a34f12330a75 100644 --- a/x-pack/plugins/ml/public/application/components/anomalies_table/links_menu.js +++ b/x-pack/plugins/ml/public/application/components/anomalies_table/links_menu.js @@ -21,7 +21,7 @@ import { checkPermission } from '../../privilege/check_privilege'; import { SEARCH_QUERY_LANGUAGE } from '../../../../common/constants/search'; import { isRuleSupported } from '../../../../common/util/anomaly_utils'; import { parseInterval } from '../../../../common/util/parse_interval'; -import { escapeDoubleQuotes } from '../kql_filter_bar/utils'; +import { escapeDoubleQuotes } from '../../explorer/explorer_utils'; import { getFieldTypeFromMapping } from '../../services/mapping_service'; import { ml } from '../../services/ml_api_service'; import { mlJobService } from '../../services/job_service'; diff --git a/x-pack/plugins/ml/public/application/components/kql_filter_bar/__snapshots__/kql_filter_bar.test.js.snap b/x-pack/plugins/ml/public/application/components/kql_filter_bar/__snapshots__/kql_filter_bar.test.js.snap deleted file mode 100644 index f2eeb00b6c643..0000000000000 --- a/x-pack/plugins/ml/public/application/components/kql_filter_bar/__snapshots__/kql_filter_bar.test.js.snap +++ /dev/null @@ -1,15 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`KqlFilterBar snapshot 1`] = ` - - - -`; diff --git a/x-pack/plugins/ml/public/application/components/kql_filter_bar/__tests__/utils.js b/x-pack/plugins/ml/public/application/components/kql_filter_bar/__tests__/utils.js deleted file mode 100644 index 6029799ffe8b8..0000000000000 --- a/x-pack/plugins/ml/public/application/components/kql_filter_bar/__tests__/utils.js +++ /dev/null @@ -1,123 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import expect from '@kbn/expect'; -import { removeFilterFromQueryString, getQueryPattern, escapeRegExp } from '../utils'; - -describe('ML - KqlFilterBar utils', () => { - const fieldName = 'http.response.status_code'; - const fieldValue = '200'; - const speciaCharFieldName = 'normal(brackets)name'; - const speciaCharFieldValue = '<>:;[})'; - - describe('removeFilterFromQueryString', () => { - it('removes selected fieldName/fieldValue from query string with one value', () => { - const currentQueryString = 'http.response.status_code : "200"'; - const expectedOutput = ''; - const result = removeFilterFromQueryString(currentQueryString, fieldName, fieldValue); - expect(result).to.be(expectedOutput); - }); - - it('removes selected fieldName/fieldValue of type number from existing query string with one value', () => { - const currentQueryString = 'http.response.status_code : 200'; - const expectedOutput = ''; - const result = removeFilterFromQueryString(currentQueryString, fieldName, fieldValue); - expect(result).to.be(expectedOutput); - }); - - it('removes selected fieldName/fieldValue from query string with multiple values', () => { - const currentQueryString = 'test_field : test_value or http.response.status_code : "200"'; - const expectedOutput = 'test_field : test_value'; - const result = removeFilterFromQueryString(currentQueryString, fieldName, fieldValue); - expect(result).to.be(expectedOutput); - }); - - it('removes selected fieldName/fieldValue of type number from existing query string with multiple values', () => { - const currentQueryString = 'http.response.status_code : 200 or test_field : test_value'; - const expectedOutput = 'test_field : test_value'; - const result = removeFilterFromQueryString(currentQueryString, fieldName, fieldValue); - expect(result).to.be(expectedOutput); - }); - - it("removes 'and' from end of the query to ensure kuery syntax is valid", () => { - const currentQueryString = 'http.response.status_code : "200" and'; - const expectedOutput = ''; - const result = removeFilterFromQueryString(currentQueryString, fieldName, fieldValue); - expect(result).to.be(expectedOutput); - }); - - it("removes 'or' from end of the query to ensure kuery syntax is valid", () => { - const currentQueryString = 'http.response.status_code : "200" or'; - const expectedOutput = ''; - const result = removeFilterFromQueryString(currentQueryString, fieldName, fieldValue); - expect(result).to.be(expectedOutput); - }); - - it("removes 'and' from start of the query to ensure kuery syntax is valid", () => { - const currentQueryString = ' and http.response.status_code : "200"'; - const expectedOutput = ''; - const result = removeFilterFromQueryString(currentQueryString, fieldName, fieldValue); - expect(result).to.be(expectedOutput); - }); - - it("removes 'or' from start of the query to ensure kuery syntax is valid", () => { - const currentQueryString = ' or http.response.status_code : "200" '; - const expectedOutput = ''; - const result = removeFilterFromQueryString(currentQueryString, fieldName, fieldValue); - expect(result).to.be(expectedOutput); - }); - - it('removes selected fieldName/fieldValue correctly from AND query string when it is the middle value', () => { - const currentQueryString = `http.response.status_code : "400" and http.response.status_code : "200" - and http.response.status_code : "300"`; - const expectedOutput = - 'http.response.status_code : "400" and http.response.status_code : "300"'; - const result = removeFilterFromQueryString(currentQueryString, fieldName, fieldValue); - expect(result).to.be(expectedOutput); - }); - - it('removes selected fieldName/fieldValue correctly from OR query string when it is the middle value', () => { - const currentQueryString = `http.response.status_code : "400" or http.response.status_code : "200" - or http.response.status_code : "300"`; - const expectedOutput = - 'http.response.status_code : "400" or http.response.status_code : "300"'; - const result = removeFilterFromQueryString(currentQueryString, fieldName, fieldValue); - expect(result).to.be(expectedOutput); - }); - }); - - describe('getQueryPattern', () => { - it('creates a regular expression pattern for given fieldName/fieldValue', () => { - // The source property returns a String containing the source text of the regexp object - // and it doesn't contain the two forward slashes on both sides and any flags. - const expectedOutput = /(http\.response\.status_code)\s?:\s?(")?(200)(")?/i.source; - const result = getQueryPattern(fieldName, fieldValue).source; - expect(result).to.be(expectedOutput); - }); - - it('creates a regular expression pattern for given fieldName/fieldValue containing special characters', () => { - // The source property returns a String containing the source text of the regexp object - // and it doesn't contain the two forward slashes on both sides and any flags. - const expectedOutput = /(normal\(brackets\)name)\s?:\s?(")?(<>:;\[\}\))(")?/i.source; - const result = getQueryPattern(speciaCharFieldName, speciaCharFieldValue).source; - expect(result).to.be(expectedOutput); - }); - }); - - describe('escapeRegExp', () => { - it('escapes regex special characters for given fieldName/fieldValue', () => { - // The source property returns a String containing the source text of the regexp object - // and it doesn't contain the two forward slashes on both sides and any flags. - const expectedFieldName = 'normal\\(brackets\\)name'; - const expectedFieldValue = '<>:;\\[\\}\\)'; - const resultFieldName = escapeRegExp(speciaCharFieldName); - const resultFieldValue = escapeRegExp(speciaCharFieldValue); - - expect(resultFieldName).to.be(expectedFieldName); - expect(resultFieldValue).to.be(expectedFieldValue); - }); - }); -}); diff --git a/x-pack/plugins/ml/public/application/components/kql_filter_bar/click_outside/__snapshots__/click_outside.test.js.snap b/x-pack/plugins/ml/public/application/components/kql_filter_bar/click_outside/__snapshots__/click_outside.test.js.snap deleted file mode 100644 index eb3e5e6005dee..0000000000000 --- a/x-pack/plugins/ml/public/application/components/kql_filter_bar/click_outside/__snapshots__/click_outside.test.js.snap +++ /dev/null @@ -1,3 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`ClickOutside snapshot 1`] = `
`; diff --git a/x-pack/plugins/ml/public/application/components/kql_filter_bar/click_outside/click_outside.js b/x-pack/plugins/ml/public/application/components/kql_filter_bar/click_outside/click_outside.js deleted file mode 100644 index 02d6750dca965..0000000000000 --- a/x-pack/plugins/ml/public/application/components/kql_filter_bar/click_outside/click_outside.js +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React, { Component } from 'react'; -import PropTypes from 'prop-types'; - -export class ClickOutside extends Component { - componentDidMount() { - document.addEventListener('mousedown', this.onClick); - } - - componentWillUnmount() { - document.removeEventListener('mousedown', this.onClick); - } - - setNodeRef = node => { - this.nodeRef = node; - }; - - onClick = event => { - if (this.nodeRef && !this.nodeRef.contains(event.target)) { - this.props.onClickOutside(); - } - }; - - render() { - const { style, children } = this.props; - - return ( -
- {children} -
- ); - } -} - -ClickOutside.propTypes = { - onClickOutside: PropTypes.func.isRequired, -}; diff --git a/x-pack/plugins/ml/public/application/components/kql_filter_bar/click_outside/click_outside.test.js b/x-pack/plugins/ml/public/application/components/kql_filter_bar/click_outside/click_outside.test.js deleted file mode 100644 index 1cd1dc6e4d715..0000000000000 --- a/x-pack/plugins/ml/public/application/components/kql_filter_bar/click_outside/click_outside.test.js +++ /dev/null @@ -1,16 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; -import { shallow } from 'enzyme'; -import { ClickOutside } from './click_outside'; - -describe('ClickOutside', () => { - test('snapshot', () => { - const wrapper = shallow( {}} />); - expect(wrapper).toMatchSnapshot(); - }); -}); diff --git a/x-pack/plugins/ml/public/application/components/kql_filter_bar/click_outside/index.js b/x-pack/plugins/ml/public/application/components/kql_filter_bar/click_outside/index.js deleted file mode 100644 index 884481f9848dd..0000000000000 --- a/x-pack/plugins/ml/public/application/components/kql_filter_bar/click_outside/index.js +++ /dev/null @@ -1,7 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export { ClickOutside } from './click_outside'; diff --git a/x-pack/plugins/ml/public/application/components/kql_filter_bar/filter_bar/__snapshots__/filter_bar.test.js.snap b/x-pack/plugins/ml/public/application/components/kql_filter_bar/filter_bar/__snapshots__/filter_bar.test.js.snap deleted file mode 100644 index f3c825a66ee2f..0000000000000 --- a/x-pack/plugins/ml/public/application/components/kql_filter_bar/filter_bar/__snapshots__/filter_bar.test.js.snap +++ /dev/null @@ -1,133 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`FilterBar snapshot suggestions not shown 1`] = ` - -
- -
- Test description for fieldValueOne

", - "end": 1, - "start": 0, - "text": "fieldValueOne", - "type": "field", - }, - Object { - "description": "

Test description for fieldValueTwo

", - "end": 1, - "start": 0, - "text": "fieldValueTwo", - "type": "field", - }, - ] - } - /> -
-`; - -exports[`FilterBar snapshot suggestions shown 1`] = ` - -
- -
- Test description for fieldValueOne

", - "end": 1, - "start": 0, - "text": "fieldValueOne", - "type": "field", - }, - Object { - "description": "

Test description for fieldValueTwo

", - "end": 1, - "start": 0, - "text": "fieldValueTwo", - "type": "field", - }, - ] - } - /> -
-`; diff --git a/x-pack/plugins/ml/public/application/components/kql_filter_bar/filter_bar/filter_bar.js b/x-pack/plugins/ml/public/application/components/kql_filter_bar/filter_bar/filter_bar.js deleted file mode 100644 index 0c1796a6e01ca..0000000000000 --- a/x-pack/plugins/ml/public/application/components/kql_filter_bar/filter_bar/filter_bar.js +++ /dev/null @@ -1,224 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React, { Component } from 'react'; -import PropTypes from 'prop-types'; -import { Suggestions } from '../suggestions'; -import { ClickOutside } from '../click_outside'; -import { EuiFieldSearch, EuiProgress, keyCodes } from '@elastic/eui'; - -export class FilterBar extends Component { - state = { - isSuggestionsVisible: false, - index: null, - value: '', - inputIsPristine: true, - }; - - static getDerivedStateFromProps(props, state) { - if (state.inputIsPristine && props.initialValue) { - return { - value: props.initialValue, - }; - } - - return null; - } - - // Set value to filter created via filter table - componentDidUpdate(oldProps) { - const newProps = this.props; - if (oldProps.valueExternal !== newProps.valueExternal) { - this.setState({ value: newProps.valueExternal, index: null }); - } - } - - incrementIndex = currentIndex => { - let nextIndex = currentIndex + 1; - if (currentIndex === null || nextIndex >= this.props.suggestions.length) { - nextIndex = 0; - } - this.setState({ index: nextIndex }); - }; - - decrementIndex = currentIndex => { - let previousIndex = currentIndex - 1; - if (previousIndex < 0) { - previousIndex = null; - } - this.setState({ index: previousIndex }); - }; - - onKeyUp = event => { - const { selectionStart } = event.target; - const { value } = this.state; - switch (event.keyCode) { - case keyCodes.LEFT: - case keyCodes.RIGHT: - this.setState({ isSuggestionsVisible: true }); - this.props.onChange(value, selectionStart); - break; - } - }; - - onKeyDown = event => { - const { isSuggestionsVisible, index, value } = this.state; - switch (event.keyCode) { - case keyCodes.DOWN: - event.preventDefault(); - if (isSuggestionsVisible) { - this.incrementIndex(index); - } else { - this.setState({ isSuggestionsVisible: true, index: 0 }); - } - break; - case keyCodes.UP: - event.preventDefault(); - if (isSuggestionsVisible) { - this.decrementIndex(index); - } - break; - case keyCodes.ENTER: - event.preventDefault(); - if (isSuggestionsVisible && this.props.suggestions[index]) { - this.selectSuggestion(this.props.suggestions[index]); - } else { - this.setState({ isSuggestionsVisible: false }); - this.props.onSubmit(value); - } - break; - case keyCodes.ESC: - event.preventDefault(); - this.setState({ isSuggestionsVisible: false }); - break; - case keyCodes.TAB: - this.setState({ isSuggestionsVisible: false }); - break; - } - }; - - selectSuggestion = suggestion => { - const nextInputValue = - this.state.value.substr(0, suggestion.start) + - suggestion.text + - this.state.value.substr(suggestion.end); - - this.setState({ value: nextInputValue, index: null }); - this.props.onChange(nextInputValue, nextInputValue.length); - }; - - onClickOutside = () => { - this.setState({ isSuggestionsVisible: false }); - }; - - onChangeInputValue = event => { - const { value, selectionStart } = event.target; - const hasValue = Boolean(value.trim()); - this.setState({ - value, - inputIsPristine: false, - isSuggestionsVisible: hasValue, - index: null, - }); - - if (!hasValue) { - this.props.onSubmit(value); - } - this.props.onChange(value, selectionStart); - }; - - onClickInput = event => { - const { selectionStart } = event.target; - this.props.onChange(this.state.value, selectionStart); - }; - - onClickSuggestion = suggestion => { - this.selectSuggestion(suggestion); - this.inputRef.focus(); - }; - - onMouseEnterSuggestion = index => { - this.setState({ index }); - }; - - onSubmit = () => { - this.props.onSubmit(this.state.value); - this.setState({ isSuggestionsVisible: false }); - }; - - render() { - const { disabled } = this.props; - const { value } = this.state; - - return ( - -
- { - if (node) { - this.inputRef = node; - } - }} - disabled={disabled} - value={value} - onKeyDown={this.onKeyDown} - onKeyUp={this.onKeyUp} - onChange={this.onChangeInputValue} - onClick={this.onClickInput} - autoComplete="off" - spellCheck={false} - data-test-subj={this.props.testSubj} - /> - - {this.props.isLoading && ( - - )} -
- - -
- ); - } -} - -FilterBar.propTypes = { - initialValue: PropTypes.string, - isLoading: PropTypes.bool, - disabled: PropTypes.bool, - onChange: PropTypes.func.isRequired, - placeholder: PropTypes.string, - onSubmit: PropTypes.func.isRequired, - valueExternal: PropTypes.string, - suggestions: PropTypes.array.isRequired, - testSubj: PropTypes.string, -}; - -FilterBar.defaultProps = { - isLoading: false, - disabled: false, - placeholder: 'tag : engineering OR tag : marketing', - suggestions: [], - testSubj: undefined, -}; diff --git a/x-pack/plugins/ml/public/application/components/kql_filter_bar/filter_bar/filter_bar.test.js b/x-pack/plugins/ml/public/application/components/kql_filter_bar/filter_bar/filter_bar.test.js deleted file mode 100644 index 287803f9eb40a..0000000000000 --- a/x-pack/plugins/ml/public/application/components/kql_filter_bar/filter_bar/filter_bar.test.js +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; -import { shallow, mount } from 'enzyme'; -import { keyCodes } from '@elastic/eui'; -import { FilterBar } from './filter_bar'; - -const defaultProps = { - disabled: false, - initialValue: '', - placeholder: 'Test placeholder', - isLoading: false, - onChange: () => {}, - onSubmit: () => {}, - suggestions: [ - { - description: '

Test description for fieldValueOne

', - end: 1, - start: 0, - text: 'fieldValueOne', - type: 'field', - }, - { - description: '

Test description for fieldValueTwo

', - end: 1, - start: 0, - text: 'fieldValueTwo', - type: 'field', - }, - ], -}; - -describe('FilterBar', () => { - test('snapshot suggestions not shown', () => { - const wrapper = shallow(); - expect(wrapper).toMatchSnapshot(); - }); - - test('snapshot suggestions shown', () => { - const wrapper = shallow(); - wrapper.setState({ isSuggestionsVisible: true }); - expect(wrapper).toMatchSnapshot(); - }); - - test('index updated in state when suggestion is navigated to via mouse', () => { - const wrapper = mount(); - wrapper.setState({ isSuggestionsVisible: true }); - - expect(wrapper.state('index')).toEqual(null); - - const firstSuggestion = wrapper.find('li').first(); - firstSuggestion.simulate('mouseenter'); - expect(wrapper.state('index')).toEqual(0); - }); - - test('index updated and suggestions set to visible when input added', () => { - const wrapper = shallow(); - // default values - expect(wrapper.state('index')).toEqual(null); - expect(wrapper.state('isSuggestionsVisible')).toBe(false); - - const searchBar = wrapper.find('EuiFieldSearch'); - searchBar.simulate('keydown', { preventDefault: () => {}, keyCode: keyCodes.DOWN }); - wrapper.update(); - // updated values - expect(wrapper.state('index')).toEqual(0); - expect(wrapper.state('isSuggestionsVisible')).toBe(true); - }); - - test('index updated in state when suggestion is navigated to via keyboard', () => { - const wrapper = shallow(); - wrapper.setState({ isSuggestionsVisible: true, value: 'f', index: 0 }); - const searchBar = wrapper.find('EuiFieldSearch'); - searchBar.simulate('keydown', { preventDefault: () => {}, keyCode: keyCodes.DOWN }); - wrapper.update(); - - expect(wrapper.state('index')).toEqual(1); - }); -}); diff --git a/x-pack/plugins/ml/public/application/components/kql_filter_bar/filter_bar/index.js b/x-pack/plugins/ml/public/application/components/kql_filter_bar/filter_bar/index.js deleted file mode 100644 index e8153037e0c10..0000000000000 --- a/x-pack/plugins/ml/public/application/components/kql_filter_bar/filter_bar/index.js +++ /dev/null @@ -1,7 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export { FilterBar } from './filter_bar'; diff --git a/x-pack/plugins/ml/public/application/components/kql_filter_bar/index.js b/x-pack/plugins/ml/public/application/components/kql_filter_bar/index.js deleted file mode 100644 index d229943f6afe7..0000000000000 --- a/x-pack/plugins/ml/public/application/components/kql_filter_bar/index.js +++ /dev/null @@ -1,7 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export { KqlFilterBar } from './kql_filter_bar'; diff --git a/x-pack/plugins/ml/public/application/components/kql_filter_bar/kql_filter_bar.js b/x-pack/plugins/ml/public/application/components/kql_filter_bar/kql_filter_bar.js deleted file mode 100644 index 0f3c6d25fe641..0000000000000 --- a/x-pack/plugins/ml/public/application/components/kql_filter_bar/kql_filter_bar.js +++ /dev/null @@ -1,115 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React, { Component, Fragment } from 'react'; -import PropTypes from 'prop-types'; -import { uniqueId } from 'lodash'; -import { FilterBar } from './filter_bar'; -import { EuiCallOut, EuiLink, EuiText } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { getSuggestions, getKqlQueryValues } from './utils'; -import { getDocLinks } from '../../util/dependency_cache'; - -function getErrorWithLink(errorMessage) { - const { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } = getDocLinks(); - return ( - - {`${errorMessage} Input must be valid `} - - {'Kibana Query Language'} - - {' (KQL) syntax.'} - - ); -} - -export class KqlFilterBar extends Component { - state = { - error: null, - suggestions: [], - isLoadingSuggestions: false, - isLoadingIndexPattern: true, - }; - - onChange = async (inputValue, selectionStart) => { - const { indexPattern } = this.props; - - this.setState({ error: null, suggestions: [], isLoadingSuggestions: true }); - - const currentRequest = uniqueId(); - this.currentRequest = currentRequest; - const boolFilter = []; - - try { - const suggestions = - (await getSuggestions(inputValue, selectionStart, indexPattern, boolFilter)) || []; - - if (currentRequest !== this.currentRequest) { - return; - } - - this.setState({ suggestions, isLoadingSuggestions: false }); - } catch (e) { - console.error('Error while fetching suggestions', e); - const errorMessage = i18n.translate('xpack.ml.explorer.fetchingSuggestionsErrorMessage', { - defaultMessage: 'Error while fetching suggestions', - }); - this.setState({ isLoadingSuggestions: false, error: e.message ? e.message : errorMessage }); - } - }; - - onSubmit = inputValue => { - const { indexPattern } = this.props; - const { onSubmit } = this.props; - - try { - // returns object with properties: { influencersFilterQuery, filteredFields, queryString, isAndOperator } - const kqlQueryValues = getKqlQueryValues(inputValue, indexPattern); - onSubmit(kqlQueryValues); - } catch (e) { - console.log('Invalid kuery syntax', e); // eslint-disable-line no-console - const errorWithLink = getErrorWithLink(e.message); - const errorMessage = i18n.translate('xpack.ml.explorer.invalidKuerySyntaxErrorMessage', { - defaultMessage: 'Invalid kuery syntax', - }); - this.setState({ error: e.message ? errorWithLink : errorMessage }); - } - }; - - render() { - const { error } = this.state; - const { initialValue, placeholder, valueExternal, testSubj } = this.props; - - return ( - - - {error && {error}} - - ); - } -} - -KqlFilterBar.propTypes = { - indexPattern: PropTypes.object.isRequired, - initialValue: PropTypes.string, - onSubmit: PropTypes.func.isRequired, - placeholder: PropTypes.string, - valueExternal: PropTypes.string, - testSubj: PropTypes.string, -}; diff --git a/x-pack/plugins/ml/public/application/components/kql_filter_bar/kql_filter_bar.test.js b/x-pack/plugins/ml/public/application/components/kql_filter_bar/kql_filter_bar.test.js deleted file mode 100644 index 610d924651406..0000000000000 --- a/x-pack/plugins/ml/public/application/components/kql_filter_bar/kql_filter_bar.test.js +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; -import { shallow } from 'enzyme'; -import { KqlFilterBar } from './kql_filter_bar'; - -const defaultProps = { - indexPattern: { - title: '.ml-anomalies-*', - fields: [ - { - name: 'nginx.access.geoip.country_iso_code', - type: 'string', - aggregatable: true, - searchable: true, - }, - { - name: 'nginx.access.url', - type: 'string', - aggregatable: true, - searchable: true, - }, - ], - }, - initialValue: '', - onSubmit: () => {}, - placeholder: undefined, -}; - -jest.mock('../../util/dependency_cache', () => ({ - getAutocomplete: () => ({ - getQuerySuggestions: () => {}, - }), -})); - -describe('KqlFilterBar', () => { - test('snapshot', () => { - const wrapper = shallow(); - expect(wrapper).toMatchSnapshot(); - }); - - test('error message callout when error is present', () => { - const wrapper = shallow(); - wrapper.setState({ error: 'Invalid syntax' }); - wrapper.update(); - const callout = wrapper.find('EuiCallOut'); - - expect(callout.contains('Invalid syntax')).toBe(true); - }); - - test('suggestions loading when typing into search bar', () => { - const wrapper = shallow(); - expect(wrapper.state('isLoadingSuggestions')).toBe(false); - // Simulate typing in by triggering change with inputValue and selectionStart - const filterBar = wrapper.find('FilterBar'); - filterBar.simulate('change', 'n', 1); - expect(wrapper.state('isLoadingSuggestions')).toBe(true); - }); -}); diff --git a/x-pack/plugins/ml/public/application/components/kql_filter_bar/suggestion/__snapshots__/suggestion.test.js.snap b/x-pack/plugins/ml/public/application/components/kql_filter_bar/suggestion/__snapshots__/suggestion.test.js.snap deleted file mode 100644 index 4eb236f50be05..0000000000000 --- a/x-pack/plugins/ml/public/application/components/kql_filter_bar/suggestion/__snapshots__/suggestion.test.js.snap +++ /dev/null @@ -1,24 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Suggestion snapshot 1`] = ` - - - - - - fieldValue - - - <p>Test description for fieldValue</p> - - -`; diff --git a/x-pack/plugins/ml/public/application/components/kql_filter_bar/suggestion/index.js b/x-pack/plugins/ml/public/application/components/kql_filter_bar/suggestion/index.js deleted file mode 100644 index 98aedf068a987..0000000000000 --- a/x-pack/plugins/ml/public/application/components/kql_filter_bar/suggestion/index.js +++ /dev/null @@ -1,7 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export { Suggestion } from './suggestion'; diff --git a/x-pack/plugins/ml/public/application/components/kql_filter_bar/suggestion/suggestion.js b/x-pack/plugins/ml/public/application/components/kql_filter_bar/suggestion/suggestion.js deleted file mode 100644 index 121082a776c80..0000000000000 --- a/x-pack/plugins/ml/public/application/components/kql_filter_bar/suggestion/suggestion.js +++ /dev/null @@ -1,117 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; -import PropTypes from 'prop-types'; -import styled from 'styled-components'; -import { EuiIcon } from '@elastic/eui'; -import { tint } from 'polished'; -import theme from '@elastic/eui/dist/eui_theme_light.json'; - -function getIconColor(type) { - switch (type) { - case 'field': - return theme.euiColorVis7; - case 'value': - return theme.euiColorVis0; - case 'operator': - return theme.euiColorVis1; - case 'conjunction': - return theme.euiColorVis3; - case 'recentSearch': - return theme.euiColorMediumShade; - } -} - -const Description = styled.div` - color: ${theme.euiColorDarkShade}; - - p { - display: inline; - - span { - font-family: ${theme.euiFontFamily}; - color: ${theme.euiColorFullShade}; - padding: 0 ${theme.euiSizeXS}; - display: inline-block; - } - } -`; - -const ListItem = styled.li` - font-size: ${theme.euiFontSizeXS}; - height: ${theme.euiSizeXL}; - align-items: center; - display: flex; - background: ${props => (props.selected ? theme.euiColorLightestShade : 'initial')}; - cursor: pointer; - border-radius: ${theme.euiSizeXS}; - - ${Description} { - p span { - background: ${props => - props.selected ? theme.euiColorEmptyShade : theme.euiColorLightestShade}; - } - } -`; - -const Icon = styled.div` - flex: 0 0 ${theme.euiSizeXL}; - background: ${props => tint(0.1, getIconColor(props.type))}; - color: ${props => getIconColor(props.type)}; - width: 100%; - height: 100%; - text-align: center; - line-height: ${theme.euiSizeXL}; -`; - -const TextValue = styled.div` - flex: 0 0 ${theme.euiSize * 16}px; - color: ${theme.euiColorDarkestShade}; - padding: 0 ${theme.euiSizeS}; -`; - -function getEuiIconType(type) { - switch (type) { - case 'field': - return 'kqlField'; - case 'value': - return 'kqlValue'; - case 'recentSearch': - return 'search'; - case 'conjunction': - return 'kqlSelector'; - case 'operator': - return 'kqlOperand'; - default: - throw new Error('Unknown type', type); - } -} - -export const Suggestion = props => { - return ( - props.onClick(props.suggestion)} - onMouseEnter={props.onMouseEnter} - > - - - - {props.suggestion.text} - {props.suggestion.description} - - ); -}; - -Suggestion.propTypes = { - onClick: PropTypes.func.isRequired, - onMouseEnter: PropTypes.func.isRequired, - selected: PropTypes.bool, - suggestion: PropTypes.object.isRequired, - innerRef: PropTypes.func.isRequired, -}; diff --git a/x-pack/plugins/ml/public/application/components/kql_filter_bar/suggestion/suggestion.test.js b/x-pack/plugins/ml/public/application/components/kql_filter_bar/suggestion/suggestion.test.js deleted file mode 100644 index d60f2004db445..0000000000000 --- a/x-pack/plugins/ml/public/application/components/kql_filter_bar/suggestion/suggestion.test.js +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; -import { shallow } from 'enzyme'; -import { Suggestion } from './suggestion'; - -const defaultProps = { - innerRef: () => {}, - onClick: () => {}, - onMouseEnter: () => {}, - selected: true, - suggestion: { - description: '

Test description for fieldValue

', - end: 1, - start: 0, - text: 'fieldValue', - type: 'field', - }, -}; - -describe('Suggestion', () => { - test('snapshot', () => { - const wrapper = shallow(); - expect(wrapper).toMatchSnapshot(); - }); -}); diff --git a/x-pack/plugins/ml/public/application/components/kql_filter_bar/suggestions/__snapshots__/suggestions.test.js.snap b/x-pack/plugins/ml/public/application/components/kql_filter_bar/suggestions/__snapshots__/suggestions.test.js.snap deleted file mode 100644 index 869e321d4a7af..0000000000000 --- a/x-pack/plugins/ml/public/application/components/kql_filter_bar/suggestions/__snapshots__/suggestions.test.js.snap +++ /dev/null @@ -1,40 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Suggestions snapshot 1`] = ` - - Test description for fieldValueOne

", - "end": 1, - "start": 0, - "text": "fieldValueOne", - "type": "field", - } - } - /> - Test description for fieldValueTwo

", - "end": 1, - "start": 0, - "text": "fieldValueTwo", - "type": "field", - } - } - /> -
-`; diff --git a/x-pack/plugins/ml/public/application/components/kql_filter_bar/suggestions/index.js b/x-pack/plugins/ml/public/application/components/kql_filter_bar/suggestions/index.js deleted file mode 100644 index 70fb46e06bfa0..0000000000000 --- a/x-pack/plugins/ml/public/application/components/kql_filter_bar/suggestions/index.js +++ /dev/null @@ -1,7 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export { Suggestions } from './suggestions'; diff --git a/x-pack/plugins/ml/public/application/components/kql_filter_bar/suggestions/suggestions.js b/x-pack/plugins/ml/public/application/components/kql_filter_bar/suggestions/suggestions.js deleted file mode 100644 index 94960e1fcc865..0000000000000 --- a/x-pack/plugins/ml/public/application/components/kql_filter_bar/suggestions/suggestions.js +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React, { Component } from 'react'; -import PropTypes from 'prop-types'; -import styled from 'styled-components'; -import { Suggestion } from '../suggestion'; -import { rgba } from 'polished'; -import theme from '@elastic/eui/dist/eui_theme_light.json'; - -const List = styled.ul` - width: 100%; - border: 1px solid ${theme.euiColorLightShade}; - border-radius: ${theme.euiSizeXS}; - box-shadow: 0px ${theme.euiSizeXS} ${theme.euiSizeXL} ${rgba(theme.euiTextColor, 0.1)}; - position: absolute; - background: #fff; - z-index: 10; - left: 0; - max-height: ${theme.euiSize * 20}px; - overflow: scroll; -`; - -export class Suggestions extends Component { - childNodes = []; - - scrollIntoView = () => { - const parent = this.parentNode; - const child = this.childNodes[this.props.index]; - - if (this.props.index == null || !parent || !child) { - return; - } - - const scrollTop = Math.max( - Math.min(parent.scrollTop, child.offsetTop), - child.offsetTop + child.offsetHeight - parent.offsetHeight - ); - - parent.scrollTop = scrollTop; - }; - - componentDidUpdate(prevProps) { - if (prevProps.index !== this.props.index) { - this.scrollIntoView(); - } - } - - render() { - if (!this.props.show || this.props.suggestions.length === 0) { - return null; - } - - const suggestions = this.props.suggestions.map((suggestion, index) => { - const key = `${suggestion}_${index}`; - return ( - (this.childNodes[index] = node)} - selected={index === this.props.index} - suggestion={suggestion} - onClick={this.props.onClick} - onMouseEnter={() => this.props.onMouseEnter(index)} - key={key} - /> - ); - }); - - return (this.parentNode = node)}>{suggestions}; - } -} - -Suggestions.propTypes = { - index: PropTypes.number, - onClick: PropTypes.func.isRequired, - onMouseEnter: PropTypes.func.isRequired, - show: PropTypes.bool, - suggestions: PropTypes.array.isRequired, -}; diff --git a/x-pack/plugins/ml/public/application/components/kql_filter_bar/suggestions/suggestions.test.js b/x-pack/plugins/ml/public/application/components/kql_filter_bar/suggestions/suggestions.test.js deleted file mode 100644 index 666bfa4cfa1fa..0000000000000 --- a/x-pack/plugins/ml/public/application/components/kql_filter_bar/suggestions/suggestions.test.js +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; -import { shallow, mount } from 'enzyme'; -import { Suggestions } from './suggestions'; - -const defaultProps = { - index: 0, - onClick: () => {}, - onMouseEnter: () => {}, - show: true, - suggestions: [ - { - description: '

Test description for fieldValueOne

', - end: 1, - start: 0, - text: 'fieldValueOne', - type: 'field', - }, - { - description: '

Test description for fieldValueTwo

', - end: 1, - start: 0, - text: 'fieldValueTwo', - type: 'field', - }, - ], -}; - -describe('Suggestions', () => { - test('snapshot', () => { - const wrapper = shallow(); - expect(wrapper).toMatchSnapshot(); - }); - - test('is null when show is false', () => { - const noShowProps = { ...defaultProps, show: false }; - const wrapper = shallow(); - expect(wrapper.isEmptyRender()).toBeTruthy(); - }); - - test('is null when no suggestions are available', () => { - const noSuggestions = { ...defaultProps, suggestions: [] }; - const wrapper = shallow(); - expect(wrapper.isEmptyRender()).toBeTruthy(); - }); - - test('creates suggestion list item for each suggestion passed in via props', () => { - const wrapper = mount(); - const suggestions = wrapper.find('li'); - expect(suggestions.length).toEqual(2); - }); -}); diff --git a/x-pack/plugins/ml/public/application/components/kql_filter_bar/utils.js b/x-pack/plugins/ml/public/application/components/kql_filter_bar/utils.js deleted file mode 100644 index d632f4079e5b9..0000000000000 --- a/x-pack/plugins/ml/public/application/components/kql_filter_bar/utils.js +++ /dev/null @@ -1,97 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -import { esKuery } from '../../../../../../../src/plugins/data/public'; -import { getAutocomplete } from '../../util/dependency_cache'; - -export function getSuggestions(query, selectionStart, indexPattern, boolFilter) { - const autocomplete = getAutocomplete(); - return autocomplete.getQuerySuggestions({ - language: 'kuery', - indexPatterns: [indexPattern], - boolFilter, - query, - selectionStart, - selectionEnd: selectionStart, - }); -} - -function convertKueryToEsQuery(kuery, indexPattern) { - const ast = esKuery.fromKueryExpression(kuery); - return esKuery.toElasticsearchQuery(ast, indexPattern); -} -// Recommended by MDN for escaping user input to be treated as a literal string within a regular expression -// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions -export function escapeRegExp(string) { - return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string -} - -export function escapeParens(string) { - return string.replace(/[()]/g, '\\$&'); -} - -export function escapeDoubleQuotes(string) { - return string.replace(/\"/g, '\\$&'); -} - -export function getKqlQueryValues(inputValue, indexPattern) { - const ast = esKuery.fromKueryExpression(inputValue); - const isAndOperator = ast.function === 'and'; - const query = convertKueryToEsQuery(inputValue, indexPattern); - const filteredFields = []; - - if (!query) { - return; - } - - // if ast.type == 'function' then layout of ast.arguments: - // [{ arguments: [ { type: 'literal', value: 'AAL' } ] },{ arguments: [ { type: 'literal', value: 'AAL' } ] }] - if (ast && Array.isArray(ast.arguments)) { - ast.arguments.forEach(arg => { - if (arg.arguments !== undefined) { - arg.arguments.forEach(nestedArg => { - if (typeof nestedArg.value === 'string') { - filteredFields.push(nestedArg.value); - } - }); - } else if (typeof arg.value === 'string') { - filteredFields.push(arg.value); - } - }); - } - - return { - filterQuery: query, - filteredFields, - queryString: inputValue, - isAndOperator, - tableQueryString: inputValue, - }; -} - -export function getQueryPattern(fieldName, fieldValue) { - const sanitizedFieldName = escapeRegExp(fieldName); - const sanitizedFieldValue = escapeRegExp(fieldValue); - - return new RegExp(`(${sanitizedFieldName})\\s?:\\s?(")?(${sanitizedFieldValue})(")?`, 'i'); -} - -export function removeFilterFromQueryString(currentQueryString, fieldName, fieldValue) { - let newQueryString = ''; - // Remove the passed in fieldName and value from the existing filter - const queryPattern = getQueryPattern(fieldName, fieldValue); - newQueryString = currentQueryString.replace(queryPattern, ''); - // match 'and' or 'or' at the start/end of the string - const endPattern = /\s(and|or)\s*$/gi; - const startPattern = /^\s*(and|or)\s/gi; - // If string has a double operator (e.g. tag:thing or or tag:other) remove and replace with the first occurring operator - const invalidOperatorPattern = /\s+(and|or)\s+(and|or)\s+/gi; - newQueryString = newQueryString.replace(invalidOperatorPattern, ' $1 '); - // If string starts/ends with 'and' or 'or' remove that as that is illegal kuery syntax - newQueryString = newQueryString.replace(endPattern, ''); - newQueryString = newQueryString.replace(startPattern, ''); - - return newQueryString; -} diff --git a/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/search_panel/search_panel.tsx b/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/search_panel/search_panel.tsx index ef13fec3589fb..16004475eb44f 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/search_panel/search_panel.tsx +++ b/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/search_panel/search_panel.tsx @@ -6,14 +6,22 @@ import React, { FC, useState } from 'react'; -import { EuiFlexItem, EuiFlexGroup, EuiIconTip, EuiSuperSelect, EuiText } from '@elastic/eui'; +import { + EuiCode, + EuiFlexItem, + EuiFlexGroup, + EuiIconTip, + EuiInputPopover, + EuiSuperSelect, + EuiText, +} from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; import { IndexPattern } from '../../../../../../../../../src/plugins/data/public'; -import { SEARCH_QUERY_LANGUAGE } from '../../../../../../common/constants/search'; +import { SEARCH_QUERY_LANGUAGE, ErrorMessage } from '../../../../../../common/constants/search'; import { esKuery, @@ -22,8 +30,6 @@ import { QueryStringInput, } from '../../../../../../../../../src/plugins/data/public'; -import { getToastNotifications } from '../../../../util/dependency_cache'; - interface Props { indexPattern: IndexPattern; searchString: Query['query']; @@ -73,6 +79,7 @@ export const SearchPanel: FC = ({ query: searchString || '', language: searchQueryLanguage, }); + const [errorMessage, setErrorMessage] = useState(undefined); const searchHandler = (query: Query) => { let filterQuery; @@ -93,13 +100,7 @@ export const SearchPanel: FC = ({ setSearchQueryLanguage(query.language); } catch (e) { console.log('Invalid syntax', JSON.stringify(e, null, 2)); // eslint-disable-line no-console - const toastNotifications = getToastNotifications(); - toastNotifications.addError(e, { - title: i18n.translate('xpack.ml.datavisualizer.invalidSyntaxErrorMessage', { - defaultMessage: 'Invalid syntax in search bar', - }), - toastMessage: e.message ? e.message : e, - }); + setErrorMessage({ query: query.query as string, message: e.message }); } }; const searchChangeHandler = (query: Query) => setSearchInput(query); @@ -107,22 +108,40 @@ export const SearchPanel: FC = ({ return ( - + setErrorMessage(undefined)} + input={ + + } + isOpen={errorMessage?.query === searchInput.query && errorMessage?.message !== ''} + > + + {i18n.translate( + 'xpack.ml.datavisualizer.searchPanel.invalidKuerySyntaxErrorMessageQueryBar', + { + defaultMessage: 'Invalid query', + } + )} + {': '} + {errorMessage?.message.split('\n')[0]} + + diff --git a/x-pack/plugins/ml/public/application/explorer/components/explorer_query_bar/explorer_query_bar.tsx b/x-pack/plugins/ml/public/application/explorer/components/explorer_query_bar/explorer_query_bar.tsx index 0789a7f8ef5ff..0263ad08b03cf 100644 --- a/x-pack/plugins/ml/public/application/explorer/components/explorer_query_bar/explorer_query_bar.tsx +++ b/x-pack/plugins/ml/public/application/explorer/components/explorer_query_bar/explorer_query_bar.tsx @@ -5,6 +5,8 @@ */ import React, { FC, useState, useEffect } from 'react'; +import { EuiCode, EuiInputPopover } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; import { Query, esKuery, @@ -12,10 +14,10 @@ import { QueryStringInput, } from '../../../../../../../../src/plugins/data/public'; import { IIndexPattern } from '../../../../../../../../src/plugins/data/common/index_patterns'; -import { QUERY_LANGUAGE_KUERY, QUERY_LANGUAGE_LUCENE } from '../../explorer_constants'; +import { SEARCH_QUERY_LANGUAGE, ErrorMessage } from '../../../../../common/constants/search'; import { explorerService } from '../../explorer_dashboard_service'; -export const DEFAULT_QUERY_LANG = QUERY_LANGUAGE_KUERY; +export const DEFAULT_QUERY_LANG = SEARCH_QUERY_LANGUAGE.KUERY; export function getKqlQueryValues({ inputString, @@ -25,11 +27,11 @@ export function getKqlQueryValues({ inputString: string | { [key: string]: any }; queryLanguage: string; indexPattern: IIndexPattern; -}) { +}): { clearSettings: boolean; settings: any } { let influencersFilterQuery: any = {}; - const ast = esKuery.fromKueryExpression(inputString); - const isAndOperator = ast.function === 'and'; const filteredFields: string[] = []; + const ast = esKuery.fromKueryExpression(inputString); + const isAndOperator = ast && ast.function === 'and'; // if ast.type == 'function' then layout of ast.arguments: // [{ arguments: [ { type: 'literal', value: 'AAL' } ] },{ arguments: [ { type: 'literal', value: 'AAL' } ] }] if (ast && Array.isArray(ast.arguments)) { @@ -45,12 +47,12 @@ export function getKqlQueryValues({ } }); } - if (queryLanguage === QUERY_LANGUAGE_KUERY) { + if (queryLanguage === SEARCH_QUERY_LANGUAGE.KUERY) { influencersFilterQuery = esKuery.toElasticsearchQuery( esKuery.fromKueryExpression(inputString), indexPattern ); - } else if (queryLanguage === QUERY_LANGUAGE_LUCENE) { + } else if (queryLanguage === SEARCH_QUERY_LANGUAGE.LUCENE) { influencersFilterQuery = esQuery.luceneStringToDsl(inputString); } @@ -78,7 +80,7 @@ function getInitSearchInputState({ }) { if (queryString !== undefined && filterActive === true) { return { - language: QUERY_LANGUAGE_KUERY, + language: SEARCH_QUERY_LANGUAGE.KUERY, query: queryString, }; } else { @@ -110,6 +112,7 @@ export const ExplorerQueryBar: FC = ({ const [searchInput, setSearchInput] = useState( getInitSearchInputState({ filterActive, queryString }) ); + const [errorMessage, setErrorMessage] = useState(undefined); useEffect(() => { if (filterIconTriggeredQuery !== undefined) { @@ -127,30 +130,50 @@ export const ExplorerQueryBar: FC = ({ setSearchInput(query); }; const applyInfluencersFilterQuery = (query: Query) => { - const { clearSettings, settings } = getKqlQueryValues({ - inputString: query.query, - queryLanguage: query.language, - indexPattern, - }); + try { + const { clearSettings, settings } = getKqlQueryValues({ + inputString: query.query, + queryLanguage: query.language, + indexPattern, + }); - if (clearSettings === true) { - explorerService.clearInfluencerFilterSettings(); - } else { - explorerService.setInfluencerFilterSettings(settings); + if (clearSettings === true) { + explorerService.clearInfluencerFilterSettings(); + } else { + explorerService.setInfluencerFilterSettings(settings); + } + } catch (e) { + console.log('Invalid query syntax in search bar', e); // eslint-disable-line no-console + setErrorMessage({ query: query.query as string, message: e.message }); } }; return ( - + setErrorMessage(undefined)} + input={ + + } + isOpen={errorMessage?.query === searchInput.query && errorMessage?.message !== ''} + > + + {i18n.translate('xpack.ml.explorer.invalidKuerySyntaxErrorMessageQueryBar', { + defaultMessage: 'Invalid query', + })} + {': '} + {errorMessage?.message.split('\n')[0]} + + ); }; diff --git a/x-pack/plugins/ml/public/application/explorer/explorer.js b/x-pack/plugins/ml/public/application/explorer/explorer.js index 9b02150bae9bb..d61d56d07b644 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer.js +++ b/x-pack/plugins/ml/public/application/explorer/explorer.js @@ -58,13 +58,12 @@ import { DEFAULT_QUERY_LANG, } from './components/explorer_query_bar/explorer_query_bar'; import { + getDateFormatTz, removeFilterFromQueryString, getQueryPattern, escapeParens, escapeDoubleQuotes, -} from '../components/kql_filter_bar/utils'; - -import { getDateFormatTz } from './explorer_utils'; +} from './explorer_utils'; import { getSwimlaneContainerWidth } from './legacy_utils'; import { @@ -266,7 +265,7 @@ export class Explorer extends React.Component { explorerService.setInfluencerFilterSettings(settings); } } catch (e) { - console.log('Invalid kuery syntax', e); // eslint-disable-line no-console + console.log('Invalid query syntax from table', e); // eslint-disable-line no-console const toastNotifications = getToastNotifications(); toastNotifications.addDanger( diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_constants.ts b/x-pack/plugins/ml/public/application/explorer/explorer_constants.ts index afec50eae06aa..b084f503272cc 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_constants.ts +++ b/x-pack/plugins/ml/public/application/explorer/explorer_constants.ts @@ -25,7 +25,6 @@ export const EXPLORER_ACTION = { SET_EXPLORER_DATA: 'setExplorerData', SET_FILTER_DATA: 'setFilterData', SET_INFLUENCER_FILTER_SETTINGS: 'setInfluencerFilterSettings', - SET_SEARCH_INPUT: 'setSearchInput', SET_SELECTED_CELLS: 'setSelectedCells', SET_SWIMLANE_CONTAINER_WIDTH: 'setSwimlaneContainerWidth', SET_SWIMLANE_LIMIT: 'setSwimlaneLimit', @@ -56,7 +55,3 @@ export const MAX_INFLUENCER_FIELD_NAMES = 50; export const VIEW_BY_JOB_LABEL = i18n.translate('xpack.ml.explorer.jobIdLabel', { defaultMessage: 'job ID', }); - -export const QUERY_LANGUAGE_KUERY = 'kuery'; -export const QUERY_LANGUAGE_LUCENE = 'lucene'; -export type QUERY_LANGUAGE = 'kuery' | 'lucene'; diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_dashboard_service.ts b/x-pack/plugins/ml/public/application/explorer/explorer_dashboard_service.ts index 277c1aa6f4566..89e1a908b1ecc 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_dashboard_service.ts +++ b/x-pack/plugins/ml/public/application/explorer/explorer_dashboard_service.ts @@ -132,12 +132,6 @@ export const explorerService = { payload, }); }, - setSearchInput: (query: any) => { - explorerAction$.next({ - type: EXPLORER_ACTION.SET_SEARCH_INPUT, - payload: query, - }); - }, setSelectedCells: (payload: AppStateSelectedCells | undefined) => { explorerAction$.next({ type: EXPLORER_ACTION.SET_SELECTED_CELLS, diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_utils.js b/x-pack/plugins/ml/public/application/explorer/explorer_utils.js index 0b41f789bb571..852b16ec581bb 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_utils.js +++ b/x-pack/plugins/ml/public/application/explorer/explorer_utils.js @@ -883,3 +883,42 @@ export async function loadTopInfluencers( } }); } + +// Recommended by MDN for escaping user input to be treated as a literal string within a regular expression +// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions +export function escapeRegExp(string) { + return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string +} + +export function escapeParens(string) { + return string.replace(/[()]/g, '\\$&'); +} + +export function escapeDoubleQuotes(string) { + return string.replace(/\"/g, '\\$&'); +} + +export function getQueryPattern(fieldName, fieldValue) { + const sanitizedFieldName = escapeRegExp(fieldName); + const sanitizedFieldValue = escapeRegExp(fieldValue); + + return new RegExp(`(${sanitizedFieldName})\\s?:\\s?(")?(${sanitizedFieldValue})(")?`, 'i'); +} + +export function removeFilterFromQueryString(currentQueryString, fieldName, fieldValue) { + let newQueryString = ''; + // Remove the passed in fieldName and value from the existing filter + const queryPattern = getQueryPattern(fieldName, fieldValue); + newQueryString = currentQueryString.replace(queryPattern, ''); + // match 'and' or 'or' at the start/end of the string + const endPattern = /\s(and|or)\s*$/gi; + const startPattern = /^\s*(and|or)\s/gi; + // If string has a double operator (e.g. tag:thing or or tag:other) remove and replace with the first occurring operator + const invalidOperatorPattern = /\s+(and|or)\s+(and|or)\s+/gi; + newQueryString = newQueryString.replace(invalidOperatorPattern, ' $1 '); + // If string starts/ends with 'and' or 'or' remove that as that is illegal kuery syntax + newQueryString = newQueryString.replace(endPattern, ''); + newQueryString = newQueryString.replace(startPattern, ''); + + return newQueryString; +} diff --git a/x-pack/plugins/ml/public/application/explorer/reducers/explorer_reducer/reducer.ts b/x-pack/plugins/ml/public/application/explorer/reducers/explorer_reducer/reducer.ts index ff659029e38d7..c31b26b7adb7b 100644 --- a/x-pack/plugins/ml/public/application/explorer/reducers/explorer_reducer/reducer.ts +++ b/x-pack/plugins/ml/public/application/explorer/reducers/explorer_reducer/reducer.ts @@ -68,13 +68,6 @@ export const explorerReducer = (state: ExplorerState, nextAction: Action): Explo nextState = setInfluencerFilterSettings(state, payload); break; - case EXPLORER_ACTION.SET_SEARCH_INPUT: - nextState = { - ...state, - searchInput: payload, - }; - break; - case EXPLORER_ACTION.SET_SELECTED_CELLS: const selectedCells = payload; nextState = { diff --git a/x-pack/plugins/ml/public/application/explorer/reducers/explorer_reducer/state.ts b/x-pack/plugins/ml/public/application/explorer/reducers/explorer_reducer/state.ts index 44e1486508ea3..0a2dbf5bcff35 100644 --- a/x-pack/plugins/ml/public/application/explorer/reducers/explorer_reducer/state.ts +++ b/x-pack/plugins/ml/public/application/explorer/reducers/explorer_reducer/state.ts @@ -20,8 +20,6 @@ import { TimeRangeBounds, } from '../../explorer_utils'; -import { QUERY_LANGUAGE_KUERY, QUERY_LANGUAGE } from '../../explorer_constants'; // QUERY_LANGUAGE_LUCENE - export interface ExplorerState { annotationsData: any[]; bounds: TimeRangeBounds | undefined; @@ -39,10 +37,6 @@ export interface ExplorerState { noInfluencersConfigured: boolean; overallSwimlaneData: SwimlaneData; queryString: string; - searchInput: { - query: string; - language: QUERY_LANGUAGE; - }; selectedCells: AppStateSelectedCells | undefined; selectedJobs: ExplorerJob[] | null; swimlaneBucketInterval: any; @@ -79,10 +73,6 @@ export function getExplorerDefaultState(): ExplorerState { noInfluencersConfigured: true, overallSwimlaneData: getDefaultSwimlaneData(), queryString: '', - searchInput: { - query: '', - language: QUERY_LANGUAGE_KUERY, - }, selectedCells: undefined, selectedJobs: null, swimlaneBucketInterval: undefined, diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.tsx index 1f9a60fa869b7..df22c3f3eb2e2 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.tsx @@ -12,6 +12,8 @@ import { i18n } from '@kbn/i18n'; import { EuiButton, EuiCodeEditor, + EuiCode, + EuiInputPopover, EuiFlexGroup, EuiFlexItem, EuiForm, @@ -76,6 +78,11 @@ export interface StepDefineExposedState { valid: boolean; } +interface ErrorMessage { + query: string; + message: string; +} + const defaultSearch = '*'; const QUERY_LANGUAGE_KUERY = 'kuery'; @@ -256,6 +263,7 @@ export const StepDefineForm: FC = React.memo(({ overrides = {}, onChange, query: defaults.searchString || '', language: defaults.searchLanguage, }); + const [errorMessage, setErrorMessage] = useState(undefined); // The state of the input query bar updated on every submit and to be exposed. const [searchLanguage, setSearchLanguage] = useState( @@ -272,18 +280,23 @@ export const StepDefineForm: FC = React.memo(({ overrides = {}, onChange, const searchSubmitHandler = (query: Query) => { setSearchLanguage(query.language as QUERY_LANGUAGE); setSearchString(query.query !== '' ? (query.query as string) : undefined); - switch (query.language) { - case QUERY_LANGUAGE_KUERY: - setSearchQuery( - esKuery.toElasticsearchQuery( - esKuery.fromKueryExpression(query.query as string), - indexPattern - ) - ); - return; - case QUERY_LANGUAGE_LUCENE: - setSearchQuery(esQuery.luceneStringToDsl(query.query as string)); - return; + try { + switch (query.language) { + case QUERY_LANGUAGE_KUERY: + setSearchQuery( + esKuery.toElasticsearchQuery( + esKuery.fromKueryExpression(query.query as string), + indexPattern + ) + ); + return; + case QUERY_LANGUAGE_LUCENE: + setSearchQuery(esQuery.luceneStringToDsl(query.query as string)); + return; + } + } catch (e) { + console.log('Invalid syntax', JSON.stringify(e, null, 2)); // eslint-disable-line no-console + setErrorMessage({ query: query.query as string, message: e.message }); } }; @@ -620,33 +633,54 @@ export const StepDefineForm: FC = React.memo(({ overrides = {}, onChange, defaultMessage: 'Use a query to filter the source data (optional).', })} > - setErrorMessage(undefined)} + input={ + + } + isOpen={ + errorMessage?.query === searchInput.query && + errorMessage?.message !== '' } - disableAutoFocus={true} - dataTestSubj="transformQueryInput" - languageSwitcherPopoverAnchorPosition="rightDown" - /> + > + + {i18n.translate( + 'xpack.transform.stepDefineForm.invalidKuerySyntaxErrorMessageQueryBar', + { + defaultMessage: 'Invalid query', + } + )} + {': '} + {errorMessage?.message.split('\n')[0]} + + )} diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 5b96a93b1ed0d..ac48a3f0efab0 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -7638,9 +7638,7 @@ "xpack.ml.explorer.distributionChart.unusualByFieldValuesLabel": "{ numberOfCauses, plural, one {# 個の異常な {byFieldName} 値 } other {#{plusSign}異常な{byFieldName}値}}", "xpack.ml.explorer.distributionChart.valueLabel": "値", "xpack.ml.explorer.distributionChart.valueWithoutAnomalyScoreLabel": "値", - "xpack.ml.explorer.fetchingSuggestionsErrorMessage": "提案の取得中にエラーが発生しました", "xpack.ml.explorer.intervalLabel": "間隔", - "xpack.ml.explorer.invalidKuerySyntaxErrorMessage": "無効な Kuery 構文", "xpack.ml.explorer.invalidKuerySyntaxErrorMessageFromTable": "クエリバーに無効な構文。インプットは有効な Kibana クエリ言語 (KQL) でなければなりません", "xpack.ml.explorer.jobIdLabel": "ジョブ ID", "xpack.ml.explorer.jobScoreAcrossAllInfluencersLabel": "(すべての影響因子のジョブスコア)", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 03a65921d669a..e93e9f60e9d6e 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -7638,9 +7638,7 @@ "xpack.ml.explorer.distributionChart.unusualByFieldValuesLabel": "{ numberOfCauses, plural, one {# 个异常 {byFieldName} 值} other {#{plusSign} 个异常 {byFieldName} 值}}", "xpack.ml.explorer.distributionChart.valueLabel": "值", "xpack.ml.explorer.distributionChart.valueWithoutAnomalyScoreLabel": "值", - "xpack.ml.explorer.fetchingSuggestionsErrorMessage": "获取建议时出错", "xpack.ml.explorer.intervalLabel": "时间间隔", - "xpack.ml.explorer.invalidKuerySyntaxErrorMessage": "kuery 语法无效", "xpack.ml.explorer.invalidKuerySyntaxErrorMessageFromTable": "查询栏中的语法无效。输入必须是有效的 Kibana 查询语言 (KQL)", "xpack.ml.explorer.jobIdLabel": "作业 ID", "xpack.ml.explorer.jobScoreAcrossAllInfluencersLabel": "(所有影响因素的作业分数)",