Skip to content

Commit

Permalink
[Lens] Prevent KQL Popovers From Stacking (#118258)
Browse files Browse the repository at this point in the history
  • Loading branch information
drewdaemon authored and dmlemeshko committed Nov 29, 2021
1 parent 99f5c33 commit 0cd068d
Show file tree
Hide file tree
Showing 3 changed files with 116 additions and 55 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* 2.0.
*/

import React, { MouseEventHandler } from 'react';
import React from 'react';
import { shallow, mount } from 'enzyme';
import { act } from 'react-dom/test-utils';
import { EuiPopover, EuiLink } from '@elastic/eui';
Expand All @@ -20,26 +20,74 @@ jest.mock('.', () => ({
defaultLabel: 'label',
}));

const defaultProps = {
filter: {
input: { query: 'bytes >= 1', language: 'kuery' },
label: 'More than one',
id: '1',
},
setFilter: jest.fn(),
indexPattern: createMockedIndexPattern(),
Button: ({ onClick }: { onClick: MouseEventHandler }) => (
<EuiLink onClick={onClick}>trigger</EuiLink>
),
initiallyOpen: true,
};
jest.mock('../../../../../../../../src/plugins/data/public', () => ({
QueryStringInput: () => {
return 'QueryStringInput';
},
}));

describe('filter popover', () => {
let defaultProps: Parameters<typeof FilterPopover>[0];
let mockOnClick: jest.Mock;

beforeEach(() => {
mockOnClick = jest.fn();

defaultProps = {
filter: {
input: { query: 'bytes >= 1', language: 'kuery' },
label: 'More than one',
id: '1',
},
setFilter: jest.fn(),
indexPattern: createMockedIndexPattern(),
Button: () => <EuiLink onClick={mockOnClick}>trigger</EuiLink>,
isOpen: true,
triggerClose: () => {},
};
});

describe('interactions', () => {
it('should open/close according to isOpen', () => {
const instance = mount(<FilterPopover {...{ ...defaultProps, isOpen: true }} />);

expect(instance.find(EuiPopover).prop('isOpen')).toEqual(true);

instance.setProps({ ...defaultProps, isOpen: false });
instance.update();

expect(instance.find(EuiPopover).prop('isOpen')).toEqual(false);
});

it('should report click event', () => {
const instance = mount(<FilterPopover {...defaultProps} />);

expect(mockOnClick).not.toHaveBeenCalled();

instance.find(EuiPopover).find('button').simulate('click', {});

expect(mockOnClick).toHaveBeenCalledTimes(1);
});

it('should trigger close', () => {
const props = { ...defaultProps, triggerClose: jest.fn() };
const instance = mount(<FilterPopover {...props} />);
expect(instance.find(EuiPopover).prop('isOpen')).toEqual(true);

// Trigger from EuiPopover
act(() => {
instance.find(EuiPopover).prop('closePopover')!();
});
expect(props.triggerClose).toHaveBeenCalledTimes(1);

// Trigger from submit
act(() => {
instance.find(LabelInput).prop('onSubmit')!();
});
expect(props.triggerClose).toHaveBeenCalledTimes(2);
});
});

it('passes correct props to QueryStringInput', () => {
const instance = mount(<FilterPopover {...defaultProps} />);
instance.update();
Expand All @@ -52,16 +100,7 @@ describe('filter popover', () => {
})
);
});
it('should be open if is open by creation', () => {
const instance = mount(<FilterPopover {...defaultProps} />);
instance.update();
expect(instance.find(EuiPopover).prop('isOpen')).toEqual(true);
act(() => {
instance.find(EuiPopover).prop('closePopover')!();
});
instance.update();
expect(instance.find(EuiPopover).prop('isOpen')).toEqual(false);
});

it('should call setFilter when modifying QueryInput', () => {
const setFilter = jest.fn();
const instance = shallow(<FilterPopover {...defaultProps} setFilter={setFilter} />);
Expand All @@ -78,6 +117,7 @@ describe('filter popover', () => {
id: '1',
});
});

it('should call setFilter when modifying LabelInput', () => {
const setFilter = jest.fn();
const instance = shallow(<FilterPopover {...defaultProps} setFilter={setFilter} />);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

import './filter_popover.scss';

import React, { MouseEventHandler, useEffect, useState } from 'react';
import React from 'react';
import { EuiPopover, EuiSpacer } from '@elastic/eui';
import { FilterValue, defaultLabel, isQueryValid } from '.';
import { IndexPattern } from '../../../types';
Expand All @@ -20,28 +20,40 @@ export const FilterPopover = ({
setFilter,
indexPattern,
Button,
initiallyOpen,
isOpen,
triggerClose,
}: {
filter: FilterValue;
setFilter: Function;
indexPattern: IndexPattern;
Button: React.FunctionComponent<{ onClick: MouseEventHandler }>;
initiallyOpen: boolean;
Button: React.FunctionComponent;
isOpen: boolean;
triggerClose: () => void;
}) => {
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
const inputRef = React.useRef<HTMLInputElement>();

// set popover open on start to work around EUI bug
useEffect(() => {
setIsPopoverOpen(initiallyOpen);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
// The following code is to prevent an <ESCAPE> keypress
// from propagating.
//
// TODO - It looks like EUI should be handling this
// (see https://github.com/elastic/eui/commit/ad97583b0d644690379f72c7a20879cfadb16e7a)
const popoverRef = React.useRef<EuiPopover>(null);
let panelElement: HTMLDivElement;
const panelRefCallback = (element: HTMLDivElement) => {
const listener = (event: KeyboardEvent) => {
if (event.key === 'Escape') {
event.stopPropagation();
panelElement.removeEventListener('keydown', listener);
popoverRef.current?.closePopover();
}
};

const closePopover = () => {
if (isPopoverOpen) {
setIsPopoverOpen(false);
if (element) {
panelElement = element;
panelElement.addEventListener('keydown', listener);
}
};
// End <ESCAPE> handling code

const setFilterLabel = (label: string) => setFilter({ ...filter, label });
const setFilterQuery = (input: Query) => setFilter({ ...filter, input });
Expand All @@ -58,19 +70,15 @@ export const FilterPopover = ({

return (
<EuiPopover
ref={popoverRef}
panelRef={panelRefCallback}
data-test-subj="indexPattern-filters-existingFilterContainer"
anchorClassName="eui-fullWidth"
panelClassName="lnsIndexPatternDimensionEditor__filtersEditor"
isOpen={isPopoverOpen}
isOpen={isOpen}
ownFocus
closePopover={() => closePopover()}
button={
<Button
onClick={() => {
setIsPopoverOpen((open) => !open);
}}
/>
}
closePopover={() => triggerClose()}
button={<Button />}
>
<QueryInput
isInvalid={!isQueryValid(filter.input, indexPattern)}
Expand All @@ -87,7 +95,7 @@ export const FilterPopover = ({
onChange={setFilterLabel}
placeholder={getPlaceholder(filter.input.query)}
inputRef={inputRef}
onSubmit={() => closePopover()}
onSubmit={() => triggerClose()}
dataTestSubj="indexPattern-filters-label"
/>
</EuiPopover>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
*/

import './filters.scss';
import React, { MouseEventHandler, useState } from 'react';
import React, { useState } from 'react';
import { fromKueryExpression, luceneStringToDsl, toElasticsearchQuery } from '@kbn/es-query';
import { omit } from 'lodash';
import { i18n } from '@kbn/i18n';
Expand Down Expand Up @@ -172,7 +172,7 @@ export const FilterList = ({
indexPattern: IndexPattern;
defaultQuery: Filter;
}) => {
const [isOpenByCreation, setIsOpenByCreation] = useState(false);
const [activeFilterId, setActiveFilterId] = useState('');
const [localFilters, setLocalFilters] = useState(() =>
filters.map((filter) => ({ ...filter, id: generateId() }))
);
Expand All @@ -183,14 +183,19 @@ export const FilterList = ({
setLocalFilters(updatedFilters);
};

const onAddFilter = () =>
const onAddFilter = () => {
const newFilterId = generateId();

updateFilters([
...localFilters,
{
...defaultQuery,
id: generateId(),
id: newFilterId,
},
]);

setActiveFilterId(newFilterId);
};
const onRemoveFilter = (id: string) =>
updateFilters(localFilters.filter((filter) => filter.id !== id));

Expand All @@ -207,6 +212,14 @@ export const FilterList = ({
)
);

const changeActiveFilter = (filterId: string) => {
let newActiveFilterId = filterId;
if (activeFilterId === filterId) {
newActiveFilterId = ''; // toggle off
}
setActiveFilterId(newActiveFilterId);
};

return (
<>
<DragDropBuckets
Expand Down Expand Up @@ -235,17 +248,18 @@ export const FilterList = ({
>
<FilterPopover
data-test-subj="indexPattern-filters-existingFilterContainer"
initiallyOpen={idx === localFilters.length - 1 && isOpenByCreation}
isOpen={filter.id === activeFilterId}
triggerClose={() => changeActiveFilter('')}
indexPattern={indexPattern}
filter={filter}
setFilter={(f: FilterValue) => {
onChangeValue(f.id, f.input, f.label);
}}
Button={({ onClick }: { onClick: MouseEventHandler }) => (
Button={() => (
<EuiLink
className="lnsFiltersOperation__popoverButton"
data-test-subj="indexPattern-filters-existingFilterTrigger"
onClick={onClick}
onClick={() => changeActiveFilter(filter.id)}
color={isInvalid ? 'danger' : 'text'}
title={i18n.translate('xpack.lens.indexPattern.filters.clickToEdit', {
defaultMessage: 'Click to edit',
Expand All @@ -262,7 +276,6 @@ export const FilterList = ({
<NewBucketButton
onClick={() => {
onAddFilter();
setIsOpenByCreation(true);
}}
label={i18n.translate('xpack.lens.indexPattern.filters.addaFilter', {
defaultMessage: 'Add a filter',
Expand Down

0 comments on commit 0cd068d

Please sign in to comment.