mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
[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:
parent
44fc2a828c
commit
1adaa3b76c
8 changed files with 493 additions and 282 deletions
|
@ -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')}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -16,7 +16,6 @@ const MyEuiButton = styled(EuiButton)`
|
|||
interface BuilderButtonOptionsProps {
|
||||
isOrDisabled: boolean;
|
||||
isAndDisabled: boolean;
|
||||
displayInitButton: boolean;
|
||||
showNestedButton: boolean;
|
||||
onAndClicked: () => void;
|
||||
onOrClicked: () => void;
|
||||
|
@ -26,64 +25,47 @@ 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}>
|
||||
<MyEuiButton
|
||||
fill
|
||||
size="s"
|
||||
iconType="plusInCircle"
|
||||
onClick={onAndClicked}
|
||||
data-test-subj="exceptionsAndButton"
|
||||
isDisabled={isAndDisabled}
|
||||
>
|
||||
{i18n.AND}
|
||||
</MyEuiButton>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<MyEuiButton
|
||||
fill
|
||||
size="s"
|
||||
iconType="plusInCircle"
|
||||
onClick={onOrClicked}
|
||||
isDisabled={isOrDisabled}
|
||||
data-test-subj="exceptionsOrButton"
|
||||
>
|
||||
{i18n.OR}
|
||||
</MyEuiButton>
|
||||
</EuiFlexItem>
|
||||
{showNestedButton && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
fill
|
||||
size="s"
|
||||
iconType="plusInCircle"
|
||||
onClick={onOrClicked}
|
||||
data-test-subj="exceptionsAddNewExceptionButton"
|
||||
iconType="nested"
|
||||
onClick={onNestedClicked}
|
||||
data-test-subj="exceptionsNestedButton"
|
||||
>
|
||||
{i18n.ADD_EXCEPTION_TITLE}
|
||||
{i18n.ADD_NESTED_DESCRIPTION}
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
) : (
|
||||
<>
|
||||
<EuiFlexItem grow={false}>
|
||||
<MyEuiButton
|
||||
fill
|
||||
size="s"
|
||||
iconType="plusInCircle"
|
||||
onClick={onAndClicked}
|
||||
data-test-subj="exceptionsAndButton"
|
||||
isDisabled={isAndDisabled}
|
||||
>
|
||||
{i18n.AND}
|
||||
</MyEuiButton>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<MyEuiButton
|
||||
fill
|
||||
size="s"
|
||||
iconType="plusInCircle"
|
||||
onClick={onOrClicked}
|
||||
isDisabled={isOrDisabled}
|
||||
data-test-subj="exceptionsOrButton"
|
||||
>
|
||||
{i18n.OR}
|
||||
</MyEuiButton>
|
||||
</EuiFlexItem>
|
||||
{showNestedButton && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
size="s"
|
||||
iconType="nested"
|
||||
onClick={onNestedClicked}
|
||||
data-test-subj="exceptionsNestedButton"
|
||||
>
|
||||
{i18n.ADD_NESTED_DESCRIPTION}
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
|
|
|
@ -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
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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';
|
|
@ -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';
|
|
@ -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}
|
||||
|
|
|
@ -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',
|
||||
});
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue