handle EuiSearchBar query parse failures (#25235) (#25598)

* handle EuiSearchBar query parse failures

* I18n parse failure messages

* review updates

* more cleanup on settings search.test
This commit is contained in:
Nathan Reese 2018-11-13 14:31:47 -07:00 committed by GitHub
parent 95c4cc65c4
commit e6ff53265a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 225 additions and 90 deletions

View file

@ -3,6 +3,11 @@
exports[`Table should render normally 1`] = `
<React.Fragment>
<EuiSearchBar
box={
Object {
"data-test-subj": "savedObjectSearchBar",
}
}
filters={
Array [
Object {

View file

@ -18,7 +18,9 @@
*/
import React from 'react';
import { shallowWithIntl } from 'test_utils/enzyme_helpers';
import { shallowWithIntl, mountWithIntl } from 'test_utils/enzyme_helpers';
import { findTestSubject } from '@elastic/eui/lib/test';
import { keyCodes } from '@elastic/eui/lib/services';
jest.mock('ui/errors', () => ({
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: () => {},
pageIndex: 1,
pageSize: 2,
items: [3],
itemId: 'id',
totalItemCount: 3,
onQueryChange: () => {},
onTableChange: () => {},
isSearching: false,
onShowRelationships: () => {},
};
const component = shallowWithIntl(
<Table.WrappedComponent
{...props}
{...defaultProps}
/>
);
expect(component).toMatchSnapshot();
});
it('should handle query parse error', () => {
const onQueryChangeMock = jest.fn();
const customizedProps = {
...defaultProps,
onQueryChange: onQueryChangeMock
};
const component = mountWithIntl(
<Table.WrappedComponent
{...customizedProps}
/>
);
const searchBar = findTestSubject(component, 'savedObjectSearchBar');
// 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);
});
});

View file

@ -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 = (
<EuiFormErrorText>
{`${parseErrorMsg}. ${this.state.parseErrorMessage}`}
</EuiFormErrorText>
);
}
return (
<Fragment>
<EuiSearchBar
box={{ 'data-test-subj': 'savedObjectSearchBar' }}
filters={filters}
onChange={onQueryChange}
onChange={this.onChange}
toolsRight={[
<EuiButton
key="deleteSO"
@ -213,6 +248,7 @@ class TableUI extends PureComponent {
</EuiButton>,
]}
/>
{queryParseError}
<EuiSpacer size="s" />
<div data-test-subj="savedObjectsTable">
<EuiBasicTable

View file

@ -1,58 +1,61 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Search should render normally 1`] = `
<EuiSearchBar
box={
Object {
"aria-label": "Search advanced settings",
"incremental": true,
}
}
filters={
Array [
<React.Fragment>
<EuiSearchBar
box={
Object {
"field": "category",
"multiSelect": "or",
"name": "Category",
"options": Array [
Object {
"name": "General",
"value": "general",
},
Object {
"name": "Dashboard",
"value": "dashboard",
},
Object {
"name": "HiddenCategory",
"value": "hiddenCategory",
},
Object {
"name": "X-pack",
"value": "x-pack",
},
],
"type": "field_value_selection",
},
]
}
onChange={[Function]}
query={
Query {
"ast": _AST {
"_clauses": Array [],
"_indexedClauses": Object {
"field": Object {},
"is": Object {},
"term": Array [],
},
},
"syntax": Object {
"parse": [Function],
"print": [Function],
},
"text": "",
"aria-label": "Search advanced settings",
"data-test-subj": "settingsSearchBar",
"incremental": true,
}
}
}
/>
filters={
Array [
Object {
"field": "category",
"multiSelect": "or",
"name": "Category",
"options": Array [
Object {
"name": "General",
"value": "general",
},
Object {
"name": "Dashboard",
"value": "dashboard",
},
Object {
"name": "HiddenCategory",
"value": "hiddenCategory",
},
Object {
"name": "X-pack",
"value": "x-pack",
},
],
"type": "field_value_selection",
},
]
}
onChange={[Function]}
query={
Query {
"ast": _AST {
"_clauses": Array [],
"_indexedClauses": Object {
"field": Object {},
"is": Object {},
"term": Array [],
},
},
"syntax": Object {
"parse": [Function],
"print": [Function],
},
"text": "",
}
}
/>
</React.Fragment>
`;

View file

@ -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 (
<EuiSearchBar
box={box}
filters={filters}
onChange={onQueryChange}
query={query}
/>
let queryParseError;
if (!this.state.isSearchTextValid) {
const parseErrorMsg = intl.formatMessage({
id: 'kbn.management.settings.searchBar.unableToParseQueryErrorMessage',
defaultMessage: 'Unable to parse query',
});
queryParseError = (
<EuiFormErrorText>
{`${parseErrorMsg}. ${this.state.parseErrorMessage}`}
</EuiFormErrorText>
);
}
return (
<Fragment>
<EuiSearchBar
box={box}
filters={filters}
onChange={this.onChange}
query={query}
/>
{queryParseError}
</Fragment>
);
}
}

View file

@ -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(
<Search.WrappedComponent
query={query}
categories={categories}
onQueryChange={onQueryChangeMock}
/>
);
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);
});
});