[Security Solution][Exceptions] - Remove initial add exception item button in builder (#72215)

## Summary

This PR addresses two issues in the builder:

- **Existing behavior:** if you add a bunch of entries then delete all but one, the indent that shows for when multiple entries exists does not go away
  - **Updated behavior:** if you add a bunch of entries and delete all but one, the indent that shows for when multiple entries exist goes away

- **Existing behavior:** on render of add exception modal, if no exception items exist (or no exception items with entries exist) an `Add Exception` button appears
  - **Updated behavior:** if only one entry exists, the delete button is disabled for that entry; on initial render of the add exception modal, if no entries exist, an empty entry is shown
This commit is contained in:
Yara Tercero 2020-07-17 12:39:51 -04:00 committed by GitHub
parent 44fc2a828c
commit 1adaa3b76c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 493 additions and 282 deletions

View file

@ -16,26 +16,12 @@ addDecorator((storyFn) => (
));
storiesOf('Components|Exceptions|BuilderButtonOptions', module)
.add('init button', () => {
return (
<BuilderButtonOptions
isAndDisabled={false}
isOrDisabled={false}
showNestedButton={false}
displayInitButton
onOrClicked={action('onClick')}
onAndClicked={action('onClick')}
onNestedClicked={action('onClick')}
/>
);
})
.add('and/or buttons', () => {
return (
<BuilderButtonOptions
isAndDisabled={false}
isOrDisabled={false}
showNestedButton={false}
displayInitButton={false}
onOrClicked={action('onClick')}
onAndClicked={action('onClick')}
onNestedClicked={action('onClick')}
@ -48,7 +34,6 @@ storiesOf('Components|Exceptions|BuilderButtonOptions', module)
isAndDisabled={false}
isOrDisabled={false}
showNestedButton
displayInitButton={false}
onOrClicked={action('onClick')}
onAndClicked={action('onClick')}
onNestedClicked={action('onClick')}
@ -61,7 +46,6 @@ storiesOf('Components|Exceptions|BuilderButtonOptions', module)
isAndDisabled
isOrDisabled={false}
showNestedButton={false}
displayInitButton={false}
onOrClicked={action('onClick')}
onAndClicked={action('onClick')}
onNestedClicked={action('onClick')}
@ -74,7 +58,6 @@ storiesOf('Components|Exceptions|BuilderButtonOptions', module)
isAndDisabled={false}
isOrDisabled
showNestedButton={false}
displayInitButton={false}
onOrClicked={action('onClick')}
onAndClicked={action('onClick')}
onNestedClicked={action('onClick')}

View file

@ -16,7 +16,6 @@ describe('BuilderButtonOptions', () => {
isAndDisabled={false}
isOrDisabled={false}
showNestedButton={false}
displayInitButton={false}
onOrClicked={jest.fn()}
onAndClicked={jest.fn()}
onNestedClicked={jest.fn()}
@ -31,44 +30,6 @@ describe('BuilderButtonOptions', () => {
expect(wrapper.find('[data-test-subj="exceptionsNestedButton"] button')).toHaveLength(0);
});
test('it renders "add exception" button if "displayInitButton" is true', () => {
const wrapper = mount(
<BuilderButtonOptions
isAndDisabled={false}
isOrDisabled={false}
showNestedButton={false}
displayInitButton
onOrClicked={jest.fn()}
onAndClicked={jest.fn()}
onNestedClicked={jest.fn()}
/>
);
expect(wrapper.find('[data-test-subj="exceptionsAddNewExceptionButton"] button')).toHaveLength(
1
);
});
test('it invokes "onAddExceptionClicked" when "add exception" button is clicked', () => {
const onOrClicked = jest.fn();
const wrapper = mount(
<BuilderButtonOptions
isAndDisabled={false}
isOrDisabled={false}
showNestedButton={false}
displayInitButton
onOrClicked={onOrClicked}
onAndClicked={jest.fn()}
onNestedClicked={jest.fn()}
/>
);
wrapper.find('[data-test-subj="exceptionsAddNewExceptionButton"] button').simulate('click');
expect(onOrClicked).toHaveBeenCalledTimes(1);
});
test('it invokes "onOrClicked" when "or" button is clicked', () => {
const onOrClicked = jest.fn();
@ -77,7 +38,6 @@ describe('BuilderButtonOptions', () => {
isAndDisabled={false}
isOrDisabled={false}
showNestedButton={false}
displayInitButton={false}
onOrClicked={onOrClicked}
onAndClicked={jest.fn()}
onNestedClicked={jest.fn()}
@ -97,7 +57,6 @@ describe('BuilderButtonOptions', () => {
isAndDisabled={false}
isOrDisabled={false}
showNestedButton={false}
displayInitButton={false}
onOrClicked={jest.fn()}
onAndClicked={onAndClicked}
onNestedClicked={jest.fn()}
@ -113,7 +72,6 @@ describe('BuilderButtonOptions', () => {
const wrapper = mount(
<BuilderButtonOptions
showNestedButton={false}
displayInitButton={false}
isOrDisabled={false}
isAndDisabled
onOrClicked={jest.fn()}
@ -131,7 +89,6 @@ describe('BuilderButtonOptions', () => {
const wrapper = mount(
<BuilderButtonOptions
showNestedButton={false}
displayInitButton={false}
isOrDisabled
isAndDisabled={false}
onOrClicked={jest.fn()}
@ -153,7 +110,6 @@ describe('BuilderButtonOptions', () => {
isAndDisabled={false}
isOrDisabled={false}
showNestedButton
displayInitButton={false}
onOrClicked={jest.fn()}
onAndClicked={jest.fn()}
onNestedClicked={onNestedClicked}

View file

@ -16,7 +16,6 @@ const MyEuiButton = styled(EuiButton)`
interface BuilderButtonOptionsProps {
isOrDisabled: boolean;
isAndDisabled: boolean;
displayInitButton: boolean;
showNestedButton: boolean;
onAndClicked: () => void;
onOrClicked: () => void;
@ -26,27 +25,12 @@ interface BuilderButtonOptionsProps {
export const BuilderButtonOptions: React.FC<BuilderButtonOptionsProps> = ({
isOrDisabled = false,
isAndDisabled = false,
displayInitButton,
showNestedButton = false,
onAndClicked,
onOrClicked,
onNestedClicked,
}) => (
<EuiFlexGroup gutterSize="s" alignItems="center">
{displayInitButton ? (
<EuiFlexItem grow={false}>
<EuiButton
fill
size="s"
iconType="plusInCircle"
onClick={onOrClicked}
data-test-subj="exceptionsAddNewExceptionButton"
>
{i18n.ADD_EXCEPTION_TITLE}
</EuiButton>
</EuiFlexItem>
) : (
<>
<EuiFlexItem grow={false}>
<MyEuiButton
fill
@ -83,7 +67,5 @@ export const BuilderButtonOptions: React.FC<BuilderButtonOptionsProps> = ({
</EuiButton>
</EuiFlexItem>
)}
</>
)}
</EuiFlexGroup>
);

View file

@ -0,0 +1,282 @@
/*
* 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 { ThemeProvider } from 'styled-components';
import { mount } from 'enzyme';
import euiLightVars from '@elastic/eui/dist/eui_theme_light.json';
import { ExceptionListItemComponent } from './builder_exception_item';
import { fields } from '../../../../../../../../src/plugins/data/common/index_patterns/fields/fields.mocks.ts';
import { getExceptionListItemSchemaMock } from '../../../../../../lists/common/schemas/response/exception_list_item_schema.mock';
import {
getEntryMatchMock,
getEntryMatchAnyMock,
} from '../../../../../../lists/common/schemas/types/entries.mock';
describe('ExceptionListItemComponent', () => {
describe('and badge logic', () => {
test('it renders "and" badge with extra top padding for the first exception item when "andLogicIncluded" is "true"', () => {
const exceptionItem = getExceptionListItemSchemaMock();
exceptionItem.entries = [getEntryMatchMock(), getEntryMatchMock()];
const wrapper = mount(
<ThemeProvider theme={() => ({ eui: euiLightVars, darkMode: false })}>
<ExceptionListItemComponent
exceptionItem={exceptionItem}
exceptionId={'123'}
exceptionItemIndex={0}
indexPattern={{
id: '1234',
title: 'logstash-*',
fields,
}}
isLoading={false}
andLogicIncluded={true}
isOnlyItem={false}
onDeleteExceptionItem={jest.fn()}
onChangeExceptionItem={jest.fn()}
/>
</ThemeProvider>
);
expect(
wrapper.find('[data-test-subj="exceptionItemEntryFirstRowAndBadge"]').exists()
).toBeTruthy();
});
test('it renders "and" badge when more than one exception item entry exists and it is not the first exception item', () => {
const exceptionItem = getExceptionListItemSchemaMock();
exceptionItem.entries = [getEntryMatchMock(), getEntryMatchMock()];
const wrapper = mount(
<ThemeProvider theme={() => ({ eui: euiLightVars, darkMode: false })}>
<ExceptionListItemComponent
exceptionItem={exceptionItem}
exceptionId={'123'}
exceptionItemIndex={1}
indexPattern={{
id: '1234',
title: 'logstash-*',
fields,
}}
isLoading={false}
andLogicIncluded={true}
isOnlyItem={false}
onDeleteExceptionItem={jest.fn()}
onChangeExceptionItem={jest.fn()}
/>
</ThemeProvider>
);
expect(wrapper.find('[data-test-subj="exceptionItemEntryAndBadge"]').exists()).toBeTruthy();
});
test('it renders indented "and" badge when "andLogicIncluded" is "true" and only one entry exists', () => {
const exceptionItem = getExceptionListItemSchemaMock();
exceptionItem.entries = [getEntryMatchMock()];
const wrapper = mount(
<ThemeProvider theme={() => ({ eui: euiLightVars, darkMode: false })}>
<ExceptionListItemComponent
exceptionItem={exceptionItem}
exceptionId={'123'}
exceptionItemIndex={1}
indexPattern={{
id: '1234',
title: 'logstash-*',
fields,
}}
isLoading={false}
andLogicIncluded={true}
isOnlyItem={false}
onDeleteExceptionItem={jest.fn()}
onChangeExceptionItem={jest.fn()}
/>
</ThemeProvider>
);
expect(
wrapper.find('[data-test-subj="exceptionItemEntryInvisibleAndBadge"]').exists()
).toBeTruthy();
});
test('it renders no "and" badge when "andLogicIncluded" is "false"', () => {
const exceptionItem = getExceptionListItemSchemaMock();
exceptionItem.entries = [getEntryMatchMock()];
const wrapper = mount(
<ThemeProvider theme={() => ({ eui: euiLightVars, darkMode: false })}>
<ExceptionListItemComponent
exceptionItem={exceptionItem}
exceptionId={'123'}
exceptionItemIndex={1}
indexPattern={{
id: '1234',
title: 'logstash-*',
fields,
}}
isLoading={false}
andLogicIncluded={false}
isOnlyItem={false}
onDeleteExceptionItem={jest.fn()}
onChangeExceptionItem={jest.fn()}
/>
</ThemeProvider>
);
expect(
wrapper.find('[data-test-subj="exceptionItemEntryInvisibleAndBadge"]').exists()
).toBeFalsy();
expect(wrapper.find('[data-test-subj="exceptionItemEntryAndBadge"]').exists()).toBeFalsy();
expect(
wrapper.find('[data-test-subj="exceptionItemEntryFirstRowAndBadge"]').exists()
).toBeFalsy();
});
});
describe('delete button logic', () => {
test('it renders delete button disabled when it is only entry left in builder', () => {
const exceptionItem = getExceptionListItemSchemaMock();
exceptionItem.entries = [getEntryMatchMock()];
const wrapper = mount(
<ExceptionListItemComponent
exceptionItem={exceptionItem}
exceptionId={'123'}
exceptionItemIndex={0}
indexPattern={{
id: '1234',
title: 'logstash-*',
fields,
}}
isLoading={false}
andLogicIncluded={false}
isOnlyItem={true}
onDeleteExceptionItem={jest.fn()}
onChangeExceptionItem={jest.fn()}
/>
);
expect(
wrapper.find('[data-test-subj="exceptionItemEntryDeleteButton"] button').props().disabled
).toBeTruthy();
});
test('it does not render delete button disabled when it is not the only entry left in builder', () => {
const exceptionItem = getExceptionListItemSchemaMock();
exceptionItem.entries = [getEntryMatchMock()];
const wrapper = mount(
<ExceptionListItemComponent
exceptionItem={exceptionItem}
exceptionId={'123'}
exceptionItemIndex={0}
indexPattern={{
id: '1234',
title: 'logstash-*',
fields,
}}
isLoading={false}
andLogicIncluded={false}
isOnlyItem={false}
onDeleteExceptionItem={jest.fn()}
onChangeExceptionItem={jest.fn()}
/>
);
expect(
wrapper.find('[data-test-subj="exceptionItemEntryDeleteButton"] button').props().disabled
).toBeFalsy();
});
test('it does not render delete button disabled when "exceptionItemIndex" is not "0"', () => {
const exceptionItem = getExceptionListItemSchemaMock();
exceptionItem.entries = [getEntryMatchMock()];
const wrapper = mount(
<ExceptionListItemComponent
exceptionItem={exceptionItem}
exceptionId={'123'}
exceptionItemIndex={1}
indexPattern={{
id: '1234',
title: 'logstash-*',
fields,
}}
isLoading={false}
andLogicIncluded={false}
// if exceptionItemIndex is not 0, wouldn't make sense for
// this to be true, but done for testing purposes
isOnlyItem={true}
onDeleteExceptionItem={jest.fn()}
onChangeExceptionItem={jest.fn()}
/>
);
expect(
wrapper.find('[data-test-subj="exceptionItemEntryDeleteButton"] button').props().disabled
).toBeFalsy();
});
test('it does not render delete button disabled when more than one entry exists', () => {
const exceptionItem = getExceptionListItemSchemaMock();
exceptionItem.entries = [getEntryMatchMock(), getEntryMatchMock()];
const wrapper = mount(
<ExceptionListItemComponent
exceptionItem={exceptionItem}
exceptionId={'123'}
exceptionItemIndex={0}
indexPattern={{
id: '1234',
title: 'logstash-*',
fields,
}}
isLoading={false}
andLogicIncluded={false}
isOnlyItem={true}
onDeleteExceptionItem={jest.fn()}
onChangeExceptionItem={jest.fn()}
/>
);
expect(
wrapper.find('[data-test-subj="exceptionItemEntryDeleteButton"] button').at(0).props()
.disabled
).toBeFalsy();
});
test('it invokes "onChangeExceptionItem" when delete button clicked', () => {
const mockOnDeleteExceptionItem = jest.fn();
const exceptionItem = getExceptionListItemSchemaMock();
exceptionItem.entries = [getEntryMatchMock(), getEntryMatchAnyMock()];
const wrapper = mount(
<ExceptionListItemComponent
exceptionItem={exceptionItem}
exceptionId={'123'}
exceptionItemIndex={0}
indexPattern={{
id: '1234',
title: 'logstash-*',
fields,
}}
isLoading={false}
andLogicIncluded={false}
isOnlyItem={true}
onDeleteExceptionItem={mockOnDeleteExceptionItem}
onChangeExceptionItem={jest.fn()}
/>
);
wrapper
.find('[data-test-subj="exceptionItemEntryDeleteButton"] button')
.at(0)
.simulate('click');
expect(mockOnDeleteExceptionItem).toHaveBeenCalledWith(
{
...exceptionItem,
entries: [getEntryMatchAnyMock()],
},
0
);
});
});
});

View file

@ -0,0 +1,161 @@
/*
* 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, { useMemo, useCallback } from 'react';
import { EuiButtonIcon, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import styled from 'styled-components';
import { IIndexPattern } from '../../../../../../../../src/plugins/data/common';
import { AndOrBadge } from '../../and_or_badge';
import { EntryItemComponent } from './entry_item';
import { getFormattedBuilderEntries } from '../helpers';
import { FormattedBuilderEntry, ExceptionsBuilderExceptionItem, BuilderEntry } from '../types';
const MyInvisibleAndBadge = styled(EuiFlexItem)`
visibility: hidden;
`;
const MyFirstRowContainer = styled(EuiFlexItem)`
padding-top: 20px;
`;
interface ExceptionListItemProps {
exceptionItem: ExceptionsBuilderExceptionItem;
exceptionId: string;
exceptionItemIndex: number;
isLoading: boolean;
indexPattern: IIndexPattern;
andLogicIncluded: boolean;
isOnlyItem: boolean;
onDeleteExceptionItem: (item: ExceptionsBuilderExceptionItem, index: number) => void;
onChangeExceptionItem: (item: ExceptionsBuilderExceptionItem, index: number) => void;
}
export const ExceptionListItemComponent = React.memo<ExceptionListItemProps>(
({
exceptionItem,
exceptionId,
exceptionItemIndex,
indexPattern,
isLoading,
isOnlyItem,
andLogicIncluded,
onDeleteExceptionItem,
onChangeExceptionItem,
}) => {
const handleEntryChange = useCallback(
(entry: BuilderEntry, entryIndex: number): void => {
const updatedEntries: BuilderEntry[] = [
...exceptionItem.entries.slice(0, entryIndex),
{ ...entry },
...exceptionItem.entries.slice(entryIndex + 1),
];
const updatedExceptionItem: ExceptionsBuilderExceptionItem = {
...exceptionItem,
entries: updatedEntries,
};
onChangeExceptionItem(updatedExceptionItem, exceptionItemIndex);
},
[onChangeExceptionItem, exceptionItem, exceptionItemIndex]
);
const handleDeleteEntry = useCallback(
(entryIndex: number): void => {
const updatedEntries: BuilderEntry[] = [
...exceptionItem.entries.slice(0, entryIndex),
...exceptionItem.entries.slice(entryIndex + 1),
];
const updatedExceptionItem: ExceptionsBuilderExceptionItem = {
...exceptionItem,
entries: updatedEntries,
};
onDeleteExceptionItem(updatedExceptionItem, exceptionItemIndex);
},
[exceptionItem, onDeleteExceptionItem, exceptionItemIndex]
);
const entries = useMemo(
(): FormattedBuilderEntry[] =>
indexPattern != null ? getFormattedBuilderEntries(indexPattern, exceptionItem.entries) : [],
[indexPattern, exceptionItem]
);
const andBadge = useMemo((): JSX.Element => {
const badge = <AndOrBadge includeAntennas type="and" />;
if (entries.length > 1 && exceptionItemIndex === 0) {
return (
<MyFirstRowContainer grow={false} data-test-subj="exceptionItemEntryFirstRowAndBadge">
{badge}
</MyFirstRowContainer>
);
} else if (entries.length > 1) {
return (
<EuiFlexItem grow={false} data-test-subj="exceptionItemEntryAndBadge">
{badge}
</EuiFlexItem>
);
} else {
return (
<MyInvisibleAndBadge grow={false} data-test-subj="exceptionItemEntryInvisibleAndBadge">
{badge}
</MyInvisibleAndBadge>
);
}
}, [entries.length, exceptionItemIndex]);
const getDeleteButton = useCallback(
(index: number): JSX.Element => {
const button = (
<EuiButtonIcon
color="danger"
iconType="trash"
onClick={() => handleDeleteEntry(index)}
isDisabled={isOnlyItem && entries.length === 1 && exceptionItemIndex === 0}
aria-label="entryDeleteButton"
className="exceptionItemEntryDeleteButton"
data-test-subj="exceptionItemEntryDeleteButton"
/>
);
if (index === 0 && exceptionItemIndex === 0) {
return <MyFirstRowContainer grow={false}>{button}</MyFirstRowContainer>;
} else {
return <EuiFlexItem grow={false}>{button}</EuiFlexItem>;
}
},
[entries.length, exceptionItemIndex, handleDeleteEntry, isOnlyItem]
);
return (
<EuiFlexGroup gutterSize="s" data-test-subj="exceptionEntriesContainer">
{andLogicIncluded && andBadge}
<EuiFlexItem grow={6}>
<EuiFlexGroup gutterSize="s" direction="column">
{entries.map((item, index) => (
<EuiFlexItem key={`${exceptionId}-${index}`} grow={1}>
<EuiFlexGroup gutterSize="xs" alignItems="center" direction="row">
<EuiFlexItem grow={1}>
<EntryItemComponent
entry={item}
entryIndex={index}
indexPattern={indexPattern}
showLabel={exceptionItemIndex === 0 && index === 0}
isLoading={isLoading}
onChange={handleEntryChange}
/>
</EuiFlexItem>
{getDeleteButton(index)}
</EuiFlexGroup>
</EuiFlexItem>
))}
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
);
}
);
ExceptionListItemComponent.displayName = 'ExceptionListItem';

View file

@ -1,140 +0,0 @@
/*
* 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, { useMemo } from 'react';
import { EuiButtonIcon, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import styled from 'styled-components';
import { IIndexPattern } from '../../../../../../../../src/plugins/data/common';
import { AndOrBadge } from '../../and_or_badge';
import { EntryItemComponent } from './entry_item';
import { getFormattedBuilderEntries } from '../helpers';
import { FormattedBuilderEntry, ExceptionsBuilderExceptionItem, BuilderEntry } from '../types';
const MyInvisibleAndBadge = styled(EuiFlexItem)`
visibility: hidden;
`;
const MyFirstRowContainer = styled(EuiFlexItem)`
padding-top: 20px;
`;
interface ExceptionListItemProps {
exceptionItem: ExceptionsBuilderExceptionItem;
exceptionId: string;
exceptionItemIndex: number;
isLoading: boolean;
indexPattern: IIndexPattern;
andLogicIncluded: boolean;
onCheckAndLogic: (item: ExceptionsBuilderExceptionItem[]) => void;
onDeleteExceptionItem: (item: ExceptionsBuilderExceptionItem, index: number) => void;
onExceptionItemChange: (item: ExceptionsBuilderExceptionItem, index: number) => void;
}
export const ExceptionListItemComponent = React.memo<ExceptionListItemProps>(
({
exceptionItem,
exceptionId,
exceptionItemIndex,
indexPattern,
isLoading,
andLogicIncluded,
onCheckAndLogic,
onDeleteExceptionItem,
onExceptionItemChange,
}) => {
const handleEntryChange = (entry: BuilderEntry, entryIndex: number): void => {
const updatedEntries: BuilderEntry[] = [
...exceptionItem.entries.slice(0, entryIndex),
{ ...entry },
...exceptionItem.entries.slice(entryIndex + 1),
];
const updatedExceptionItem: ExceptionsBuilderExceptionItem = {
...exceptionItem,
entries: updatedEntries,
};
onExceptionItemChange(updatedExceptionItem, exceptionItemIndex);
};
const handleDeleteEntry = (entryIndex: number): void => {
const updatedEntries: BuilderEntry[] = [
...exceptionItem.entries.slice(0, entryIndex),
...exceptionItem.entries.slice(entryIndex + 1),
];
const updatedExceptionItem: ExceptionsBuilderExceptionItem = {
...exceptionItem,
entries: updatedEntries,
};
onDeleteExceptionItem(updatedExceptionItem, exceptionItemIndex);
};
const entries = useMemo((): FormattedBuilderEntry[] => {
onCheckAndLogic([exceptionItem]);
return indexPattern != null
? getFormattedBuilderEntries(indexPattern, exceptionItem.entries)
: [];
}, [indexPattern, exceptionItem, onCheckAndLogic]);
const andBadge = useMemo((): JSX.Element => {
const badge = <AndOrBadge includeAntennas type="and" />;
if (entries.length > 1 && exceptionItemIndex === 0) {
return <MyFirstRowContainer grow={false}>{badge}</MyFirstRowContainer>;
} else if (entries.length > 1) {
return <EuiFlexItem grow={false}>{badge}</EuiFlexItem>;
} else {
return <MyInvisibleAndBadge grow={false}>{badge}</MyInvisibleAndBadge>;
}
}, [entries.length, exceptionItemIndex]);
const getDeleteButton = (index: number): JSX.Element => {
const button = (
<EuiButtonIcon
color="danger"
iconType="trash"
onClick={() => handleDeleteEntry(index)}
aria-label="entryDeleteButton"
className="exceptionItemEntryDeleteButton"
data-test-subj="exceptionItemEntryDeleteButton"
/>
);
if (index === 0 && exceptionItemIndex === 0) {
return <MyFirstRowContainer grow={false}>{button}</MyFirstRowContainer>;
} else {
return <EuiFlexItem grow={false}>{button}</EuiFlexItem>;
}
};
return (
<EuiFlexGroup gutterSize="s" data-test-subj="exceptionEntriesContainer">
{andLogicIncluded && andBadge}
<EuiFlexItem grow={6}>
<EuiFlexGroup gutterSize="s" direction="column">
{entries.map((item, index) => (
<EuiFlexItem key={`${exceptionId}-${index}`} grow={1}>
<EuiFlexGroup gutterSize="xs" alignItems="center" direction="row">
<EuiFlexItem grow={1}>
<EntryItemComponent
entry={item}
entryIndex={index}
indexPattern={indexPattern}
showLabel={exceptionItemIndex === 0 && index === 0}
isLoading={isLoading}
onChange={handleEntryChange}
/>
</EuiFlexItem>
{getDeleteButton(index)}
</EuiFlexGroup>
</EuiFlexItem>
))}
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
);
}
);
ExceptionListItemComponent.displayName = 'ExceptionListItem';

View file

@ -3,11 +3,11 @@
* 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, { useMemo, useCallback, useEffect, useState } from 'react';
import React, { useCallback, useEffect, useState } from 'react';
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import styled from 'styled-components';
import { ExceptionListItemComponent } from './exception_item';
import { ExceptionListItemComponent } from './builder_exception_item';
import { useFetchIndexPatterns } from '../../../../detections/containers/detection_engine/rules/fetch_index_patterns';
import {
ExceptionListItemSchema,
@ -80,20 +80,9 @@ export const ExceptionBuilder = ({
);
const handleCheckAndLogic = (items: ExceptionsBuilderExceptionItem[]): void => {
setAndLogicIncluded((includesAnd: boolean): boolean => {
if (includesAnd) {
return true;
} else {
return items.filter(({ entries }) => entries.length > 1).length > 0;
}
});
setAndLogicIncluded(items.filter(({ entries }) => entries.length > 1).length > 0);
};
// Bubble up changes to parent
useEffect(() => {
onChange({ exceptionItems: filterExceptionItems(exceptions), exceptionsToDelete });
}, [onChange, exceptionsToDelete, exceptions]);
const handleDeleteExceptionItem = (
item: ExceptionsBuilderExceptionItem,
itemIndex: number
@ -164,16 +153,6 @@ export const ExceptionBuilder = ({
setExceptions((existingExceptions) => [...existingExceptions, { ...newException }]);
}, [setExceptions, listType, listId, listNamespaceType, ruleName]);
// An exception item can have an empty array for `entries`
const displayInitialAddExceptionButton = useMemo((): boolean => {
return (
exceptions.length === 0 ||
(exceptions.length === 1 &&
exceptions[0].entries != null &&
exceptions[0].entries.length === 0)
);
}, [exceptions]);
// Filters index pattern fields by exceptionable fields if list type is endpoint
const filterIndexPatterns = useCallback(() => {
if (listType === 'endpoint') {
@ -199,6 +178,22 @@ export const ExceptionBuilder = ({
}
};
// Bubble up changes to parent
useEffect(() => {
onChange({ exceptionItems: filterExceptionItems(exceptions), exceptionsToDelete });
}, [onChange, exceptionsToDelete, exceptions]);
useEffect(() => {
if (
exceptions.length === 0 ||
(exceptions.length === 1 &&
exceptions[0].entries != null &&
exceptions[0].entries.length === 0)
) {
handleAddNewExceptionItem();
}
}, [exceptions, handleAddNewExceptionItem]);
return (
<EuiFlexGroup gutterSize="s" direction="column">
{(isLoading || indexPatternLoading) && (
@ -233,9 +228,9 @@ export const ExceptionBuilder = ({
isLoading={indexPatternLoading}
exceptionItemIndex={index}
andLogicIncluded={andLogicIncluded}
onCheckAndLogic={handleCheckAndLogic}
isOnlyItem={exceptions.length === 1}
onDeleteExceptionItem={handleDeleteExceptionItem}
onExceptionItemChange={handleExceptionItemChange}
onChangeExceptionItem={handleExceptionItemChange}
/>
</EuiFlexItem>
</EuiFlexGroup>
@ -253,7 +248,6 @@ export const ExceptionBuilder = ({
<BuilderButtonOptions
isOrDisabled={isOrDisabled}
isAndDisabled={isAndDisabled}
displayInitButton={displayInitialAddExceptionButton}
showNestedButton={false}
onOrClicked={handleAddNewExceptionItem}
onAndClicked={handleAddNewExceptionItemEntry}

View file

@ -179,13 +179,6 @@ export const EXCEPTION_FIELD_LISTS_PLACEHOLDER = i18n.translate(
}
);
export const ADD_EXCEPTION_TITLE = i18n.translate(
'xpack.securitySolution.exceptions.addExceptionTitle',
{
defaultMessage: 'Add exception',
}
);
export const AND = i18n.translate('xpack.securitySolution.exceptions.andDescription', {
defaultMessage: 'AND',
});