mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[Security Solution][Exceptions] - Moves ExceptionItem component to lists plugin (#95246)
## Summary Moves part of the exceptions UI out of the security solution plugin and into the lists plugin. In order to keep PRs (relatively) small, I am moving single components at a time. This should also then help more easily pinpoint the source of any issues that come up along the way. The next couple PRs will focus on the exception builder. This one in particular is focused on moving over the `BuilderExceptionItem` which deals with rendering the individual exception items.
This commit is contained in:
parent
80fdcde813
commit
17d3907730
27 changed files with 1271 additions and 826 deletions
|
@ -0,0 +1,13 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { RecursivePartial } from '@elastic/eui/src/components/common';
|
||||
|
||||
import { EuiTheme } from '../../../../../../src/plugins/kibana_react/common';
|
||||
|
||||
export const getMockTheme = (partialTheme: RecursivePartial<EuiTheme>): EuiTheme =>
|
||||
partialTheme as EuiTheme;
|
|
@ -0,0 +1,79 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { Story, addDecorator } from '@storybook/react';
|
||||
import React from 'react';
|
||||
import { ThemeProvider } from 'styled-components';
|
||||
import euiLightVars from '@elastic/eui/dist/eui_theme_light.json';
|
||||
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
|
||||
import { getMockTheme } from '../../../common/test_utils/kibana_react.mock';
|
||||
|
||||
import { AndOrBadge, AndOrBadgeProps } from '.';
|
||||
|
||||
const sampleText =
|
||||
'Doggo ipsum i am bekom fat snoot wow such tempt waggy wags floofs, ruff heckin good boys and girls mlem. Ruff heckin good boys and girls mlem stop it fren borkf borking doggo very hand that feed shibe, you are doing me the shock big ol heck smol borking doggo with a long snoot for pats heckin good boys. You are doing me the shock smol borking doggo with a long snoot for pats wow very biscit, length boy. Doggo ipsum i am bekom fat snoot wow such tempt waggy wags floofs, ruff heckin good boys and girls mlem. Ruff heckin good boys and girls mlem stop it fren borkf borking doggo very hand that feed shibe, you are doing me the shock big ol heck smol borking doggo with a long snoot for pats heckin good boys.';
|
||||
|
||||
const mockTheme = getMockTheme({
|
||||
darkMode: false,
|
||||
eui: euiLightVars,
|
||||
});
|
||||
|
||||
addDecorator((storyFn) => <ThemeProvider theme={mockTheme}>{storyFn()}</ThemeProvider>);
|
||||
|
||||
export default {
|
||||
argTypes: {
|
||||
includeAntennas: {
|
||||
control: {
|
||||
type: 'boolean',
|
||||
},
|
||||
description: 'Determines whether extending vertical lines appear extended off of round badge',
|
||||
table: {
|
||||
defaultValue: {
|
||||
summary: false,
|
||||
},
|
||||
},
|
||||
type: {
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
type: {
|
||||
control: {
|
||||
options: ['and', 'or'],
|
||||
type: 'select',
|
||||
},
|
||||
description: '`and | or` - determines text displayed in badge.',
|
||||
table: {
|
||||
defaultValue: {
|
||||
summary: 'and',
|
||||
},
|
||||
},
|
||||
type: {
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
component: AndOrBadge,
|
||||
title: 'AndOrBadge',
|
||||
};
|
||||
|
||||
const AndOrBadgeTemplate: Story<AndOrBadgeProps> = (args) => (
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem grow={false}>
|
||||
<AndOrBadge {...args} />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<p>{sampleText}</p>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
|
||||
export const Default = AndOrBadgeTemplate.bind({});
|
||||
Default.args = {
|
||||
includeAntennas: false,
|
||||
type: 'and',
|
||||
};
|
|
@ -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
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { ThemeProvider } from 'styled-components';
|
||||
import { mount } from 'enzyme';
|
||||
|
||||
import { getMockTheme } from '../../../common/test_utils/kibana_react.mock';
|
||||
|
||||
import { AndOrBadge } from './';
|
||||
|
||||
const mockTheme = getMockTheme({ eui: { euiColorLightShade: '#ece' } });
|
||||
|
||||
describe('AndOrBadge', () => {
|
||||
test('it renders top and bottom antenna bars when "includeAntennas" is true', () => {
|
||||
const wrapper = mount(
|
||||
<ThemeProvider theme={mockTheme}>
|
||||
<AndOrBadge includeAntennas type="and" />
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
||||
expect(wrapper.find('[data-test-subj="and-or-badge"]').at(0).text()).toEqual('AND');
|
||||
expect(wrapper.find('[data-test-subj="andOrBadgeBarTop"]').exists()).toBeTruthy();
|
||||
expect(wrapper.find('[data-test-subj="andOrBadgeBarBottom"]').exists()).toBeTruthy();
|
||||
});
|
||||
|
||||
test('it does not render top and bottom antenna bars when "includeAntennas" is false', () => {
|
||||
const wrapper = mount(
|
||||
<ThemeProvider theme={mockTheme}>
|
||||
<AndOrBadge type="or" />
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
||||
expect(wrapper.find('[data-test-subj="and-or-badge"]').at(0).text()).toEqual('OR');
|
||||
expect(wrapper.find('[data-test-subj="andOrBadgeBarTop"]').exists()).toBeFalsy();
|
||||
expect(wrapper.find('[data-test-subj="andOrBadgeBarBottom"]').exists()).toBeFalsy();
|
||||
});
|
||||
|
||||
test('it renders "and" when "type" is "and"', () => {
|
||||
const wrapper = mount(
|
||||
<ThemeProvider theme={mockTheme}>
|
||||
<AndOrBadge type="and" />
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
||||
expect(wrapper.find('[data-test-subj="and-or-badge"]').at(0).text()).toEqual('AND');
|
||||
});
|
||||
|
||||
test('it renders "or" when "type" is "or"', () => {
|
||||
const wrapper = mount(
|
||||
<ThemeProvider theme={mockTheme}>
|
||||
<AndOrBadge type="or" />
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
||||
expect(wrapper.find('[data-test-subj="and-or-badge"]').at(0).text()).toEqual('OR');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { RoundedBadge } from './rounded_badge';
|
||||
import { RoundedBadgeAntenna } from './rounded_badge_antenna';
|
||||
|
||||
export type AndOr = 'and' | 'or';
|
||||
export interface AndOrBadgeProps {
|
||||
type: AndOr;
|
||||
includeAntennas?: boolean;
|
||||
}
|
||||
/** Displays AND / OR in a round badge */
|
||||
// This ticket is closed, however, as of 3/23/21 no round badge yet
|
||||
// Ref: https://github.com/elastic/eui/issues/1655
|
||||
export const AndOrBadge = React.memo<AndOrBadgeProps>(({ type, includeAntennas = false }) => {
|
||||
return includeAntennas ? <RoundedBadgeAntenna type={type} /> : <RoundedBadge type={type} />;
|
||||
});
|
||||
|
||||
AndOrBadge.displayName = 'AndOrBadge';
|
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { mount } from 'enzyme';
|
||||
|
||||
import { RoundedBadge } from './rounded_badge';
|
||||
|
||||
describe('RoundedBadge', () => {
|
||||
test('it renders "and" when "type" is "and"', () => {
|
||||
const wrapper = mount(<RoundedBadge type="and" />);
|
||||
|
||||
expect(wrapper.find('[data-test-subj="and-or-badge"]').at(0).text()).toEqual('AND');
|
||||
});
|
||||
|
||||
test('it renders "or" when "type" is "or"', () => {
|
||||
const wrapper = mount(<RoundedBadge type="or" />);
|
||||
|
||||
expect(wrapper.find('[data-test-subj="and-or-badge"]').at(0).text()).toEqual('OR');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { EuiBadge } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import * as i18n from './translations';
|
||||
|
||||
import { AndOr } from '.';
|
||||
|
||||
const RoundBadge = (styled(EuiBadge)`
|
||||
align-items: center;
|
||||
border-radius: 100%;
|
||||
display: inline-flex;
|
||||
font-size: 9px;
|
||||
height: 34px;
|
||||
justify-content: center;
|
||||
margin: 0 5px 0 5px;
|
||||
padding: 7px 6px 4px 6px;
|
||||
user-select: none;
|
||||
width: 34px;
|
||||
.euiBadge__content {
|
||||
position: relative;
|
||||
top: -1px;
|
||||
}
|
||||
.euiBadge__text {
|
||||
text-overflow: clip;
|
||||
}
|
||||
` as unknown) as typeof EuiBadge;
|
||||
|
||||
RoundBadge.displayName = 'RoundBadge';
|
||||
|
||||
export const RoundedBadge: React.FC<{ type: AndOr }> = ({ type }) => (
|
||||
<RoundBadge data-test-subj="and-or-badge" color="hollow">
|
||||
{type === 'and' ? i18n.AND : i18n.OR}
|
||||
</RoundBadge>
|
||||
);
|
||||
|
||||
RoundedBadge.displayName = 'RoundedBadge';
|
|
@ -0,0 +1,50 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { ThemeProvider } from 'styled-components';
|
||||
import { mount } from 'enzyme';
|
||||
|
||||
import { getMockTheme } from '../../../common/test_utils/kibana_react.mock';
|
||||
|
||||
import { RoundedBadgeAntenna } from './rounded_badge_antenna';
|
||||
|
||||
const mockTheme = getMockTheme({ eui: { euiColorLightShade: '#ece' } });
|
||||
|
||||
describe('RoundedBadgeAntenna', () => {
|
||||
test('it renders top and bottom antenna bars', () => {
|
||||
const wrapper = mount(
|
||||
<ThemeProvider theme={mockTheme}>
|
||||
<RoundedBadgeAntenna type="and" />
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
||||
expect(wrapper.find('[data-test-subj="and-or-badge"]').at(0).text()).toEqual('AND');
|
||||
expect(wrapper.find('[data-test-subj="andOrBadgeBarTop"]').exists()).toBeTruthy();
|
||||
expect(wrapper.find('[data-test-subj="andOrBadgeBarBottom"]').exists()).toBeTruthy();
|
||||
});
|
||||
|
||||
test('it renders "and" when "type" is "and"', () => {
|
||||
const wrapper = mount(
|
||||
<ThemeProvider theme={mockTheme}>
|
||||
<RoundedBadgeAntenna type="and" />
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
||||
expect(wrapper.find('[data-test-subj="and-or-badge"]').at(0).text()).toEqual('AND');
|
||||
});
|
||||
|
||||
test('it renders "or" when "type" is "or"', () => {
|
||||
const wrapper = mount(
|
||||
<ThemeProvider theme={mockTheme}>
|
||||
<RoundedBadgeAntenna type="or" />
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
||||
expect(wrapper.find('[data-test-subj="and-or-badge"]').at(0).text()).toEqual('OR');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,59 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import styled, { css } from 'styled-components';
|
||||
|
||||
import { RoundedBadge } from './rounded_badge';
|
||||
|
||||
import { AndOr } from '.';
|
||||
|
||||
const antennaStyles = css`
|
||||
background: ${({ theme }): string => theme.eui.euiColorLightShade};
|
||||
position: relative;
|
||||
width: 2px;
|
||||
&:after {
|
||||
background: ${({ theme }): string => theme.eui.euiColorLightShade};
|
||||
content: '';
|
||||
height: 8px;
|
||||
right: -4px;
|
||||
position: absolute;
|
||||
width: 10px;
|
||||
clip-path: circle();
|
||||
}
|
||||
`;
|
||||
|
||||
const TopAntenna = styled(EuiFlexItem)`
|
||||
${antennaStyles}
|
||||
&:after {
|
||||
top: 0;
|
||||
}
|
||||
`;
|
||||
const BottomAntenna = styled(EuiFlexItem)`
|
||||
${antennaStyles}
|
||||
&:after {
|
||||
bottom: 0;
|
||||
}
|
||||
`;
|
||||
|
||||
export const RoundedBadgeAntenna: React.FC<{ type: AndOr }> = ({ type }) => (
|
||||
<EuiFlexGroup
|
||||
className="andBadgeContainer"
|
||||
gutterSize="none"
|
||||
direction="column"
|
||||
alignItems="center"
|
||||
>
|
||||
<TopAntenna data-test-subj="andOrBadgeBarTop" grow={1} />
|
||||
<EuiFlexItem grow={false}>
|
||||
<RoundedBadge type={type} />
|
||||
</EuiFlexItem>
|
||||
<BottomAntenna data-test-subj="andOrBadgeBarBottom" grow={1} />
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
|
||||
RoundedBadgeAntenna.displayName = 'RoundedBadgeAntenna';
|
|
@ -0,0 +1,16 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
export const AND = i18n.translate('xpack.lists.andOrBadge.andLabel', {
|
||||
defaultMessage: 'AND',
|
||||
});
|
||||
|
||||
export const OR = i18n.translate('xpack.lists.andOrBadge.orLabel', {
|
||||
defaultMessage: 'OR',
|
||||
});
|
|
@ -9,8 +9,9 @@ import React from 'react';
|
|||
import { ThemeProvider } from 'styled-components';
|
||||
import { mount } from 'enzyme';
|
||||
|
||||
import { getMockTheme } from '../../../common/test_utils/kibana_react.mock';
|
||||
|
||||
import { BuilderAndBadgeComponent } from './and_badge';
|
||||
import { getMockTheme } from '../../../lib/kibana/kibana_react.mock';
|
||||
|
||||
const mockTheme = getMockTheme({ eui: { euiColorLightShade: '#ece' } });
|
||||
|
|
@ -9,7 +9,7 @@ import React from 'react';
|
|||
import { EuiFlexItem } from '@elastic/eui';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { AndOrBadge } from '../../and_or_badge';
|
||||
import { AndOrBadge } from '../and_or_badge';
|
||||
|
||||
const MyInvisibleAndBadge = styled(EuiFlexItem)`
|
||||
visibility: hidden;
|
|
@ -8,8 +8,8 @@
|
|||
import { mount } from 'enzyme';
|
||||
import React from 'react';
|
||||
|
||||
import { getExceptionListItemSchemaMock } from '../../../../../../lists/common/schemas/response/exception_list_item_schema.mock';
|
||||
import { getEntryMatchMock } from '../../../../../../lists/common/schemas/types/entry_match.mock';
|
||||
import { getExceptionListItemSchemaMock } from '../../../../common/schemas/response/exception_list_item_schema.mock';
|
||||
import { getEntryMatchMock } from '../../../../common/schemas/types/entry_match.mock';
|
||||
|
||||
import { BuilderEntryDeleteButtonComponent } from './entry_delete_button';
|
||||
|
|
@ -9,7 +9,7 @@ import React, { useCallback } from 'react';
|
|||
import { EuiButtonIcon, EuiFlexItem } from '@elastic/eui';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { BuilderEntry } from '../types';
|
||||
import { BuilderEntry } from './types';
|
||||
|
||||
const MyFirstRowContainer = styled(EuiFlexItem)`
|
||||
padding-top: 20px;
|
|
@ -15,10 +15,11 @@ import { HttpStart } from 'kibana/public';
|
|||
import { OperatorEnum, OperatorTypeEnum } from '../../../../common';
|
||||
import { AutocompleteStart } from '../../../../../../../src/plugins/data/public';
|
||||
import { fields } from '../../../../../../../src/plugins/data/common/index_patterns/fields/fields.mocks';
|
||||
import { getMockTheme } from '../../../common/test_utils/kibana_react.mock';
|
||||
|
||||
import { BuilderEntryItem, EntryItemProps } from './entry_renderer';
|
||||
|
||||
const mockTheme = (): { darkMode: boolean; eui: unknown } => ({
|
||||
const mockTheme = getMockTheme({
|
||||
darkMode: false,
|
||||
eui: euiLightVars,
|
||||
});
|
||||
|
|
|
@ -8,39 +8,28 @@
|
|||
import React from 'react';
|
||||
import { ThemeProvider } from 'styled-components';
|
||||
import { mount } from 'enzyme';
|
||||
import { dataPluginMock } from 'src/plugins/data/public/mocks';
|
||||
|
||||
import { useKibana } from '../../../../common/lib/kibana';
|
||||
import { fields } from '../../../../../../../../src/plugins/data/common/index_patterns/fields/fields.mocks';
|
||||
import { getExceptionListItemSchemaMock } from '../../../../../../lists/common/schemas/response/exception_list_item_schema.mock';
|
||||
import { getEntryMatchMock } from '../../../../../../lists/common/schemas/types/entry_match.mock';
|
||||
import { getEntryMatchAnyMock } from '../../../../../../lists/common/schemas/types/entry_match_any.mock';
|
||||
import { fields } from '../../../../../../../src/plugins/data/common/index_patterns/fields/fields.mocks';
|
||||
import { getExceptionListItemSchemaMock } from '../../../../common/schemas/response/exception_list_item_schema.mock';
|
||||
import { getEntryMatchMock } from '../../../../common/schemas/types/entry_match.mock';
|
||||
import { getEntryMatchAnyMock } from '../../../../common/schemas/types/entry_match_any.mock';
|
||||
import { coreMock } from '../../../../../../../src/core/public/mocks';
|
||||
import { getMockTheme } from '../../../common/test_utils/kibana_react.mock';
|
||||
|
||||
import { BuilderExceptionListItemComponent } from './exception_item';
|
||||
import { getMockTheme } from '../../../lib/kibana/kibana_react.mock';
|
||||
import { BuilderExceptionListItemComponent } from './exception_item_renderer';
|
||||
|
||||
const mockTheme = getMockTheme({
|
||||
eui: {
|
||||
euiColorLightShade: '#ece',
|
||||
},
|
||||
});
|
||||
|
||||
jest.mock('../../../../common/lib/kibana');
|
||||
const mockKibanaHttpService = coreMock.createStart().http;
|
||||
const { autocomplete: autocompleteStartMock } = dataPluginMock.createStartContract();
|
||||
|
||||
describe('BuilderExceptionListItemComponent', () => {
|
||||
const getValueSuggestionsMock = jest.fn().mockResolvedValue(['value 1', 'value 2']);
|
||||
|
||||
beforeAll(() => {
|
||||
(useKibana as jest.Mock).mockReturnValue({
|
||||
services: {
|
||||
data: {
|
||||
autocomplete: {
|
||||
getValueSuggestions: getValueSuggestionsMock,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
getValueSuggestionsMock.mockClear();
|
||||
});
|
||||
|
@ -54,19 +43,22 @@ describe('BuilderExceptionListItemComponent', () => {
|
|||
const wrapper = mount(
|
||||
<ThemeProvider theme={mockTheme}>
|
||||
<BuilderExceptionListItemComponent
|
||||
allowLargeValueLists={true}
|
||||
andLogicIncluded={true}
|
||||
autocompleteService={autocompleteStartMock}
|
||||
exceptionItem={exceptionItem}
|
||||
exceptionItemIndex={0}
|
||||
httpService={mockKibanaHttpService}
|
||||
indexPattern={{
|
||||
fields,
|
||||
id: '1234',
|
||||
title: 'logstash-*',
|
||||
fields,
|
||||
}}
|
||||
andLogicIncluded={true}
|
||||
isOnlyItem={false}
|
||||
listType="detection"
|
||||
setErrorsExist={jest.fn()}
|
||||
onDeleteExceptionItem={jest.fn()}
|
||||
onChangeExceptionItem={jest.fn()}
|
||||
onDeleteExceptionItem={jest.fn()}
|
||||
setErrorsExist={jest.fn()}
|
||||
/>
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
@ -82,19 +74,22 @@ describe('BuilderExceptionListItemComponent', () => {
|
|||
const wrapper = mount(
|
||||
<ThemeProvider theme={mockTheme}>
|
||||
<BuilderExceptionListItemComponent
|
||||
allowLargeValueLists={true}
|
||||
andLogicIncluded={true}
|
||||
autocompleteService={autocompleteStartMock}
|
||||
exceptionItem={exceptionItem}
|
||||
exceptionItemIndex={1}
|
||||
httpService={mockKibanaHttpService}
|
||||
indexPattern={{
|
||||
fields,
|
||||
id: '1234',
|
||||
title: 'logstash-*',
|
||||
fields,
|
||||
}}
|
||||
andLogicIncluded={true}
|
||||
isOnlyItem={false}
|
||||
listType="detection"
|
||||
setErrorsExist={jest.fn()}
|
||||
onDeleteExceptionItem={jest.fn()}
|
||||
onChangeExceptionItem={jest.fn()}
|
||||
onDeleteExceptionItem={jest.fn()}
|
||||
setErrorsExist={jest.fn()}
|
||||
/>
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
@ -108,19 +103,22 @@ describe('BuilderExceptionListItemComponent', () => {
|
|||
const wrapper = mount(
|
||||
<ThemeProvider theme={mockTheme}>
|
||||
<BuilderExceptionListItemComponent
|
||||
allowLargeValueLists={true}
|
||||
andLogicIncluded={true}
|
||||
autocompleteService={autocompleteStartMock}
|
||||
exceptionItem={exceptionItem}
|
||||
exceptionItemIndex={1}
|
||||
httpService={mockKibanaHttpService}
|
||||
indexPattern={{
|
||||
fields,
|
||||
id: '1234',
|
||||
title: 'logstash-*',
|
||||
fields,
|
||||
}}
|
||||
andLogicIncluded={true}
|
||||
isOnlyItem={false}
|
||||
listType="detection"
|
||||
setErrorsExist={jest.fn()}
|
||||
onDeleteExceptionItem={jest.fn()}
|
||||
onChangeExceptionItem={jest.fn()}
|
||||
onDeleteExceptionItem={jest.fn()}
|
||||
setErrorsExist={jest.fn()}
|
||||
/>
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
@ -136,19 +134,22 @@ describe('BuilderExceptionListItemComponent', () => {
|
|||
const wrapper = mount(
|
||||
<ThemeProvider theme={mockTheme}>
|
||||
<BuilderExceptionListItemComponent
|
||||
allowLargeValueLists={true}
|
||||
andLogicIncluded={false}
|
||||
autocompleteService={autocompleteStartMock}
|
||||
exceptionItem={exceptionItem}
|
||||
exceptionItemIndex={1}
|
||||
httpService={mockKibanaHttpService}
|
||||
indexPattern={{
|
||||
fields,
|
||||
id: '1234',
|
||||
title: 'logstash-*',
|
||||
fields,
|
||||
}}
|
||||
andLogicIncluded={false}
|
||||
isOnlyItem={false}
|
||||
listType="detection"
|
||||
setErrorsExist={jest.fn()}
|
||||
onDeleteExceptionItem={jest.fn()}
|
||||
onChangeExceptionItem={jest.fn()}
|
||||
onDeleteExceptionItem={jest.fn()}
|
||||
setErrorsExist={jest.fn()}
|
||||
/>
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
@ -171,19 +172,22 @@ describe('BuilderExceptionListItemComponent', () => {
|
|||
};
|
||||
const wrapper = mount(
|
||||
<BuilderExceptionListItemComponent
|
||||
allowLargeValueLists={true}
|
||||
andLogicIncluded={false}
|
||||
autocompleteService={autocompleteStartMock}
|
||||
exceptionItem={exceptionItem}
|
||||
exceptionItemIndex={0}
|
||||
httpService={mockKibanaHttpService}
|
||||
indexPattern={{
|
||||
fields,
|
||||
id: '1234',
|
||||
title: 'logstash-*',
|
||||
fields,
|
||||
}}
|
||||
andLogicIncluded={false}
|
||||
isOnlyItem={true}
|
||||
listType="detection"
|
||||
setErrorsExist={jest.fn()}
|
||||
onDeleteExceptionItem={jest.fn()}
|
||||
onChangeExceptionItem={jest.fn()}
|
||||
onDeleteExceptionItem={jest.fn()}
|
||||
setErrorsExist={jest.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
|
@ -198,19 +202,22 @@ describe('BuilderExceptionListItemComponent', () => {
|
|||
|
||||
const wrapper = mount(
|
||||
<BuilderExceptionListItemComponent
|
||||
allowLargeValueLists={true}
|
||||
andLogicIncluded={false}
|
||||
autocompleteService={autocompleteStartMock}
|
||||
exceptionItem={exceptionItem}
|
||||
exceptionItemIndex={0}
|
||||
httpService={mockKibanaHttpService}
|
||||
indexPattern={{
|
||||
fields,
|
||||
id: '1234',
|
||||
title: 'logstash-*',
|
||||
fields,
|
||||
}}
|
||||
andLogicIncluded={false}
|
||||
isOnlyItem={false}
|
||||
listType="detection"
|
||||
setErrorsExist={jest.fn()}
|
||||
onDeleteExceptionItem={jest.fn()}
|
||||
onChangeExceptionItem={jest.fn()}
|
||||
onDeleteExceptionItem={jest.fn()}
|
||||
setErrorsExist={jest.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
|
@ -224,21 +231,24 @@ describe('BuilderExceptionListItemComponent', () => {
|
|||
exceptionItem.entries = [getEntryMatchMock()];
|
||||
const wrapper = mount(
|
||||
<BuilderExceptionListItemComponent
|
||||
allowLargeValueLists={true}
|
||||
andLogicIncluded={false}
|
||||
autocompleteService={autocompleteStartMock}
|
||||
exceptionItem={exceptionItem}
|
||||
exceptionItemIndex={1}
|
||||
httpService={mockKibanaHttpService}
|
||||
indexPattern={{
|
||||
fields,
|
||||
id: '1234',
|
||||
title: 'logstash-*',
|
||||
fields,
|
||||
}}
|
||||
andLogicIncluded={false}
|
||||
// if exceptionItemIndex is not 0, wouldn't make sense for
|
||||
// this to be true, but done for testing purposes
|
||||
isOnlyItem={true}
|
||||
listType="detection"
|
||||
setErrorsExist={jest.fn()}
|
||||
onDeleteExceptionItem={jest.fn()}
|
||||
onChangeExceptionItem={jest.fn()}
|
||||
onDeleteExceptionItem={jest.fn()}
|
||||
setErrorsExist={jest.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
|
@ -252,19 +262,22 @@ describe('BuilderExceptionListItemComponent', () => {
|
|||
exceptionItem.entries = [getEntryMatchMock(), getEntryMatchMock()];
|
||||
const wrapper = mount(
|
||||
<BuilderExceptionListItemComponent
|
||||
allowLargeValueLists={true}
|
||||
andLogicIncluded={false}
|
||||
autocompleteService={autocompleteStartMock}
|
||||
exceptionItem={exceptionItem}
|
||||
exceptionItemIndex={0}
|
||||
httpService={mockKibanaHttpService}
|
||||
indexPattern={{
|
||||
fields,
|
||||
id: '1234',
|
||||
title: 'logstash-*',
|
||||
fields,
|
||||
}}
|
||||
andLogicIncluded={false}
|
||||
isOnlyItem={true}
|
||||
listType="detection"
|
||||
setErrorsExist={jest.fn()}
|
||||
onDeleteExceptionItem={jest.fn()}
|
||||
onChangeExceptionItem={jest.fn()}
|
||||
onDeleteExceptionItem={jest.fn()}
|
||||
setErrorsExist={jest.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
|
@ -280,19 +293,22 @@ describe('BuilderExceptionListItemComponent', () => {
|
|||
exceptionItem.entries = [getEntryMatchMock(), getEntryMatchAnyMock()];
|
||||
const wrapper = mount(
|
||||
<BuilderExceptionListItemComponent
|
||||
allowLargeValueLists={true}
|
||||
andLogicIncluded={false}
|
||||
autocompleteService={autocompleteStartMock}
|
||||
exceptionItem={exceptionItem}
|
||||
exceptionItemIndex={0}
|
||||
httpService={mockKibanaHttpService}
|
||||
indexPattern={{
|
||||
fields,
|
||||
id: '1234',
|
||||
title: 'logstash-*',
|
||||
fields,
|
||||
}}
|
||||
andLogicIncluded={false}
|
||||
isOnlyItem={true}
|
||||
listType="detection"
|
||||
setErrorsExist={jest.fn()}
|
||||
onDeleteExceptionItem={mockOnDeleteExceptionItem}
|
||||
onChangeExceptionItem={jest.fn()}
|
||||
onDeleteExceptionItem={mockOnDeleteExceptionItem}
|
||||
setErrorsExist={jest.fn()}
|
||||
/>
|
||||
);
|
||||
|
|
@ -5,23 +5,24 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useMemo, useCallback } from 'react';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import styled from 'styled-components';
|
||||
import { HttpStart } from 'kibana/public';
|
||||
import { AutocompleteStart } from 'src/plugins/data/public';
|
||||
|
||||
import { Type } from '../../../../../common/detection_engine/schemas/common/schemas';
|
||||
import { IIndexPattern } from '../../../../../../../../src/plugins/data/common';
|
||||
import { getFormattedBuilderEntries, getUpdatedEntriesOnDelete } from './helpers';
|
||||
import { FormattedBuilderEntry, ExceptionsBuilderExceptionItem, BuilderEntry } from '../types';
|
||||
import { BuilderEntryItem, ExceptionListType } from '../../../../shared_imports';
|
||||
import { BuilderEntryDeleteButtonComponent } from './entry_delete_button';
|
||||
import { ExceptionListType } from '../../../../common';
|
||||
import { IIndexPattern } from '../../../../../../../src/plugins/data/common';
|
||||
|
||||
import { BuilderEntry, ExceptionsBuilderExceptionItem, FormattedBuilderEntry } from './types';
|
||||
import { BuilderAndBadgeComponent } from './and_badge';
|
||||
import { isEqlRule, isThresholdRule } from '../../../../../common/detection_engine/utils';
|
||||
import { useKibana } from '../../../lib/kibana';
|
||||
import { BuilderEntryDeleteButtonComponent } from './entry_delete_button';
|
||||
import { BuilderEntryItem } from './entry_renderer';
|
||||
import { getFormattedBuilderEntries, getUpdatedEntriesOnDelete } from './helpers';
|
||||
|
||||
const MyBeautifulLine = styled(EuiFlexItem)`
|
||||
&:after {
|
||||
background: ${({ theme }) => theme.eui.euiColorLightShade};
|
||||
background: ${({ theme }): string => theme.eui.euiColorLightShade};
|
||||
content: '';
|
||||
width: 2px;
|
||||
height: 40px;
|
||||
|
@ -35,6 +36,9 @@ const MyOverflowContainer = styled(EuiFlexItem)`
|
|||
`;
|
||||
|
||||
interface BuilderExceptionListItemProps {
|
||||
allowLargeValueLists: boolean;
|
||||
httpService: HttpStart;
|
||||
autocompleteService: AutocompleteStart;
|
||||
exceptionItem: ExceptionsBuilderExceptionItem;
|
||||
exceptionItemIndex: number;
|
||||
indexPattern: IIndexPattern;
|
||||
|
@ -45,11 +49,13 @@ interface BuilderExceptionListItemProps {
|
|||
onChangeExceptionItem: (item: ExceptionsBuilderExceptionItem, index: number) => void;
|
||||
setErrorsExist: (arg: boolean) => void;
|
||||
onlyShowListOperators?: boolean;
|
||||
ruleType?: Type;
|
||||
}
|
||||
|
||||
export const BuilderExceptionListItemComponent = React.memo<BuilderExceptionListItemProps>(
|
||||
({
|
||||
allowLargeValueLists,
|
||||
httpService,
|
||||
autocompleteService,
|
||||
exceptionItem,
|
||||
exceptionItemIndex,
|
||||
indexPattern,
|
||||
|
@ -60,9 +66,7 @@ export const BuilderExceptionListItemComponent = React.memo<BuilderExceptionList
|
|||
onChangeExceptionItem,
|
||||
setErrorsExist,
|
||||
onlyShowListOperators = false,
|
||||
ruleType,
|
||||
}) => {
|
||||
const { http, data } = useKibana().services;
|
||||
const handleEntryChange = useCallback(
|
||||
(entry: BuilderEntry, entryIndex: number): void => {
|
||||
const updatedEntries: BuilderEntry[] = [
|
||||
|
@ -119,9 +123,9 @@ export const BuilderExceptionListItemComponent = React.memo<BuilderExceptionList
|
|||
{item.nested === 'child' && <MyBeautifulLine grow={false} />}
|
||||
<MyOverflowContainer grow={1}>
|
||||
<BuilderEntryItem
|
||||
allowLargeValueLists={!isEqlRule(ruleType) && !isThresholdRule(ruleType)}
|
||||
httpService={http}
|
||||
autocompleteService={data.autocomplete}
|
||||
allowLargeValueLists={allowLargeValueLists}
|
||||
httpService={httpService}
|
||||
autocompleteService={autocompleteService}
|
||||
entry={item}
|
||||
indexPattern={indexPattern}
|
||||
listType={listType}
|
|
@ -5,6 +5,9 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { ENTRIES_WITH_IDS } from '../../../../common/constants.mock';
|
||||
import { getEntryExistsMock } from '../../../../common/schemas/types/entry_exists.mock';
|
||||
import { getExceptionListItemSchemaMock } from '../../../../common/schemas/response/exception_list_item_schema.mock';
|
||||
import {
|
||||
fields,
|
||||
getField,
|
||||
|
@ -37,8 +40,9 @@ import {
|
|||
} from '../../../../common';
|
||||
import { OperatorOption } from '../autocomplete/types';
|
||||
|
||||
import { BuilderEntry, FormattedBuilderEntry } from './types';
|
||||
import { BuilderEntry, ExceptionsBuilderExceptionItem, FormattedBuilderEntry } from './types';
|
||||
import {
|
||||
getCorrespondingKeywordField,
|
||||
getEntryFromOperator,
|
||||
getEntryOnFieldChange,
|
||||
getEntryOnListChange,
|
||||
|
@ -46,13 +50,22 @@ import {
|
|||
getEntryOnMatchChange,
|
||||
getEntryOnOperatorChange,
|
||||
getFilteredIndexPatterns,
|
||||
getFormattedBuilderEntries,
|
||||
getFormattedBuilderEntry,
|
||||
getOperatorOptions,
|
||||
getUpdatedEntriesOnDelete,
|
||||
isEntryNested,
|
||||
} from './helpers';
|
||||
|
||||
jest.mock('uuid', () => ({
|
||||
v4: jest.fn().mockReturnValue('123'),
|
||||
}));
|
||||
|
||||
const getEntryExistsWithIdMock = (): EntryExists & { id: string } => ({
|
||||
...getEntryExistsMock(),
|
||||
id: '123',
|
||||
});
|
||||
|
||||
const getEntryNestedWithIdMock = (): EntryNested & { id: string } => ({
|
||||
...getEntryNestedMock(),
|
||||
id: '123',
|
||||
|
@ -995,4 +1008,422 @@ describe('Exception builder helpers', () => {
|
|||
expect(output).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#getFormattedBuilderEntries', () => {
|
||||
test('it returns formatted entry with field undefined if it unable to find a matching index pattern field', () => {
|
||||
const payloadIndexPattern: IIndexPattern = getMockIndexPattern();
|
||||
const payloadItems: BuilderEntry[] = [getEntryMatchWithIdMock()];
|
||||
const output = getFormattedBuilderEntries(payloadIndexPattern, payloadItems);
|
||||
const expected: FormattedBuilderEntry[] = [
|
||||
{
|
||||
correspondingKeywordField: undefined,
|
||||
entryIndex: 0,
|
||||
field: undefined,
|
||||
id: '123',
|
||||
nested: undefined,
|
||||
operator: isOperator,
|
||||
parent: undefined,
|
||||
value: 'some host name',
|
||||
},
|
||||
];
|
||||
expect(output).toEqual(expected);
|
||||
});
|
||||
|
||||
test('it returns formatted entries when no nested entries exist', () => {
|
||||
const payloadIndexPattern: IIndexPattern = getMockIndexPattern();
|
||||
const payloadItems: BuilderEntry[] = [
|
||||
{ ...getEntryMatchWithIdMock(), field: 'ip', value: 'some ip' },
|
||||
{ ...getEntryMatchAnyWithIdMock(), field: 'extension', value: ['some extension'] },
|
||||
];
|
||||
const output = getFormattedBuilderEntries(payloadIndexPattern, payloadItems);
|
||||
const expected: FormattedBuilderEntry[] = [
|
||||
{
|
||||
correspondingKeywordField: undefined,
|
||||
entryIndex: 0,
|
||||
field: {
|
||||
aggregatable: true,
|
||||
count: 0,
|
||||
esTypes: ['ip'],
|
||||
name: 'ip',
|
||||
readFromDocValues: true,
|
||||
scripted: false,
|
||||
searchable: true,
|
||||
type: 'ip',
|
||||
},
|
||||
id: '123',
|
||||
nested: undefined,
|
||||
operator: isOperator,
|
||||
parent: undefined,
|
||||
value: 'some ip',
|
||||
},
|
||||
{
|
||||
correspondingKeywordField: undefined,
|
||||
entryIndex: 1,
|
||||
field: {
|
||||
aggregatable: true,
|
||||
count: 0,
|
||||
esTypes: ['keyword'],
|
||||
name: 'extension',
|
||||
readFromDocValues: true,
|
||||
scripted: false,
|
||||
searchable: true,
|
||||
type: 'string',
|
||||
},
|
||||
id: '123',
|
||||
nested: undefined,
|
||||
operator: isOneOfOperator,
|
||||
parent: undefined,
|
||||
value: ['some extension'],
|
||||
},
|
||||
];
|
||||
expect(output).toEqual(expected);
|
||||
});
|
||||
|
||||
test('it returns formatted entries when nested entries exist', () => {
|
||||
const payloadIndexPattern: IIndexPattern = getMockIndexPattern();
|
||||
const payloadParent: EntryNested = {
|
||||
...getEntryNestedWithIdMock(),
|
||||
entries: [{ ...getEntryMatchWithIdMock(), field: 'child' }],
|
||||
field: 'nestedField',
|
||||
};
|
||||
const payloadItems: BuilderEntry[] = [
|
||||
{ ...getEntryMatchWithIdMock(), field: 'ip', value: 'some ip' },
|
||||
{ ...payloadParent },
|
||||
];
|
||||
|
||||
const output = getFormattedBuilderEntries(payloadIndexPattern, payloadItems);
|
||||
const expected: FormattedBuilderEntry[] = [
|
||||
{
|
||||
correspondingKeywordField: undefined,
|
||||
entryIndex: 0,
|
||||
field: {
|
||||
aggregatable: true,
|
||||
count: 0,
|
||||
esTypes: ['ip'],
|
||||
name: 'ip',
|
||||
readFromDocValues: true,
|
||||
scripted: false,
|
||||
searchable: true,
|
||||
type: 'ip',
|
||||
},
|
||||
id: '123',
|
||||
nested: undefined,
|
||||
operator: isOperator,
|
||||
parent: undefined,
|
||||
value: 'some ip',
|
||||
},
|
||||
{
|
||||
correspondingKeywordField: undefined,
|
||||
entryIndex: 1,
|
||||
field: {
|
||||
aggregatable: false,
|
||||
esTypes: ['nested'],
|
||||
name: 'nestedField',
|
||||
searchable: false,
|
||||
type: 'string',
|
||||
},
|
||||
id: '123',
|
||||
nested: 'parent',
|
||||
operator: isOperator,
|
||||
parent: undefined,
|
||||
value: undefined,
|
||||
},
|
||||
{
|
||||
correspondingKeywordField: undefined,
|
||||
entryIndex: 0,
|
||||
field: {
|
||||
aggregatable: false,
|
||||
count: 0,
|
||||
esTypes: ['text'],
|
||||
name: 'child',
|
||||
readFromDocValues: false,
|
||||
scripted: false,
|
||||
searchable: true,
|
||||
subType: {
|
||||
nested: {
|
||||
path: 'nestedField',
|
||||
},
|
||||
},
|
||||
type: 'string',
|
||||
},
|
||||
id: '123',
|
||||
nested: 'child',
|
||||
operator: isOperator,
|
||||
parent: {
|
||||
parent: {
|
||||
entries: [
|
||||
{
|
||||
field: 'child',
|
||||
id: '123',
|
||||
operator: OperatorEnum.INCLUDED,
|
||||
type: OperatorTypeEnum.MATCH,
|
||||
value: 'some host name',
|
||||
},
|
||||
],
|
||||
field: 'nestedField',
|
||||
id: '123',
|
||||
type: OperatorTypeEnum.NESTED,
|
||||
},
|
||||
parentIndex: 1,
|
||||
},
|
||||
value: 'some host name',
|
||||
},
|
||||
];
|
||||
expect(output).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#getUpdatedEntriesOnDelete', () => {
|
||||
test('it removes entry corresponding to "entryIndex"', () => {
|
||||
const payloadItem: ExceptionsBuilderExceptionItem = {
|
||||
...getExceptionListItemSchemaMock(),
|
||||
entries: ENTRIES_WITH_IDS,
|
||||
};
|
||||
const output = getUpdatedEntriesOnDelete(payloadItem, 0, null);
|
||||
const expected: ExceptionsBuilderExceptionItem = {
|
||||
...getExceptionListItemSchemaMock(),
|
||||
entries: [
|
||||
{
|
||||
field: 'some.not.nested.field',
|
||||
id: '123',
|
||||
operator: OperatorEnum.INCLUDED,
|
||||
type: OperatorTypeEnum.MATCH,
|
||||
value: 'some value',
|
||||
},
|
||||
],
|
||||
};
|
||||
expect(output).toEqual(expected);
|
||||
});
|
||||
|
||||
test('it removes nested entry of "entryIndex" with corresponding parent index', () => {
|
||||
const payloadItem: ExceptionsBuilderExceptionItem = {
|
||||
...getExceptionListItemSchemaMock(),
|
||||
entries: [
|
||||
{
|
||||
...getEntryNestedWithIdMock(),
|
||||
entries: [{ ...getEntryExistsWithIdMock() }, { ...getEntryMatchAnyWithIdMock() }],
|
||||
},
|
||||
],
|
||||
};
|
||||
const output = getUpdatedEntriesOnDelete(payloadItem, 0, 0);
|
||||
const expected: ExceptionsBuilderExceptionItem = {
|
||||
...getExceptionListItemSchemaMock(),
|
||||
entries: [
|
||||
{ ...getEntryNestedWithIdMock(), entries: [{ ...getEntryMatchAnyWithIdMock() }] },
|
||||
],
|
||||
};
|
||||
expect(output).toEqual(expected);
|
||||
});
|
||||
|
||||
test('it removes entire nested entry if after deleting specified nested entry, there are no more nested entries left', () => {
|
||||
const payloadItem: ExceptionsBuilderExceptionItem = {
|
||||
...getExceptionListItemSchemaMock(),
|
||||
entries: [
|
||||
{
|
||||
...getEntryNestedWithIdMock(),
|
||||
entries: [{ ...getEntryExistsWithIdMock() }],
|
||||
},
|
||||
],
|
||||
};
|
||||
const output = getUpdatedEntriesOnDelete(payloadItem, 0, 0);
|
||||
const expected: ExceptionsBuilderExceptionItem = {
|
||||
...getExceptionListItemSchemaMock(),
|
||||
entries: [],
|
||||
};
|
||||
expect(output).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#getFormattedBuilderEntry', () => {
|
||||
test('it returns entry with a value for "correspondingKeywordField" when "item.field" is of type "text" and matching keyword field exists', () => {
|
||||
const payloadIndexPattern: IIndexPattern = {
|
||||
...getMockIndexPattern(),
|
||||
fields: [
|
||||
...fields,
|
||||
{
|
||||
aggregatable: false,
|
||||
count: 0,
|
||||
esTypes: ['text'],
|
||||
name: 'machine.os.raw.text',
|
||||
readFromDocValues: true,
|
||||
scripted: false,
|
||||
searchable: false,
|
||||
type: 'string',
|
||||
},
|
||||
],
|
||||
};
|
||||
const payloadItem: BuilderEntry = {
|
||||
...getEntryMatchWithIdMock(),
|
||||
field: 'machine.os.raw.text',
|
||||
value: 'some os',
|
||||
};
|
||||
const output = getFormattedBuilderEntry(
|
||||
payloadIndexPattern,
|
||||
payloadItem,
|
||||
0,
|
||||
undefined,
|
||||
undefined
|
||||
);
|
||||
const expected: FormattedBuilderEntry = {
|
||||
correspondingKeywordField: getField('machine.os.raw'),
|
||||
entryIndex: 0,
|
||||
field: {
|
||||
aggregatable: false,
|
||||
count: 0,
|
||||
esTypes: ['text'],
|
||||
name: 'machine.os.raw.text',
|
||||
readFromDocValues: true,
|
||||
scripted: false,
|
||||
searchable: false,
|
||||
type: 'string',
|
||||
},
|
||||
id: '123',
|
||||
nested: undefined,
|
||||
operator: isOperator,
|
||||
parent: undefined,
|
||||
value: 'some os',
|
||||
};
|
||||
expect(output).toEqual(expected);
|
||||
});
|
||||
|
||||
test('it returns "FormattedBuilderEntry" with value "nested" of "child" when "parent" and "parentIndex" are defined', () => {
|
||||
const payloadIndexPattern: IIndexPattern = getMockIndexPattern();
|
||||
const payloadItem: BuilderEntry = { ...getEntryMatchWithIdMock(), field: 'child' };
|
||||
const payloadParent: EntryNested = {
|
||||
...getEntryNestedWithIdMock(),
|
||||
entries: [{ ...getEntryMatchWithIdMock(), field: 'child' }],
|
||||
field: 'nestedField',
|
||||
};
|
||||
const output = getFormattedBuilderEntry(
|
||||
payloadIndexPattern,
|
||||
payloadItem,
|
||||
0,
|
||||
payloadParent,
|
||||
1
|
||||
);
|
||||
const expected: FormattedBuilderEntry = {
|
||||
correspondingKeywordField: undefined,
|
||||
entryIndex: 0,
|
||||
field: {
|
||||
aggregatable: false,
|
||||
count: 0,
|
||||
esTypes: ['text'],
|
||||
name: 'child',
|
||||
readFromDocValues: false,
|
||||
scripted: false,
|
||||
searchable: true,
|
||||
subType: {
|
||||
nested: {
|
||||
path: 'nestedField',
|
||||
},
|
||||
},
|
||||
type: 'string',
|
||||
},
|
||||
id: '123',
|
||||
nested: 'child',
|
||||
operator: isOperator,
|
||||
parent: {
|
||||
parent: {
|
||||
entries: [{ ...payloadItem }],
|
||||
field: 'nestedField',
|
||||
id: '123',
|
||||
type: OperatorTypeEnum.NESTED,
|
||||
},
|
||||
parentIndex: 1,
|
||||
},
|
||||
value: 'some host name',
|
||||
};
|
||||
expect(output).toEqual(expected);
|
||||
});
|
||||
|
||||
test('it returns non nested "FormattedBuilderEntry" when "parent" and "parentIndex" are not defined', () => {
|
||||
const payloadIndexPattern: IIndexPattern = getMockIndexPattern();
|
||||
const payloadItem: BuilderEntry = {
|
||||
...getEntryMatchWithIdMock(),
|
||||
field: 'ip',
|
||||
value: 'some ip',
|
||||
};
|
||||
const output = getFormattedBuilderEntry(
|
||||
payloadIndexPattern,
|
||||
payloadItem,
|
||||
0,
|
||||
undefined,
|
||||
undefined
|
||||
);
|
||||
const expected: FormattedBuilderEntry = {
|
||||
correspondingKeywordField: undefined,
|
||||
entryIndex: 0,
|
||||
field: {
|
||||
aggregatable: true,
|
||||
count: 0,
|
||||
esTypes: ['ip'],
|
||||
name: 'ip',
|
||||
readFromDocValues: true,
|
||||
scripted: false,
|
||||
searchable: true,
|
||||
type: 'ip',
|
||||
},
|
||||
id: '123',
|
||||
nested: undefined,
|
||||
operator: isOperator,
|
||||
parent: undefined,
|
||||
value: 'some ip',
|
||||
};
|
||||
expect(output).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#isEntryNested', () => {
|
||||
test('it returns "false" if payload is not of type EntryNested', () => {
|
||||
const payload: BuilderEntry = getEntryMatchWithIdMock();
|
||||
const output = isEntryNested(payload);
|
||||
const expected = false;
|
||||
expect(output).toEqual(expected);
|
||||
});
|
||||
|
||||
test('it returns "true if payload is of type EntryNested', () => {
|
||||
const payload: EntryNested = getEntryNestedWithIdMock();
|
||||
const output = isEntryNested(payload);
|
||||
const expected = true;
|
||||
expect(output).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#getCorrespondingKeywordField', () => {
|
||||
test('it returns matching keyword field if "selectedFieldIsTextType" is true and keyword field exists', () => {
|
||||
const output = getCorrespondingKeywordField({
|
||||
fields,
|
||||
selectedField: 'machine.os.raw.text',
|
||||
});
|
||||
|
||||
expect(output).toEqual(getField('machine.os.raw'));
|
||||
});
|
||||
|
||||
test('it returns undefined if "selectedFieldIsTextType" is false', () => {
|
||||
const output = getCorrespondingKeywordField({
|
||||
fields,
|
||||
selectedField: 'machine.os.raw',
|
||||
});
|
||||
|
||||
expect(output).toEqual(undefined);
|
||||
});
|
||||
|
||||
test('it returns undefined if "selectedField" is empty string', () => {
|
||||
const output = getCorrespondingKeywordField({
|
||||
fields,
|
||||
selectedField: '',
|
||||
});
|
||||
|
||||
expect(output).toEqual(undefined);
|
||||
});
|
||||
|
||||
test('it returns undefined if "selectedField" is undefined', () => {
|
||||
const output = getCorrespondingKeywordField({
|
||||
fields,
|
||||
selectedField: undefined,
|
||||
});
|
||||
|
||||
expect(output).toEqual(undefined);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -9,6 +9,7 @@ import { IFieldType, IIndexPattern } from '../../../../../../../src/plugins/data
|
|||
import { addIdToItem } from '../../../../common/shared_imports';
|
||||
import {
|
||||
Entry,
|
||||
EntryNested,
|
||||
ExceptionListType,
|
||||
ListSchema,
|
||||
OperatorTypeEnum,
|
||||
|
@ -25,7 +26,132 @@ import {
|
|||
} from '../autocomplete/operators';
|
||||
import { OperatorOption } from '../autocomplete/types';
|
||||
|
||||
import { BuilderEntry, FormattedBuilderEntry } from './types';
|
||||
import {
|
||||
BuilderEntry,
|
||||
EmptyNestedEntry,
|
||||
ExceptionsBuilderExceptionItem,
|
||||
FormattedBuilderEntry,
|
||||
} from './types';
|
||||
|
||||
export const isEntryNested = (item: BuilderEntry): item is EntryNested => {
|
||||
return (item as EntryNested).entries != null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the operator type, may not need this if using io-ts types
|
||||
*
|
||||
* @param item a single ExceptionItem entry
|
||||
*/
|
||||
export const getOperatorType = (item: BuilderEntry): OperatorTypeEnum => {
|
||||
switch (item.type) {
|
||||
case 'match':
|
||||
return OperatorTypeEnum.MATCH;
|
||||
case 'match_any':
|
||||
return OperatorTypeEnum.MATCH_ANY;
|
||||
case 'list':
|
||||
return OperatorTypeEnum.LIST;
|
||||
default:
|
||||
return OperatorTypeEnum.EXISTS;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Determines operator selection (is/is not/is one of, etc.)
|
||||
* Default operator is "is"
|
||||
*
|
||||
* @param item a single ExceptionItem entry
|
||||
*/
|
||||
export const getExceptionOperatorSelect = (item: BuilderEntry): OperatorOption => {
|
||||
if (item.type === 'nested') {
|
||||
return isOperator;
|
||||
} else {
|
||||
const operatorType = getOperatorType(item);
|
||||
const foundOperator = EXCEPTION_OPERATORS.find((operatorOption) => {
|
||||
return item.operator === operatorOption.operator && operatorType === operatorOption.type;
|
||||
});
|
||||
|
||||
return foundOperator ?? isOperator;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the fields corresponding value for an entry
|
||||
*
|
||||
* @param item a single ExceptionItem entry
|
||||
*/
|
||||
export const getEntryValue = (item: BuilderEntry): string | string[] | undefined => {
|
||||
switch (item.type) {
|
||||
case OperatorTypeEnum.MATCH:
|
||||
case OperatorTypeEnum.MATCH_ANY:
|
||||
return item.value;
|
||||
case OperatorTypeEnum.EXISTS:
|
||||
return undefined;
|
||||
case OperatorTypeEnum.LIST:
|
||||
return item.list.id;
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Determines whether an entire entry, exception item, or entry within a nested
|
||||
* entry needs to be removed
|
||||
*
|
||||
* @param exceptionItem
|
||||
* @param entryIndex index of given entry, for nested entries, this will correspond
|
||||
* to their parent index
|
||||
* @param nestedEntryIndex index of nested entry
|
||||
*
|
||||
*/
|
||||
export const getUpdatedEntriesOnDelete = (
|
||||
exceptionItem: ExceptionsBuilderExceptionItem,
|
||||
entryIndex: number,
|
||||
nestedParentIndex: number | null
|
||||
): ExceptionsBuilderExceptionItem => {
|
||||
const itemOfInterest: BuilderEntry = exceptionItem.entries[nestedParentIndex ?? entryIndex];
|
||||
|
||||
if (nestedParentIndex != null && itemOfInterest.type === OperatorTypeEnum.NESTED) {
|
||||
const updatedEntryEntries = [
|
||||
...itemOfInterest.entries.slice(0, entryIndex),
|
||||
...itemOfInterest.entries.slice(entryIndex + 1),
|
||||
];
|
||||
|
||||
if (updatedEntryEntries.length === 0) {
|
||||
return {
|
||||
...exceptionItem,
|
||||
entries: [
|
||||
...exceptionItem.entries.slice(0, nestedParentIndex),
|
||||
...exceptionItem.entries.slice(nestedParentIndex + 1),
|
||||
],
|
||||
};
|
||||
} else {
|
||||
const { field } = itemOfInterest;
|
||||
const updatedItemOfInterest: EntryNested | EmptyNestedEntry = {
|
||||
entries: updatedEntryEntries,
|
||||
field,
|
||||
id: itemOfInterest.id ?? `${entryIndex}`,
|
||||
type: OperatorTypeEnum.NESTED,
|
||||
};
|
||||
|
||||
return {
|
||||
...exceptionItem,
|
||||
entries: [
|
||||
...exceptionItem.entries.slice(0, nestedParentIndex),
|
||||
updatedItemOfInterest,
|
||||
...exceptionItem.entries.slice(nestedParentIndex + 1),
|
||||
],
|
||||
};
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
...exceptionItem,
|
||||
entries: [
|
||||
...exceptionItem.entries.slice(0, entryIndex),
|
||||
...exceptionItem.entries.slice(entryIndex + 1),
|
||||
],
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns filtered index patterns based on the field - if a user selects to
|
||||
|
@ -387,3 +513,155 @@ export const getOperatorOptions = (
|
|||
: EXCEPTION_OPERATORS_SANS_LISTS;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Fields of type 'text' do not generate autocomplete values, we want
|
||||
* to find it's corresponding keyword type (if available) which does
|
||||
* generate autocomplete values
|
||||
*
|
||||
* @param fields IFieldType fields
|
||||
* @param selectedField the field name that was selected
|
||||
* @param isTextType we only want a corresponding keyword field if
|
||||
* the selected field is of type 'text'
|
||||
*
|
||||
*/
|
||||
export const getCorrespondingKeywordField = ({
|
||||
fields,
|
||||
selectedField,
|
||||
}: {
|
||||
fields: IFieldType[];
|
||||
selectedField: string | undefined;
|
||||
}): IFieldType | undefined => {
|
||||
const selectedFieldBits =
|
||||
selectedField != null && selectedField !== '' ? selectedField.split('.') : [];
|
||||
const selectedFieldIsTextType = selectedFieldBits.slice(-1)[0] === 'text';
|
||||
|
||||
if (selectedFieldIsTextType && selectedFieldBits.length > 0) {
|
||||
const keywordField = selectedFieldBits.slice(0, selectedFieldBits.length - 1).join('.');
|
||||
const [foundKeywordField] = fields.filter(
|
||||
({ name }) => keywordField !== '' && keywordField === name
|
||||
);
|
||||
return foundKeywordField;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* Formats the entry into one that is easily usable for the UI, most of the
|
||||
* complexity was introduced with nested fields
|
||||
*
|
||||
* @param patterns IIndexPattern containing available fields on rule index
|
||||
* @param item exception item entry
|
||||
* @param itemIndex entry index
|
||||
* @param parent nested entries hold copy of their parent for use in various logic
|
||||
* @param parentIndex corresponds to the entry index, this might seem obvious, but
|
||||
* was added to ensure that nested items could be identified with their parent entry
|
||||
*/
|
||||
export const getFormattedBuilderEntry = (
|
||||
indexPattern: IIndexPattern,
|
||||
item: BuilderEntry,
|
||||
itemIndex: number,
|
||||
parent: EntryNested | undefined,
|
||||
parentIndex: number | undefined
|
||||
): FormattedBuilderEntry => {
|
||||
const { fields } = indexPattern;
|
||||
const field = parent != null ? `${parent.field}.${item.field}` : item.field;
|
||||
const [foundField] = fields.filter(({ name }) => field != null && field === name);
|
||||
const correspondingKeywordField = getCorrespondingKeywordField({
|
||||
fields,
|
||||
selectedField: field,
|
||||
});
|
||||
|
||||
if (parent != null && parentIndex != null) {
|
||||
return {
|
||||
correspondingKeywordField,
|
||||
entryIndex: itemIndex,
|
||||
field:
|
||||
foundField != null
|
||||
? { ...foundField, name: foundField.name.split('.').slice(-1)[0] }
|
||||
: foundField,
|
||||
id: item.id ?? `${itemIndex}`,
|
||||
nested: 'child',
|
||||
operator: getExceptionOperatorSelect(item),
|
||||
parent: { parent, parentIndex },
|
||||
value: getEntryValue(item),
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
correspondingKeywordField,
|
||||
entryIndex: itemIndex,
|
||||
field: foundField,
|
||||
id: item.id ?? `${itemIndex}`,
|
||||
nested: undefined,
|
||||
operator: getExceptionOperatorSelect(item),
|
||||
parent: undefined,
|
||||
value: getEntryValue(item),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Formats the entries to be easily usable for the UI, most of the
|
||||
* complexity was introduced with nested fields
|
||||
*
|
||||
* @param patterns IIndexPattern containing available fields on rule index
|
||||
* @param entries exception item entries
|
||||
* @param addNested boolean noting whether or not UI is currently
|
||||
* set to add a nested field
|
||||
* @param parent nested entries hold copy of their parent for use in various logic
|
||||
* @param parentIndex corresponds to the entry index, this might seem obvious, but
|
||||
* was added to ensure that nested items could be identified with their parent entry
|
||||
*/
|
||||
export const getFormattedBuilderEntries = (
|
||||
indexPattern: IIndexPattern,
|
||||
entries: BuilderEntry[],
|
||||
parent?: EntryNested,
|
||||
parentIndex?: number
|
||||
): FormattedBuilderEntry[] => {
|
||||
return entries.reduce<FormattedBuilderEntry[]>((acc, item, index) => {
|
||||
const isNewNestedEntry = item.type === 'nested' && item.entries.length === 0;
|
||||
if (item.type !== 'nested' && !isNewNestedEntry) {
|
||||
const newItemEntry: FormattedBuilderEntry = getFormattedBuilderEntry(
|
||||
indexPattern,
|
||||
item,
|
||||
index,
|
||||
parent,
|
||||
parentIndex
|
||||
);
|
||||
return [...acc, newItemEntry];
|
||||
} else {
|
||||
const parentEntry: FormattedBuilderEntry = {
|
||||
correspondingKeywordField: undefined,
|
||||
entryIndex: index,
|
||||
field: isNewNestedEntry
|
||||
? undefined
|
||||
: {
|
||||
aggregatable: false,
|
||||
esTypes: ['nested'],
|
||||
name: item.field ?? '',
|
||||
searchable: false,
|
||||
type: 'string',
|
||||
},
|
||||
id: item.id ?? `${index}`,
|
||||
nested: 'parent',
|
||||
operator: isOperator,
|
||||
parent: undefined,
|
||||
value: undefined,
|
||||
};
|
||||
|
||||
// User has selected to add a nested field, but not yet selected the field
|
||||
if (isNewNestedEntry) {
|
||||
return [...acc, parentEntry];
|
||||
}
|
||||
|
||||
if (isEntryNested(item)) {
|
||||
const nestedItems = getFormattedBuilderEntries(indexPattern, item.entries, item, index);
|
||||
|
||||
return [...acc, parentEntry, ...nestedItems];
|
||||
}
|
||||
|
||||
return [...acc];
|
||||
}
|
||||
}, []);
|
||||
};
|
||||
|
|
|
@ -39,3 +39,6 @@ export {
|
|||
UseExceptionListsSuccess,
|
||||
} from './exceptions/types';
|
||||
export { BuilderEntryItem } from './exceptions/components/builder/entry_renderer';
|
||||
export { BuilderAndBadgeComponent } from './exceptions/components/builder/and_badge';
|
||||
export { BuilderEntryDeleteButtonComponent } from './exceptions/components/builder/entry_delete_button';
|
||||
export { BuilderExceptionListItemComponent } from './exceptions/components/builder/exception_item_renderer';
|
||||
|
|
|
@ -115,7 +115,7 @@ export const AddExceptionModal = memo(function AddExceptionModal({
|
|||
onRuleChange,
|
||||
alertStatus,
|
||||
}: AddExceptionModalProps) {
|
||||
const { http } = useKibana().services;
|
||||
const { http, data } = useKibana().services;
|
||||
const [errorsExist, setErrorExists] = useState(false);
|
||||
const [comment, setComment] = useState('');
|
||||
const { rule: maybeRule, loading: isRuleLoading } = useRuleAsync(ruleId);
|
||||
|
@ -394,6 +394,8 @@ export const AddExceptionModal = memo(function AddExceptionModal({
|
|||
<EuiText>{i18n.EXCEPTION_BUILDER_INFO}</EuiText>
|
||||
<EuiSpacer />
|
||||
<ExceptionBuilderComponent
|
||||
httpService={http}
|
||||
autocompleteService={data.autocomplete}
|
||||
exceptionListItems={initialExceptionItems}
|
||||
listType={exceptionListType}
|
||||
listId={ruleExceptionList.list_id}
|
||||
|
|
|
@ -5,54 +5,15 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import {
|
||||
fields,
|
||||
getField,
|
||||
} from '../../../../../../../../src/plugins/data/common/index_patterns/fields/fields.mocks';
|
||||
import { getEntryNestedMock } from '../../../../../../lists/common/schemas/types/entry_nested.mock';
|
||||
import { getEntryMatchMock } from '../../../../../../lists/common/schemas/types/entry_match.mock';
|
||||
import { getEntryMatchAnyMock } from '../../../../../../lists/common/schemas/types/entry_match_any.mock';
|
||||
import { getEntryExistsMock } from '../../../../../../lists/common/schemas/types/entry_exists.mock';
|
||||
import { getExceptionListItemSchemaMock } from '../../../../../../lists/common/schemas/response/exception_list_item_schema.mock';
|
||||
import { isOneOfOperator, isOperator } from '../../autocomplete/operators';
|
||||
import { BuilderEntry, ExceptionsBuilderExceptionItem, FormattedBuilderEntry } from '../types';
|
||||
import { fields } from '../../../../../../../../src/plugins/data/common/index_patterns/fields/fields.mocks';
|
||||
import { IFieldType, IIndexPattern } from '../../../../../../../../src/plugins/data/common';
|
||||
import { EntryNested, OperatorTypeEnum, OperatorEnum } from '../../../../shared_imports';
|
||||
|
||||
import {
|
||||
filterIndexPatterns,
|
||||
getFormattedBuilderEntries,
|
||||
getFormattedBuilderEntry,
|
||||
getUpdatedEntriesOnDelete,
|
||||
isEntryNested,
|
||||
getCorrespondingKeywordField,
|
||||
} from './helpers';
|
||||
import { ENTRIES_WITH_IDS } from '../../../../../../lists/common/constants.mock';
|
||||
import { filterIndexPatterns } from './helpers';
|
||||
|
||||
jest.mock('uuid', () => ({
|
||||
v4: jest.fn().mockReturnValue('123'),
|
||||
}));
|
||||
|
||||
const getEntryNestedWithIdMock = () => ({
|
||||
id: '123',
|
||||
...getEntryNestedMock(),
|
||||
});
|
||||
|
||||
const getEntryExistsWithIdMock = () => ({
|
||||
id: '123',
|
||||
...getEntryExistsMock(),
|
||||
});
|
||||
|
||||
const getEntryMatchWithIdMock = () => ({
|
||||
id: '123',
|
||||
...getEntryMatchMock(),
|
||||
});
|
||||
|
||||
const getEntryMatchAnyWithIdMock = () => ({
|
||||
id: '123',
|
||||
...getEntryMatchAnyMock(),
|
||||
});
|
||||
|
||||
const getMockIndexPattern = (): IIndexPattern => ({
|
||||
id: '1234',
|
||||
title: 'logstash-*',
|
||||
|
@ -105,421 +66,4 @@ describe('Exception builder helpers', () => {
|
|||
expect(output).toEqual({ ...getMockIndexPattern(), fields: [...mockEndpointFields] });
|
||||
});
|
||||
});
|
||||
describe('#getCorrespondingKeywordField', () => {
|
||||
test('it returns matching keyword field if "selectedFieldIsTextType" is true and keyword field exists', () => {
|
||||
const output = getCorrespondingKeywordField({
|
||||
fields,
|
||||
selectedField: 'machine.os.raw.text',
|
||||
});
|
||||
|
||||
expect(output).toEqual(getField('machine.os.raw'));
|
||||
});
|
||||
|
||||
test('it returns undefined if "selectedFieldIsTextType" is false', () => {
|
||||
const output = getCorrespondingKeywordField({
|
||||
fields,
|
||||
selectedField: 'machine.os.raw',
|
||||
});
|
||||
|
||||
expect(output).toEqual(undefined);
|
||||
});
|
||||
|
||||
test('it returns undefined if "selectedField" is empty string', () => {
|
||||
const output = getCorrespondingKeywordField({
|
||||
fields,
|
||||
selectedField: '',
|
||||
});
|
||||
|
||||
expect(output).toEqual(undefined);
|
||||
});
|
||||
|
||||
test('it returns undefined if "selectedField" is undefined', () => {
|
||||
const output = getCorrespondingKeywordField({
|
||||
fields,
|
||||
selectedField: undefined,
|
||||
});
|
||||
|
||||
expect(output).toEqual(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#getFormattedBuilderEntry', () => {
|
||||
test('it returns entry with a value for "correspondingKeywordField" when "item.field" is of type "text" and matching keyword field exists', () => {
|
||||
const payloadIndexPattern: IIndexPattern = {
|
||||
...getMockIndexPattern(),
|
||||
fields: [
|
||||
...fields,
|
||||
{
|
||||
name: 'machine.os.raw.text',
|
||||
type: 'string',
|
||||
esTypes: ['text'],
|
||||
count: 0,
|
||||
scripted: false,
|
||||
searchable: false,
|
||||
aggregatable: false,
|
||||
readFromDocValues: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
const payloadItem: BuilderEntry = {
|
||||
...getEntryMatchWithIdMock(),
|
||||
field: 'machine.os.raw.text',
|
||||
value: 'some os',
|
||||
};
|
||||
const output = getFormattedBuilderEntry(
|
||||
payloadIndexPattern,
|
||||
payloadItem,
|
||||
0,
|
||||
undefined,
|
||||
undefined
|
||||
);
|
||||
const expected: FormattedBuilderEntry = {
|
||||
id: '123',
|
||||
entryIndex: 0,
|
||||
field: {
|
||||
name: 'machine.os.raw.text',
|
||||
type: 'string',
|
||||
esTypes: ['text'],
|
||||
count: 0,
|
||||
scripted: false,
|
||||
searchable: false,
|
||||
aggregatable: false,
|
||||
readFromDocValues: true,
|
||||
},
|
||||
nested: undefined,
|
||||
operator: isOperator,
|
||||
parent: undefined,
|
||||
value: 'some os',
|
||||
correspondingKeywordField: getField('machine.os.raw'),
|
||||
};
|
||||
expect(output).toEqual(expected);
|
||||
});
|
||||
|
||||
test('it returns "FormattedBuilderEntry" with value "nested" of "child" when "parent" and "parentIndex" are defined', () => {
|
||||
const payloadIndexPattern: IIndexPattern = getMockIndexPattern();
|
||||
const payloadItem: BuilderEntry = { ...getEntryMatchWithIdMock(), field: 'child' };
|
||||
const payloadParent: EntryNested = {
|
||||
...getEntryNestedWithIdMock(),
|
||||
field: 'nestedField',
|
||||
entries: [{ ...getEntryMatchWithIdMock(), field: 'child' }],
|
||||
};
|
||||
const output = getFormattedBuilderEntry(
|
||||
payloadIndexPattern,
|
||||
payloadItem,
|
||||
0,
|
||||
payloadParent,
|
||||
1
|
||||
);
|
||||
const expected: FormattedBuilderEntry = {
|
||||
id: '123',
|
||||
entryIndex: 0,
|
||||
field: {
|
||||
aggregatable: false,
|
||||
count: 0,
|
||||
esTypes: ['text'],
|
||||
name: 'child',
|
||||
readFromDocValues: false,
|
||||
scripted: false,
|
||||
searchable: true,
|
||||
subType: {
|
||||
nested: {
|
||||
path: 'nestedField',
|
||||
},
|
||||
},
|
||||
type: 'string',
|
||||
},
|
||||
nested: 'child',
|
||||
operator: isOperator,
|
||||
parent: {
|
||||
parent: {
|
||||
id: '123',
|
||||
entries: [{ ...payloadItem }],
|
||||
field: 'nestedField',
|
||||
type: OperatorTypeEnum.NESTED,
|
||||
},
|
||||
parentIndex: 1,
|
||||
},
|
||||
value: 'some host name',
|
||||
correspondingKeywordField: undefined,
|
||||
};
|
||||
expect(output).toEqual(expected);
|
||||
});
|
||||
|
||||
test('it returns non nested "FormattedBuilderEntry" when "parent" and "parentIndex" are not defined', () => {
|
||||
const payloadIndexPattern: IIndexPattern = getMockIndexPattern();
|
||||
const payloadItem: BuilderEntry = {
|
||||
...getEntryMatchWithIdMock(),
|
||||
field: 'ip',
|
||||
value: 'some ip',
|
||||
};
|
||||
const output = getFormattedBuilderEntry(
|
||||
payloadIndexPattern,
|
||||
payloadItem,
|
||||
0,
|
||||
undefined,
|
||||
undefined
|
||||
);
|
||||
const expected: FormattedBuilderEntry = {
|
||||
id: '123',
|
||||
entryIndex: 0,
|
||||
field: {
|
||||
aggregatable: true,
|
||||
count: 0,
|
||||
esTypes: ['ip'],
|
||||
name: 'ip',
|
||||
readFromDocValues: true,
|
||||
scripted: false,
|
||||
searchable: true,
|
||||
type: 'ip',
|
||||
},
|
||||
nested: undefined,
|
||||
operator: isOperator,
|
||||
parent: undefined,
|
||||
value: 'some ip',
|
||||
correspondingKeywordField: undefined,
|
||||
};
|
||||
expect(output).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#isEntryNested', () => {
|
||||
test('it returns "false" if payload is not of type EntryNested', () => {
|
||||
const payload: BuilderEntry = getEntryMatchWithIdMock();
|
||||
const output = isEntryNested(payload);
|
||||
const expected = false;
|
||||
expect(output).toEqual(expected);
|
||||
});
|
||||
|
||||
test('it returns "true if payload is of type EntryNested', () => {
|
||||
const payload: EntryNested = getEntryNestedWithIdMock();
|
||||
const output = isEntryNested(payload);
|
||||
const expected = true;
|
||||
expect(output).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#getFormattedBuilderEntries', () => {
|
||||
test('it returns formatted entry with field undefined if it unable to find a matching index pattern field', () => {
|
||||
const payloadIndexPattern: IIndexPattern = getMockIndexPattern();
|
||||
const payloadItems: BuilderEntry[] = [getEntryMatchWithIdMock()];
|
||||
const output = getFormattedBuilderEntries(payloadIndexPattern, payloadItems);
|
||||
const expected: FormattedBuilderEntry[] = [
|
||||
{
|
||||
id: '123',
|
||||
entryIndex: 0,
|
||||
field: undefined,
|
||||
nested: undefined,
|
||||
operator: isOperator,
|
||||
parent: undefined,
|
||||
value: 'some host name',
|
||||
correspondingKeywordField: undefined,
|
||||
},
|
||||
];
|
||||
expect(output).toEqual(expected);
|
||||
});
|
||||
|
||||
test('it returns formatted entries when no nested entries exist', () => {
|
||||
const payloadIndexPattern: IIndexPattern = getMockIndexPattern();
|
||||
const payloadItems: BuilderEntry[] = [
|
||||
{ ...getEntryMatchWithIdMock(), field: 'ip', value: 'some ip' },
|
||||
{ ...getEntryMatchAnyWithIdMock(), field: 'extension', value: ['some extension'] },
|
||||
];
|
||||
const output = getFormattedBuilderEntries(payloadIndexPattern, payloadItems);
|
||||
const expected: FormattedBuilderEntry[] = [
|
||||
{
|
||||
id: '123',
|
||||
entryIndex: 0,
|
||||
field: {
|
||||
aggregatable: true,
|
||||
count: 0,
|
||||
esTypes: ['ip'],
|
||||
name: 'ip',
|
||||
readFromDocValues: true,
|
||||
scripted: false,
|
||||
searchable: true,
|
||||
type: 'ip',
|
||||
},
|
||||
nested: undefined,
|
||||
operator: isOperator,
|
||||
parent: undefined,
|
||||
value: 'some ip',
|
||||
correspondingKeywordField: undefined,
|
||||
},
|
||||
{
|
||||
id: '123',
|
||||
entryIndex: 1,
|
||||
field: {
|
||||
aggregatable: true,
|
||||
count: 0,
|
||||
esTypes: ['keyword'],
|
||||
name: 'extension',
|
||||
readFromDocValues: true,
|
||||
scripted: false,
|
||||
searchable: true,
|
||||
type: 'string',
|
||||
},
|
||||
nested: undefined,
|
||||
operator: isOneOfOperator,
|
||||
parent: undefined,
|
||||
value: ['some extension'],
|
||||
correspondingKeywordField: undefined,
|
||||
},
|
||||
];
|
||||
expect(output).toEqual(expected);
|
||||
});
|
||||
|
||||
test('it returns formatted entries when nested entries exist', () => {
|
||||
const payloadIndexPattern: IIndexPattern = getMockIndexPattern();
|
||||
const payloadParent: EntryNested = {
|
||||
...getEntryNestedWithIdMock(),
|
||||
field: 'nestedField',
|
||||
entries: [{ ...getEntryMatchWithIdMock(), field: 'child' }],
|
||||
};
|
||||
const payloadItems: BuilderEntry[] = [
|
||||
{ ...getEntryMatchWithIdMock(), field: 'ip', value: 'some ip' },
|
||||
{ ...payloadParent },
|
||||
];
|
||||
|
||||
const output = getFormattedBuilderEntries(payloadIndexPattern, payloadItems);
|
||||
const expected: FormattedBuilderEntry[] = [
|
||||
{
|
||||
id: '123',
|
||||
entryIndex: 0,
|
||||
field: {
|
||||
aggregatable: true,
|
||||
count: 0,
|
||||
esTypes: ['ip'],
|
||||
name: 'ip',
|
||||
readFromDocValues: true,
|
||||
scripted: false,
|
||||
searchable: true,
|
||||
type: 'ip',
|
||||
},
|
||||
nested: undefined,
|
||||
operator: isOperator,
|
||||
parent: undefined,
|
||||
value: 'some ip',
|
||||
correspondingKeywordField: undefined,
|
||||
},
|
||||
{
|
||||
id: '123',
|
||||
entryIndex: 1,
|
||||
field: {
|
||||
aggregatable: false,
|
||||
esTypes: ['nested'],
|
||||
name: 'nestedField',
|
||||
searchable: false,
|
||||
type: 'string',
|
||||
},
|
||||
nested: 'parent',
|
||||
operator: isOperator,
|
||||
parent: undefined,
|
||||
value: undefined,
|
||||
correspondingKeywordField: undefined,
|
||||
},
|
||||
{
|
||||
id: '123',
|
||||
entryIndex: 0,
|
||||
field: {
|
||||
aggregatable: false,
|
||||
count: 0,
|
||||
esTypes: ['text'],
|
||||
name: 'child',
|
||||
readFromDocValues: false,
|
||||
scripted: false,
|
||||
searchable: true,
|
||||
subType: {
|
||||
nested: {
|
||||
path: 'nestedField',
|
||||
},
|
||||
},
|
||||
type: 'string',
|
||||
},
|
||||
nested: 'child',
|
||||
operator: isOperator,
|
||||
parent: {
|
||||
parent: {
|
||||
id: '123',
|
||||
entries: [
|
||||
{
|
||||
id: '123',
|
||||
field: 'child',
|
||||
operator: OperatorEnum.INCLUDED,
|
||||
type: OperatorTypeEnum.MATCH,
|
||||
value: 'some host name',
|
||||
},
|
||||
],
|
||||
field: 'nestedField',
|
||||
type: OperatorTypeEnum.NESTED,
|
||||
},
|
||||
parentIndex: 1,
|
||||
},
|
||||
value: 'some host name',
|
||||
correspondingKeywordField: undefined,
|
||||
},
|
||||
];
|
||||
expect(output).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#getUpdatedEntriesOnDelete', () => {
|
||||
test('it removes entry corresponding to "entryIndex"', () => {
|
||||
const payloadItem: ExceptionsBuilderExceptionItem = {
|
||||
...getExceptionListItemSchemaMock(),
|
||||
entries: ENTRIES_WITH_IDS,
|
||||
};
|
||||
const output = getUpdatedEntriesOnDelete(payloadItem, 0, null);
|
||||
const expected: ExceptionsBuilderExceptionItem = {
|
||||
...getExceptionListItemSchemaMock(),
|
||||
entries: [
|
||||
{
|
||||
id: '123',
|
||||
field: 'some.not.nested.field',
|
||||
operator: OperatorEnum.INCLUDED,
|
||||
type: OperatorTypeEnum.MATCH,
|
||||
value: 'some value',
|
||||
},
|
||||
],
|
||||
};
|
||||
expect(output).toEqual(expected);
|
||||
});
|
||||
|
||||
test('it removes nested entry of "entryIndex" with corresponding parent index', () => {
|
||||
const payloadItem: ExceptionsBuilderExceptionItem = {
|
||||
...getExceptionListItemSchemaMock(),
|
||||
entries: [
|
||||
{
|
||||
...getEntryNestedWithIdMock(),
|
||||
entries: [{ ...getEntryExistsWithIdMock() }, { ...getEntryMatchAnyWithIdMock() }],
|
||||
},
|
||||
],
|
||||
};
|
||||
const output = getUpdatedEntriesOnDelete(payloadItem, 0, 0);
|
||||
const expected: ExceptionsBuilderExceptionItem = {
|
||||
...getExceptionListItemSchemaMock(),
|
||||
entries: [
|
||||
{ ...getEntryNestedWithIdMock(), entries: [{ ...getEntryMatchAnyWithIdMock() }] },
|
||||
],
|
||||
};
|
||||
expect(output).toEqual(expected);
|
||||
});
|
||||
|
||||
test('it removes entire nested entry if after deleting specified nested entry, there are no more nested entries left', () => {
|
||||
const payloadItem: ExceptionsBuilderExceptionItem = {
|
||||
...getExceptionListItemSchemaMock(),
|
||||
entries: [
|
||||
{
|
||||
...getEntryNestedWithIdMock(),
|
||||
entries: [{ ...getEntryExistsWithIdMock() }],
|
||||
},
|
||||
],
|
||||
};
|
||||
const output = getUpdatedEntriesOnDelete(payloadItem, 0, 0);
|
||||
const expected: ExceptionsBuilderExceptionItem = {
|
||||
...getExceptionListItemSchemaMock(),
|
||||
entries: [],
|
||||
};
|
||||
expect(output).toEqual(expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -7,22 +7,9 @@
|
|||
|
||||
import uuid from 'uuid';
|
||||
|
||||
import { IIndexPattern, IFieldType } from '../../../../../../../../src/plugins/data/common';
|
||||
import {
|
||||
OperatorTypeEnum,
|
||||
EntryNested,
|
||||
ExceptionListType,
|
||||
OperatorEnum,
|
||||
} from '../../../../lists_plugin_deps';
|
||||
import { isOperator } from '../../autocomplete/operators';
|
||||
import {
|
||||
FormattedBuilderEntry,
|
||||
ExceptionsBuilderExceptionItem,
|
||||
EmptyEntry,
|
||||
EmptyNestedEntry,
|
||||
BuilderEntry,
|
||||
} from '../types';
|
||||
import { getEntryValue, getExceptionOperatorSelect } from '../helpers';
|
||||
import { IIndexPattern } from '../../../../../../../../src/plugins/data/common';
|
||||
import { OperatorTypeEnum, ExceptionListType, OperatorEnum } from '../../../../lists_plugin_deps';
|
||||
import { ExceptionsBuilderExceptionItem, EmptyEntry, EmptyNestedEntry } from '../types';
|
||||
import exceptionableFields from '../exceptionable_fields.json';
|
||||
|
||||
export const filterIndexPatterns = (
|
||||
|
@ -37,222 +24,6 @@ export const filterIndexPatterns = (
|
|||
: patterns;
|
||||
};
|
||||
|
||||
/**
|
||||
* Fields of type 'text' do not generate autocomplete values, we want
|
||||
* to find it's corresponding keyword type (if available) which does
|
||||
* generate autocomplete values
|
||||
*
|
||||
* @param fields IFieldType fields
|
||||
* @param selectedField the field name that was selected
|
||||
* @param isTextType we only want a corresponding keyword field if
|
||||
* the selected field is of type 'text'
|
||||
*
|
||||
*/
|
||||
export const getCorrespondingKeywordField = ({
|
||||
fields,
|
||||
selectedField,
|
||||
}: {
|
||||
fields: IFieldType[];
|
||||
selectedField: string | undefined;
|
||||
}): IFieldType | undefined => {
|
||||
const selectedFieldBits =
|
||||
selectedField != null && selectedField !== '' ? selectedField.split('.') : [];
|
||||
const selectedFieldIsTextType = selectedFieldBits.slice(-1)[0] === 'text';
|
||||
|
||||
if (selectedFieldIsTextType && selectedFieldBits.length > 0) {
|
||||
const keywordField = selectedFieldBits.slice(0, selectedFieldBits.length - 1).join('.');
|
||||
const [foundKeywordField] = fields.filter(
|
||||
({ name }) => keywordField !== '' && keywordField === name
|
||||
);
|
||||
return foundKeywordField;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* Formats the entry into one that is easily usable for the UI, most of the
|
||||
* complexity was introduced with nested fields
|
||||
*
|
||||
* @param patterns IIndexPattern containing available fields on rule index
|
||||
* @param item exception item entry
|
||||
* @param itemIndex entry index
|
||||
* @param parent nested entries hold copy of their parent for use in various logic
|
||||
* @param parentIndex corresponds to the entry index, this might seem obvious, but
|
||||
* was added to ensure that nested items could be identified with their parent entry
|
||||
*/
|
||||
export const getFormattedBuilderEntry = (
|
||||
indexPattern: IIndexPattern,
|
||||
item: BuilderEntry,
|
||||
itemIndex: number,
|
||||
parent: EntryNested | undefined,
|
||||
parentIndex: number | undefined
|
||||
): FormattedBuilderEntry => {
|
||||
const { fields } = indexPattern;
|
||||
const field = parent != null ? `${parent.field}.${item.field}` : item.field;
|
||||
const [foundField] = fields.filter(({ name }) => field != null && field === name);
|
||||
const correspondingKeywordField = getCorrespondingKeywordField({
|
||||
fields,
|
||||
selectedField: field,
|
||||
});
|
||||
|
||||
if (parent != null && parentIndex != null) {
|
||||
return {
|
||||
field:
|
||||
foundField != null
|
||||
? { ...foundField, name: foundField.name.split('.').slice(-1)[0] }
|
||||
: foundField,
|
||||
correspondingKeywordField,
|
||||
id: item.id ?? `${itemIndex}`,
|
||||
operator: getExceptionOperatorSelect(item),
|
||||
value: getEntryValue(item),
|
||||
nested: 'child',
|
||||
parent: { parent, parentIndex },
|
||||
entryIndex: itemIndex,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
field: foundField,
|
||||
id: item.id ?? `${itemIndex}`,
|
||||
correspondingKeywordField,
|
||||
operator: getExceptionOperatorSelect(item),
|
||||
value: getEntryValue(item),
|
||||
nested: undefined,
|
||||
parent: undefined,
|
||||
entryIndex: itemIndex,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export const isEntryNested = (item: BuilderEntry): item is EntryNested => {
|
||||
return (item as EntryNested).entries != null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Formats the entries to be easily usable for the UI, most of the
|
||||
* complexity was introduced with nested fields
|
||||
*
|
||||
* @param patterns IIndexPattern containing available fields on rule index
|
||||
* @param entries exception item entries
|
||||
* @param addNested boolean noting whether or not UI is currently
|
||||
* set to add a nested field
|
||||
* @param parent nested entries hold copy of their parent for use in various logic
|
||||
* @param parentIndex corresponds to the entry index, this might seem obvious, but
|
||||
* was added to ensure that nested items could be identified with their parent entry
|
||||
*/
|
||||
export const getFormattedBuilderEntries = (
|
||||
indexPattern: IIndexPattern,
|
||||
entries: BuilderEntry[],
|
||||
parent?: EntryNested,
|
||||
parentIndex?: number
|
||||
): FormattedBuilderEntry[] => {
|
||||
return entries.reduce<FormattedBuilderEntry[]>((acc, item, index) => {
|
||||
const isNewNestedEntry = item.type === 'nested' && item.entries.length === 0;
|
||||
if (item.type !== 'nested' && !isNewNestedEntry) {
|
||||
const newItemEntry: FormattedBuilderEntry = getFormattedBuilderEntry(
|
||||
indexPattern,
|
||||
item,
|
||||
index,
|
||||
parent,
|
||||
parentIndex
|
||||
);
|
||||
return [...acc, newItemEntry];
|
||||
} else {
|
||||
const parentEntry: FormattedBuilderEntry = {
|
||||
operator: isOperator,
|
||||
id: item.id ?? `${index}`,
|
||||
nested: 'parent',
|
||||
field: isNewNestedEntry
|
||||
? undefined
|
||||
: {
|
||||
name: item.field ?? '',
|
||||
aggregatable: false,
|
||||
searchable: false,
|
||||
type: 'string',
|
||||
esTypes: ['nested'],
|
||||
},
|
||||
value: undefined,
|
||||
entryIndex: index,
|
||||
parent: undefined,
|
||||
correspondingKeywordField: undefined,
|
||||
};
|
||||
|
||||
// User has selected to add a nested field, but not yet selected the field
|
||||
if (isNewNestedEntry) {
|
||||
return [...acc, parentEntry];
|
||||
}
|
||||
|
||||
if (isEntryNested(item)) {
|
||||
const nestedItems = getFormattedBuilderEntries(indexPattern, item.entries, item, index);
|
||||
|
||||
return [...acc, parentEntry, ...nestedItems];
|
||||
}
|
||||
|
||||
return [...acc];
|
||||
}
|
||||
}, []);
|
||||
};
|
||||
|
||||
/**
|
||||
* Determines whether an entire entry, exception item, or entry within a nested
|
||||
* entry needs to be removed
|
||||
*
|
||||
* @param exceptionItem
|
||||
* @param entryIndex index of given entry, for nested entries, this will correspond
|
||||
* to their parent index
|
||||
* @param nestedEntryIndex index of nested entry
|
||||
*
|
||||
*/
|
||||
export const getUpdatedEntriesOnDelete = (
|
||||
exceptionItem: ExceptionsBuilderExceptionItem,
|
||||
entryIndex: number,
|
||||
nestedParentIndex: number | null
|
||||
): ExceptionsBuilderExceptionItem => {
|
||||
const itemOfInterest: BuilderEntry = exceptionItem.entries[nestedParentIndex ?? entryIndex];
|
||||
|
||||
if (nestedParentIndex != null && itemOfInterest.type === OperatorTypeEnum.NESTED) {
|
||||
const updatedEntryEntries = [
|
||||
...itemOfInterest.entries.slice(0, entryIndex),
|
||||
...itemOfInterest.entries.slice(entryIndex + 1),
|
||||
];
|
||||
|
||||
if (updatedEntryEntries.length === 0) {
|
||||
return {
|
||||
...exceptionItem,
|
||||
entries: [
|
||||
...exceptionItem.entries.slice(0, nestedParentIndex),
|
||||
...exceptionItem.entries.slice(nestedParentIndex + 1),
|
||||
],
|
||||
};
|
||||
} else {
|
||||
const { field } = itemOfInterest;
|
||||
const updatedItemOfInterest: EntryNested | EmptyNestedEntry = {
|
||||
field,
|
||||
id: itemOfInterest.id ?? `${entryIndex}`,
|
||||
type: OperatorTypeEnum.NESTED,
|
||||
entries: updatedEntryEntries,
|
||||
};
|
||||
|
||||
return {
|
||||
...exceptionItem,
|
||||
entries: [
|
||||
...exceptionItem.entries.slice(0, nestedParentIndex),
|
||||
updatedItemOfInterest,
|
||||
...exceptionItem.entries.slice(nestedParentIndex + 1),
|
||||
],
|
||||
};
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
...exceptionItem,
|
||||
entries: [
|
||||
...exceptionItem.entries.slice(0, entryIndex),
|
||||
...exceptionItem.entries.slice(entryIndex + 1),
|
||||
],
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export const getDefaultEmptyEntry = (): EmptyEntry => ({
|
||||
id: uuid.v4(),
|
||||
field: '',
|
||||
|
|
|
@ -17,37 +17,26 @@ import {
|
|||
import { getExceptionListItemSchemaMock } from '../../../../../../lists/common/schemas/response/exception_list_item_schema.mock';
|
||||
import { getEntryMatchAnyMock } from '../../../../../../lists/common/schemas/types/entry_match_any.mock';
|
||||
|
||||
import { useKibana } from '../../../../common/lib/kibana';
|
||||
import { getEmptyValue } from '../../empty_value';
|
||||
|
||||
import { ExceptionBuilderComponent } from './';
|
||||
import { getMockTheme } from '../../../lib/kibana/kibana_react.mock';
|
||||
import { coreMock } from 'src/core/public/mocks';
|
||||
import { dataPluginMock } from 'src/plugins/data/public/mocks';
|
||||
|
||||
const mockTheme = getMockTheme({
|
||||
eui: {
|
||||
euiColorLightShade: '#ece',
|
||||
},
|
||||
});
|
||||
|
||||
jest.mock('../../../../common/lib/kibana');
|
||||
const mockKibanaHttpService = coreMock.createStart().http;
|
||||
const { autocomplete: autocompleteStartMock } = dataPluginMock.createStartContract();
|
||||
|
||||
describe('ExceptionBuilderComponent', () => {
|
||||
let wrapper: ReactWrapper;
|
||||
|
||||
const getValueSuggestionsMock = jest.fn().mockResolvedValue(['value 1', 'value 2']);
|
||||
|
||||
beforeEach(() => {
|
||||
(useKibana as jest.Mock).mockReturnValue({
|
||||
services: {
|
||||
data: {
|
||||
autocomplete: {
|
||||
getValueSuggestions: getValueSuggestionsMock,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
getValueSuggestionsMock.mockClear();
|
||||
jest.clearAllMocks();
|
||||
|
@ -58,6 +47,8 @@ describe('ExceptionBuilderComponent', () => {
|
|||
wrapper = mount(
|
||||
<ThemeProvider theme={mockTheme}>
|
||||
<ExceptionBuilderComponent
|
||||
httpService={mockKibanaHttpService}
|
||||
autocompleteService={autocompleteStartMock}
|
||||
exceptionListItems={[]}
|
||||
listType="detection"
|
||||
listId="list_id"
|
||||
|
@ -94,6 +85,8 @@ describe('ExceptionBuilderComponent', () => {
|
|||
wrapper = mount(
|
||||
<ThemeProvider theme={mockTheme}>
|
||||
<ExceptionBuilderComponent
|
||||
httpService={mockKibanaHttpService}
|
||||
autocompleteService={autocompleteStartMock}
|
||||
exceptionListItems={[
|
||||
{
|
||||
...getExceptionListItemSchemaMock(),
|
||||
|
@ -136,6 +129,8 @@ describe('ExceptionBuilderComponent', () => {
|
|||
wrapper = mount(
|
||||
<ThemeProvider theme={mockTheme}>
|
||||
<ExceptionBuilderComponent
|
||||
httpService={mockKibanaHttpService}
|
||||
autocompleteService={autocompleteStartMock}
|
||||
exceptionListItems={[]}
|
||||
listType="detection"
|
||||
listId="list_id"
|
||||
|
@ -169,6 +164,8 @@ describe('ExceptionBuilderComponent', () => {
|
|||
wrapper = mount(
|
||||
<ThemeProvider theme={mockTheme}>
|
||||
<ExceptionBuilderComponent
|
||||
httpService={mockKibanaHttpService}
|
||||
autocompleteService={autocompleteStartMock}
|
||||
exceptionListItems={[]}
|
||||
listType="detection"
|
||||
listId="list_id"
|
||||
|
@ -223,6 +220,8 @@ describe('ExceptionBuilderComponent', () => {
|
|||
wrapper = mount(
|
||||
<ThemeProvider theme={mockTheme}>
|
||||
<ExceptionBuilderComponent
|
||||
httpService={mockKibanaHttpService}
|
||||
autocompleteService={autocompleteStartMock}
|
||||
exceptionListItems={[]}
|
||||
listType="detection"
|
||||
listId="list_id"
|
||||
|
@ -281,6 +280,8 @@ describe('ExceptionBuilderComponent', () => {
|
|||
wrapper = mount(
|
||||
<ThemeProvider theme={mockTheme}>
|
||||
<ExceptionBuilderComponent
|
||||
httpService={mockKibanaHttpService}
|
||||
autocompleteService={autocompleteStartMock}
|
||||
exceptionListItems={[
|
||||
{
|
||||
...getExceptionListItemSchemaMock(),
|
||||
|
@ -333,6 +334,8 @@ describe('ExceptionBuilderComponent', () => {
|
|||
wrapper = mount(
|
||||
<ThemeProvider theme={mockTheme}>
|
||||
<ExceptionBuilderComponent
|
||||
httpService={mockKibanaHttpService}
|
||||
autocompleteService={autocompleteStartMock}
|
||||
exceptionListItems={[]}
|
||||
listType="detection"
|
||||
listId="list_id"
|
||||
|
@ -366,6 +369,8 @@ describe('ExceptionBuilderComponent', () => {
|
|||
wrapper = mount(
|
||||
<ThemeProvider theme={mockTheme}>
|
||||
<ExceptionBuilderComponent
|
||||
httpService={mockKibanaHttpService}
|
||||
autocompleteService={autocompleteStartMock}
|
||||
exceptionListItems={[]}
|
||||
listType="detection"
|
||||
listId="list_id"
|
||||
|
@ -402,6 +407,8 @@ describe('ExceptionBuilderComponent', () => {
|
|||
wrapper = mount(
|
||||
<ThemeProvider theme={mockTheme}>
|
||||
<ExceptionBuilderComponent
|
||||
httpService={mockKibanaHttpService}
|
||||
autocompleteService={autocompleteStartMock}
|
||||
exceptionListItems={[]}
|
||||
listType="detection"
|
||||
listId="list_id"
|
||||
|
|
|
@ -9,11 +9,14 @@ import React, { useCallback, useEffect, useReducer } from 'react';
|
|||
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { HttpStart } from 'kibana/public';
|
||||
import { AutocompleteStart } from 'src/plugins/data/public';
|
||||
import { isEqlRule, isThresholdRule } from '../../../../../common/detection_engine/utils';
|
||||
import { addIdToItem } from '../../../../../common';
|
||||
import { Type } from '../../../../../common/detection_engine/schemas/common/schemas';
|
||||
import { BuilderExceptionListItemComponent } from './exception_item';
|
||||
import { IIndexPattern } from '../../../../../../../../src/plugins/data/common';
|
||||
import {
|
||||
BuilderExceptionListItemComponent,
|
||||
ExceptionListItemSchema,
|
||||
NamespaceType,
|
||||
exceptionListItemSchema,
|
||||
|
@ -22,7 +25,7 @@ import {
|
|||
CreateExceptionListItemSchema,
|
||||
ExceptionListType,
|
||||
entriesNested,
|
||||
} from '../../../../../public/lists_plugin_deps';
|
||||
} from '../../../../../public/shared_imports';
|
||||
import { AndOrBadge } from '../../and_or_badge';
|
||||
import { BuilderLogicButtons } from './logic_buttons';
|
||||
import { getNewExceptionItem, filterExceptionItems } from '../helpers';
|
||||
|
@ -66,6 +69,8 @@ interface OnChangeProps {
|
|||
}
|
||||
|
||||
interface ExceptionBuilderProps {
|
||||
httpService: HttpStart;
|
||||
autocompleteService: AutocompleteStart;
|
||||
exceptionListItems: ExceptionsBuilderExceptionItem[];
|
||||
listType: ExceptionListType;
|
||||
listId: string;
|
||||
|
@ -80,6 +85,8 @@ interface ExceptionBuilderProps {
|
|||
}
|
||||
|
||||
export const ExceptionBuilderComponent = ({
|
||||
httpService,
|
||||
autocompleteService,
|
||||
exceptionListItems,
|
||||
listType,
|
||||
listId,
|
||||
|
@ -374,6 +381,9 @@ export const ExceptionBuilderComponent = ({
|
|||
))}
|
||||
<EuiFlexItem grow={false}>
|
||||
<BuilderExceptionListItemComponent
|
||||
allowLargeValueLists={!isEqlRule(ruleType) && !isThresholdRule(ruleType)}
|
||||
httpService={httpService}
|
||||
autocompleteService={autocompleteService}
|
||||
key={getExceptionListItemId(exceptionListItem, index)}
|
||||
exceptionItem={exceptionListItem}
|
||||
indexPattern={indexPatterns}
|
||||
|
@ -385,7 +395,6 @@ export const ExceptionBuilderComponent = ({
|
|||
onChangeExceptionItem={handleExceptionItemChange}
|
||||
onlyShowListOperators={containsValueListEntry(exceptions)}
|
||||
setErrorsExist={setErrorsExist}
|
||||
ruleType={ruleType}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
|
|
@ -98,7 +98,7 @@ export const EditExceptionModal = memo(function EditExceptionModal({
|
|||
onConfirm,
|
||||
onRuleChange,
|
||||
}: EditExceptionModalProps) {
|
||||
const { http } = useKibana().services;
|
||||
const { http, data } = useKibana().services;
|
||||
const [comment, setComment] = useState('');
|
||||
const [errorsExist, setErrorExists] = useState(false);
|
||||
const { rule: maybeRule, loading: isRuleLoading } = useRuleAsync(ruleId);
|
||||
|
@ -313,6 +313,8 @@ export const EditExceptionModal = memo(function EditExceptionModal({
|
|||
<EuiText>{i18n.EXCEPTION_BUILDER_INFO}</EuiText>
|
||||
<EuiSpacer />
|
||||
<ExceptionBuilderComponent
|
||||
httpService={http}
|
||||
autocompleteService={data.autocomplete}
|
||||
exceptionListItems={[exceptionItem]}
|
||||
listType={exceptionListType}
|
||||
listId={exceptionItem.list_id}
|
||||
|
|
|
@ -1512,13 +1512,13 @@ Object {
|
|||
data-test-subj="trustedAppsListPage"
|
||||
>
|
||||
<header
|
||||
class="sc-pZNLs imoSYB siemHeaderPage"
|
||||
class="sc-pjGtN iqOyKV siemHeaderPage"
|
||||
>
|
||||
<div
|
||||
class="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--alignItemsCenter euiFlexGroup--directionRow euiFlexGroup--responsive"
|
||||
>
|
||||
<div
|
||||
class="euiFlexItem sc-oTOtL cKcAUE"
|
||||
class="euiFlexItem sc-psbuI gHZSXZ"
|
||||
>
|
||||
<h1
|
||||
class="euiTitle euiTitle--large"
|
||||
|
@ -1538,7 +1538,7 @@ Object {
|
|||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="euiFlexItem euiFlexItem--flexGrowZero sc-oTOtL cKcAUE"
|
||||
class="euiFlexItem euiFlexItem--flexGrowZero sc-psbuI gHZSXZ"
|
||||
data-test-subj="header-page-supplements"
|
||||
>
|
||||
<button
|
||||
|
@ -1599,7 +1599,7 @@ Object {
|
|||
class="euiSpacer euiSpacer--l"
|
||||
/>
|
||||
<div
|
||||
class="euiPanel euiPanel--paddingMedium euiPanel--borderRadiusMedium euiPanel--plain euiPanel--shadow sc-oUqyN jIrDlk"
|
||||
class="euiPanel euiPanel--paddingMedium euiPanel--borderRadiusMedium euiPanel--plain euiPanel--shadow sc-plgA-D cjCgbf"
|
||||
>
|
||||
<div
|
||||
data-eui="EuiFocusTrap"
|
||||
|
@ -1784,7 +1784,7 @@ Object {
|
|||
>
|
||||
<div>
|
||||
<div
|
||||
class="euiFlexGroup euiFlexGroup--gutterExtraSmall euiFlexGroup--directionRow euiFlexGroup--responsive sc-pcLzI gaXJWX"
|
||||
class="euiFlexGroup euiFlexGroup--gutterExtraSmall euiFlexGroup--directionRow euiFlexGroup--responsive sc-ptBBy hxgCsO"
|
||||
data-test-subj="addTrustedAppFlyout-createForm-conditionsBuilder-group1"
|
||||
>
|
||||
<div
|
||||
|
@ -2060,7 +2060,7 @@ Object {
|
|||
class="euiFormRow__fieldWrapper"
|
||||
>
|
||||
<div
|
||||
class="sc-plgA-D bsISVJ"
|
||||
class="sc-qPzgd jNOPOu"
|
||||
>
|
||||
<div
|
||||
class="euiFormRow euiFormRow--fullWidth"
|
||||
|
@ -2390,18 +2390,18 @@ Object {
|
|||
class="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--directionRow euiFlexGroup--responsive"
|
||||
>
|
||||
<div
|
||||
class="euiFlexItem euiFlexItem--flexGrow2 sc-qPzgd aiUzK"
|
||||
class="euiFlexItem euiFlexItem--flexGrow2 sc-pAkMi kGVKuo"
|
||||
>
|
||||
<dl
|
||||
class="euiDescriptionList euiDescriptionList--column euiDescriptionList--compressed"
|
||||
>
|
||||
<dt
|
||||
class="euiDescriptionList__title sc-pAkMi hqItBe"
|
||||
class="euiDescriptionList__title sc-pQfSI jJIOeJ"
|
||||
>
|
||||
Name
|
||||
</dt>
|
||||
<dd
|
||||
class="euiDescriptionList__description sc-pIgJL cnELLG eui-textBreakWord"
|
||||
class="euiDescriptionList__description sc-pYbQl KYizM eui-textBreakWord"
|
||||
>
|
||||
<span
|
||||
class="euiToolTipAnchor"
|
||||
|
@ -2412,12 +2412,12 @@ Object {
|
|||
</span>
|
||||
</dd>
|
||||
<dt
|
||||
class="euiDescriptionList__title sc-pAkMi hqItBe"
|
||||
class="euiDescriptionList__title sc-pQfSI jJIOeJ"
|
||||
>
|
||||
OS
|
||||
</dt>
|
||||
<dd
|
||||
class="euiDescriptionList__description sc-pIgJL cnELLG eui-textBreakWord"
|
||||
class="euiDescriptionList__description sc-pYbQl KYizM eui-textBreakWord"
|
||||
>
|
||||
<span
|
||||
class="euiToolTipAnchor"
|
||||
|
@ -2428,12 +2428,12 @@ Object {
|
|||
</span>
|
||||
</dd>
|
||||
<dt
|
||||
class="euiDescriptionList__title sc-pAkMi hqItBe"
|
||||
class="euiDescriptionList__title sc-pQfSI jJIOeJ"
|
||||
>
|
||||
Date Created
|
||||
</dt>
|
||||
<dd
|
||||
class="euiDescriptionList__description sc-pIgJL cnELLG eui-textBreakWord"
|
||||
class="euiDescriptionList__description sc-pYbQl KYizM eui-textBreakWord"
|
||||
>
|
||||
<span
|
||||
class="euiToolTipAnchor eui-textTruncate"
|
||||
|
@ -2442,12 +2442,12 @@ Object {
|
|||
</span>
|
||||
</dd>
|
||||
<dt
|
||||
class="euiDescriptionList__title sc-pAkMi hqItBe"
|
||||
class="euiDescriptionList__title sc-pQfSI jJIOeJ"
|
||||
>
|
||||
Created By
|
||||
</dt>
|
||||
<dd
|
||||
class="euiDescriptionList__description sc-pIgJL cnELLG eui-textBreakWord"
|
||||
class="euiDescriptionList__description sc-pYbQl KYizM eui-textBreakWord"
|
||||
>
|
||||
<span
|
||||
class="euiToolTipAnchor"
|
||||
|
@ -2458,12 +2458,12 @@ Object {
|
|||
</span>
|
||||
</dd>
|
||||
<dt
|
||||
class="euiDescriptionList__title sc-pAkMi hqItBe"
|
||||
class="euiDescriptionList__title sc-pQfSI jJIOeJ"
|
||||
>
|
||||
Description
|
||||
</dt>
|
||||
<dd
|
||||
class="euiDescriptionList__description sc-pIgJL cnELLG eui-textBreakWord"
|
||||
class="euiDescriptionList__description sc-pYbQl KYizM eui-textBreakWord"
|
||||
>
|
||||
<span
|
||||
class="euiToolTipAnchor"
|
||||
|
@ -2476,7 +2476,7 @@ Object {
|
|||
</dl>
|
||||
</div>
|
||||
<div
|
||||
class="euiFlexItem euiFlexItem--flexGrow5 sc-qXUgY fyGsco"
|
||||
class="euiFlexItem euiFlexItem--flexGrow5 sc-pIgJL dxkHYu"
|
||||
>
|
||||
<div
|
||||
class="euiFlexGroup euiFlexGroup--gutterMedium euiFlexGroup--directionColumn euiFlexGroup--responsive"
|
||||
|
@ -4295,13 +4295,13 @@ Object {
|
|||
data-test-subj="trustedAppsListPage"
|
||||
>
|
||||
<header
|
||||
class="sc-pZNLs imoSYB siemHeaderPage"
|
||||
class="sc-pjGtN iqOyKV siemHeaderPage"
|
||||
>
|
||||
<div
|
||||
class="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--alignItemsCenter euiFlexGroup--directionRow euiFlexGroup--responsive"
|
||||
>
|
||||
<div
|
||||
class="euiFlexItem sc-oTOtL cKcAUE"
|
||||
class="euiFlexItem sc-psbuI gHZSXZ"
|
||||
>
|
||||
<h1
|
||||
class="euiTitle euiTitle--large"
|
||||
|
@ -4321,7 +4321,7 @@ Object {
|
|||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="euiFlexItem euiFlexItem--flexGrowZero sc-oTOtL cKcAUE"
|
||||
class="euiFlexItem euiFlexItem--flexGrowZero sc-psbuI gHZSXZ"
|
||||
data-test-subj="header-page-supplements"
|
||||
>
|
||||
<button
|
||||
|
@ -4382,7 +4382,7 @@ Object {
|
|||
class="euiSpacer euiSpacer--l"
|
||||
/>
|
||||
<div
|
||||
class="euiPanel euiPanel--paddingMedium euiPanel--borderRadiusMedium euiPanel--plain euiPanel--shadow sc-oUqyN jIrDlk"
|
||||
class="euiPanel euiPanel--paddingMedium euiPanel--borderRadiusMedium euiPanel--plain euiPanel--shadow sc-plgA-D cjCgbf"
|
||||
>
|
||||
<div
|
||||
data-eui="EuiFocusTrap"
|
||||
|
@ -4567,7 +4567,7 @@ Object {
|
|||
>
|
||||
<div>
|
||||
<div
|
||||
class="euiFlexGroup euiFlexGroup--gutterExtraSmall euiFlexGroup--directionRow euiFlexGroup--responsive sc-pcLzI gaXJWX"
|
||||
class="euiFlexGroup euiFlexGroup--gutterExtraSmall euiFlexGroup--directionRow euiFlexGroup--responsive sc-ptBBy hxgCsO"
|
||||
data-test-subj="addTrustedAppFlyout-createForm-conditionsBuilder-group1"
|
||||
>
|
||||
<div
|
||||
|
@ -5016,18 +5016,18 @@ Object {
|
|||
class="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--directionRow euiFlexGroup--responsive"
|
||||
>
|
||||
<div
|
||||
class="euiFlexItem euiFlexItem--flexGrow2 sc-qPzgd aiUzK"
|
||||
class="euiFlexItem euiFlexItem--flexGrow2 sc-pAkMi kGVKuo"
|
||||
>
|
||||
<dl
|
||||
class="euiDescriptionList euiDescriptionList--column euiDescriptionList--compressed"
|
||||
>
|
||||
<dt
|
||||
class="euiDescriptionList__title sc-pAkMi hqItBe"
|
||||
class="euiDescriptionList__title sc-pQfSI jJIOeJ"
|
||||
>
|
||||
Name
|
||||
</dt>
|
||||
<dd
|
||||
class="euiDescriptionList__description sc-pIgJL cnELLG eui-textBreakWord"
|
||||
class="euiDescriptionList__description sc-pYbQl KYizM eui-textBreakWord"
|
||||
>
|
||||
<span
|
||||
class="euiToolTipAnchor"
|
||||
|
@ -5038,12 +5038,12 @@ Object {
|
|||
</span>
|
||||
</dd>
|
||||
<dt
|
||||
class="euiDescriptionList__title sc-pAkMi hqItBe"
|
||||
class="euiDescriptionList__title sc-pQfSI jJIOeJ"
|
||||
>
|
||||
OS
|
||||
</dt>
|
||||
<dd
|
||||
class="euiDescriptionList__description sc-pIgJL cnELLG eui-textBreakWord"
|
||||
class="euiDescriptionList__description sc-pYbQl KYizM eui-textBreakWord"
|
||||
>
|
||||
<span
|
||||
class="euiToolTipAnchor"
|
||||
|
@ -5054,12 +5054,12 @@ Object {
|
|||
</span>
|
||||
</dd>
|
||||
<dt
|
||||
class="euiDescriptionList__title sc-pAkMi hqItBe"
|
||||
class="euiDescriptionList__title sc-pQfSI jJIOeJ"
|
||||
>
|
||||
Date Created
|
||||
</dt>
|
||||
<dd
|
||||
class="euiDescriptionList__description sc-pIgJL cnELLG eui-textBreakWord"
|
||||
class="euiDescriptionList__description sc-pYbQl KYizM eui-textBreakWord"
|
||||
>
|
||||
<span
|
||||
class="euiToolTipAnchor eui-textTruncate"
|
||||
|
@ -5068,12 +5068,12 @@ Object {
|
|||
</span>
|
||||
</dd>
|
||||
<dt
|
||||
class="euiDescriptionList__title sc-pAkMi hqItBe"
|
||||
class="euiDescriptionList__title sc-pQfSI jJIOeJ"
|
||||
>
|
||||
Created By
|
||||
</dt>
|
||||
<dd
|
||||
class="euiDescriptionList__description sc-pIgJL cnELLG eui-textBreakWord"
|
||||
class="euiDescriptionList__description sc-pYbQl KYizM eui-textBreakWord"
|
||||
>
|
||||
<span
|
||||
class="euiToolTipAnchor"
|
||||
|
@ -5084,12 +5084,12 @@ Object {
|
|||
</span>
|
||||
</dd>
|
||||
<dt
|
||||
class="euiDescriptionList__title sc-pAkMi hqItBe"
|
||||
class="euiDescriptionList__title sc-pQfSI jJIOeJ"
|
||||
>
|
||||
Description
|
||||
</dt>
|
||||
<dd
|
||||
class="euiDescriptionList__description sc-pIgJL cnELLG eui-textBreakWord"
|
||||
class="euiDescriptionList__description sc-pYbQl KYizM eui-textBreakWord"
|
||||
>
|
||||
<span
|
||||
class="euiToolTipAnchor"
|
||||
|
@ -5102,7 +5102,7 @@ Object {
|
|||
</dl>
|
||||
</div>
|
||||
<div
|
||||
class="euiFlexItem euiFlexItem--flexGrow5 sc-qXUgY fyGsco"
|
||||
class="euiFlexItem euiFlexItem--flexGrow5 sc-pIgJL dxkHYu"
|
||||
>
|
||||
<div
|
||||
class="euiFlexGroup euiFlexGroup--gutterMedium euiFlexGroup--directionColumn euiFlexGroup--responsive"
|
||||
|
|
|
@ -59,4 +59,7 @@ export {
|
|||
addEndpointExceptionList,
|
||||
withOptionalSignal,
|
||||
BuilderEntryItem,
|
||||
BuilderAndBadgeComponent,
|
||||
BuilderEntryDeleteButtonComponent,
|
||||
BuilderExceptionListItemComponent,
|
||||
} from '../../lists/public';
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue