[Discover] EUI-fication of the filters (#48452)

* New filter UI

* Prototype of functionality

* EUIFication of filters in Discover

* Adding tests for field_selector component

* Replacing FieldSelector with EuiForm components

* Adding more discover field search tests

* Removing console statement

* Fixing failing functional test

* Removing obsolete snapshot

* design tweaks for filter popover

* use compressed style inputs

* Changing selectors to be EuiButtonGroup

* Removing unnecessary if statement

* Getting rid of ts-ignore warning
This commit is contained in:
Maja Grubic 2019-10-31 17:53:40 +00:00 committed by GitHub
parent f4f43f5ad7
commit 559ac840c5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 345 additions and 90 deletions

View file

@ -27,3 +27,11 @@
padding-left: $euiSizeXS;
margin-left: $euiSizeXS;
}
.dscFieldSearch__filterWrapper {
flex-grow: 0;
}
.dscFieldSearch__formWrapper {
padding: $euiSizeM;
}

View file

@ -17,54 +17,123 @@
* under the License.
*/
import React from 'react';
import { act } from 'react-dom/test-utils';
import { mountWithIntl } from 'test_utils/enzyme_helpers';
// @ts-ignore
import { findTestSubject } from '@elastic/eui/lib/test';
import { DiscoverFieldSearch } from './discover_field_search';
import { DiscoverFieldSearch, Props } from './discover_field_search';
import { EuiButtonGroupProps } from '@elastic/eui';
import { ReactWrapper } from 'enzyme';
describe('DiscoverFieldSearch', () => {
function mountComponent() {
const props = {
onChange: jest.fn(),
onShowFilter: jest.fn(),
showFilter: false,
filtersActive: 0,
value: 'test',
};
const comp = mountWithIntl(<DiscoverFieldSearch {...props} />);
const input = findTestSubject(comp, 'fieldFilterSearchInput');
const btn = findTestSubject(comp, 'toggleFieldFilterButton');
return { comp, input, btn, props };
const defaultProps = {
onChange: jest.fn(),
value: 'test',
types: ['any', 'string', '_source'],
};
function mountComponent(props?: Props) {
const compProps = props || defaultProps;
const comp = mountWithIntl(<DiscoverFieldSearch {...compProps} />);
return comp;
}
function findButtonGroup(component: ReactWrapper, id: string) {
return component.find(`[data-test-subj="${id}ButtonGroup"]`).first();
}
test('enter value', () => {
const { input, props } = mountComponent();
const component = mountComponent();
const input = findTestSubject(component, 'fieldFilterSearchInput');
input.simulate('change', { target: { value: 'new filter' } });
expect(props.onChange).toBeCalledTimes(1);
expect(defaultProps.onChange).toBeCalledTimes(1);
});
// this should work, but doesn't, have to do some research
test('click toggle filter button', () => {
const { btn, props } = mountComponent();
btn.simulate('click');
expect(props.onShowFilter).toBeCalledTimes(1);
});
test('change showFilter value should change aria label', () => {
const { comp } = mountComponent();
let btn = findTestSubject(comp, 'toggleFieldFilterButton');
expect(btn.prop('aria-label')).toEqual('Show field filter settings');
comp.setProps({ showFilter: true });
btn = findTestSubject(comp, 'toggleFieldFilterButton');
expect(btn.prop('aria-label')).toEqual('Hide field filter settings');
});
test('change filtersActive should change facet selection', () => {
const { comp } = mountComponent();
let btn = findTestSubject(comp, 'toggleFieldFilterButton');
test('change in active filters should change facet selection and call onChange', () => {
const onChange = jest.fn();
const component = mountComponent({ ...defaultProps, ...{ onChange } });
let btn = findTestSubject(component, 'toggleFieldFilterButton');
expect(btn.hasClass('euiFacetButton--isSelected')).toBeFalsy();
comp.setProps({ filtersActive: 3 });
btn = findTestSubject(comp, 'toggleFieldFilterButton');
btn.simulate('click');
const aggregatableButtonGroup = findButtonGroup(component, 'aggregatable');
act(() => {
// @ts-ignore
(aggregatableButtonGroup.props() as EuiButtonGroupProps).onChange('aggregatable-true', null);
});
component.update();
btn = findTestSubject(component, 'toggleFieldFilterButton');
expect(btn.hasClass('euiFacetButton--isSelected')).toBe(true);
expect(onChange).toBeCalledWith('aggregatable', true);
});
test('change in active filters should change filters count', () => {
const component = mountComponent();
let btn = findTestSubject(component, 'toggleFieldFilterButton');
btn.simulate('click');
btn = findTestSubject(component, 'toggleFieldFilterButton');
const badge = btn.find('.euiNotificationBadge');
// no active filters
expect(badge.text()).toEqual('0');
// change value of aggregatable select
const aggregatableButtonGroup = findButtonGroup(component, 'aggregatable');
act(() => {
// @ts-ignore
(aggregatableButtonGroup.props() as EuiButtonGroupProps).onChange('aggregatable-true', null);
});
component.update();
expect(badge.text()).toEqual('1');
// change value of searchable select
const searchableButtonGroup = findButtonGroup(component, 'searchable');
act(() => {
// @ts-ignore
(searchableButtonGroup.props() as EuiButtonGroupProps).onChange('searchable-true', null);
});
component.update();
expect(badge.text()).toEqual('2');
// change value of searchable select
act(() => {
// @ts-ignore
(searchableButtonGroup.props() as EuiButtonGroupProps).onChange('searchable-any', null);
});
component.update();
expect(badge.text()).toEqual('1');
});
test('change in missing fields switch should not change filter count', () => {
const component = mountComponent();
const btn = findTestSubject(component, 'toggleFieldFilterButton');
btn.simulate('click');
const badge = btn.find('.euiNotificationBadge');
expect(badge.text()).toEqual('0');
const missingSwitch = findTestSubject(component, 'missingSwitch');
missingSwitch.simulate('change', { target: { value: false } });
expect(badge.text()).toEqual('0');
});
test('change in filters triggers onChange', () => {
const onChange = jest.fn();
const component = mountComponent({ ...defaultProps, ...{ onChange } });
const btn = findTestSubject(component, 'toggleFieldFilterButton');
btn.simulate('click');
const aggregtableButtonGroup = findButtonGroup(component, 'aggregatable');
const missingSwitch = findTestSubject(component, 'missingSwitch');
act(() => {
// @ts-ignore
(aggregtableButtonGroup.props() as EuiButtonGroupProps).onChange('aggregatable-true', null);
});
missingSwitch.simulate('change', { target: { value: false } });
expect(onChange).toBeCalledTimes(2);
});
test('change in type filters triggers onChange with appropriate value', () => {
const onChange = jest.fn();
const component = mountComponent({ ...defaultProps, ...{ onChange } });
const btn = findTestSubject(component, 'toggleFieldFilterButton');
btn.simulate('click');
const typeSelector = findTestSubject(component, 'typeSelect');
typeSelector.simulate('change', { target: { value: 'string' } });
expect(onChange).toBeCalledWith('type', 'string');
typeSelector.simulate('change', { target: { value: 'any' } });
expect(onChange).toBeCalledWith('type', 'any');
});
});

View file

@ -16,61 +16,234 @@
* specific language governing permissions and limitations
* under the License.
*/
import React from 'react';
import React, { OptionHTMLAttributes, ReactNode, useState } from 'react';
import { i18n } from '@kbn/i18n';
import { EuiFacetButton, EuiFieldSearch, EuiFlexGroup, EuiFlexItem, EuiIcon } from '@elastic/eui';
import {
EuiFacetButton,
EuiFieldSearch,
EuiFlexGroup,
EuiFlexItem,
EuiIcon,
EuiPopover,
EuiPopoverFooter,
EuiPopoverTitle,
EuiSelect,
EuiSwitch,
EuiForm,
EuiFormRow,
EuiButtonGroup,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
export interface State {
searchable: string;
aggregatable: string;
type: string;
missing: boolean;
[index: string]: string | boolean;
}
export interface Props {
/**
* triggered on input of user into search field
*/
onChange: (field: string, value: string) => void;
/**
* triggered when the "additional filter btn" is clicked
*/
onShowFilter: () => void;
/**
* determines whether additional filter fields are displayed
*/
showFilter: boolean;
onChange: (field: string, value: string | boolean | undefined) => void;
/**
* the input value of the user
*/
value?: string;
/**
* the number of selected filters
* types for the type filter
*/
filtersActive: number;
types: string[];
}
/**
* Component is Discover's side bar to search of available fields
* Additionally there's a button displayed that allows the user to show/hide more filter fields
*/
export function DiscoverFieldSearch({
showFilter,
onChange,
onShowFilter,
value,
filtersActive,
}: Props) {
export function DiscoverFieldSearch({ onChange, value, types }: Props) {
if (typeof value !== 'string') {
// at initial rendering value is undefined (angular related), this catches the warning
// should be removed once all is react
return null;
}
const filterBtnAriaLabel = showFilter
const searchPlaceholder = i18n.translate('kbn.discover.fieldChooser.searchPlaceHolder', {
defaultMessage: 'Search field names',
});
const aggregatableLabel = i18n.translate('kbn.discover.fieldChooser.filter.aggregatableLabel', {
defaultMessage: 'Aggregatable',
});
const searchableLabel = i18n.translate('kbn.discover.fieldChooser.filter.searchableLabel', {
defaultMessage: 'Searchable',
});
const typeLabel = i18n.translate('kbn.discover.fieldChooser.filter.typeLabel', {
defaultMessage: 'Type',
});
const typeOptions = types
? types.map(type => {
return { value: type, text: type };
})
: [{ value: 'any', text: 'any' }];
const [activeFiltersCount, setActiveFiltersCount] = useState(0);
const [isPopoverOpen, setPopoverOpen] = useState(false);
const [values, setValues] = useState<State>({
searchable: 'any',
aggregatable: 'any',
type: 'any',
missing: true,
});
const filterBtnAriaLabel = isPopoverOpen
? i18n.translate('kbn.discover.fieldChooser.toggleFieldFilterButtonHideAriaLabel', {
defaultMessage: 'Hide field filter settings',
})
: i18n.translate('kbn.discover.fieldChooser.toggleFieldFilterButtonShowAriaLabel', {
defaultMessage: 'Show field filter settings',
});
const searchPlaceholder = i18n.translate('kbn.discover.fieldChooser.searchPlaceHolder', {
defaultMessage: 'Search field names',
});
const handleFacetButtonClicked = () => {
setPopoverOpen(!isPopoverOpen);
};
const applyFilterValue = (id: string, filterValue: string | boolean) => {
switch (filterValue) {
case 'any':
if (id !== 'type') {
onChange(id, undefined);
} else {
onChange(id, filterValue);
}
break;
case 'true':
onChange(id, true);
break;
case 'false':
onChange(id, false);
break;
default:
onChange(id, filterValue);
}
};
const isFilterActive = (name: string, filterValue: string | boolean) => {
return name !== 'missing' && filterValue !== 'any';
};
const handleValueChange = (name: string, filterValue: string | boolean) => {
const previousValue = values[name];
updateFilterCount(name, previousValue, filterValue);
const updatedValues = { ...values };
updatedValues[name] = filterValue;
setValues(updatedValues);
applyFilterValue(name, filterValue);
};
const updateFilterCount = (
name: string,
previousValue: string | boolean,
currentValue: string | boolean
) => {
const previouslyFilterActive = isFilterActive(name, previousValue);
const filterActive = isFilterActive(name, currentValue);
const diff = Number(filterActive) - Number(previouslyFilterActive);
setActiveFiltersCount(activeFiltersCount + diff);
};
const handleMissingChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const missingValue = e.target.checked;
handleValueChange('missing', missingValue);
};
const buttonContent = (
<EuiFacetButton
aria-label={filterBtnAriaLabel}
data-test-subj="toggleFieldFilterButton"
className="dscToggleFieldFilterButton"
icon={<EuiIcon type="filter" />}
isSelected={activeFiltersCount > 0}
quantity={activeFiltersCount}
onClick={handleFacetButtonClicked}
>
<FormattedMessage
id="kbn.discover.fieldChooser.fieldFilterFacetButtonLabel"
defaultMessage="Filter by type"
/>
</EuiFacetButton>
);
const select = (
id: string,
selectOptions: Array<{ text: ReactNode } & OptionHTMLAttributes<HTMLOptionElement>>,
selectValue: string
) => {
return (
<EuiSelect
id={`${id}-select`}
options={selectOptions}
value={selectValue}
onChange={(e: React.ChangeEvent<HTMLSelectElement>) =>
handleValueChange(id, e.target.value)
}
aria-label={i18n.translate('kbn.discover.fieldChooser.filter.fieldSelectorLabel', {
defaultMessage: 'Selection of {id} filter options',
values: { id },
})}
data-test-subj={`${id}Select`}
compressed
/>
);
};
const toggleButtons = (id: string) => {
return [
{
id: `${id}-any`,
label: 'any',
},
{
id: `${id}-true`,
label: 'yes',
},
{
id: `${id}-false`,
label: 'no',
},
];
};
const buttonGroup = (id: string, legend: string) => {
return (
<EuiButtonGroup
legend={legend}
options={toggleButtons(id)}
idSelected={`${id}-${values[id]}`}
onChange={optionId => handleValueChange(id, optionId.replace(`${id}-`, ''))}
buttonSize="compressed"
isFullWidth
data-test-subj={`${id}ButtonGroup`}
/>
);
};
const selectionPanel = (
<div className="dscFieldSearch__formWrapper">
<EuiForm data-test-subj="filterSelectionPanel">
<EuiFormRow fullWidth label={aggregatableLabel} display="columnCompressed">
{buttonGroup('aggregatable', aggregatableLabel)}
</EuiFormRow>
<EuiFormRow fullWidth label={searchableLabel} display="columnCompressed">
{buttonGroup('searchable', searchableLabel)}
</EuiFormRow>
<EuiFormRow fullWidth label={typeLabel} display="columnCompressed">
{select('type', typeOptions, values.type)}
</EuiFormRow>
</EuiForm>
</div>
);
return (
<React.Fragment>
<EuiFlexGroup responsive={false} gutterSize={'s'}>
@ -86,20 +259,35 @@ export function DiscoverFieldSearch({
/>
</EuiFlexItem>
</EuiFlexGroup>
<EuiFacetButton
aria-label={filterBtnAriaLabel}
data-test-subj="toggleFieldFilterButton"
className="dscToggleFieldFilterButton"
icon={<EuiIcon type="filter" />}
isSelected={filtersActive > 0}
quantity={filtersActive}
onClick={() => onShowFilter()}
>
<FormattedMessage
id="kbn.discover.fieldChooser.fieldFilterFacetButtonLabel"
defaultMessage="Filter by type"
/>
</EuiFacetButton>
<div className="dscFieldSearch__filterWrapper">
<EuiPopover
id="dataPanelTypeFilter"
panelClassName="euiFilterGroup__popoverPanel"
panelPaddingSize="none"
anchorPosition="downLeft"
display="block"
isOpen={isPopoverOpen}
closePopover={() => {}}
button={buttonContent}
>
<EuiPopoverTitle>
{i18n.translate('kbn.discover.fieldChooser.filter.filterByTypeLabel', {
defaultMessage: 'Filter by type',
})}
</EuiPopoverTitle>
{selectionPanel}
<EuiPopoverFooter>
<EuiSwitch
label={i18n.translate('kbn.discover.fieldChooser.filter.hideMissingFieldsLabel', {
defaultMessage: 'Hide missing fields',
})}
checked={values.missing}
onChange={handleMissingChange}
data-test-subj="missingSwitch"
/>
</EuiPopoverFooter>
</EuiPopover>
</div>
</React.Fragment>
);
}

View file

@ -27,9 +27,7 @@ const app = uiModules.get('apps/discover');
app.directive('discoverFieldSearch', function(reactDirective: any) {
return reactDirective(wrapInI18nContext(DiscoverFieldSearch), [
['onChange', { watchDepth: 'reference' }],
['onShowFilter', { watchDepth: 'reference' }],
['showFilter', { watchDepth: 'value' }],
['value', { watchDepth: 'value' }],
['filtersActive', { watchDepth: 'value' }],
['types', { watchDepth: 'value' }],
]);
});

View file

@ -9,10 +9,8 @@
<form>
<discover-field-search
on-change="setFilterValue"
on-show-filter="toggleShowFilter"
show-filter="showFilter"
value="filter.vals.name"
filters-active="filtersActive"
types="fieldTypes"
>
</discover-field-search>
<div data-test-subj="discoverFieldFilter" class="dscFieldFilter" ng-show="showFilter">

View file

@ -283,19 +283,13 @@ export function DiscoverPageProvider({ getService, getPageObjects }) {
}
async openSidebarFieldFilter() {
const fieldFilterFormExists = await testSubjects.exists('discoverFieldFilter');
if (!fieldFilterFormExists) {
await testSubjects.click('toggleFieldFilterButton');
await testSubjects.existOrFail('discoverFieldFilter');
}
await testSubjects.click('toggleFieldFilterButton');
await testSubjects.existOrFail('filterSelectionPanel');
}
async closeSidebarFieldFilter() {
const fieldFilterFormExists = await testSubjects.exists('discoverFieldFilter');
if (fieldFilterFormExists) {
await testSubjects.click('toggleFieldFilterButton');
await testSubjects.missingOrFail('discoverFieldFilter', { allowHidden: true });
}
await testSubjects.click('toggleFieldFilterButton');
await testSubjects.missingOrFail('filterSelectionPanel', { allowHidden: true });
}
}