diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/entry_item.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/entry_item.test.tsx
new file mode 100644
index 000000000000..791782b0f015
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/entry_item.test.tsx
@@ -0,0 +1,438 @@
+/*
+ * 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 { mount } from 'enzyme';
+import React from 'react';
+import { EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui';
+
+import { EntryItemComponent } from './entry_item';
+import {
+ isOperator,
+ isNotOperator,
+ isOneOfOperator,
+ isNotOneOfOperator,
+ isInListOperator,
+ isNotInListOperator,
+ existsOperator,
+ doesNotExistOperator,
+} from '../../autocomplete/operators';
+import {
+ fields,
+ getField,
+} from '../../../../../../../../src/plugins/data/common/index_patterns/fields/fields.mocks.ts';
+import { getFoundListSchemaMock } from '../../../../../../lists/common/schemas/response/found_list_schema.mock';
+import { getEmptyValue } from '../../empty_value';
+
+// mock out lists hook
+const mockStart = jest.fn();
+const mockResult = getFoundListSchemaMock();
+jest.mock('../../../../common/lib/kibana');
+jest.mock('../../../../lists_plugin_deps', () => {
+ const originalModule = jest.requireActual('../../../../lists_plugin_deps');
+
+ return {
+ ...originalModule,
+ useFindLists: () => ({
+ loading: false,
+ start: mockStart.mockReturnValue(mockResult),
+ result: mockResult,
+ error: undefined,
+ }),
+ };
+});
+
+describe('EntryItemComponent', () => {
+ test('it renders fields disabled if "isLoading" is "true"', () => {
+ const wrapper = mount(
+
+ );
+
+ expect(
+ wrapper.find('[data-test-subj="exceptionBuilderEntryField"] input').props().disabled
+ ).toBeTruthy();
+ expect(
+ wrapper.find('[data-test-subj="exceptionBuilderEntryOperator"] input').props().disabled
+ ).toBeTruthy();
+ expect(
+ wrapper.find('[data-test-subj="exceptionBuilderEntryFieldMatch"] input').props().disabled
+ ).toBeTruthy();
+ expect(wrapper.find('[data-test-subj="exceptionBuilderEntryFieldFormRow"]')).toHaveLength(0);
+ });
+
+ test('it renders field labels if "showLabel" is "true"', () => {
+ const wrapper = mount(
+
+ );
+
+ expect(wrapper.find('[data-test-subj="exceptionBuilderEntryFieldFormRow"]')).not.toEqual(0);
+ });
+
+ test('it renders field values correctly when operator is "isOperator"', () => {
+ const wrapper = mount(
+
+ );
+
+ expect(wrapper.find('[data-test-subj="exceptionBuilderEntryField"]').text()).toEqual('ip');
+ expect(wrapper.find('[data-test-subj="exceptionBuilderEntryOperator"]').text()).toEqual('is');
+ expect(wrapper.find('[data-test-subj="exceptionBuilderEntryFieldMatch"]').text()).toEqual(
+ '1234'
+ );
+ });
+
+ test('it renders field values correctly when operator is "isNotOperator"', () => {
+ const wrapper = mount(
+
+ );
+
+ expect(wrapper.find('[data-test-subj="exceptionBuilderEntryField"]').text()).toEqual('ip');
+ expect(wrapper.find('[data-test-subj="exceptionBuilderEntryOperator"]').text()).toEqual(
+ 'is not'
+ );
+ expect(wrapper.find('[data-test-subj="exceptionBuilderEntryFieldMatch"]').text()).toEqual(
+ '1234'
+ );
+ });
+
+ test('it renders field values correctly when operator is "isOneOfOperator"', () => {
+ const wrapper = mount(
+
+ );
+
+ expect(wrapper.find('[data-test-subj="exceptionBuilderEntryField"]').text()).toEqual('ip');
+ expect(wrapper.find('[data-test-subj="exceptionBuilderEntryOperator"]').text()).toEqual(
+ 'is one of'
+ );
+ expect(wrapper.find('[data-test-subj="exceptionBuilderEntryFieldMatchAny"]').text()).toEqual(
+ '1234'
+ );
+ });
+
+ test('it renders field values correctly when operator is "isNotOneOfOperator"', () => {
+ const wrapper = mount(
+
+ );
+
+ expect(wrapper.find('[data-test-subj="exceptionBuilderEntryField"]').text()).toEqual('ip');
+ expect(wrapper.find('[data-test-subj="exceptionBuilderEntryOperator"]').text()).toEqual(
+ 'is not one of'
+ );
+ expect(wrapper.find('[data-test-subj="exceptionBuilderEntryFieldMatchAny"]').text()).toEqual(
+ '1234'
+ );
+ });
+
+ test('it renders field values correctly when operator is "isInListOperator"', () => {
+ const wrapper = mount(
+
+ );
+
+ expect(wrapper.find('[data-test-subj="exceptionBuilderEntryField"]').text()).toEqual('ip');
+ expect(wrapper.find('[data-test-subj="exceptionBuilderEntryOperator"]').text()).toEqual(
+ 'is in list'
+ );
+ expect(wrapper.find('[data-test-subj="exceptionBuilderEntryFieldList"]').text()).toEqual(
+ 'some name'
+ );
+ });
+
+ test('it renders field values correctly when operator is "isNotInListOperator"', () => {
+ const wrapper = mount(
+
+ );
+
+ expect(wrapper.find('[data-test-subj="exceptionBuilderEntryField"]').text()).toEqual('ip');
+ expect(wrapper.find('[data-test-subj="exceptionBuilderEntryOperator"]').text()).toEqual(
+ 'is not in list'
+ );
+ expect(wrapper.find('[data-test-subj="exceptionBuilderEntryFieldList"]').text()).toEqual(
+ 'some name'
+ );
+ });
+
+ test('it renders field values correctly when operator is "existsOperator"', () => {
+ const wrapper = mount(
+
+ );
+
+ expect(wrapper.find('[data-test-subj="exceptionBuilderEntryField"]').text()).toEqual('ip');
+ expect(wrapper.find('[data-test-subj="exceptionBuilderEntryOperator"]').text()).toEqual(
+ 'exists'
+ );
+ expect(wrapper.find('[data-test-subj="exceptionBuilderEntryFieldExists"]').text()).toEqual(
+ getEmptyValue()
+ );
+ expect(
+ wrapper.find('[data-test-subj="exceptionBuilderEntryFieldExists"] input').props().disabled
+ ).toBeTruthy();
+ });
+
+ test('it renders field values correctly when operator is "doesNotExistOperator"', () => {
+ const wrapper = mount(
+
+ );
+
+ expect(wrapper.find('[data-test-subj="exceptionBuilderEntryField"]').text()).toEqual('ip');
+ expect(wrapper.find('[data-test-subj="exceptionBuilderEntryOperator"]').text()).toEqual(
+ 'does not exist'
+ );
+ expect(wrapper.find('[data-test-subj="exceptionBuilderEntryFieldExists"]').text()).toEqual(
+ getEmptyValue()
+ );
+ expect(
+ wrapper.find('[data-test-subj="exceptionBuilderEntryFieldExists"] input').props().disabled
+ ).toBeTruthy();
+ });
+
+ test('it invokes "onChange" when new field is selected and resets operator and value fields', () => {
+ const mockOnChange = jest.fn();
+ const wrapper = mount(
+
+ );
+
+ ((wrapper.find(EuiComboBox).at(0).props() as unknown) as {
+ onChange: (a: EuiComboBoxOptionOption[]) => void;
+ }).onChange([{ label: 'machine.os' }]);
+
+ expect(mockOnChange).toHaveBeenCalledWith(
+ { field: 'machine.os', operator: 'included', type: 'match', value: undefined },
+ 0
+ );
+ });
+
+ test('it invokes "onChange" when new operator is selected and resets value field', () => {
+ const mockOnChange = jest.fn();
+ const wrapper = mount(
+
+ );
+
+ ((wrapper.find(EuiComboBox).at(1).props() as unknown) as {
+ onChange: (a: EuiComboBoxOptionOption[]) => void;
+ }).onChange([{ label: 'is not' }]);
+
+ expect(mockOnChange).toHaveBeenCalledWith(
+ { field: 'ip', operator: 'excluded', type: 'match', value: '' },
+ 0
+ );
+ });
+
+ test('it invokes "onChange" when new value field is entered for match operator', () => {
+ const mockOnChange = jest.fn();
+ const wrapper = mount(
+
+ );
+
+ ((wrapper.find(EuiComboBox).at(2).props() as unknown) as {
+ onCreateOption: (a: string) => void;
+ }).onCreateOption('126.45.211.34');
+
+ expect(mockOnChange).toHaveBeenCalledWith(
+ { field: 'ip', operator: 'excluded', type: 'match', value: '126.45.211.34' },
+ 0
+ );
+ });
+
+ test('it invokes "onChange" when new value field is entered for match_any operator', () => {
+ const mockOnChange = jest.fn();
+ const wrapper = mount(
+
+ );
+
+ ((wrapper.find(EuiComboBox).at(2).props() as unknown) as {
+ onCreateOption: (a: string) => void;
+ }).onCreateOption('126.45.211.34');
+
+ expect(mockOnChange).toHaveBeenCalledWith(
+ { field: 'ip', operator: 'included', type: 'match_any', value: ['126.45.211.34'] },
+ 0
+ );
+ });
+
+ test('it invokes "onChange" when new value field is entered for list operator', () => {
+ const mockOnChange = jest.fn();
+ const wrapper = mount(
+
+ );
+
+ ((wrapper.find(EuiComboBox).at(2).props() as unknown) as {
+ onChange: (a: EuiComboBoxOptionOption[]) => void;
+ }).onChange([{ label: 'some name' }]);
+
+ expect(mockOnChange).toHaveBeenCalledWith(
+ {
+ field: 'ip',
+ operator: 'excluded',
+ type: 'list',
+ list: { id: 'some-list-id', type: 'ip' },
+ },
+ 0
+ );
+ });
+});
diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/entry_item.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/entry_item.tsx
index 39a1e1bdbad5..0f5000c8c0ab 100644
--- a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/entry_item.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/entry_item.tsx
@@ -67,13 +67,13 @@ export const EntryItemComponent: React.FC = ({
{
field: entry.field != null ? entry.field.name : undefined,
type: OperatorTypeEnum.MATCH,
- operator: isOperator.operator,
+ operator: entry.operator.operator,
value: newField,
},
entryIndex
);
},
- [onChange, entryIndex, entry.field]
+ [onChange, entryIndex, entry.field, entry.operator.operator]
);
const handleFieldMatchAnyValueChange = useCallback(
@@ -82,13 +82,13 @@ export const EntryItemComponent: React.FC = ({
{
field: entry.field != null ? entry.field.name : undefined,
type: OperatorTypeEnum.MATCH_ANY,
- operator: isOperator.operator,
+ operator: entry.operator.operator,
value: newField,
},
entryIndex
);
},
- [onChange, entryIndex, entry.field]
+ [onChange, entryIndex, entry.field, entry.operator.operator]
);
const handleFieldListValueChange = useCallback(
@@ -97,13 +97,13 @@ export const EntryItemComponent: React.FC = ({
{
field: entry.field != null ? entry.field.name : undefined,
type: OperatorTypeEnum.LIST,
- operator: isOperator.operator,
+ operator: entry.operator.operator,
list: { id: newField.id, type: newField.type },
},
entryIndex
);
},
- [onChange, entryIndex, entry.field]
+ [onChange, entryIndex, entry.field, entry.operator.operator]
);
const renderFieldInput = (isFirst: boolean): JSX.Element => {
@@ -114,9 +114,9 @@ export const EntryItemComponent: React.FC = ({
selectedField={entry.field}
isLoading={isLoading}
isClearable={false}
- isDisabled={indexPattern == null}
+ isDisabled={isLoading}
onChange={handleFieldChange}
- data-test-subj="filterFieldSuggestionList"
+ data-test-subj="exceptionBuilderEntryField"
/>
);
@@ -137,11 +137,11 @@ export const EntryItemComponent: React.FC = ({
placeholder={i18n.EXCEPTION_OPERATOR_PLACEHOLDER}
selectedField={entry.field}
operator={entry.operator}
- isDisabled={false}
+ isDisabled={isLoading}
isLoading={false}
isClearable={false}
onChange={handleOperatorChange}
- data-test-subj="filterFieldSuggestionList"
+ data-test-subj="exceptionBuilderEntryOperator"
/>
);
@@ -165,12 +165,12 @@ export const EntryItemComponent: React.FC = ({
placeholder={i18n.EXCEPTION_FIELD_VALUE_PLACEHOLDER}
selectedField={entry.field}
selectedValue={value}
- isDisabled={false}
+ isDisabled={isLoading}
isLoading={isLoading}
isClearable={false}
indexPattern={indexPattern}
onChange={handleFieldMatchValueChange}
- data-test-subj="filterFieldSuggestionList"
+ data-test-subj="exceptionBuilderEntryFieldMatch"
/>
);
case OperatorTypeEnum.MATCH_ANY:
@@ -180,12 +180,12 @@ export const EntryItemComponent: React.FC = ({
placeholder={i18n.EXCEPTION_FIELD_VALUE_PLACEHOLDER}
selectedField={entry.field}
selectedValue={values}
- isDisabled={false}
+ isDisabled={isLoading}
isLoading={isLoading}
isClearable={false}
indexPattern={indexPattern}
onChange={handleFieldMatchAnyValueChange}
- data-test-subj="filterFieldSuggestionList"
+ data-test-subj="exceptionBuilderEntryFieldMatchAny"
/>
);
case OperatorTypeEnum.LIST:
@@ -195,17 +195,18 @@ export const EntryItemComponent: React.FC = ({
selectedField={entry.field}
placeholder={i18n.EXCEPTION_FIELD_LISTS_PLACEHOLDER}
selectedValue={id}
- isLoading={false}
- isDisabled={false}
+ isLoading={isLoading}
+ isDisabled={isLoading}
isClearable={false}
onChange={handleFieldListValueChange}
+ data-test-subj="exceptionBuilderEntryFieldList"
/>
);
case OperatorTypeEnum.EXISTS:
return (
);
default:
diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/exception_item.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/exception_item.tsx
index 3afdf43ec7df..5e53ce3ba657 100644
--- a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/exception_item.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/exception_item.tsx
@@ -29,6 +29,7 @@ interface ExceptionListItemProps {
isLoading: boolean;
indexPattern: IIndexPattern;
andLogicIncluded: boolean;
+ onCheckAndLogic: (item: ExceptionsBuilderExceptionItem[]) => void;
onDeleteExceptionItem: (item: ExceptionsBuilderExceptionItem, index: number) => void;
onExceptionItemChange: (item: ExceptionsBuilderExceptionItem, index: number) => void;
}
@@ -41,6 +42,7 @@ export const ExceptionListItemComponent = React.memo(
indexPattern,
isLoading,
andLogicIncluded,
+ onCheckAndLogic,
onDeleteExceptionItem,
onExceptionItemChange,
}) => {
@@ -70,11 +72,12 @@ export const ExceptionListItemComponent = React.memo(
onDeleteExceptionItem(updatedExceptionItem, exceptionItemIndex);
};
- const entries = useMemo(
- (): FormattedBuilderEntry[] =>
- indexPattern != null ? getFormattedBuilderEntries(indexPattern, exceptionItem.entries) : [],
- [indexPattern, exceptionItem.entries]
- );
+ const entries = useMemo((): FormattedBuilderEntry[] => {
+ onCheckAndLogic([exceptionItem]);
+ return indexPattern != null
+ ? getFormattedBuilderEntries(indexPattern, exceptionItem.entries)
+ : [];
+ }, [indexPattern, exceptionItem, onCheckAndLogic]);
const andBadge = useMemo((): JSX.Element => {
const badge = ;
diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/index.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/index.tsx
index c6376c34c768..dcf06eeeffaf 100644
--- a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/index.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/index.tsx
@@ -77,15 +77,21 @@ export const ExceptionBuilder = ({
indexPatternConfig ?? []
);
+ const handleCheckAndLogic = (items: ExceptionsBuilderExceptionItem[]): void => {
+ setAndLogicIncluded((includesAnd: boolean): boolean => {
+ if (includesAnd) {
+ return true;
+ } else {
+ return items.filter(({ entries }) => entries.length > 1).length > 0;
+ }
+ });
+ };
+
// Bubble up changes to parent
useEffect(() => {
onChange({ exceptionItems: filterExceptionItems(exceptions), exceptionsToDelete });
}, [onChange, exceptionsToDelete, exceptions]);
- const checkAndLogic = (items: ExceptionsBuilderExceptionItem[]): void => {
- setAndLogicIncluded(items.filter(({ entries }) => entries.length > 1).length > 0);
- };
-
const handleDeleteExceptionItem = (
item: ExceptionsBuilderExceptionItem,
itemIndex: number
@@ -100,7 +106,7 @@ export const ExceptionBuilder = ({
...existingExceptions.slice(0, itemIndex),
...existingExceptions.slice(itemIndex + 1),
];
- checkAndLogic(updatedExceptions);
+ handleCheckAndLogic(updatedExceptions);
return updatedExceptions;
});
@@ -118,7 +124,7 @@ export const ExceptionBuilder = ({
...exceptions.slice(index + 1),
];
- checkAndLogic(updatedExceptions);
+ handleCheckAndLogic(updatedExceptions);
setExceptions(updatedExceptions);
};
@@ -214,6 +220,7 @@ export const ExceptionBuilder = ({
isLoading={indexPatternLoading}
exceptionItemIndex={index}
andLogicIncluded={andLogicIncluded}
+ onCheckAndLogic={handleCheckAndLogic}
onDeleteExceptionItem={handleDeleteExceptionItem}
onExceptionItemChange={handleExceptionItemChange}
/>