diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filter_popover.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filter_popover.test.tsx
index 4d4b4018d75a7..2fb2bef7f9787 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filter_popover.test.tsx
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filter_popover.test.tsx
@@ -9,7 +9,8 @@ import { shallow } from 'enzyme';
import { act } from 'react-dom/test-utils';
import { EuiPopover, EuiLink } from '@elastic/eui';
import { createMockedIndexPattern } from '../../../mocks';
-import { FilterPopover, QueryInput, LabelInput } from './filter_popover';
+import { FilterPopover, QueryInput } from './filter_popover';
+import { LabelInput } from '../shared_components';
jest.mock('.', () => ({
isQueryValid: () => true,
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filter_popover.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filter_popover.tsx
index cdfa19f53a13a..91adbcecaf897 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filter_popover.tsx
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filter_popover.tsx
@@ -7,11 +7,12 @@ import './filter_popover.scss';
import React, { MouseEventHandler, useState } from 'react';
import { useDebounce } from 'react-use';
-import { EuiPopover, EuiFieldText, EuiSpacer, keys } from '@elastic/eui';
+import { EuiPopover, EuiSpacer } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FilterValue, defaultLabel, isQueryValid } from '.';
import { IndexPattern } from '../../../types';
import { QueryStringInput, Query } from '../../../../../../../../src/plugins/data/public';
+import { LabelInput } from '../shared_components';
export const FilterPopover = ({
filter,
@@ -51,6 +52,7 @@ export const FilterPopover = ({
return (
setPopoverOpen(false)}
+ dataTestSubj="indexPattern-filters-label"
/>
);
@@ -141,53 +144,3 @@ export const QueryInput = ({
/>
);
};
-
-export const LabelInput = ({
- value,
- onChange,
- placeholder,
- inputRef,
- onSubmit,
-}: {
- value: string;
- onChange: (value: string) => void;
- placeholder: string;
- inputRef: React.MutableRefObject;
- onSubmit: () => void;
-}) => {
- const [inputValue, setInputValue] = useState(value);
-
- React.useEffect(() => {
- setInputValue(value);
- }, [value, setInputValue]);
-
- useDebounce(() => onChange(inputValue), 256, [inputValue]);
-
- const handleInputChange = (e: React.ChangeEvent) => {
- const val = String(e.target.value);
- setInputValue(val);
- };
-
- return (
- {
- if (node) {
- inputRef.current = node;
- }
- }}
- onKeyDown={({ key }: React.KeyboardEvent) => {
- if (keys.ENTER === key) {
- onSubmit();
- }
- }}
- prepend={i18n.translate('xpack.lens.indexPattern.filters.label', {
- defaultMessage: 'Label',
- })}
- />
- );
-};
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filters.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filters.test.tsx
index 13854d1ca91d6..6364d3913bf57 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filters.test.tsx
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filters.test.tsx
@@ -231,7 +231,7 @@ describe('filters', () => {
expect(
instance
.find('[data-test-subj="indexPattern-filters-existingFilterContainer"]')
- .at(2)
+ .at(3)
.text()
).toEqual('src : 2');
});
@@ -250,7 +250,7 @@ describe('filters', () => {
);
instance
- .find('[data-test-subj="indexPattern-filters-existingFilterDelete"]')
+ .find('[data-test-subj="lns-customBucketContainer-remove"]')
.at(2)
.simulate('click');
expect(setStateSpy).toHaveBeenCalledWith({
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filters.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filters.tsx
index c740f8466e1b1..bca64d1f2f362 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filters.tsx
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filters.tsx
@@ -8,27 +8,14 @@ import './filters.scss';
import React, { MouseEventHandler, useState } from 'react';
import { omit } from 'lodash';
import { i18n } from '@kbn/i18n';
-import {
- EuiDragDropContext,
- EuiDraggable,
- EuiDroppable,
- EuiFlexGroup,
- EuiFlexItem,
- EuiPanel,
- euiDragDropReorder,
- EuiButtonIcon,
- EuiButtonEmpty,
- EuiIcon,
- EuiFormRow,
- EuiLink,
- htmlIdGenerator,
-} from '@elastic/eui';
+import { EuiFormRow, EuiLink, htmlIdGenerator } from '@elastic/eui';
import { updateColumnParam } from '../../../state_helpers';
import { OperationDefinition } from '../index';
import { FieldBasedIndexPatternColumn } from '../column_types';
import { FilterPopover } from './filter_popover';
import { IndexPattern } from '../../../types';
import { Query, esKuery, esQuery } from '../../../../../../../../src/plugins/data/public';
+import { NewBucketButton, DragDropBuckets, DraggableBucketContainer } from '../shared_components';
const generateId = htmlIdGenerator();
@@ -37,10 +24,11 @@ export interface Filter {
input: Query;
label: string;
}
+
export interface FilterValue {
+ id: string;
input: Query;
label: string;
- id: string;
}
const customQueryLabel = i18n.translate('xpack.lens.indexPattern.customQuery', {
@@ -73,11 +61,6 @@ export const isQueryValid = (input: Query, indexPattern: IndexPattern) => {
}
};
-interface DraggableLocation {
- droppableId: string;
- index: number;
-}
-
export interface FiltersIndexPatternColumn extends FieldBasedIndexPatternColumn {
operationType: 'filters';
params: {
@@ -219,123 +202,67 @@ export const FilterList = ({
)
);
- const onDragEnd = ({
- source,
- destination,
- }: {
- source?: DraggableLocation;
- destination?: DraggableLocation;
- }) => {
- if (source && destination) {
- const items = euiDragDropReorder(localFilters, source.index, destination.index);
- updateFilters(items);
- }
- };
-
return (
<>
- setIsOpenByCreation(false)}>
-
- {localFilters?.map((filter: FilterValue, idx: number) => {
- const { input, label, id } = filter;
- const queryIsValid = isQueryValid(input, indexPattern);
+ setIsOpenByCreation(false)}
+ droppableId="FILTERS_DROPPABLE_AREA"
+ items={localFilters}
+ >
+ {localFilters?.map((filter: FilterValue, idx: number) => {
+ const isInvalid = !isQueryValid(filter.input, indexPattern);
- return (
-
- {(provided) => (
-
-
- {/* Empty for spacing */}
-
-
-
-
- (
-
- {label || input.query || defaultLabel}
-
- )}
- setFilter={(f: FilterValue) => {
- onChangeValue(f.id, f.input, f.label);
- }}
- />
-
-
- {
- onRemoveFilter(filter.id);
- }}
- aria-label={i18n.translate(
- 'xpack.lens.indexPattern.filters.removeCustomQuery',
- {
- defaultMessage: 'Remove custom query',
- }
- )}
- title={i18n.translate('xpack.lens.indexPattern.filters.remove', {
- defaultMessage: 'Remove',
- })}
- />
-
-
-
+ return (
+ onRemoveFilter(filter.id)}
+ removeTitle={i18n.translate('xpack.lens.indexPattern.filters.removeCustomQuery', {
+ defaultMessage: 'Remove custom query',
+ })}
+ >
+ {
+ onChangeValue(f.id, f.input, f.label);
+ }}
+ Button={({ onClick }: { onClick: MouseEventHandler }) => (
+
+ {filter.label || filter.input.query || defaultLabel}
+
)}
-
- );
- })}
-
-
-
-
+
+ );
+ })}
+
+ {
onAddFilter();
setIsOpenByCreation(true);
}}
- >
- {i18n.translate('xpack.lens.indexPattern.filters.addCustomQuery', {
+ label={i18n.translate('xpack.lens.indexPattern.filters.addCustomQuery', {
defaultMessage: 'Add a custom query',
})}
-
+ />
>
);
};
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/shared_components/buckets.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/shared_components/buckets.test.tsx
new file mode 100644
index 0000000000000..fc0ea92614370
--- /dev/null
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/shared_components/buckets.test.tsx
@@ -0,0 +1,78 @@
+/*
+ * 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 React from 'react';
+import { mount, shallow } from 'enzyme';
+import { act } from 'react-dom/test-utils';
+import { EuiIcon } from '@elastic/eui';
+import { DragDropBuckets, DraggableBucketContainer } from '../shared_components';
+
+jest.mock('@elastic/eui', () => {
+ const original = jest.requireActual('@elastic/eui');
+ return {
+ ...original,
+ EuiDragDropContext: 'eui-drag-drop-context',
+ EuiDroppable: 'eui-droppable',
+ EuiDraggable: (props: any) => props.children(), // eslint-disable-line @typescript-eslint/no-explicit-any
+ };
+});
+
+describe('buckets shared components', () => {
+ describe('DragDropBuckets', () => {
+ it('should call onDragEnd when dragging ended with reordered items', () => {
+ const items = [first
, second
, third
];
+ const defaultProps = {
+ items,
+ onDragStart: jest.fn(),
+ onDragEnd: jest.fn(),
+ droppableId: 'TEST_ID',
+ children: items,
+ };
+ const instance = shallow();
+ act(() => {
+ // simulate dragging ending
+ instance.props().onDragEnd({ source: { index: 0 }, destination: { index: 1 } });
+ });
+
+ expect(defaultProps.onDragEnd).toHaveBeenCalledWith([
+ second
,
+ first
,
+ third
,
+ ]);
+ });
+ });
+ describe('DraggableBucketContainer', () => {
+ const defaultProps = {
+ isInvalid: false,
+ invalidMessage: 'invalid',
+ onRemoveClick: jest.fn(),
+ removeTitle: 'remove',
+ children: popover
,
+ id: '0',
+ idx: 0,
+ };
+ it('should render valid component', () => {
+ const instance = mount();
+ const popover = instance.find('[data-test-subj="popover"]');
+ expect(popover).toHaveLength(1);
+ });
+ it('should render invalid component', () => {
+ const instance = mount();
+ const iconProps = instance.find(EuiIcon).first().props();
+ expect(iconProps.color).toEqual('danger');
+ expect(iconProps.type).toEqual('alert');
+ expect(iconProps.title).toEqual('invalid');
+ });
+ it('should call onRemoveClick when remove icon is clicked', () => {
+ const instance = mount();
+ const removeIcon = instance
+ .find('[data-test-subj="lns-customBucketContainer-remove"]')
+ .first();
+ removeIcon.simulate('click');
+ expect(defaultProps.onRemoveClick).toHaveBeenCalled();
+ });
+ });
+});
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/shared_components/buckets.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/shared_components/buckets.tsx
new file mode 100644
index 0000000000000..62b5f64fb26f2
--- /dev/null
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/shared_components/buckets.tsx
@@ -0,0 +1,134 @@
+/*
+ * 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 React from 'react';
+import { i18n } from '@kbn/i18n';
+import {
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiPanel,
+ EuiButtonIcon,
+ EuiIcon,
+ EuiDragDropContext,
+ euiDragDropReorder,
+ EuiDraggable,
+ EuiDroppable,
+ EuiButtonEmpty,
+} from '@elastic/eui';
+
+export const NewBucketButton = ({ label, onClick }: { label: string; onClick: () => void }) => (
+
+ {label}
+
+);
+
+interface BucketContainerProps {
+ isInvalid?: boolean;
+ invalidMessage: string;
+ onRemoveClick: () => void;
+ removeTitle: string;
+ children: React.ReactNode;
+ dataTestSubj?: string;
+}
+
+const BucketContainer = ({
+ isInvalid,
+ invalidMessage,
+ onRemoveClick,
+ removeTitle,
+ children,
+ dataTestSubj,
+}: BucketContainerProps) => {
+ return (
+
+
+ {/* Empty for spacing */}
+
+
+
+ {children}
+
+
+
+
+
+ );
+};
+
+export const DraggableBucketContainer = ({
+ id,
+ idx,
+ children,
+ ...bucketContainerProps
+}: {
+ id: string;
+ idx: number;
+ children: React.ReactNode;
+} & BucketContainerProps) => {
+ return (
+
+ {(provided) => {children}}
+
+ );
+};
+
+interface DraggableLocation {
+ droppableId: string;
+ index: number;
+}
+
+export const DragDropBuckets = ({
+ items,
+ onDragStart,
+ onDragEnd,
+ droppableId,
+ children,
+}: {
+ items: any; // eslint-disable-line @typescript-eslint/no-explicit-any
+ onDragStart: () => void;
+ onDragEnd: (items: any) => void; // eslint-disable-line @typescript-eslint/no-explicit-any
+ droppableId: string;
+ children: React.ReactElement[];
+}) => {
+ const handleDragEnd = ({
+ source,
+ destination,
+ }: {
+ source?: DraggableLocation;
+ destination?: DraggableLocation;
+ }) => {
+ if (source && destination) {
+ const newItems = euiDragDropReorder(items, source.index, destination.index);
+ onDragEnd(newItems);
+ }
+ };
+ return (
+
+
+ {children}
+
+
+ );
+};
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/shared_components/index.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/shared_components/index.tsx
new file mode 100644
index 0000000000000..a5cac12196959
--- /dev/null
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/shared_components/index.tsx
@@ -0,0 +1,8 @@
+/*
+ * 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.
+ */
+
+export * from './label_input';
+export * from './buckets';
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/shared_components/label_input.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/shared_components/label_input.tsx
new file mode 100644
index 0000000000000..882169c0675b0
--- /dev/null
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/shared_components/label_input.tsx
@@ -0,0 +1,62 @@
+/*
+ * 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 React, { useState, useEffect } from 'react';
+import { useDebounce } from 'react-use';
+import { EuiFieldText, keys } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+
+export const LabelInput = ({
+ value,
+ onChange,
+ placeholder,
+ inputRef,
+ onSubmit,
+ dataTestSubj,
+}: {
+ value: string;
+ onChange: (value: string) => void;
+ placeholder?: string;
+ inputRef?: React.MutableRefObject;
+ onSubmit?: () => void;
+ dataTestSubj?: string;
+}) => {
+ const [inputValue, setInputValue] = useState(value);
+
+ useEffect(() => {
+ setInputValue(value);
+ }, [value, setInputValue]);
+
+ useDebounce(() => onChange(inputValue), 256, [inputValue]);
+
+ const handleInputChange = (e: React.ChangeEvent) => {
+ const val = String(e.target.value);
+ setInputValue(val);
+ };
+
+ return (
+ {
+ if (inputRef && node) {
+ inputRef.current = node;
+ }
+ }}
+ onKeyDown={({ key }: React.KeyboardEvent) => {
+ if (keys.ENTER === key && onSubmit) {
+ onSubmit();
+ }
+ }}
+ prepend={i18n.translate('xpack.lens.labelInput.label', {
+ defaultMessage: 'Label',
+ })}
+ />
+ );
+};