diff --git a/src/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/table/__tests__/__snapshots__/table.test.js.snap b/src/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/table/__tests__/__snapshots__/table.test.js.snap index eecc496328f3d..d3ca955a95a54 100644 --- a/src/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/table/__tests__/__snapshots__/table.test.js.snap +++ b/src/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/table/__tests__/__snapshots__/table.test.js.snap @@ -3,6 +3,11 @@ exports[`Table should render normally 1`] = ` ({ SavedObjectNotFound: class SavedObjectNotFound extends Error { @@ -39,37 +41,62 @@ jest.mock('ui/chrome', () => ({ import { Table } from '../table'; +const defaultProps = { + selectedSavedObjects: [1], + selectionConfig: { + onSelectionChange: () => {}, + }, + filterOptions: [{ value: 2 }], + onDelete: () => {}, + onExport: () => {}, + getEditUrl: () => {}, + goInApp: () => {}, + pageIndex: 1, + pageSize: 2, + items: [3], + itemId: 'id', + totalItemCount: 3, + onQueryChange: () => {}, + onTableChange: () => {}, + isSearching: false, + onShowRelationships: () => {}, +}; + describe('Table', () => { it('should render normally', () => { - const props = { - selectedSavedObjects: [1], - selectionConfig: { - onSelectionChange: () => {}, - }, - filterOptions: [{ value: 2 }], - onDelete: () => {}, - onExport: () => {}, - getEditUrl: () => {}, - goInApp: () => {}, + const component = shallowWithIntl( + + ); - pageIndex: 1, - pageSize: 2, - items: [3], - itemId: 'id', - totalItemCount: 3, - onQueryChange: () => {}, - onTableChange: () => {}, - isSearching: false, + expect(component).toMatchSnapshot(); + }); - onShowRelationships: () => {}, + it('should handle query parse error', () => { + const onQueryChangeMock = jest.fn(); + const customizedProps = { + ...defaultProps, + onQueryChange: onQueryChangeMock }; - const component = shallowWithIntl( + const component = mountWithIntl( ); + const searchBar = findTestSubject(component, 'savedObjectSearchBar'); - expect(component).toMatchSnapshot(); + // Send invalid query + searchBar.simulate('keyup', { keyCode: keyCodes.ENTER, target: { value: '?' } }); + expect(onQueryChangeMock).toHaveBeenCalledTimes(0); + expect(component.state().isSearchTextValid).toBe(false); + + onQueryChangeMock.mockReset(); + + // Send valid query to ensure component can recover from invalid query + searchBar.simulate('keyup', { keyCode: keyCodes.ENTER, target: { value: 'I am valid' } }); + expect(onQueryChangeMock).toHaveBeenCalledTimes(1); + expect(component.state().isSearchTextValid).toBe(true); }); }); diff --git a/src/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/table/table.js b/src/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/table/table.js index 0effa28d9c40c..09ba15cc16b4b 100644 --- a/src/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/table/table.js +++ b/src/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/table/table.js @@ -27,7 +27,8 @@ import { EuiIcon, EuiLink, EuiSpacer, - EuiToolTip + EuiToolTip, + EuiFormErrorText } from '@elastic/eui'; import { getSavedObjectLabel, getSavedObjectIcon } from '../../../../lib'; import { FormattedMessage, injectI18n } from '@kbn/i18n/react'; @@ -61,6 +62,27 @@ class TableUI extends PureComponent { onShowRelationships: PropTypes.func.isRequired, }; + state = { + isSearchTextValid: true, + parseErrorMessage: null, + } + + onChange = ({ query, error }) => { + if (error) { + this.setState({ + isSearchTextValid: false, + parseErrorMessage: error.message, + }); + return; + } + + this.setState({ + isSearchTextValid: true, + parseErrorMessage: null, + }); + this.props.onQueryChange({ query }); + } + render() { const { pageIndex, @@ -74,7 +96,6 @@ class TableUI extends PureComponent { onDelete, onExport, selectedSavedObjects, - onQueryChange, onTableChange, goInApp, getEditUrl, @@ -182,11 +203,25 @@ class TableUI extends PureComponent { }, ]; + let queryParseError; + if (!this.state.isSearchTextValid) { + const parseErrorMsg = intl.formatMessage({ + id: 'kbn.management.objects.objectsTable.searchBar.unableToParseQueryErrorMessage', + defaultMessage: 'Unable to parse query', + }); + queryParseError = ( + + {`${parseErrorMsg}. ${this.state.parseErrorMessage}`} + + ); + } + return ( , ]} /> + {queryParseError}
+ + /> + `; diff --git a/src/core_plugins/kibana/public/management/sections/settings/components/search/search.js b/src/core_plugins/kibana/public/management/sections/settings/components/search/search.js index 919bcfe4da7a3..2006839877d5b 100644 --- a/src/core_plugins/kibana/public/management/sections/settings/components/search/search.js +++ b/src/core_plugins/kibana/public/management/sections/settings/components/search/search.js @@ -17,12 +17,13 @@ * under the License. */ -import React, { PureComponent } from 'react'; +import React, { Fragment, PureComponent } from 'react'; import PropTypes from 'prop-types'; import { injectI18n } from '@kbn/i18n/react'; import { EuiSearchBar, + EuiFormErrorText, } from '@elastic/eui'; import { getCategoryName } from '../../lib'; @@ -46,11 +47,33 @@ class SearchUI extends PureComponent { }); } + state = { + isSearchTextValid: true, + parseErrorMessage: null, + } + + onChange = ({ query, error }) => { + if (error) { + this.setState({ + isSearchTextValid: false, + parseErrorMessage: error.message, + }); + return; + } + + this.setState({ + isSearchTextValid: true, + parseErrorMessage: null, + }); + this.props.onQueryChange({ query }); + } + render() { - const { query, onQueryChange, intl } = this.props; + const { query, intl } = this.props; const box = { incremental: true, + 'data-test-subj': 'settingsSearchBar', 'aria-label': intl.formatMessage({ id: 'kbn.management.settings.searchBarAriaLabel', defaultMessage: 'Search advanced settings', @@ -71,14 +94,29 @@ class SearchUI extends PureComponent { } ]; - return ( - + let queryParseError; + if (!this.state.isSearchTextValid) { + const parseErrorMsg = intl.formatMessage({ + id: 'kbn.management.settings.searchBar.unableToParseQueryErrorMessage', + defaultMessage: 'Unable to parse query', + }); + queryParseError = ( + + {`${parseErrorMsg}. ${this.state.parseErrorMessage}`} + + ); + } + return ( + + + {queryParseError} + ); } } diff --git a/src/core_plugins/kibana/public/management/sections/settings/components/search/search.test.js b/src/core_plugins/kibana/public/management/sections/settings/components/search/search.test.js index 2b76394ccc71e..40532605d807a 100644 --- a/src/core_plugins/kibana/public/management/sections/settings/components/search/search.test.js +++ b/src/core_plugins/kibana/public/management/sections/settings/components/search/search.test.js @@ -19,6 +19,7 @@ import React from 'react'; import { shallowWithIntl, mountWithIntl } from 'test_utils/enzyme_helpers'; +import { findTestSubject } from '@elastic/eui/lib/test'; import { Query } from '@elastic/eui'; @@ -52,7 +53,32 @@ describe('Search', () => { onQueryChange={onQueryChange} /> ); - component.find('input').simulate('keyup', { target: { value: 'new filter' } }); + findTestSubject(component, 'settingsSearchBar').simulate('keyup', { target: { value: 'new filter' } }); expect(onQueryChange).toHaveBeenCalledTimes(1); }); + + it('should handle query parse error', async () => { + const onQueryChangeMock = jest.fn(); + const component = mountWithIntl( + + ); + + const searchBar = findTestSubject(component, 'settingsSearchBar'); + + // Send invalid query + searchBar.simulate('keyup', { target: { value: '?' } }); + expect(onQueryChangeMock).toHaveBeenCalledTimes(0); + expect(component.state().isSearchTextValid).toBe(false); + + onQueryChangeMock.mockReset(); + + // Send valid query to ensure component can recover from invalid query + searchBar.simulate('keyup', { target: { value: 'dateFormat' } }); + expect(onQueryChangeMock).toHaveBeenCalledTimes(1); + expect(component.state().isSearchTextValid).toBe(true); + }); });