[Upgrade Assistant] Fix filter deprecations search filter (#57541) (#57780)

* Made eui search field not a controlled component
Added validateRegExpString util

* Update error message display. Use EuiCallOut and i18n to replicate other search filter behaviour, e.g. index management.

* Remove unused variable

* Update Jest snapshot

* Updated layout for callout

The previous callout layout looked off-center next to the controls in the table.

* Update copy and remove intl

Update "Filter Invalid:" to sentence case
Remove inject intl wrapper from CheckupControls component
Remove unnecessary grow={true}

* Updated Jest component snapshot

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
Jean-Louis Leysens 2020-02-17 13:45:56 +01:00 committed by GitHub
parent 72df0bf006
commit 87f366de2d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 121 additions and 45 deletions

View file

@ -57,7 +57,7 @@ exports[`CheckupTab render with deprecations 1`] = `
<EuiSpacer />
<EuiPageContent>
<EuiPageContentBody>
<InjectIntl(CheckupControlsUI)
<CheckupControls
allDeprecations={
Array [
Object {
@ -170,7 +170,6 @@ exports[`CheckupTab render with deprecations 1`] = `
onFilterChange={[Function]}
onGroupByChange={[Function]}
onSearchChange={[Function]}
search=""
/>
<EuiSpacer />
<GroupedDeprecations

View file

@ -68,7 +68,7 @@ export class CheckupTab extends UpgradeAssistantTabComponent<CheckupTabProps, Ch
setSelectedTabIndex,
showBackupWarning = false,
} = this.props;
const { currentFilter, search, currentGroupBy } = this.state;
const { currentFilter, currentGroupBy } = this.state;
return (
<Fragment>
@ -143,7 +143,6 @@ export class CheckupTab extends UpgradeAssistantTabComponent<CheckupTabProps, Ch
loadData={refreshCheckupData}
currentFilter={currentFilter}
onFilterChange={this.changeFilter}
search={search}
onSearchChange={this.changeSearch}
availableGroupByOptions={this.availableGroupByOptions()}
currentGroupBy={currentGroupBy}

View file

@ -3,73 +3,112 @@
* 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, { FunctionComponent } from 'react';
import { EuiButton, EuiFieldSearch, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { FormattedMessage, injectI18n } from '@kbn/i18n/react';
import React, { FunctionComponent, useState } from 'react';
import { i18n } from '@kbn/i18n';
import { EuiButton, EuiFieldSearch, EuiFlexGroup, EuiFlexItem, EuiCallOut } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { DeprecationInfo } from 'src/legacy/core_plugins/elasticsearch';
import { GroupByOption, LevelFilterOption, LoadingState } from '../../types';
import { FilterBar } from './filter_bar';
import { GroupByBar } from './group_by_bar';
interface CheckupControlsProps extends ReactIntl.InjectedIntlProps {
import { validateRegExpString } from '../../../utils';
interface CheckupControlsProps {
allDeprecations?: DeprecationInfo[];
loadingState: LoadingState;
loadData: () => void;
currentFilter: LevelFilterOption;
onFilterChange: (filter: LevelFilterOption) => void;
search: string;
onSearchChange: (filter: string) => void;
availableGroupByOptions: GroupByOption[];
currentGroupBy: GroupByOption;
onGroupByChange: (groupBy: GroupByOption) => void;
}
export const CheckupControlsUI: FunctionComponent<CheckupControlsProps> = ({
export const CheckupControls: FunctionComponent<CheckupControlsProps> = ({
allDeprecations,
loadingState,
loadData,
currentFilter,
onFilterChange,
search,
onSearchChange,
availableGroupByOptions,
currentGroupBy,
onGroupByChange,
intl,
}) => (
<EuiFlexGroup alignItems="center" wrap={true} responsive={false}>
<EuiFlexItem grow={true}>
<EuiFieldSearch
aria-label="Filter"
placeholder={intl.formatMessage({
id: 'xpack.upgradeAssistant.checkupTab.controls.searchBarPlaceholder',
defaultMessage: 'Filter',
})}
value={search}
onChange={e => onSearchChange(e.target.value)}
/>
</EuiFlexItem>
}) => {
const [searchTermError, setSearchTermError] = useState<null | string>(null);
const filterInvalid = Boolean(searchTermError);
return (
<EuiFlexGroup direction="column" responsive={false}>
<EuiFlexItem grow={true}>
<EuiFlexGroup alignItems="center" wrap={true} responsive={false}>
<EuiFlexItem>
<EuiFieldSearch
isInvalid={filterInvalid}
aria-label={i18n.translate(
'xpack.upgradeAssistant.checkupTab.controls.searchBarPlaceholderAriaLabel',
{ defaultMessage: 'Filter' }
)}
placeholder={i18n.translate(
'xpack.upgradeAssistant.checkupTab.controls.searchBarPlaceholder',
{
defaultMessage: 'Filter',
}
)}
onChange={e => {
const string = e.target.value;
const errorMessage = validateRegExpString(string);
if (errorMessage) {
// Emit an empty search term to listeners if search term is invalid.
onSearchChange('');
setSearchTermError(errorMessage);
} else {
onSearchChange(e.target.value);
if (searchTermError) {
setSearchTermError(null);
}
}
}}
/>
</EuiFlexItem>
{/* These two components provide their own EuiFlexItem wrappers */}
<FilterBar {...{ allDeprecations, currentFilter, onFilterChange }} />
<GroupByBar {...{ availableGroupByOptions, currentGroupBy, onGroupByChange }} />
{/* These two components provide their own EuiFlexItem wrappers */}
<FilterBar {...{ allDeprecations, currentFilter, onFilterChange }} />
<GroupByBar {...{ availableGroupByOptions, currentGroupBy, onGroupByChange }} />
<EuiFlexItem grow={false}>
<EuiButton
fill
onClick={loadData}
iconType="refresh"
isLoading={loadingState === LoadingState.Loading}
>
<FormattedMessage
id="xpack.upgradeAssistant.checkupTab.controls.refreshButtonLabel"
defaultMessage="Refresh"
/>
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
);
export const CheckupControls = injectI18n(CheckupControlsUI);
<EuiFlexItem grow={false}>
<EuiButton
fill
onClick={loadData}
iconType="refresh"
isLoading={loadingState === LoadingState.Loading}
>
<FormattedMessage
id="xpack.upgradeAssistant.checkupTab.controls.refreshButtonLabel"
defaultMessage="Refresh"
/>
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
{filterInvalid && (
<EuiFlexItem grow={false}>
<EuiCallOut
color="danger"
title={i18n.translate(
'xpack.upgradeAssistant.checkupTab.controls.filterErrorMessageLabel',
{
defaultMessage: 'Filter invalid: {searchTermError}',
values: { searchTermError },
}
)}
iconType="faceSad"
/>
</EuiFlexItem>
)}
</EuiFlexGroup>
);
};

View file

@ -0,0 +1,20 @@
/*
* 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 { validateRegExpString } from './utils';
describe('validRegExpString', () => {
it('correctly returns false for invalid strings', () => {
expect(validateRegExpString('?asd')).toContain(`Invalid regular expression`);
expect(validateRegExpString('*asd')).toContain(`Invalid regular expression`);
expect(validateRegExpString('(')).toContain(`Invalid regular expression`);
});
it('correctly returns true for valid strings', () => {
expect(validateRegExpString('asd')).toBe('');
expect(validateRegExpString('.*asd')).toBe('');
expect(validateRegExpString('')).toBe('');
});
});

View file

@ -0,0 +1,19 @@
/*
* 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 { pipe } from 'fp-ts/lib/pipeable';
import { tryCatch, fold } from 'fp-ts/lib/Either';
export const validateRegExpString = (s: string) =>
pipe(
tryCatch(
() => new RegExp(s),
e => (e as Error).message
),
fold(
(errorMessage: string) => errorMessage,
() => ''
)
);