mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[Security Solution] - Update exceptions item UI (#135255)
## Summary Addresses https://github.com/elastic/kibana/issues/135254
This commit is contained in:
parent
6b018f797b
commit
0a0b0a18e2
25 changed files with 1051 additions and 1823 deletions
|
@ -14,7 +14,7 @@ import { addExceptionFromFirstAlert, goToClosedAlerts, goToOpenedAlerts } from '
|
|||
import { createCustomRuleEnabled } from '../../tasks/api_calls/rules';
|
||||
import { goToRuleDetails } from '../../tasks/alerts_detection_rules';
|
||||
import { waitForAlertsToPopulate } from '../../tasks/create_new_rule';
|
||||
import { esArchiverLoad, esArchiverUnload } from '../../tasks/es_archiver';
|
||||
import { esArchiverLoad, esArchiverUnload, esArchiverResetKibana } from '../../tasks/es_archiver';
|
||||
import { login, visitWithoutDateRange } from '../../tasks/login';
|
||||
import {
|
||||
addsException,
|
||||
|
@ -26,15 +26,15 @@ import {
|
|||
} from '../../tasks/rule_details';
|
||||
|
||||
import { DETECTIONS_RULE_MANAGEMENT_URL } from '../../urls/navigation';
|
||||
import { cleanKibana, deleteAlertsAndRules } from '../../tasks/common';
|
||||
import { deleteAlertsAndRules } from '../../tasks/common';
|
||||
|
||||
describe('Adds rule exception', () => {
|
||||
const NUMBER_OF_AUDITBEAT_EXCEPTIONS_ALERTS = '1 alert';
|
||||
|
||||
before(() => {
|
||||
cleanKibana();
|
||||
login();
|
||||
esArchiverResetKibana();
|
||||
esArchiverLoad('exceptions');
|
||||
login();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
|
|
|
@ -13,7 +13,11 @@ import { createCustomRule } from '../../tasks/api_calls/rules';
|
|||
import { goToRuleDetails } from '../../tasks/alerts_detection_rules';
|
||||
import { esArchiverLoad, esArchiverResetKibana, esArchiverUnload } from '../../tasks/es_archiver';
|
||||
import { login, visitWithoutDateRange } from '../../tasks/login';
|
||||
import { openExceptionFlyoutFromRuleSettings, goToExceptionsTab } from '../../tasks/rule_details';
|
||||
import {
|
||||
openExceptionFlyoutFromRuleSettings,
|
||||
goToExceptionsTab,
|
||||
editException,
|
||||
} from '../../tasks/rule_details';
|
||||
import {
|
||||
addExceptionEntryFieldMatchAnyValue,
|
||||
addExceptionEntryFieldValue,
|
||||
|
@ -32,7 +36,6 @@ import {
|
|||
EXCEPTION_ITEM_CONTAINER,
|
||||
ADD_EXCEPTIONS_BTN,
|
||||
EXCEPTION_FIELD_LIST,
|
||||
EDIT_EXCEPTIONS_BTN,
|
||||
EXCEPTION_EDIT_FLYOUT_SAVE_BTN,
|
||||
EXCEPTION_FLYOUT_VERSION_CONFLICT,
|
||||
EXCEPTION_FLYOUT_LIST_DELETED_ERROR,
|
||||
|
@ -302,8 +305,7 @@ describe('Exceptions flyout', () => {
|
|||
|
||||
context('When updating an item with version conflict', () => {
|
||||
it('Displays version conflict error', () => {
|
||||
cy.get(EDIT_EXCEPTIONS_BTN).should('be.visible');
|
||||
cy.get(EDIT_EXCEPTIONS_BTN).click({ force: true });
|
||||
editException();
|
||||
|
||||
// update exception item via api
|
||||
updateExceptionListItem('simple_list_item', {
|
||||
|
@ -334,8 +336,7 @@ describe('Exceptions flyout', () => {
|
|||
|
||||
context('When updating an item for a list that has since been deleted', () => {
|
||||
it('Displays missing exception list error', () => {
|
||||
cy.get(EDIT_EXCEPTIONS_BTN).should('be.visible');
|
||||
cy.get(EDIT_EXCEPTIONS_BTN).click({ force: true });
|
||||
editException();
|
||||
|
||||
// delete exception list via api
|
||||
deleteExceptionList(getExceptionList().list_id, getExceptionList().namespace_type);
|
||||
|
|
|
@ -5,8 +5,6 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
export const EDIT_EXCEPTIONS_BTN = '[data-test-subj="exceptionsViewerEditBtn"]';
|
||||
|
||||
export const ADD_EXCEPTIONS_BTN = '[data-test-subj="exceptionsHeaderAddExceptionBtn"]';
|
||||
|
||||
export const CLOSE_ALERTS_CHECKBOX =
|
||||
|
|
|
@ -78,7 +78,12 @@ export const RISK_SCORE_OVERRIDE_DETAILS = 'Risk score override';
|
|||
|
||||
export const REFERENCE_URLS_DETAILS = 'Reference URLs';
|
||||
|
||||
export const REMOVE_EXCEPTION_BTN = '[data-test-subj="exceptionsViewerDeleteBtn"]';
|
||||
export const EXCEPTION_ITEM_ACTIONS_BUTTON =
|
||||
'button[data-test-subj="exceptionItemCardHeader-actionButton"]';
|
||||
|
||||
export const REMOVE_EXCEPTION_BTN = '[data-test-subj="exceptionItemCardHeader-actionItem-delete"]';
|
||||
|
||||
export const EDIT_EXCEPTION_BTN = '[data-test-subj="exceptionItemCardHeader-actionItem-edit"]';
|
||||
|
||||
export const RULE_SWITCH = '[data-test-subj="ruleSwitch"]';
|
||||
|
||||
|
|
|
@ -29,6 +29,8 @@ import {
|
|||
INDEX_PATTERNS_DETAILS,
|
||||
DETAILS_TITLE,
|
||||
DETAILS_DESCRIPTION,
|
||||
EXCEPTION_ITEM_ACTIONS_BUTTON,
|
||||
EDIT_EXCEPTION_BTN,
|
||||
} from '../screens/rule_details';
|
||||
import { addsFields, closeFieldsBrowser, filterFieldsBrowser } from './fields_browser';
|
||||
|
||||
|
@ -96,7 +98,15 @@ export const goToExceptionsTab = () => {
|
|||
.should('be.visible');
|
||||
};
|
||||
|
||||
export const editException = () => {
|
||||
cy.get(EXCEPTION_ITEM_ACTIONS_BUTTON).click();
|
||||
|
||||
cy.get(EDIT_EXCEPTION_BTN).click();
|
||||
};
|
||||
|
||||
export const removeException = () => {
|
||||
cy.get(EXCEPTION_ITEM_ACTIONS_BUTTON).click();
|
||||
|
||||
cy.get(REMOVE_EXCEPTION_BTN).click();
|
||||
};
|
||||
|
||||
|
|
|
@ -1,252 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 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 moment from 'moment-timezone';
|
||||
|
||||
import { ExceptionDetails } from './exception_details';
|
||||
import { getExceptionListItemSchemaMock } from '@kbn/lists-plugin/common/schemas/response/exception_list_item_schema.mock';
|
||||
import { getCommentsArrayMock } from '@kbn/lists-plugin/common/schemas/types/comment.mock';
|
||||
import { getMockTheme } from '../../../../lib/kibana/kibana_react.mock';
|
||||
|
||||
const mockTheme = getMockTheme({
|
||||
eui: {
|
||||
euiColorLightestShade: '#ece',
|
||||
},
|
||||
});
|
||||
|
||||
describe('ExceptionDetails', () => {
|
||||
beforeEach(() => {
|
||||
moment.tz.setDefault('UTC');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
moment.tz.setDefault('Browser');
|
||||
});
|
||||
|
||||
test('it renders no comments button if no comments exist', () => {
|
||||
const exceptionItem = getExceptionListItemSchemaMock();
|
||||
exceptionItem.comments = [];
|
||||
|
||||
const wrapper = mount(
|
||||
<ThemeProvider theme={mockTheme}>
|
||||
<ExceptionDetails
|
||||
showComments={false}
|
||||
onCommentsClick={jest.fn()}
|
||||
exceptionItem={exceptionItem}
|
||||
/>
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
||||
expect(wrapper.find('[data-test-subj="exceptionsViewerItemCommentsBtn"]')).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('it renders comments button if comments exist', () => {
|
||||
const exceptionItem = getExceptionListItemSchemaMock();
|
||||
exceptionItem.comments = getCommentsArrayMock();
|
||||
const wrapper = mount(
|
||||
<ThemeProvider theme={mockTheme}>
|
||||
<ExceptionDetails
|
||||
showComments={false}
|
||||
onCommentsClick={jest.fn()}
|
||||
exceptionItem={exceptionItem}
|
||||
/>
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
||||
expect(
|
||||
wrapper.find('.euiButtonEmpty[data-test-subj="exceptionsViewerItemCommentsBtn"]')
|
||||
).toHaveLength(1);
|
||||
});
|
||||
|
||||
test('it renders correct number of comments', () => {
|
||||
const exceptionItem = getExceptionListItemSchemaMock();
|
||||
exceptionItem.comments = [getCommentsArrayMock()[0]];
|
||||
const wrapper = mount(
|
||||
<ThemeProvider theme={mockTheme}>
|
||||
<ExceptionDetails
|
||||
showComments={false}
|
||||
onCommentsClick={jest.fn()}
|
||||
exceptionItem={exceptionItem}
|
||||
/>
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
||||
expect(wrapper.find('[data-test-subj="exceptionsViewerItemCommentsBtn"]').at(0).text()).toEqual(
|
||||
'Show (1) Comment'
|
||||
);
|
||||
});
|
||||
|
||||
test('it renders comments plural if more than one', () => {
|
||||
const exceptionItem = getExceptionListItemSchemaMock();
|
||||
exceptionItem.comments = getCommentsArrayMock();
|
||||
const wrapper = mount(
|
||||
<ThemeProvider theme={mockTheme}>
|
||||
<ExceptionDetails
|
||||
showComments={false}
|
||||
onCommentsClick={jest.fn()}
|
||||
exceptionItem={exceptionItem}
|
||||
/>
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
||||
expect(wrapper.find('[data-test-subj="exceptionsViewerItemCommentsBtn"]').at(0).text()).toEqual(
|
||||
'Show (2) Comments'
|
||||
);
|
||||
});
|
||||
|
||||
test('it renders comments show text if "showComments" is false', () => {
|
||||
const exceptionItem = getExceptionListItemSchemaMock();
|
||||
exceptionItem.comments = getCommentsArrayMock();
|
||||
const wrapper = mount(
|
||||
<ThemeProvider theme={mockTheme}>
|
||||
<ExceptionDetails
|
||||
showComments={false}
|
||||
onCommentsClick={jest.fn()}
|
||||
exceptionItem={exceptionItem}
|
||||
/>
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
||||
expect(wrapper.find('[data-test-subj="exceptionsViewerItemCommentsBtn"]').at(0).text()).toEqual(
|
||||
'Show (2) Comments'
|
||||
);
|
||||
});
|
||||
|
||||
test('it renders comments hide text if "showComments" is true', () => {
|
||||
const exceptionItem = getExceptionListItemSchemaMock();
|
||||
exceptionItem.comments = getCommentsArrayMock();
|
||||
const wrapper = mount(
|
||||
<ThemeProvider theme={mockTheme}>
|
||||
<ExceptionDetails
|
||||
showComments={true}
|
||||
onCommentsClick={jest.fn()}
|
||||
exceptionItem={exceptionItem}
|
||||
/>
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
||||
expect(wrapper.find('[data-test-subj="exceptionsViewerItemCommentsBtn"]').at(0).text()).toEqual(
|
||||
'Hide (2) Comments'
|
||||
);
|
||||
});
|
||||
|
||||
test('it invokes "onCommentsClick" when comments button clicked', () => {
|
||||
const mockOnCommentsClick = jest.fn();
|
||||
const exceptionItem = getExceptionListItemSchemaMock();
|
||||
exceptionItem.comments = getCommentsArrayMock();
|
||||
const wrapper = mount(
|
||||
<ThemeProvider theme={mockTheme}>
|
||||
<ExceptionDetails
|
||||
showComments={true}
|
||||
onCommentsClick={mockOnCommentsClick}
|
||||
exceptionItem={exceptionItem}
|
||||
/>
|
||||
</ThemeProvider>
|
||||
);
|
||||
const commentsBtn = wrapper.find('[data-test-subj="exceptionsViewerItemCommentsBtn"]').at(0);
|
||||
commentsBtn.simulate('click');
|
||||
|
||||
expect(mockOnCommentsClick).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('it renders the operating system if one is specified in the exception item', () => {
|
||||
const exceptionItem = getExceptionListItemSchemaMock({ os_types: ['linux'] });
|
||||
const wrapper = mount(
|
||||
<ThemeProvider theme={mockTheme}>
|
||||
<ExceptionDetails
|
||||
showComments={true}
|
||||
onCommentsClick={jest.fn()}
|
||||
exceptionItem={exceptionItem}
|
||||
/>
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
||||
expect(wrapper.find('EuiDescriptionListTitle').at(0).text()).toEqual('OS');
|
||||
expect(wrapper.find('EuiDescriptionListDescription').at(0).text()).toEqual('Linux');
|
||||
});
|
||||
|
||||
test('it renders the exception item creator', () => {
|
||||
const exceptionItem = getExceptionListItemSchemaMock({ os_types: ['linux'] });
|
||||
const wrapper = mount(
|
||||
<ThemeProvider theme={mockTheme}>
|
||||
<ExceptionDetails
|
||||
showComments={true}
|
||||
onCommentsClick={jest.fn()}
|
||||
exceptionItem={exceptionItem}
|
||||
/>
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
||||
expect(wrapper.find('EuiDescriptionListTitle').at(1).text()).toEqual('Date created');
|
||||
expect(wrapper.find('EuiDescriptionListDescription').at(1).text()).toEqual(
|
||||
'April 20th 2020 @ 15:25:31'
|
||||
);
|
||||
});
|
||||
|
||||
test('it renders the exception item creation timestamp', () => {
|
||||
const exceptionItem = getExceptionListItemSchemaMock({ os_types: ['linux'] });
|
||||
const wrapper = mount(
|
||||
<ThemeProvider theme={mockTheme}>
|
||||
<ExceptionDetails
|
||||
showComments={true}
|
||||
onCommentsClick={jest.fn()}
|
||||
exceptionItem={exceptionItem}
|
||||
/>
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
||||
expect(wrapper.find('EuiDescriptionListTitle').at(2).text()).toEqual('Created by');
|
||||
expect(wrapper.find('EuiDescriptionListDescription').at(2).text()).toEqual('some user');
|
||||
});
|
||||
|
||||
test('it renders the description if one is included on the exception item', () => {
|
||||
const exceptionItem = getExceptionListItemSchemaMock({ os_types: ['linux'] });
|
||||
const wrapper = mount(
|
||||
<ThemeProvider theme={mockTheme}>
|
||||
<ExceptionDetails
|
||||
showComments={true}
|
||||
onCommentsClick={jest.fn()}
|
||||
exceptionItem={exceptionItem}
|
||||
/>
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
||||
expect(wrapper.find('EuiDescriptionListTitle').at(3).text()).toEqual('Description');
|
||||
expect(wrapper.find('EuiDescriptionListDescription').at(3).text()).toEqual('some description');
|
||||
});
|
||||
|
||||
test('it renders with Name and Modified info when showName and showModified props are true', () => {
|
||||
const exceptionItem = getExceptionListItemSchemaMock({ os_types: ['linux'] });
|
||||
exceptionItem.comments = [];
|
||||
|
||||
const wrapper = mount(
|
||||
<ThemeProvider theme={mockTheme}>
|
||||
<ExceptionDetails
|
||||
showComments={false}
|
||||
onCommentsClick={jest.fn()}
|
||||
exceptionItem={exceptionItem}
|
||||
showName={true}
|
||||
showModified={true}
|
||||
/>
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
||||
expect(wrapper.find('EuiDescriptionListTitle').at(0).text()).toEqual('Name');
|
||||
expect(wrapper.find('EuiDescriptionListDescription').at(0).text()).toEqual('some name');
|
||||
|
||||
expect(wrapper.find('EuiDescriptionListTitle').at(4).text()).toEqual('Date modified');
|
||||
expect(wrapper.find('EuiDescriptionListDescription').at(4).text()).toEqual(
|
||||
'April 20th 2020 @ 15:25:31'
|
||||
);
|
||||
|
||||
expect(wrapper.find('EuiDescriptionListTitle').at(5).text()).toEqual('Modified by');
|
||||
expect(wrapper.find('EuiDescriptionListDescription').at(5).text()).toEqual('some user');
|
||||
});
|
||||
});
|
|
@ -1,121 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import {
|
||||
EuiFlexItem,
|
||||
EuiFlexGroup,
|
||||
EuiDescriptionList,
|
||||
EuiButtonEmpty,
|
||||
EuiDescriptionListTitle,
|
||||
EuiToolTip,
|
||||
} from '@elastic/eui';
|
||||
import React, { useMemo, Fragment } from 'react';
|
||||
import styled, { css } from 'styled-components';
|
||||
|
||||
import type { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types';
|
||||
import type { DescriptionListItem } from '../../types';
|
||||
import { getDescriptionListContent } from '../helpers';
|
||||
import * as i18n from '../../translations';
|
||||
|
||||
const MyExceptionDetails = styled(EuiFlexItem)`
|
||||
${({ theme }) => css`
|
||||
background-color: ${theme.eui.euiColorLightestShade};
|
||||
padding: ${theme.eui.euiSize};
|
||||
.eventFiltersDescriptionList {
|
||||
margin: ${theme.eui.euiSize} ${theme.eui.euiSize} 0 ${theme.eui.euiSize};
|
||||
}
|
||||
.eventFiltersDescriptionListTitle {
|
||||
width: 40%;
|
||||
margin-top: 0;
|
||||
margin-bottom: ${theme.eui.euiSizeS};
|
||||
}
|
||||
.eventFiltersDescriptionListDescription {
|
||||
width: 60%;
|
||||
margin-top: 0;
|
||||
margin-bottom: ${theme.eui.euiSizeS};
|
||||
}
|
||||
`}
|
||||
`;
|
||||
|
||||
const StyledCommentsSection = styled(EuiFlexItem)`
|
||||
${({ theme }) => css`
|
||||
&&& {
|
||||
margin: ${theme.eui.euiSizeXS} ${theme.eui.euiSize} ${theme.eui.euiSizeL} ${theme.eui.euiSize};
|
||||
}
|
||||
`}
|
||||
`;
|
||||
|
||||
const ExceptionDetailsComponent = ({
|
||||
showComments,
|
||||
showModified = false,
|
||||
showName = false,
|
||||
onCommentsClick,
|
||||
exceptionItem,
|
||||
}: {
|
||||
showComments: boolean;
|
||||
showModified?: boolean;
|
||||
showName?: boolean;
|
||||
exceptionItem: ExceptionListItemSchema;
|
||||
onCommentsClick: () => void;
|
||||
}): JSX.Element => {
|
||||
const descriptionListItems = useMemo(
|
||||
(): DescriptionListItem[] => getDescriptionListContent(exceptionItem, showModified, showName),
|
||||
[exceptionItem, showModified, showName]
|
||||
);
|
||||
|
||||
const commentsSection = useMemo((): JSX.Element => {
|
||||
const { comments } = exceptionItem;
|
||||
if (comments.length > 0) {
|
||||
return (
|
||||
<EuiButtonEmpty
|
||||
onClick={onCommentsClick}
|
||||
flush="left"
|
||||
size="xs"
|
||||
data-test-subj="exceptionsViewerItemCommentsBtn"
|
||||
>
|
||||
{!showComments
|
||||
? i18n.COMMENTS_SHOW(comments.length)
|
||||
: i18n.COMMENTS_HIDE(comments.length)}
|
||||
</EuiButtonEmpty>
|
||||
);
|
||||
} else {
|
||||
return <></>;
|
||||
}
|
||||
}, [showComments, onCommentsClick, exceptionItem]);
|
||||
|
||||
return (
|
||||
<MyExceptionDetails grow={2}>
|
||||
<EuiFlexGroup direction="column" alignItems="flexStart">
|
||||
<EuiFlexItem grow={1} className="eventFiltersDescriptionList">
|
||||
<EuiDescriptionList
|
||||
compressed
|
||||
type="responsiveColumn"
|
||||
data-test-subj="exceptionsViewerItemDetails"
|
||||
>
|
||||
{descriptionListItems.map((item) => (
|
||||
<Fragment key={`${item.title}`}>
|
||||
<EuiToolTip content={item.title} anchorClassName="eventFiltersDescriptionListTitle">
|
||||
<EuiDescriptionListTitle className="eui-textTruncate eui-fullWidth">
|
||||
{item.title}
|
||||
</EuiDescriptionListTitle>
|
||||
</EuiToolTip>
|
||||
{item.description}
|
||||
</Fragment>
|
||||
))}
|
||||
</EuiDescriptionList>
|
||||
</EuiFlexItem>
|
||||
<StyledCommentsSection grow={false}>{commentsSection}</StyledCommentsSection>
|
||||
</EuiFlexGroup>
|
||||
</MyExceptionDetails>
|
||||
);
|
||||
};
|
||||
|
||||
ExceptionDetailsComponent.displayName = 'ExceptionDetailsComponent';
|
||||
|
||||
export const ExceptionDetails = React.memo(ExceptionDetailsComponent);
|
||||
|
||||
ExceptionDetails.displayName = 'ExceptionDetails';
|
|
@ -1,193 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 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 { ExceptionEntries } from './exception_entries';
|
||||
import { getFormattedEntryMock } from '../../exceptions.mock';
|
||||
import { getEmptyValue } from '../../../empty_value';
|
||||
import { getMockTheme } from '../../../../lib/kibana/kibana_react.mock';
|
||||
|
||||
const mockTheme = getMockTheme({
|
||||
eui: { euiSize: '10px', euiColorPrimary: '#ece', euiColorDanger: '#ece' },
|
||||
});
|
||||
|
||||
describe('ExceptionEntries', () => {
|
||||
test('it does NOT render the and badge if only one exception item entry exists', () => {
|
||||
const wrapper = mount(
|
||||
<ThemeProvider theme={mockTheme}>
|
||||
<ExceptionEntries
|
||||
disableActions={false}
|
||||
entries={[getFormattedEntryMock()]}
|
||||
onDelete={jest.fn()}
|
||||
onEdit={jest.fn()}
|
||||
/>
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
||||
expect(wrapper.find('[data-test-subj="exceptionsViewerAndBadge"]')).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('it renders the and badge if more than one exception item exists', () => {
|
||||
const wrapper = mount(
|
||||
<ThemeProvider theme={mockTheme}>
|
||||
<ExceptionEntries
|
||||
disableActions={false}
|
||||
entries={[getFormattedEntryMock(), getFormattedEntryMock()]}
|
||||
onDelete={jest.fn()}
|
||||
onEdit={jest.fn()}
|
||||
/>
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
||||
expect(wrapper.find('[data-test-subj="exceptionsViewerAndBadge"]')).toHaveLength(1);
|
||||
});
|
||||
|
||||
test('it invokes "onEdit" when edit button clicked', () => {
|
||||
const mockOnEdit = jest.fn();
|
||||
const wrapper = mount(
|
||||
<ThemeProvider theme={mockTheme}>
|
||||
<ExceptionEntries
|
||||
disableActions={false}
|
||||
entries={[getFormattedEntryMock()]}
|
||||
onDelete={jest.fn()}
|
||||
onEdit={mockOnEdit}
|
||||
/>
|
||||
</ThemeProvider>
|
||||
);
|
||||
const editBtn = wrapper.find('[data-test-subj="exceptionsViewerEditBtn"] button').at(0);
|
||||
editBtn.simulate('click');
|
||||
|
||||
expect(mockOnEdit).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('it invokes "onDelete" when delete button clicked', () => {
|
||||
const mockOnDelete = jest.fn();
|
||||
const wrapper = mount(
|
||||
<ThemeProvider theme={mockTheme}>
|
||||
<ExceptionEntries
|
||||
disableActions={false}
|
||||
entries={[getFormattedEntryMock()]}
|
||||
onDelete={mockOnDelete}
|
||||
onEdit={jest.fn()}
|
||||
/>
|
||||
</ThemeProvider>
|
||||
);
|
||||
const deleteBtn = wrapper.find('[data-test-subj="exceptionsViewerDeleteBtn"] button').at(0);
|
||||
deleteBtn.simulate('click');
|
||||
|
||||
expect(mockOnDelete).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('it does not render edit button if "disableActions" is "true"', () => {
|
||||
const wrapper = mount(
|
||||
<ThemeProvider theme={mockTheme}>
|
||||
<ExceptionEntries
|
||||
disableActions={true}
|
||||
entries={[getFormattedEntryMock()]}
|
||||
onDelete={jest.fn()}
|
||||
onEdit={jest.fn()}
|
||||
/>
|
||||
</ThemeProvider>
|
||||
);
|
||||
const editBtns = wrapper.find('[data-test-subj="exceptionsViewerEditBtn"] button');
|
||||
|
||||
expect(editBtns).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('it does not render delete button if "disableActions" is "true"', () => {
|
||||
const wrapper = mount(
|
||||
<ThemeProvider theme={mockTheme}>
|
||||
<ExceptionEntries
|
||||
disableActions={true}
|
||||
entries={[getFormattedEntryMock()]}
|
||||
onDelete={jest.fn()}
|
||||
onEdit={jest.fn()}
|
||||
/>
|
||||
</ThemeProvider>
|
||||
);
|
||||
const deleteBtns = wrapper.find('[data-test-subj="exceptionsViewerDeleteBtn"] button').at(0);
|
||||
|
||||
expect(deleteBtns).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('it renders nested entry', () => {
|
||||
const parentEntry = getFormattedEntryMock();
|
||||
parentEntry.operator = undefined;
|
||||
parentEntry.value = undefined;
|
||||
|
||||
const wrapper = mount(
|
||||
<ThemeProvider theme={mockTheme}>
|
||||
<ExceptionEntries
|
||||
disableActions={false}
|
||||
entries={[parentEntry, getFormattedEntryMock(true)]}
|
||||
onDelete={jest.fn()}
|
||||
onEdit={jest.fn()}
|
||||
/>
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
||||
const parentField = wrapper
|
||||
.find('[data-test-subj="exceptionFieldNameCell"] .euiTableCellContent')
|
||||
.at(0);
|
||||
const parentOperator = wrapper
|
||||
.find('[data-test-subj="exceptionFieldOperatorCell"] .euiTableCellContent')
|
||||
.at(0);
|
||||
const parentValue = wrapper
|
||||
.find('[data-test-subj="exceptionFieldValueCell"] .euiTableCellContent')
|
||||
.at(0);
|
||||
|
||||
const nestedField = wrapper
|
||||
.find('[data-test-subj="exceptionFieldNameCell"] .euiTableCellContent')
|
||||
.at(1);
|
||||
const nestedOperator = wrapper
|
||||
.find('[data-test-subj="exceptionFieldOperatorCell"] .euiTableCellContent')
|
||||
.at(1);
|
||||
const nestedValue = wrapper
|
||||
.find('[data-test-subj="exceptionFieldValueCell"] .euiTableCellContent')
|
||||
.at(1);
|
||||
|
||||
expect(parentField.text()).toEqual('host.name');
|
||||
expect(parentOperator.text()).toEqual(getEmptyValue());
|
||||
expect(parentValue.text()).toEqual(getEmptyValue());
|
||||
|
||||
expect(nestedField.exists('.euiToolTipAnchor')).toBeTruthy();
|
||||
expect(nestedField.text()).toContain('host.name');
|
||||
expect(nestedOperator.text()).toEqual('is');
|
||||
expect(nestedValue.text()).toEqual('some name');
|
||||
});
|
||||
|
||||
test('it renders non-nested entries', () => {
|
||||
const wrapper = mount(
|
||||
<ThemeProvider theme={mockTheme}>
|
||||
<ExceptionEntries
|
||||
disableActions={false}
|
||||
entries={[getFormattedEntryMock()]}
|
||||
onDelete={jest.fn()}
|
||||
onEdit={jest.fn()}
|
||||
/>
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
||||
const field = wrapper
|
||||
.find('[data-test-subj="exceptionFieldNameCell"] .euiTableCellContent')
|
||||
.at(0);
|
||||
const operator = wrapper
|
||||
.find('[data-test-subj="exceptionFieldOperatorCell"] .euiTableCellContent')
|
||||
.at(0);
|
||||
const value = wrapper
|
||||
.find('[data-test-subj="exceptionFieldValueCell"] .euiTableCellContent')
|
||||
.at(0);
|
||||
|
||||
expect(field.exists('.euiToolTipAnchor')).toBeFalsy();
|
||||
expect(field.text()).toEqual('host.name');
|
||||
expect(operator.text()).toEqual('is');
|
||||
expect(value.text()).toEqual('some name');
|
||||
});
|
||||
});
|
|
@ -1,218 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import {
|
||||
EuiBasicTable,
|
||||
EuiIconTip,
|
||||
EuiFlexItem,
|
||||
EuiFlexGroup,
|
||||
EuiButton,
|
||||
EuiTableFieldDataColumnType,
|
||||
EuiHideFor,
|
||||
EuiBadge,
|
||||
EuiBadgeGroup,
|
||||
EuiToolTip,
|
||||
} from '@elastic/eui';
|
||||
import React, { useMemo } from 'react';
|
||||
import styled, { css } from 'styled-components';
|
||||
import { transparentize } from 'polished';
|
||||
|
||||
import { AndOrBadge } from '../../../and_or_badge';
|
||||
import { getEmptyValue } from '../../../empty_value';
|
||||
import * as i18n from '../../translations';
|
||||
import { FormattedEntry } from '../../types';
|
||||
|
||||
const MyEntriesDetails = styled(EuiFlexItem)`
|
||||
${({ theme }) => css`
|
||||
padding: ${theme.eui.euiSize} ${theme.eui.euiSizeL} ${theme.eui.euiSizeL} ${theme.eui.euiSizeXS};
|
||||
&&& {
|
||||
margin-left: 0;
|
||||
}
|
||||
`}
|
||||
`;
|
||||
|
||||
const MyEditButton = styled(EuiButton)`
|
||||
${({ theme }) => css`
|
||||
background-color: ${transparentize(0.9, theme.eui.euiColorPrimary)};
|
||||
border: none;
|
||||
font-weight: ${theme.eui.euiFontWeightSemiBold};
|
||||
`}
|
||||
`;
|
||||
|
||||
const MyRemoveButton = styled(EuiButton)`
|
||||
${({ theme }) => css`
|
||||
background-color: ${transparentize(0.9, theme.eui.euiColorDanger)};
|
||||
border: none;
|
||||
font-weight: ${theme.eui.euiFontWeightSemiBold};
|
||||
`}
|
||||
`;
|
||||
|
||||
const MyAndOrBadgeContainer = styled(EuiFlexItem)`
|
||||
${({ theme }) => css`
|
||||
padding: ${theme.eui.euiSizeXL} ${theme.eui.euiSize} ${theme.eui.euiSizeS} 0;
|
||||
`}
|
||||
`;
|
||||
|
||||
const MyActionButton = styled(EuiFlexItem)`
|
||||
align-self: flex-end;
|
||||
`;
|
||||
|
||||
const MyNestedValueContainer = styled.div`
|
||||
margin-left: ${({ theme }) => theme.eui.euiSizeL};
|
||||
`;
|
||||
|
||||
const MyNestedValue = styled.span`
|
||||
margin-left: ${({ theme }) => theme.eui.euiSizeS};
|
||||
`;
|
||||
|
||||
const ValueBadgeGroup = styled(EuiBadgeGroup)`
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
interface ExceptionEntriesComponentProps {
|
||||
entries: FormattedEntry[];
|
||||
disableActions: boolean;
|
||||
onDelete: () => void;
|
||||
onEdit: () => void;
|
||||
}
|
||||
|
||||
const ExceptionEntriesComponent = ({
|
||||
entries,
|
||||
disableActions,
|
||||
onDelete,
|
||||
onEdit,
|
||||
}: ExceptionEntriesComponentProps): JSX.Element => {
|
||||
const columns = useMemo(
|
||||
(): Array<EuiTableFieldDataColumnType<FormattedEntry>> => [
|
||||
{
|
||||
field: 'fieldName',
|
||||
name: 'Field',
|
||||
sortable: false,
|
||||
truncateText: true,
|
||||
textOnly: true,
|
||||
'data-test-subj': 'exceptionFieldNameCell',
|
||||
width: '30%',
|
||||
render: (value: string | null, data: FormattedEntry) => {
|
||||
if (value != null && data.isNested) {
|
||||
return (
|
||||
<MyNestedValueContainer>
|
||||
<EuiIconTip type="nested" size="s" />
|
||||
<MyNestedValue>{value}</MyNestedValue>
|
||||
</MyNestedValueContainer>
|
||||
);
|
||||
} else {
|
||||
return value ?? getEmptyValue();
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'operator',
|
||||
name: 'Operator',
|
||||
sortable: false,
|
||||
truncateText: true,
|
||||
'data-test-subj': 'exceptionFieldOperatorCell',
|
||||
width: '20%',
|
||||
render: (value: string | null) => value ?? getEmptyValue(),
|
||||
},
|
||||
{
|
||||
field: 'value',
|
||||
name: 'Value',
|
||||
sortable: false,
|
||||
truncateText: true,
|
||||
'data-test-subj': 'exceptionFieldValueCell',
|
||||
width: '60%',
|
||||
render: (values: string | string[] | null) => {
|
||||
if (Array.isArray(values)) {
|
||||
return (
|
||||
<ValueBadgeGroup gutterSize="xs">
|
||||
{values.map((value) => {
|
||||
return (
|
||||
<EuiBadge color="#DDD" key={value}>
|
||||
{value}
|
||||
</EuiBadge>
|
||||
);
|
||||
})}
|
||||
</ValueBadgeGroup>
|
||||
);
|
||||
} else {
|
||||
return values ? (
|
||||
<EuiToolTip content={values} anchorClassName="eui-textTruncate">
|
||||
<span>{values}</span>
|
||||
</EuiToolTip>
|
||||
) : (
|
||||
getEmptyValue()
|
||||
);
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
[]
|
||||
);
|
||||
|
||||
return (
|
||||
<MyEntriesDetails grow={5}>
|
||||
<EuiFlexGroup direction="column" gutterSize="m">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFlexGroup direction="row" gutterSize="none">
|
||||
{entries.length > 1 && (
|
||||
<EuiHideFor sizes={['xs', 's']}>
|
||||
<MyAndOrBadgeContainer grow={false}>
|
||||
<AndOrBadge
|
||||
type="and"
|
||||
includeAntennas
|
||||
data-test-subj="exceptionsViewerAndBadge"
|
||||
/>
|
||||
</MyAndOrBadgeContainer>
|
||||
</EuiHideFor>
|
||||
)}
|
||||
<EuiFlexItem grow={1}>
|
||||
<EuiBasicTable
|
||||
isSelectable={false}
|
||||
itemId="id"
|
||||
columns={columns}
|
||||
items={entries}
|
||||
responsive
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
{!disableActions && (
|
||||
<EuiFlexItem grow={1}>
|
||||
<EuiFlexGroup gutterSize="s" justifyContent="flexEnd">
|
||||
<MyActionButton grow={false}>
|
||||
<MyEditButton
|
||||
size="s"
|
||||
color="primary"
|
||||
onClick={onEdit}
|
||||
data-test-subj="exceptionsViewerEditBtn"
|
||||
>
|
||||
{i18n.EDIT}
|
||||
</MyEditButton>
|
||||
</MyActionButton>
|
||||
<MyActionButton grow={false}>
|
||||
<MyRemoveButton
|
||||
size="s"
|
||||
color="danger"
|
||||
onClick={onDelete}
|
||||
data-test-subj="exceptionsViewerDeleteBtn"
|
||||
>
|
||||
{i18n.REMOVE}
|
||||
</MyRemoveButton>
|
||||
</MyActionButton>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
</MyEntriesDetails>
|
||||
);
|
||||
};
|
||||
|
||||
ExceptionEntriesComponent.displayName = 'ExceptionEntriesComponent';
|
||||
|
||||
export const ExceptionEntries = React.memo(ExceptionEntriesComponent);
|
||||
|
||||
ExceptionEntries.displayName = 'ExceptionEntries';
|
|
@ -1,162 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { storiesOf, addDecorator } from '@storybook/react';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import React from 'react';
|
||||
import { ThemeProvider } from 'styled-components';
|
||||
import { euiLightVars } from '@kbn/ui-theme';
|
||||
|
||||
import { getExceptionListItemSchemaMock } from '@kbn/lists-plugin/common/schemas/response/exception_list_item_schema.mock';
|
||||
import { getCommentsArrayMock } from '@kbn/lists-plugin/common/schemas/types/comment.mock';
|
||||
import { ExceptionItem } from '.';
|
||||
|
||||
addDecorator((storyFn) => (
|
||||
<ThemeProvider theme={() => ({ eui: euiLightVars, darkMode: false })}>{storyFn()}</ThemeProvider>
|
||||
));
|
||||
|
||||
storiesOf('Components/ExceptionItem', module)
|
||||
.add('with os', () => {
|
||||
const payload = getExceptionListItemSchemaMock();
|
||||
payload.description = '';
|
||||
payload.comments = [];
|
||||
payload.entries = [
|
||||
{
|
||||
field: 'actingProcess.file.signer',
|
||||
type: 'match',
|
||||
operator: 'included',
|
||||
value: 'Elastic, N.V.',
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<ExceptionItem
|
||||
disableActions={false}
|
||||
loadingItemIds={[]}
|
||||
commentsAccordionId={'accordion--comments'}
|
||||
exceptionItem={payload}
|
||||
onDeleteException={action('onClick')}
|
||||
onEditException={action('onClick')}
|
||||
/>
|
||||
);
|
||||
})
|
||||
.add('with description', () => {
|
||||
const payload = getExceptionListItemSchemaMock();
|
||||
payload.comments = [];
|
||||
payload.entries = [
|
||||
{
|
||||
field: 'actingProcess.file.signer',
|
||||
type: 'match',
|
||||
operator: 'included',
|
||||
value: 'Elastic, N.V.',
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<ExceptionItem
|
||||
disableActions={false}
|
||||
loadingItemIds={[]}
|
||||
commentsAccordionId={'accordion--comments'}
|
||||
exceptionItem={payload}
|
||||
onDeleteException={action('onClick')}
|
||||
onEditException={action('onClick')}
|
||||
/>
|
||||
);
|
||||
})
|
||||
.add('with comments', () => {
|
||||
const payload = getExceptionListItemSchemaMock();
|
||||
payload.description = '';
|
||||
payload.comments = getCommentsArrayMock();
|
||||
payload.entries = [
|
||||
{
|
||||
field: 'actingProcess.file.signer',
|
||||
type: 'match',
|
||||
operator: 'included',
|
||||
value: 'Elastic, N.V.',
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<ExceptionItem
|
||||
disableActions={false}
|
||||
loadingItemIds={[]}
|
||||
commentsAccordionId={'accordion--comments'}
|
||||
exceptionItem={payload}
|
||||
onDeleteException={action('onClick')}
|
||||
onEditException={action('onClick')}
|
||||
/>
|
||||
);
|
||||
})
|
||||
.add('with nested entries', () => {
|
||||
const payload = getExceptionListItemSchemaMock();
|
||||
payload.description = '';
|
||||
payload.comments = [];
|
||||
|
||||
return (
|
||||
<ExceptionItem
|
||||
disableActions={false}
|
||||
loadingItemIds={[]}
|
||||
commentsAccordionId={'accordion--comments'}
|
||||
exceptionItem={payload}
|
||||
onDeleteException={action('onClick')}
|
||||
onEditException={action('onClick')}
|
||||
/>
|
||||
);
|
||||
})
|
||||
.add('with everything', () => {
|
||||
const payload = getExceptionListItemSchemaMock();
|
||||
payload.comments = getCommentsArrayMock();
|
||||
return (
|
||||
<ExceptionItem
|
||||
disableActions={false}
|
||||
loadingItemIds={[]}
|
||||
commentsAccordionId={'accordion--comments'}
|
||||
exceptionItem={payload}
|
||||
onDeleteException={action('onClick')}
|
||||
onEditException={action('onClick')}
|
||||
/>
|
||||
);
|
||||
})
|
||||
.add('with loadingItemIds', () => {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
const { id, namespace_type, ...rest } = getExceptionListItemSchemaMock();
|
||||
|
||||
return (
|
||||
<ExceptionItem
|
||||
disableActions={false}
|
||||
loadingItemIds={[{ id, namespaceType: namespace_type }]}
|
||||
commentsAccordionId={'accordion--comments'}
|
||||
exceptionItem={{ id, namespace_type, ...rest }}
|
||||
onDeleteException={action('onClick')}
|
||||
onEditException={action('onClick')}
|
||||
/>
|
||||
);
|
||||
})
|
||||
.add('with actions disabled', () => {
|
||||
const payload = getExceptionListItemSchemaMock();
|
||||
payload.description = '';
|
||||
payload.comments = getCommentsArrayMock();
|
||||
payload.entries = [
|
||||
{
|
||||
field: 'actingProcess.file.signer',
|
||||
type: 'match',
|
||||
operator: 'included',
|
||||
value: 'Elastic, N.V.',
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<ExceptionItem
|
||||
disableActions
|
||||
loadingItemIds={[]}
|
||||
commentsAccordionId={'accordion--comments'}
|
||||
exceptionItem={payload}
|
||||
onDeleteException={action('onClick')}
|
||||
onEditException={action('onClick')}
|
||||
/>
|
||||
);
|
||||
});
|
|
@ -1,127 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import {
|
||||
EuiPanel,
|
||||
EuiFlexGroup,
|
||||
EuiCommentProps,
|
||||
EuiCommentList,
|
||||
EuiAccordion,
|
||||
EuiFlexItem,
|
||||
} from '@elastic/eui';
|
||||
import React, { useEffect, useState, useMemo, useCallback } from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import type { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types';
|
||||
import { ExceptionDetails } from './exception_details';
|
||||
import { ExceptionEntries } from './exception_entries';
|
||||
import { getFormattedComments } from '../../helpers';
|
||||
import { getFormattedEntries } from '../helpers';
|
||||
import type { FormattedEntry, ExceptionListItemIdentifiers } from '../../types';
|
||||
|
||||
const MyFlexItem = styled(EuiFlexItem)`
|
||||
&.comments--show {
|
||||
padding: ${({ theme }) => theme.eui.euiSize};
|
||||
border-top: ${({ theme }) => `${theme.eui.euiBorderThin}`};
|
||||
}
|
||||
`;
|
||||
|
||||
export interface ExceptionItemProps {
|
||||
loadingItemIds: ExceptionListItemIdentifiers[];
|
||||
exceptionItem: ExceptionListItemSchema;
|
||||
commentsAccordionId: string;
|
||||
onDeleteException: (arg: ExceptionListItemIdentifiers) => void;
|
||||
onEditException: (item: ExceptionListItemSchema) => void;
|
||||
showName?: boolean;
|
||||
showModified?: boolean;
|
||||
disableActions: boolean;
|
||||
'data-test-subj'?: string;
|
||||
}
|
||||
|
||||
const ExceptionItemComponent = ({
|
||||
disableActions,
|
||||
loadingItemIds,
|
||||
exceptionItem,
|
||||
commentsAccordionId,
|
||||
onDeleteException,
|
||||
onEditException,
|
||||
showModified = false,
|
||||
showName = false,
|
||||
'data-test-subj': dataTestSubj,
|
||||
}: ExceptionItemProps): JSX.Element => {
|
||||
const [entryItems, setEntryItems] = useState<FormattedEntry[]>([]);
|
||||
const [showComments, setShowComments] = useState(false);
|
||||
|
||||
useEffect((): void => {
|
||||
const formattedEntries = getFormattedEntries(exceptionItem.entries);
|
||||
setEntryItems(formattedEntries);
|
||||
}, [exceptionItem.entries]);
|
||||
|
||||
const handleDelete = useCallback((): void => {
|
||||
onDeleteException({
|
||||
id: exceptionItem.id,
|
||||
namespaceType: exceptionItem.namespace_type,
|
||||
});
|
||||
}, [onDeleteException, exceptionItem.id, exceptionItem.namespace_type]);
|
||||
|
||||
const handleEdit = useCallback((): void => {
|
||||
onEditException(exceptionItem);
|
||||
}, [onEditException, exceptionItem]);
|
||||
|
||||
const onCommentsClick = useCallback((): void => {
|
||||
setShowComments(!showComments);
|
||||
}, [setShowComments, showComments]);
|
||||
|
||||
const formattedComments = useMemo((): EuiCommentProps[] => {
|
||||
return getFormattedComments(exceptionItem.comments);
|
||||
}, [exceptionItem.comments]);
|
||||
|
||||
const disableItemActions = useMemo((): boolean => {
|
||||
const foundItems = loadingItemIds.filter(({ id }) => id === exceptionItem.id);
|
||||
return foundItems.length > 0;
|
||||
}, [loadingItemIds, exceptionItem.id]);
|
||||
|
||||
return (
|
||||
<EuiPanel paddingSize="none" data-test-subj={dataTestSubj} hasBorder hasShadow={false}>
|
||||
<EuiFlexGroup direction="column" gutterSize="none">
|
||||
<EuiFlexItem>
|
||||
<EuiFlexGroup direction="row">
|
||||
<ExceptionDetails
|
||||
showComments={showComments}
|
||||
exceptionItem={exceptionItem}
|
||||
onCommentsClick={onCommentsClick}
|
||||
showModified={showModified}
|
||||
showName={showName}
|
||||
/>
|
||||
<ExceptionEntries
|
||||
disableActions={disableItemActions || disableActions}
|
||||
entries={entryItems}
|
||||
onDelete={handleDelete}
|
||||
onEdit={handleEdit}
|
||||
/>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
<MyFlexItem className={showComments ? 'comments--show' : ''}>
|
||||
<EuiAccordion
|
||||
id={commentsAccordionId}
|
||||
arrowDisplay="none"
|
||||
forceState={showComments ? 'open' : 'closed'}
|
||||
data-test-subj="exceptionsViewerCommentAccordion"
|
||||
>
|
||||
<EuiCommentList comments={formattedComments} />
|
||||
</EuiAccordion>
|
||||
</MyFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiPanel>
|
||||
);
|
||||
};
|
||||
|
||||
ExceptionItemComponent.displayName = 'ExceptionItemComponent';
|
||||
|
||||
export const ExceptionItem = React.memo(ExceptionItemComponent);
|
||||
|
||||
ExceptionItem.displayName = 'ExceptionItem';
|
|
@ -0,0 +1,111 @@
|
|||
/*
|
||||
* 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 { TestProviders } from '../../../../mock';
|
||||
import { ExceptionItemCardConditions } from './exception_item_card_conditions';
|
||||
|
||||
describe('ExceptionItemCardConditions', () => {
|
||||
it('it includes os condition if one exists', () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<ExceptionItemCardConditions
|
||||
os={['linux']}
|
||||
entries={[
|
||||
{
|
||||
field: 'host.name',
|
||||
operator: 'included',
|
||||
type: 'match',
|
||||
value: 'host',
|
||||
},
|
||||
{
|
||||
field: 'threat.indicator.port',
|
||||
operator: 'included',
|
||||
type: 'exists',
|
||||
},
|
||||
{
|
||||
entries: [
|
||||
{
|
||||
field: 'valid',
|
||||
operator: 'included',
|
||||
type: 'match',
|
||||
value: 'true',
|
||||
},
|
||||
],
|
||||
field: 'file.Ext.code_signature',
|
||||
type: 'nested',
|
||||
},
|
||||
]}
|
||||
dataTestSubj="exceptionItemConditions"
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
// Text is gonna look a bit off unformatted
|
||||
expect(wrapper.find('[data-test-subj="exceptionItemConditions-os"]').at(0).text()).toEqual(
|
||||
' OSIS Linux'
|
||||
);
|
||||
expect(
|
||||
wrapper.find('[data-test-subj="exceptionItemConditions-condition"]').at(0).text()
|
||||
).toEqual(' host.nameIS host');
|
||||
expect(
|
||||
wrapper.find('[data-test-subj="exceptionItemConditions-condition"]').at(1).text()
|
||||
).toEqual('AND threat.indicator.portexists ');
|
||||
expect(
|
||||
wrapper.find('[data-test-subj="exceptionItemConditions-condition"]').at(2).text()
|
||||
).toEqual('AND file.Ext.code_signature validIS true');
|
||||
});
|
||||
|
||||
it('it renders item conditions', () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<ExceptionItemCardConditions
|
||||
entries={[
|
||||
{
|
||||
field: 'host.name',
|
||||
operator: 'included',
|
||||
type: 'match',
|
||||
value: 'host',
|
||||
},
|
||||
{
|
||||
field: 'threat.indicator.port',
|
||||
operator: 'included',
|
||||
type: 'exists',
|
||||
},
|
||||
{
|
||||
entries: [
|
||||
{
|
||||
field: 'valid',
|
||||
operator: 'included',
|
||||
type: 'match',
|
||||
value: 'true',
|
||||
},
|
||||
],
|
||||
field: 'file.Ext.code_signature',
|
||||
type: 'nested',
|
||||
},
|
||||
]}
|
||||
dataTestSubj="exceptionItemConditions"
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
// Text is gonna look a bit off unformatted
|
||||
expect(wrapper.find('[data-test-subj="exceptionItemConditions-os"]').exists()).toBeFalsy();
|
||||
expect(
|
||||
wrapper.find('[data-test-subj="exceptionItemConditions-condition"]').at(0).text()
|
||||
).toEqual(' host.nameIS host');
|
||||
expect(
|
||||
wrapper.find('[data-test-subj="exceptionItemConditions-condition"]').at(1).text()
|
||||
).toEqual('AND threat.indicator.portexists ');
|
||||
expect(
|
||||
wrapper.find('[data-test-subj="exceptionItemConditions-condition"]').at(2).text()
|
||||
).toEqual('AND file.Ext.code_signature validIS true');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,160 @@
|
|||
/*
|
||||
* 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, { memo, useMemo, useCallback } from 'react';
|
||||
import { EuiExpression, EuiToken, EuiFlexGroup, EuiFlexItem, EuiBadge } from '@elastic/eui';
|
||||
import styled from 'styled-components';
|
||||
import {
|
||||
ExceptionListItemSchema,
|
||||
ListOperatorTypeEnum,
|
||||
NonEmptyNestedEntriesArray,
|
||||
} from '@kbn/securitysolution-io-ts-list-types';
|
||||
|
||||
import * as i18n from './translations';
|
||||
|
||||
const OS_LABELS = Object.freeze({
|
||||
linux: i18n.OS_LINUX,
|
||||
mac: i18n.OS_MAC,
|
||||
macos: i18n.OS_MAC,
|
||||
windows: i18n.OS_WINDOWS,
|
||||
});
|
||||
|
||||
const OPERATOR_TYPE_LABELS_INCLUDED = Object.freeze({
|
||||
[ListOperatorTypeEnum.NESTED]: i18n.CONDITION_OPERATOR_TYPE_NESTED,
|
||||
[ListOperatorTypeEnum.MATCH_ANY]: i18n.CONDITION_OPERATOR_TYPE_MATCH_ANY,
|
||||
[ListOperatorTypeEnum.MATCH]: i18n.CONDITION_OPERATOR_TYPE_MATCH,
|
||||
[ListOperatorTypeEnum.WILDCARD]: i18n.CONDITION_OPERATOR_TYPE_WILDCARD_MATCHES,
|
||||
[ListOperatorTypeEnum.EXISTS]: i18n.CONDITION_OPERATOR_TYPE_EXISTS,
|
||||
[ListOperatorTypeEnum.LIST]: i18n.CONDITION_OPERATOR_TYPE_LIST,
|
||||
});
|
||||
|
||||
const OPERATOR_TYPE_LABELS_EXCLUDED = Object.freeze({
|
||||
[ListOperatorTypeEnum.MATCH_ANY]: i18n.CONDITION_OPERATOR_TYPE_NOT_MATCH_ANY,
|
||||
[ListOperatorTypeEnum.MATCH]: i18n.CONDITION_OPERATOR_TYPE_NOT_MATCH,
|
||||
});
|
||||
|
||||
const EuiFlexGroupNested = styled(EuiFlexGroup)`
|
||||
margin-left: ${({ theme }) => theme.eui.euiSizeXL};
|
||||
`;
|
||||
|
||||
const EuiFlexItemNested = styled(EuiFlexItem)`
|
||||
margin-bottom: 6px !important;
|
||||
margin-top: 6px !important;
|
||||
`;
|
||||
|
||||
const StyledCondition = styled('span')`
|
||||
margin-right: 6px;
|
||||
`;
|
||||
|
||||
export interface CriteriaConditionsProps {
|
||||
entries: ExceptionListItemSchema['entries'];
|
||||
dataTestSubj: string;
|
||||
os?: ExceptionListItemSchema['os_types'];
|
||||
}
|
||||
|
||||
export const ExceptionItemCardConditions = memo<CriteriaConditionsProps>(
|
||||
({ os, entries, dataTestSubj }) => {
|
||||
const osLabel = useMemo(() => {
|
||||
if (os != null && os.length > 0) {
|
||||
return os
|
||||
.map((osValue) => OS_LABELS[osValue as keyof typeof OS_LABELS] ?? osValue)
|
||||
.join(', ');
|
||||
}
|
||||
|
||||
return null;
|
||||
}, [os]);
|
||||
|
||||
const getEntryValue = (type: string, value: string | string[] | undefined) => {
|
||||
if (type === 'match_any' && Array.isArray(value)) {
|
||||
return value.map((currentValue) => <EuiBadge color="hollow">{currentValue}</EuiBadge>);
|
||||
}
|
||||
return value ?? '';
|
||||
};
|
||||
|
||||
const getEntryOperator = (type: string, operator: string) => {
|
||||
if (type === 'nested') return '';
|
||||
return operator === 'included'
|
||||
? OPERATOR_TYPE_LABELS_INCLUDED[type as keyof typeof OPERATOR_TYPE_LABELS_INCLUDED] ?? type
|
||||
: OPERATOR_TYPE_LABELS_EXCLUDED[type as keyof typeof OPERATOR_TYPE_LABELS_EXCLUDED] ?? type;
|
||||
};
|
||||
|
||||
const getNestedEntriesContent = useCallback(
|
||||
(type: string, nestedEntries: NonEmptyNestedEntriesArray) => {
|
||||
if (type === 'nested' && nestedEntries.length) {
|
||||
return nestedEntries.map((entry) => {
|
||||
const { field: nestedField, type: nestedType, operator: nestedOperator } = entry;
|
||||
const nestedValue = 'value' in entry ? entry.value : '';
|
||||
|
||||
return (
|
||||
<EuiFlexGroupNested
|
||||
data-test-subj={`${dataTestSubj}-nestedCondition`}
|
||||
key={nestedField + nestedType + nestedValue}
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
gutterSize="m"
|
||||
responsive={false}
|
||||
>
|
||||
<EuiFlexItemNested grow={false}>
|
||||
<EuiToken iconType="tokenNested" size="s" />
|
||||
</EuiFlexItemNested>
|
||||
<EuiFlexItemNested grow={false}>
|
||||
<EuiExpression description={''} value={nestedField} color="subdued" />
|
||||
</EuiFlexItemNested>
|
||||
<EuiFlexItemNested grow={false}>
|
||||
<EuiExpression
|
||||
description={getEntryOperator(nestedType, nestedOperator)}
|
||||
value={getEntryValue(nestedType, nestedValue)}
|
||||
/>
|
||||
</EuiFlexItemNested>
|
||||
</EuiFlexGroupNested>
|
||||
);
|
||||
});
|
||||
}
|
||||
},
|
||||
[dataTestSubj]
|
||||
);
|
||||
|
||||
return (
|
||||
<div data-test-subj={dataTestSubj}>
|
||||
{osLabel != null && (
|
||||
<div data-test-subj={`${dataTestSubj}-os`}>
|
||||
<strong>
|
||||
<EuiExpression description={''} value={i18n.CONDITION_OS} />
|
||||
<EuiExpression description={i18n.CONDITION_OPERATOR_TYPE_MATCH} value={osLabel} />
|
||||
</strong>
|
||||
</div>
|
||||
)}
|
||||
{entries.map((entry, index) => {
|
||||
const { field, type } = entry;
|
||||
const value = 'value' in entry ? entry.value : '';
|
||||
const nestedEntries = 'entries' in entry ? entry.entries : [];
|
||||
const operator = 'operator' in entry ? entry.operator : '';
|
||||
|
||||
return (
|
||||
<div data-test-subj={`${dataTestSubj}-condition`} key={field + type + value + index}>
|
||||
<div className="eui-xScroll">
|
||||
<EuiExpression
|
||||
description={
|
||||
index === 0 ? '' : <StyledCondition>{i18n.CONDITION_AND}</StyledCondition>
|
||||
}
|
||||
value={field}
|
||||
color={index === 0 ? 'primary' : 'subdued'}
|
||||
/>
|
||||
<EuiExpression
|
||||
description={getEntryOperator(type, operator)}
|
||||
value={getEntryValue(type, value)}
|
||||
/>
|
||||
</div>
|
||||
{nestedEntries != null && getNestedEntriesContent(type, nestedEntries)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
ExceptionItemCardConditions.displayName = 'ExceptionItemCardConditions';
|
|
@ -0,0 +1,128 @@
|
|||
/*
|
||||
* 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 { getExceptionListItemSchemaMock } from '@kbn/lists-plugin/common/schemas/response/exception_list_item_schema.mock';
|
||||
import { ThemeProvider } from 'styled-components';
|
||||
|
||||
import * as i18n from './translations';
|
||||
import { ExceptionItemCardHeader } from './exception_item_card_header';
|
||||
import { getMockTheme } from '../../../../lib/kibana/kibana_react.mock';
|
||||
|
||||
const mockTheme = getMockTheme({
|
||||
eui: {
|
||||
euiSize: '10px',
|
||||
euiColorPrimary: '#ece',
|
||||
euiColorDanger: '#ece',
|
||||
},
|
||||
});
|
||||
|
||||
describe('ExceptionItemCardHeader', () => {
|
||||
it('it renders item name', () => {
|
||||
const wrapper = mount(
|
||||
<ThemeProvider theme={mockTheme}>
|
||||
<ExceptionItemCardHeader
|
||||
item={getExceptionListItemSchemaMock()}
|
||||
dataTestSubj="exceptionItemHeader"
|
||||
actions={[
|
||||
{
|
||||
key: 'edit',
|
||||
icon: 'pencil',
|
||||
label: i18n.EXCEPTION_ITEM_EDIT_BUTTON,
|
||||
onClick: jest.fn(),
|
||||
},
|
||||
{
|
||||
key: 'delete',
|
||||
icon: 'trash',
|
||||
label: i18n.EXCEPTION_ITEM_DELETE_BUTTON,
|
||||
onClick: jest.fn(),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
||||
expect(wrapper.find('[data-test-subj="exceptionItemHeader-title"]').at(0).text()).toEqual(
|
||||
'some name'
|
||||
);
|
||||
});
|
||||
|
||||
it('it displays actions', () => {
|
||||
const handleEdit = jest.fn();
|
||||
const handleDelete = jest.fn();
|
||||
const wrapper = mount(
|
||||
<ThemeProvider theme={mockTheme}>
|
||||
<ExceptionItemCardHeader
|
||||
actions={[
|
||||
{
|
||||
key: 'edit',
|
||||
icon: 'pencil',
|
||||
label: i18n.EXCEPTION_ITEM_EDIT_BUTTON,
|
||||
onClick: handleEdit,
|
||||
},
|
||||
{
|
||||
key: 'delete',
|
||||
icon: 'trash',
|
||||
label: i18n.EXCEPTION_ITEM_DELETE_BUTTON,
|
||||
onClick: handleDelete,
|
||||
},
|
||||
]}
|
||||
item={getExceptionListItemSchemaMock()}
|
||||
dataTestSubj="exceptionItemHeader"
|
||||
/>
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
||||
// click on popover
|
||||
wrapper
|
||||
.find('button[data-test-subj="exceptionItemHeader-actionButton"]')
|
||||
.at(0)
|
||||
.simulate('click');
|
||||
|
||||
wrapper.find('button[data-test-subj="exceptionItemHeader-actionItem-edit"]').simulate('click');
|
||||
expect(handleEdit).toHaveBeenCalled();
|
||||
|
||||
wrapper
|
||||
.find('button[data-test-subj="exceptionItemHeader-actionItem-delete"]')
|
||||
.simulate('click');
|
||||
expect(handleDelete).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('it disables actions if disableActions is true', () => {
|
||||
const handleEdit = jest.fn();
|
||||
const handleDelete = jest.fn();
|
||||
const wrapper = mount(
|
||||
<ThemeProvider theme={mockTheme}>
|
||||
<ExceptionItemCardHeader
|
||||
actions={[
|
||||
{
|
||||
key: 'edit',
|
||||
icon: 'pencil',
|
||||
label: i18n.EXCEPTION_ITEM_EDIT_BUTTON,
|
||||
onClick: handleEdit,
|
||||
},
|
||||
{
|
||||
key: 'delete',
|
||||
icon: 'trash',
|
||||
label: i18n.EXCEPTION_ITEM_DELETE_BUTTON,
|
||||
onClick: handleDelete,
|
||||
},
|
||||
]}
|
||||
item={getExceptionListItemSchemaMock()}
|
||||
disableActions
|
||||
dataTestSubj="exceptionItemHeader"
|
||||
/>
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
||||
expect(
|
||||
wrapper.find('button[data-test-subj="exceptionItemHeader-actionButton"]').at(0).props()
|
||||
.disabled
|
||||
).toBeTruthy();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,82 @@
|
|||
/*
|
||||
* 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, { memo, useMemo, useState } from 'react';
|
||||
import {
|
||||
EuiButtonIcon,
|
||||
EuiContextMenuPanelProps,
|
||||
EuiContextMenuPanel,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiPopover,
|
||||
EuiTitle,
|
||||
EuiContextMenuItem,
|
||||
} from '@elastic/eui';
|
||||
import { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types';
|
||||
|
||||
export interface ExceptionItemCardHeaderProps {
|
||||
item: ExceptionListItemSchema;
|
||||
actions: Array<{ key: string; icon: string; label: string; onClick: () => void }>;
|
||||
disableActions?: boolean;
|
||||
dataTestSubj: string;
|
||||
}
|
||||
|
||||
export const ExceptionItemCardHeader = memo<ExceptionItemCardHeaderProps>(
|
||||
({ item, actions, disableActions = false, dataTestSubj }) => {
|
||||
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
|
||||
|
||||
const onItemActionsClick = () => setIsPopoverOpen((isOpen) => !isOpen);
|
||||
const onClosePopover = () => setIsPopoverOpen(false);
|
||||
|
||||
const itemActions = useMemo((): EuiContextMenuPanelProps['items'] => {
|
||||
return actions.map((action) => (
|
||||
<EuiContextMenuItem
|
||||
data-test-subj={`${dataTestSubj}-actionItem-${action.key}`}
|
||||
key={action.key}
|
||||
icon={action.icon}
|
||||
onClick={() => {
|
||||
onClosePopover();
|
||||
action.onClick();
|
||||
}}
|
||||
>
|
||||
{action.label}
|
||||
</EuiContextMenuItem>
|
||||
));
|
||||
}, [dataTestSubj, actions]);
|
||||
|
||||
return (
|
||||
<EuiFlexGroup data-test-subj={dataTestSubj} justifyContent="spaceBetween">
|
||||
<EuiFlexItem grow={9}>
|
||||
<EuiTitle size="xs" textTransform="uppercase" data-test-subj={`${dataTestSubj}-title`}>
|
||||
<h3>{item.name}</h3>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiPopover
|
||||
button={
|
||||
<EuiButtonIcon
|
||||
isDisabled={disableActions}
|
||||
aria-label="Exception item actions menu"
|
||||
iconType="boxesHorizontal"
|
||||
onClick={onItemActionsClick}
|
||||
data-test-subj={`${dataTestSubj}-actionButton`}
|
||||
/>
|
||||
}
|
||||
panelPaddingSize="none"
|
||||
isOpen={isPopoverOpen}
|
||||
closePopover={onClosePopover}
|
||||
data-test-subj={`${dataTestSubj}-items`}
|
||||
>
|
||||
<EuiContextMenuPanel size="s" items={itemActions} />
|
||||
</EuiPopover>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
ExceptionItemCardHeader.displayName = 'ExceptionItemCardHeader';
|
|
@ -0,0 +1,51 @@
|
|||
/*
|
||||
* 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 { getExceptionListItemSchemaMock } from '@kbn/lists-plugin/common/schemas/response/exception_list_item_schema.mock';
|
||||
|
||||
import { TestProviders } from '../../../../mock';
|
||||
import { ExceptionItemCardMetaInfo } from './exception_item_card_meta';
|
||||
|
||||
describe('ExceptionItemCardMetaInfo', () => {
|
||||
it('it renders item creation info', () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<ExceptionItemCardMetaInfo
|
||||
item={getExceptionListItemSchemaMock()}
|
||||
dataTestSubj="exceptionItemMeta"
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(
|
||||
wrapper.find('[data-test-subj="exceptionItemMeta-createdBy-value1"]').at(0).text()
|
||||
).toEqual('Apr 20, 2020 @ 15:25:31.830');
|
||||
expect(
|
||||
wrapper.find('[data-test-subj="exceptionItemMeta-createdBy-value2"]').at(0).text()
|
||||
).toEqual('some user');
|
||||
});
|
||||
|
||||
it('it renders item update info', () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<ExceptionItemCardMetaInfo
|
||||
item={getExceptionListItemSchemaMock()}
|
||||
dataTestSubj="exceptionItemMeta"
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(
|
||||
wrapper.find('[data-test-subj="exceptionItemMeta-updatedBy-value1"]').at(0).text()
|
||||
).toEqual('Apr 20, 2020 @ 15:25:31.830');
|
||||
expect(
|
||||
wrapper.find('[data-test-subj="exceptionItemMeta-updatedBy-value2"]').at(0).text()
|
||||
).toEqual('some user');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,111 @@
|
|||
/*
|
||||
* 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, { memo } from 'react';
|
||||
import { EuiAvatar, EuiBadge, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui';
|
||||
import { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import * as i18n from './translations';
|
||||
import { FormattedDate, FormattedRelativePreferenceDate } from '../../../formatted_date';
|
||||
|
||||
const StyledCondition = styled('div')`
|
||||
padding-top: 4px !important;
|
||||
`;
|
||||
export interface ExceptionItemCardMetaInfoProps {
|
||||
item: ExceptionListItemSchema;
|
||||
dataTestSubj: string;
|
||||
}
|
||||
|
||||
export const ExceptionItemCardMetaInfo = memo<ExceptionItemCardMetaInfoProps>(
|
||||
({ item, dataTestSubj }) => {
|
||||
return (
|
||||
<EuiFlexGroup
|
||||
alignItems="center"
|
||||
responsive={false}
|
||||
gutterSize="s"
|
||||
data-test-subj={dataTestSubj}
|
||||
>
|
||||
<EuiFlexItem grow={false}>
|
||||
<MetaInfoDetails
|
||||
fieldName="created_by"
|
||||
label={i18n.EXCEPTION_ITEM_CREATED_LABEL}
|
||||
value1={<FormattedDate fieldName="created_by" value={item.created_at} />}
|
||||
value2={item.created_by}
|
||||
dataTestSubj={`${dataTestSubj}-createdBy`}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<MetaInfoDetails
|
||||
fieldName="updated_by"
|
||||
label={i18n.EXCEPTION_ITEM_UPDATED_LABEL}
|
||||
value1={
|
||||
<StyledCondition>
|
||||
<FormattedRelativePreferenceDate
|
||||
value={item.updated_at}
|
||||
tooltipFieldName="updated_by"
|
||||
tooltipAnchorClassName="eui-textTruncate"
|
||||
/>
|
||||
</StyledCondition>
|
||||
}
|
||||
value2={item.updated_by}
|
||||
dataTestSubj={`${dataTestSubj}-updatedBy`}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
||||
);
|
||||
ExceptionItemCardMetaInfo.displayName = 'ExceptionItemCardMetaInfo';
|
||||
|
||||
interface MetaInfoDetailsProps {
|
||||
fieldName: string;
|
||||
label: string;
|
||||
value1: JSX.Element | string;
|
||||
value2: string;
|
||||
dataTestSubj: string;
|
||||
}
|
||||
|
||||
const MetaInfoDetails = memo<MetaInfoDetailsProps>(({ label, value1, value2, dataTestSubj }) => {
|
||||
return (
|
||||
<EuiFlexGroup alignItems="center" gutterSize="s" wrap={false} responsive={false}>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiBadge color="default" style={{ fontFamily: 'Inter' }}>
|
||||
{label}
|
||||
</EuiBadge>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false} data-test-subj={`${dataTestSubj}-value1`}>
|
||||
<EuiText size="xs" style={{ fontFamily: 'Inter' }}>
|
||||
{value1}
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiText size="xs" style={{ fontStyle: 'italic', fontFamily: 'Inter' }}>
|
||||
{i18n.EXCEPTION_ITEM_META_BY}
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFlexGroup responsive={false} gutterSize="xs" alignItems="center" wrap={false}>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiAvatar initialsLength={2} name={value2.toUpperCase()} size="s" />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false} className="eui-textTruncate">
|
||||
<EuiText
|
||||
size="xs"
|
||||
style={{ fontFamily: 'Inter' }}
|
||||
data-test-subj={`${dataTestSubj}-value2`}
|
||||
>
|
||||
{value2}
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
});
|
||||
|
||||
MetaInfoDetails.displayName = 'MetaInfoDetails';
|
|
@ -9,7 +9,7 @@ import React from 'react';
|
|||
import { ThemeProvider } from 'styled-components';
|
||||
import { mount } from 'enzyme';
|
||||
|
||||
import { ExceptionItem } from '.';
|
||||
import { ExceptionItemCard } from '.';
|
||||
import { getExceptionListItemSchemaMock } from '@kbn/lists-plugin/common/schemas/response/exception_list_item_schema.mock';
|
||||
import { getCommentsArrayMock } from '@kbn/lists-plugin/common/schemas/types/comment.mock';
|
||||
import { getMockTheme } from '../../../../lib/kibana/kibana_react.mock';
|
||||
|
@ -25,75 +25,72 @@ const mockTheme = getMockTheme({
|
|||
},
|
||||
});
|
||||
|
||||
describe('ExceptionItem', () => {
|
||||
it('it renders ExceptionDetails and ExceptionEntries', () => {
|
||||
const exceptionItem = getExceptionListItemSchemaMock();
|
||||
describe('ExceptionItemCard', () => {
|
||||
it('it renders header, item meta information and conditions', () => {
|
||||
const exceptionItem = { ...getExceptionListItemSchemaMock(), comments: [] };
|
||||
|
||||
const wrapper = mount(
|
||||
<ThemeProvider theme={mockTheme}>
|
||||
<ExceptionItem
|
||||
<ExceptionItemCard
|
||||
disableActions={false}
|
||||
loadingItemIds={[]}
|
||||
commentsAccordionId={'accordion--comments'}
|
||||
onDeleteException={jest.fn()}
|
||||
onEditException={jest.fn()}
|
||||
exceptionItem={exceptionItem}
|
||||
dataTestSubj="item"
|
||||
/>
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
||||
expect(wrapper.find('ExceptionDetails')).toHaveLength(1);
|
||||
expect(wrapper.find('ExceptionEntries')).toHaveLength(1);
|
||||
expect(wrapper.find('ExceptionItemCardHeader')).toHaveLength(1);
|
||||
expect(wrapper.find('ExceptionItemCardMetaInfo')).toHaveLength(1);
|
||||
expect(wrapper.find('ExceptionItemCardConditions')).toHaveLength(1);
|
||||
expect(
|
||||
wrapper.find('[data-test-subj="exceptionsViewerCommentAccordion"]').exists()
|
||||
).toBeFalsy();
|
||||
});
|
||||
|
||||
it('it renders ExceptionDetails with Name and Modified info when showName and showModified are true ', () => {
|
||||
const exceptionItem = getExceptionListItemSchemaMock();
|
||||
it('it renders header, item meta information, conditions, and comments if any exist', () => {
|
||||
const exceptionItem = { ...getExceptionListItemSchemaMock(), comments: getCommentsArrayMock() };
|
||||
|
||||
const wrapper = mount(
|
||||
<ThemeProvider theme={mockTheme}>
|
||||
<ExceptionItem
|
||||
<ExceptionItemCard
|
||||
disableActions={false}
|
||||
loadingItemIds={[]}
|
||||
commentsAccordionId={'accordion--comments'}
|
||||
onDeleteException={jest.fn()}
|
||||
onEditException={jest.fn()}
|
||||
exceptionItem={exceptionItem}
|
||||
showModified={true}
|
||||
showName={true}
|
||||
dataTestSubj="item"
|
||||
/>
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
||||
expect(wrapper.find('ExceptionDetails').props()).toEqual(
|
||||
expect.objectContaining({
|
||||
showModified: true,
|
||||
showName: true,
|
||||
})
|
||||
);
|
||||
expect(wrapper.find('ExceptionItemCardHeader')).toHaveLength(1);
|
||||
expect(wrapper.find('ExceptionItemCardMetaInfo')).toHaveLength(1);
|
||||
expect(wrapper.find('ExceptionItemCardConditions')).toHaveLength(1);
|
||||
expect(
|
||||
wrapper.find('[data-test-subj="exceptionsViewerCommentAccordion"]').exists()
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
it('it does not render edit or delete action buttons when "disableActions" is "true"', () => {
|
||||
const mockOnEditException = jest.fn();
|
||||
const exceptionItem = getExceptionListItemSchemaMock();
|
||||
|
||||
const wrapper = mount(
|
||||
<ThemeProvider theme={mockTheme}>
|
||||
<ExceptionItem
|
||||
<ExceptionItemCard
|
||||
disableActions
|
||||
loadingItemIds={[]}
|
||||
commentsAccordionId={'accordion--comments'}
|
||||
onDeleteException={jest.fn()}
|
||||
onEditException={mockOnEditException}
|
||||
onEditException={jest.fn()}
|
||||
exceptionItem={exceptionItem}
|
||||
dataTestSubj="item"
|
||||
/>
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
||||
const editBtn = wrapper.find('[data-test-subj="exceptionsViewerEditBtn"] button');
|
||||
const deleteBtn = wrapper.find('[data-test-subj="exceptionsViewerDeleteBtn"] button');
|
||||
|
||||
expect(editBtn).toHaveLength(0);
|
||||
expect(deleteBtn).toHaveLength(0);
|
||||
expect(wrapper.find('button[data-test-subj="item-actionButton"]').exists()).toBeFalsy();
|
||||
});
|
||||
|
||||
it('it invokes "onEditException" when edit button clicked', () => {
|
||||
|
@ -102,19 +99,25 @@ describe('ExceptionItem', () => {
|
|||
|
||||
const wrapper = mount(
|
||||
<ThemeProvider theme={mockTheme}>
|
||||
<ExceptionItem
|
||||
<ExceptionItemCard
|
||||
disableActions={false}
|
||||
loadingItemIds={[]}
|
||||
commentsAccordionId={'accordion--comments'}
|
||||
onDeleteException={jest.fn()}
|
||||
onEditException={mockOnEditException}
|
||||
exceptionItem={exceptionItem}
|
||||
dataTestSubj="item"
|
||||
/>
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
||||
const editBtn = wrapper.find('[data-test-subj="exceptionsViewerEditBtn"] button').at(0);
|
||||
editBtn.simulate('click');
|
||||
// click on popover
|
||||
wrapper
|
||||
.find('button[data-test-subj="exceptionItemCardHeader-actionButton"]')
|
||||
.at(0)
|
||||
.simulate('click');
|
||||
wrapper
|
||||
.find('button[data-test-subj="exceptionItemCardHeader-actionItem-edit"]')
|
||||
.simulate('click');
|
||||
|
||||
expect(mockOnEditException).toHaveBeenCalledWith(getExceptionListItemSchemaMock());
|
||||
});
|
||||
|
@ -125,19 +128,25 @@ describe('ExceptionItem', () => {
|
|||
|
||||
const wrapper = mount(
|
||||
<ThemeProvider theme={mockTheme}>
|
||||
<ExceptionItem
|
||||
<ExceptionItemCard
|
||||
disableActions={false}
|
||||
loadingItemIds={[]}
|
||||
commentsAccordionId={'accordion--comments'}
|
||||
onDeleteException={mockOnDeleteException}
|
||||
onEditException={jest.fn()}
|
||||
exceptionItem={exceptionItem}
|
||||
dataTestSubj="item"
|
||||
/>
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
||||
const deleteBtn = wrapper.find('[data-test-subj="exceptionsViewerDeleteBtn"] button').at(0);
|
||||
deleteBtn.simulate('click');
|
||||
// click on popover
|
||||
wrapper
|
||||
.find('button[data-test-subj="exceptionItemCardHeader-actionButton"]')
|
||||
.at(0)
|
||||
.simulate('click');
|
||||
wrapper
|
||||
.find('button[data-test-subj="exceptionItemCardHeader-actionItem-delete"]')
|
||||
.simulate('click');
|
||||
|
||||
expect(mockOnDeleteException).toHaveBeenCalledWith({
|
||||
id: '1',
|
||||
|
@ -146,47 +155,21 @@ describe('ExceptionItem', () => {
|
|||
});
|
||||
|
||||
it('it renders comment accordion closed to begin with', () => {
|
||||
const mockOnDeleteException = jest.fn();
|
||||
const exceptionItem = getExceptionListItemSchemaMock();
|
||||
exceptionItem.comments = getCommentsArrayMock();
|
||||
const wrapper = mount(
|
||||
<ThemeProvider theme={mockTheme}>
|
||||
<ExceptionItem
|
||||
<ExceptionItemCard
|
||||
disableActions={false}
|
||||
loadingItemIds={[]}
|
||||
commentsAccordionId={'accordion--comments'}
|
||||
onDeleteException={mockOnDeleteException}
|
||||
onDeleteException={jest.fn()}
|
||||
onEditException={jest.fn()}
|
||||
exceptionItem={exceptionItem}
|
||||
dataTestSubj="item"
|
||||
/>
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
||||
expect(wrapper.find('.euiAccordion-isOpen')).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('it renders comment accordion open when showComments is true', () => {
|
||||
const mockOnDeleteException = jest.fn();
|
||||
const exceptionItem = getExceptionListItemSchemaMock();
|
||||
exceptionItem.comments = getCommentsArrayMock();
|
||||
const wrapper = mount(
|
||||
<ThemeProvider theme={mockTheme}>
|
||||
<ExceptionItem
|
||||
disableActions={false}
|
||||
loadingItemIds={[]}
|
||||
commentsAccordionId={'accordion--comments'}
|
||||
onDeleteException={mockOnDeleteException}
|
||||
onEditException={jest.fn()}
|
||||
exceptionItem={exceptionItem}
|
||||
/>
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
||||
const commentsBtn = wrapper
|
||||
.find('.euiButtonEmpty[data-test-subj="exceptionsViewerItemCommentsBtn"]')
|
||||
.at(0);
|
||||
commentsBtn.simulate('click');
|
||||
|
||||
expect(wrapper.find('.euiAccordion-isOpen')).toHaveLength(1);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,131 @@
|
|||
/*
|
||||
* 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 {
|
||||
EuiPanel,
|
||||
EuiFlexGroup,
|
||||
EuiCommentProps,
|
||||
EuiCommentList,
|
||||
EuiAccordion,
|
||||
EuiFlexItem,
|
||||
EuiText,
|
||||
useEuiTheme,
|
||||
} from '@elastic/eui';
|
||||
import React, { useMemo, useCallback } from 'react';
|
||||
import type { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types';
|
||||
|
||||
import { getFormattedComments } from '../../helpers';
|
||||
import type { ExceptionListItemIdentifiers } from '../../types';
|
||||
import * as i18n from './translations';
|
||||
import { ExceptionItemCardHeader } from './exception_item_card_header';
|
||||
import { ExceptionItemCardConditions } from './exception_item_card_conditions';
|
||||
import { ExceptionItemCardMetaInfo } from './exception_item_card_meta';
|
||||
|
||||
export interface ExceptionItemProps {
|
||||
loadingItemIds: ExceptionListItemIdentifiers[];
|
||||
exceptionItem: ExceptionListItemSchema;
|
||||
onDeleteException: (arg: ExceptionListItemIdentifiers) => void;
|
||||
onEditException: (item: ExceptionListItemSchema) => void;
|
||||
disableActions: boolean;
|
||||
dataTestSubj: string;
|
||||
}
|
||||
|
||||
const ExceptionItemCardComponent = ({
|
||||
disableActions,
|
||||
loadingItemIds,
|
||||
exceptionItem,
|
||||
onDeleteException,
|
||||
onEditException,
|
||||
dataTestSubj,
|
||||
}: ExceptionItemProps): JSX.Element => {
|
||||
const { euiTheme } = useEuiTheme();
|
||||
|
||||
const handleDelete = useCallback((): void => {
|
||||
onDeleteException({
|
||||
id: exceptionItem.id,
|
||||
namespaceType: exceptionItem.namespace_type,
|
||||
});
|
||||
}, [onDeleteException, exceptionItem.id, exceptionItem.namespace_type]);
|
||||
|
||||
const handleEdit = useCallback((): void => {
|
||||
onEditException(exceptionItem);
|
||||
}, [onEditException, exceptionItem]);
|
||||
|
||||
const formattedComments = useMemo((): EuiCommentProps[] => {
|
||||
return getFormattedComments(exceptionItem.comments);
|
||||
}, [exceptionItem.comments]);
|
||||
|
||||
const disableItemActions = useMemo((): boolean => {
|
||||
const foundItems = loadingItemIds.some(({ id }) => id === exceptionItem.id);
|
||||
return disableActions || foundItems;
|
||||
}, [loadingItemIds, exceptionItem.id, disableActions]);
|
||||
|
||||
return (
|
||||
<EuiPanel paddingSize="l" data-test-subj={dataTestSubj} hasBorder hasShadow={false}>
|
||||
<EuiFlexGroup gutterSize="m" direction="column">
|
||||
<EuiFlexItem data-test-subj={`${dataTestSubj}-header`}>
|
||||
<ExceptionItemCardHeader
|
||||
item={exceptionItem}
|
||||
actions={[
|
||||
{
|
||||
key: 'edit',
|
||||
icon: 'pencil',
|
||||
label: i18n.EXCEPTION_ITEM_EDIT_BUTTON,
|
||||
onClick: handleEdit,
|
||||
},
|
||||
{
|
||||
key: 'delete',
|
||||
icon: 'trash',
|
||||
label: i18n.EXCEPTION_ITEM_DELETE_BUTTON,
|
||||
onClick: handleDelete,
|
||||
},
|
||||
]}
|
||||
disableActions={disableItemActions}
|
||||
dataTestSubj="exceptionItemCardHeader"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem data-test-subj={`${dataTestSubj}-meta`}>
|
||||
<ExceptionItemCardMetaInfo
|
||||
item={exceptionItem}
|
||||
dataTestSubj="exceptionItemCardMetaInfo"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<ExceptionItemCardConditions
|
||||
os={exceptionItem.os_types}
|
||||
entries={exceptionItem.entries}
|
||||
dataTestSubj="exceptionItemCardConditions"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
{formattedComments.length > 0 && (
|
||||
<EuiFlexItem>
|
||||
<EuiAccordion
|
||||
id="exceptionItemCardComments"
|
||||
buttonContent={
|
||||
<EuiText size="s" style={{ color: euiTheme.colors.primary }}>
|
||||
{i18n.exceptionItemCommentsAccordion(formattedComments.length)}
|
||||
</EuiText>
|
||||
}
|
||||
arrowDisplay="none"
|
||||
data-test-subj="exceptionsViewerCommentAccordion"
|
||||
>
|
||||
<EuiPanel hasBorder={false} hasShadow={false} paddingSize="m">
|
||||
<EuiCommentList comments={formattedComments} />
|
||||
</EuiPanel>
|
||||
</EuiAccordion>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
</EuiPanel>
|
||||
);
|
||||
};
|
||||
|
||||
ExceptionItemCardComponent.displayName = 'ExceptionItemCardComponent';
|
||||
|
||||
export const ExceptionItemCard = React.memo(ExceptionItemCardComponent);
|
||||
|
||||
ExceptionItemCard.displayName = 'ExceptionItemCard';
|
|
@ -0,0 +1,140 @@
|
|||
/*
|
||||
* 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 EXCEPTION_ITEM_EDIT_BUTTON = i18n.translate(
|
||||
'xpack.securitySolution.exceptions.exceptionItem.editItemButton',
|
||||
{
|
||||
defaultMessage: 'Edit item',
|
||||
}
|
||||
);
|
||||
|
||||
export const EXCEPTION_ITEM_DELETE_BUTTON = i18n.translate(
|
||||
'xpack.securitySolution.exceptions.exceptionItem.deleteItemButton',
|
||||
{
|
||||
defaultMessage: 'Delete item',
|
||||
}
|
||||
);
|
||||
|
||||
export const EXCEPTION_ITEM_CREATED_LABEL = i18n.translate(
|
||||
'xpack.securitySolution.exceptions.exceptionItem.createdLabel',
|
||||
{
|
||||
defaultMessage: 'Created',
|
||||
}
|
||||
);
|
||||
|
||||
export const EXCEPTION_ITEM_UPDATED_LABEL = i18n.translate(
|
||||
'xpack.securitySolution.exceptions.exceptionItem.updatedLabel',
|
||||
{
|
||||
defaultMessage: 'Updated',
|
||||
}
|
||||
);
|
||||
|
||||
export const EXCEPTION_ITEM_META_BY = i18n.translate(
|
||||
'xpack.securitySolution.exceptions.exceptionItem.metaDetailsBy',
|
||||
{
|
||||
defaultMessage: 'by',
|
||||
}
|
||||
);
|
||||
|
||||
export const exceptionItemCommentsAccordion = (comments: number) =>
|
||||
i18n.translate('xpack.securitySolution.exceptions.exceptionItem.showCommentsLabel', {
|
||||
values: { comments },
|
||||
defaultMessage: 'Show {comments, plural, =1 {comment} other {comments}} ({comments})',
|
||||
});
|
||||
|
||||
export const CONDITION_OPERATOR_TYPE_MATCH = i18n.translate(
|
||||
'xpack.securitySolution.exceptions.exceptionItem.conditions.matchOperator',
|
||||
{
|
||||
defaultMessage: 'IS',
|
||||
}
|
||||
);
|
||||
|
||||
export const CONDITION_OPERATOR_TYPE_NOT_MATCH = i18n.translate(
|
||||
'xpack.securitySolution.exceptions.exceptionItem.conditions.matchOperator.not',
|
||||
{
|
||||
defaultMessage: 'IS NOT',
|
||||
}
|
||||
);
|
||||
|
||||
export const CONDITION_OPERATOR_TYPE_WILDCARD_MATCHES = i18n.translate(
|
||||
'xpack.securitySolution.exceptions.exceptionItem.conditions.wildcardMatchesOperator',
|
||||
{
|
||||
defaultMessage: 'MATCHES',
|
||||
}
|
||||
);
|
||||
|
||||
export const CONDITION_OPERATOR_TYPE_NESTED = i18n.translate(
|
||||
'xpack.securitySolution.exceptions.exceptionItem.conditions.nestedOperator',
|
||||
{
|
||||
defaultMessage: 'has',
|
||||
}
|
||||
);
|
||||
|
||||
export const CONDITION_OPERATOR_TYPE_MATCH_ANY = i18n.translate(
|
||||
'xpack.securitySolution.exceptions.exceptionItem.conditions.matchAnyOperator',
|
||||
{
|
||||
defaultMessage: 'is one of',
|
||||
}
|
||||
);
|
||||
|
||||
export const CONDITION_OPERATOR_TYPE_NOT_MATCH_ANY = i18n.translate(
|
||||
'xpack.securitySolution.exceptions.exceptionItem.conditions.matchAnyOperator.not',
|
||||
{
|
||||
defaultMessage: 'is not one of',
|
||||
}
|
||||
);
|
||||
|
||||
export const CONDITION_OPERATOR_TYPE_EXISTS = i18n.translate(
|
||||
'xpack.securitySolution.exceptions.exceptionItem.conditions.existsOperator',
|
||||
{
|
||||
defaultMessage: 'exists',
|
||||
}
|
||||
);
|
||||
|
||||
export const CONDITION_OPERATOR_TYPE_LIST = i18n.translate(
|
||||
'xpack.securitySolution.exceptions.exceptionItem.conditions.listOperator',
|
||||
{
|
||||
defaultMessage: 'included in',
|
||||
}
|
||||
);
|
||||
|
||||
export const CONDITION_AND = i18n.translate(
|
||||
'xpack.securitySolution.exceptions.exceptionItem.conditions.and',
|
||||
{
|
||||
defaultMessage: 'AND',
|
||||
}
|
||||
);
|
||||
|
||||
export const CONDITION_OS = i18n.translate(
|
||||
'xpack.securitySolution.exceptions.exceptionItem.conditions.os',
|
||||
{
|
||||
defaultMessage: 'OS',
|
||||
}
|
||||
);
|
||||
|
||||
export const OS_WINDOWS = i18n.translate(
|
||||
'xpack.securitySolution.exceptions.exceptionItem.conditions.windows',
|
||||
{
|
||||
defaultMessage: 'Windows',
|
||||
}
|
||||
);
|
||||
|
||||
export const OS_LINUX = i18n.translate(
|
||||
'xpack.securitySolution.exceptions.exceptionItem.conditions.linux',
|
||||
{
|
||||
defaultMessage: 'Linux',
|
||||
}
|
||||
);
|
||||
|
||||
export const OS_MAC = i18n.translate(
|
||||
'xpack.securitySolution.exceptions.exceptionItem.conditions.macos',
|
||||
{
|
||||
defaultMessage: 'Mac',
|
||||
}
|
||||
);
|
|
@ -13,6 +13,7 @@ import * as i18n from '../translations';
|
|||
import { getExceptionListItemSchemaMock } from '@kbn/lists-plugin/common/schemas/response/exception_list_item_schema.mock';
|
||||
import { ExceptionsViewerItems } from './exceptions_viewer_items';
|
||||
import { getMockTheme } from '../../../lib/kibana/kibana_react.mock';
|
||||
import { TestProviders } from '../../../mock';
|
||||
|
||||
const mockTheme = getMockTheme({
|
||||
eui: {
|
||||
|
@ -25,17 +26,18 @@ const mockTheme = getMockTheme({
|
|||
describe('ExceptionsViewerItems', () => {
|
||||
it('it renders empty prompt if "showEmpty" is "true"', () => {
|
||||
const wrapper = mount(
|
||||
<ExceptionsViewerItems
|
||||
showEmpty
|
||||
showNoResults={false}
|
||||
isInitLoading={false}
|
||||
disableActions={false}
|
||||
exceptions={[]}
|
||||
loadingItemIds={[]}
|
||||
commentsAccordionId="comments-accordion-id"
|
||||
onDeleteException={jest.fn()}
|
||||
onEditExceptionItem={jest.fn()}
|
||||
/>
|
||||
<TestProviders>
|
||||
<ExceptionsViewerItems
|
||||
showEmpty
|
||||
showNoResults={false}
|
||||
isInitLoading={false}
|
||||
disableActions={false}
|
||||
exceptions={[]}
|
||||
loadingItemIds={[]}
|
||||
onDeleteException={jest.fn()}
|
||||
onEditExceptionItem={jest.fn()}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(wrapper.find('[data-test-subj="exceptionsEmptyPrompt"]').exists()).toBeTruthy();
|
||||
|
@ -50,19 +52,20 @@ describe('ExceptionsViewerItems', () => {
|
|||
|
||||
it('it renders no search results found prompt if "showNoResults" is "true"', () => {
|
||||
const wrapper = mount(
|
||||
<ThemeProvider theme={mockTheme}>
|
||||
<ExceptionsViewerItems
|
||||
showEmpty={false}
|
||||
showNoResults
|
||||
isInitLoading={false}
|
||||
disableActions={false}
|
||||
exceptions={[]}
|
||||
loadingItemIds={[]}
|
||||
commentsAccordionId="comments-accordion-id"
|
||||
onDeleteException={jest.fn()}
|
||||
onEditExceptionItem={jest.fn()}
|
||||
/>
|
||||
</ThemeProvider>
|
||||
<TestProviders>
|
||||
<ThemeProvider theme={mockTheme}>
|
||||
<ExceptionsViewerItems
|
||||
showEmpty={false}
|
||||
showNoResults
|
||||
isInitLoading={false}
|
||||
disableActions={false}
|
||||
exceptions={[]}
|
||||
loadingItemIds={[]}
|
||||
onDeleteException={jest.fn()}
|
||||
onEditExceptionItem={jest.fn()}
|
||||
/>
|
||||
</ThemeProvider>
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(wrapper.find('[data-test-subj="exceptionsEmptyPrompt"]').exists()).toBeTruthy();
|
||||
|
@ -75,19 +78,20 @@ describe('ExceptionsViewerItems', () => {
|
|||
|
||||
it('it renders exceptions if "showEmpty" and "isInitLoading" is "false", and exceptions exist', () => {
|
||||
const wrapper = mount(
|
||||
<ThemeProvider theme={mockTheme}>
|
||||
<ExceptionsViewerItems
|
||||
showEmpty={false}
|
||||
showNoResults={false}
|
||||
isInitLoading={false}
|
||||
disableActions={false}
|
||||
exceptions={[getExceptionListItemSchemaMock()]}
|
||||
loadingItemIds={[]}
|
||||
commentsAccordionId="comments-accordion-id"
|
||||
onDeleteException={jest.fn()}
|
||||
onEditExceptionItem={jest.fn()}
|
||||
/>
|
||||
</ThemeProvider>
|
||||
<TestProviders>
|
||||
<ThemeProvider theme={mockTheme}>
|
||||
<ExceptionsViewerItems
|
||||
showEmpty={false}
|
||||
showNoResults={false}
|
||||
isInitLoading={false}
|
||||
disableActions={false}
|
||||
exceptions={[getExceptionListItemSchemaMock()]}
|
||||
loadingItemIds={[]}
|
||||
onDeleteException={jest.fn()}
|
||||
onEditExceptionItem={jest.fn()}
|
||||
/>
|
||||
</ThemeProvider>
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(wrapper.find('[data-test-subj="exceptionsContainer"]').exists()).toBeTruthy();
|
||||
|
@ -96,103 +100,23 @@ describe('ExceptionsViewerItems', () => {
|
|||
|
||||
it('it does not render exceptions if "isInitLoading" is "true"', () => {
|
||||
const wrapper = mount(
|
||||
<ThemeProvider theme={mockTheme}>
|
||||
<ExceptionsViewerItems
|
||||
showEmpty={false}
|
||||
showNoResults={false}
|
||||
isInitLoading={true}
|
||||
disableActions={false}
|
||||
exceptions={[]}
|
||||
loadingItemIds={[]}
|
||||
commentsAccordionId="comments-accordion-id"
|
||||
onDeleteException={jest.fn()}
|
||||
onEditExceptionItem={jest.fn()}
|
||||
/>
|
||||
</ThemeProvider>
|
||||
<TestProviders>
|
||||
<ThemeProvider theme={mockTheme}>
|
||||
<ExceptionsViewerItems
|
||||
showEmpty={false}
|
||||
showNoResults={false}
|
||||
isInitLoading={true}
|
||||
disableActions={false}
|
||||
exceptions={[]}
|
||||
loadingItemIds={[]}
|
||||
onDeleteException={jest.fn()}
|
||||
onEditExceptionItem={jest.fn()}
|
||||
/>
|
||||
</ThemeProvider>
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(wrapper.find('[data-test-subj="exceptionsContainer"]').exists()).toBeFalsy();
|
||||
expect(wrapper.find('[data-test-subj="exceptionsEmptyPrompt"]').exists()).toBeTruthy();
|
||||
});
|
||||
|
||||
it('it does not render or badge for first exception displayed', () => {
|
||||
const exception1 = getExceptionListItemSchemaMock();
|
||||
const exception2 = getExceptionListItemSchemaMock();
|
||||
exception2.id = 'newId';
|
||||
|
||||
const wrapper = mount(
|
||||
<ThemeProvider theme={mockTheme}>
|
||||
<ExceptionsViewerItems
|
||||
showEmpty={false}
|
||||
showNoResults={false}
|
||||
isInitLoading={false}
|
||||
disableActions={false}
|
||||
exceptions={[exception1, exception2]}
|
||||
loadingItemIds={[]}
|
||||
commentsAccordionId="comments-accordion-id"
|
||||
onDeleteException={jest.fn()}
|
||||
onEditExceptionItem={jest.fn()}
|
||||
/>
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
||||
const firstExceptionItem = wrapper.find('[data-test-subj="exceptionItemContainer"]').at(0);
|
||||
|
||||
expect(firstExceptionItem.find('[data-test-subj="exceptionItemOrBadge"]').exists()).toBeFalsy();
|
||||
});
|
||||
|
||||
it('it does render or badge with exception displayed', () => {
|
||||
const exception1 = getExceptionListItemSchemaMock();
|
||||
const exception2 = getExceptionListItemSchemaMock();
|
||||
exception2.id = 'newId';
|
||||
|
||||
const wrapper = mount(
|
||||
<ThemeProvider theme={mockTheme}>
|
||||
<ExceptionsViewerItems
|
||||
showEmpty={false}
|
||||
showNoResults={false}
|
||||
isInitLoading={false}
|
||||
disableActions={false}
|
||||
exceptions={[exception1, exception2]}
|
||||
loadingItemIds={[]}
|
||||
commentsAccordionId="comments-accordion-id"
|
||||
onDeleteException={jest.fn()}
|
||||
onEditExceptionItem={jest.fn()}
|
||||
/>
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
||||
const notFirstExceptionItem = wrapper.find('[data-test-subj="exceptionItemContainer"]').at(1);
|
||||
|
||||
expect(
|
||||
notFirstExceptionItem.find('[data-test-subj="exceptionItemOrBadge"]').exists()
|
||||
).toBeFalsy();
|
||||
});
|
||||
|
||||
it('it invokes "onDeleteException" when delete button is clicked', () => {
|
||||
const mockOnDeleteException = jest.fn();
|
||||
|
||||
const wrapper = mount(
|
||||
<ThemeProvider theme={mockTheme}>
|
||||
<ExceptionsViewerItems
|
||||
showEmpty={false}
|
||||
showNoResults={false}
|
||||
isInitLoading={false}
|
||||
disableActions={false}
|
||||
exceptions={[getExceptionListItemSchemaMock()]}
|
||||
loadingItemIds={[]}
|
||||
commentsAccordionId="comments-accordion-id"
|
||||
onDeleteException={mockOnDeleteException}
|
||||
onEditExceptionItem={jest.fn()}
|
||||
/>
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
||||
wrapper.find('[data-test-subj="exceptionsViewerDeleteBtn"] button').at(0).simulate('click');
|
||||
|
||||
expect(mockOnDeleteException).toHaveBeenCalledWith({
|
||||
id: '1',
|
||||
namespaceType: 'single',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -6,13 +6,12 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { EuiEmptyPrompt, EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui';
|
||||
import { EuiEmptyPrompt, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import type { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types';
|
||||
import * as i18n from '../translations';
|
||||
import { ExceptionItem } from './exception_item';
|
||||
import { AndOrBadge } from '../../and_or_badge';
|
||||
import { ExceptionItemCard } from './exception_item_card';
|
||||
import type { ExceptionListItemIdentifiers } from '../types';
|
||||
|
||||
const MyFlexItem = styled(EuiFlexItem)`
|
||||
|
@ -34,7 +33,6 @@ interface ExceptionsViewerItemsProps {
|
|||
disableActions: boolean;
|
||||
exceptions: ExceptionListItemSchema[];
|
||||
loadingItemIds: ExceptionListItemIdentifiers[];
|
||||
commentsAccordionId: string;
|
||||
onDeleteException: (arg: ExceptionListItemIdentifiers) => void;
|
||||
onEditExceptionItem: (item: ExceptionListItemSchema) => void;
|
||||
}
|
||||
|
@ -45,7 +43,6 @@ const ExceptionsViewerItemsComponent: React.FC<ExceptionsViewerItemsProps> = ({
|
|||
isInitLoading,
|
||||
exceptions,
|
||||
loadingItemIds,
|
||||
commentsAccordionId,
|
||||
onDeleteException,
|
||||
onEditExceptionItem,
|
||||
disableActions,
|
||||
|
@ -79,23 +76,15 @@ const ExceptionsViewerItemsComponent: React.FC<ExceptionsViewerItemsProps> = ({
|
|||
>
|
||||
{!isInitLoading &&
|
||||
exceptions.length > 0 &&
|
||||
exceptions.map((exception, index) => (
|
||||
exceptions.map((exception) => (
|
||||
<MyFlexItem data-test-subj="exceptionItemContainer" grow={false} key={exception.id}>
|
||||
{index !== 0 ? (
|
||||
<>
|
||||
<AndOrBadge data-test-subj="exceptionItemOrBadge" type="or" />
|
||||
<EuiSpacer />
|
||||
</>
|
||||
) : (
|
||||
<EuiSpacer size="s" />
|
||||
)}
|
||||
<ExceptionItem
|
||||
<ExceptionItemCard
|
||||
disableActions={disableActions}
|
||||
loadingItemIds={loadingItemIds}
|
||||
commentsAccordionId={commentsAccordionId}
|
||||
exceptionItem={exception}
|
||||
onDeleteException={onDeleteException}
|
||||
onEditException={onEditExceptionItem}
|
||||
dataTestSubj={`exceptionItemCard-${exception.name}`}
|
||||
/>
|
||||
</MyFlexItem>
|
||||
))}
|
||||
|
|
|
@ -1,354 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import moment from 'moment-timezone';
|
||||
|
||||
import { getFormattedEntries, formatEntry, getDescriptionListContent } from './helpers';
|
||||
import { FormattedEntry } from '../types';
|
||||
import { getExceptionListItemSchemaMock } from '@kbn/lists-plugin/common/schemas/response/exception_list_item_schema.mock';
|
||||
import { getEntriesArrayMock } from '@kbn/lists-plugin/common/schemas/types/entries.mock';
|
||||
import { getEntryMatchMock } from '@kbn/lists-plugin/common/schemas/types/entry_match.mock';
|
||||
import { getEntryMatchAnyMock } from '@kbn/lists-plugin/common/schemas/types/entry_match_any.mock';
|
||||
import { getEntryExistsMock } from '@kbn/lists-plugin/common/schemas/types/entry_exists.mock';
|
||||
|
||||
describe('Exception viewer helpers', () => {
|
||||
beforeEach(() => {
|
||||
moment.tz.setDefault('UTC');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
moment.tz.setDefault('Browser');
|
||||
});
|
||||
|
||||
describe('#getFormattedEntries', () => {
|
||||
test('it returns empty array if no entries passed', () => {
|
||||
const result = getFormattedEntries([]);
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
test('it formats nested entries as expected', () => {
|
||||
const payload = [getEntryMatchMock()];
|
||||
const result = getFormattedEntries(payload);
|
||||
const expected: FormattedEntry[] = [
|
||||
{
|
||||
fieldName: 'host.name',
|
||||
isNested: false,
|
||||
operator: 'is',
|
||||
value: 'some host name',
|
||||
},
|
||||
];
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
|
||||
test('it formats "exists" entries as expected', () => {
|
||||
const payload = [getEntryExistsMock()];
|
||||
const result = getFormattedEntries(payload);
|
||||
const expected: FormattedEntry[] = [
|
||||
{
|
||||
fieldName: 'host.name',
|
||||
isNested: false,
|
||||
operator: 'exists',
|
||||
value: undefined,
|
||||
},
|
||||
];
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
|
||||
test('it formats non-nested entries as expected', () => {
|
||||
const payload = [getEntryMatchAnyMock(), getEntryMatchMock()];
|
||||
const result = getFormattedEntries(payload);
|
||||
const expected: FormattedEntry[] = [
|
||||
{
|
||||
fieldName: 'host.name',
|
||||
isNested: false,
|
||||
operator: 'is one of',
|
||||
value: ['some host name'],
|
||||
},
|
||||
{
|
||||
fieldName: 'host.name',
|
||||
isNested: false,
|
||||
operator: 'is',
|
||||
value: 'some host name',
|
||||
},
|
||||
];
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
|
||||
test('it formats a mix of nested and non-nested entries as expected', () => {
|
||||
const payload = getEntriesArrayMock();
|
||||
const result = getFormattedEntries(payload);
|
||||
const expected: FormattedEntry[] = [
|
||||
{
|
||||
fieldName: 'host.name',
|
||||
isNested: false,
|
||||
operator: 'is',
|
||||
value: 'some host name',
|
||||
},
|
||||
{
|
||||
fieldName: 'host.name',
|
||||
isNested: false,
|
||||
operator: 'is one of',
|
||||
value: ['some host name'],
|
||||
},
|
||||
{
|
||||
fieldName: 'host.name',
|
||||
isNested: false,
|
||||
operator: 'exists',
|
||||
value: undefined,
|
||||
},
|
||||
{
|
||||
fieldName: 'parent.field',
|
||||
isNested: false,
|
||||
operator: undefined,
|
||||
value: undefined,
|
||||
},
|
||||
{
|
||||
fieldName: 'host.name',
|
||||
isNested: true,
|
||||
operator: 'is',
|
||||
value: 'some host name',
|
||||
},
|
||||
{
|
||||
fieldName: 'host.name',
|
||||
isNested: true,
|
||||
operator: 'is one of',
|
||||
value: ['some host name'],
|
||||
},
|
||||
];
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#formatEntry', () => {
|
||||
test('it formats an entry', () => {
|
||||
const payload = getEntryMatchMock();
|
||||
const formattedEntry = formatEntry({ isNested: false, item: payload });
|
||||
const expected: FormattedEntry = {
|
||||
fieldName: 'host.name',
|
||||
isNested: false,
|
||||
operator: 'is',
|
||||
value: 'some host name',
|
||||
};
|
||||
|
||||
expect(formattedEntry).toEqual(expected);
|
||||
});
|
||||
|
||||
test('it formats as expected when "isNested" is "true"', () => {
|
||||
const payload = getEntryMatchMock();
|
||||
const formattedEntry = formatEntry({ isNested: true, item: payload });
|
||||
const expected: FormattedEntry = {
|
||||
fieldName: 'host.name',
|
||||
isNested: true,
|
||||
operator: 'is',
|
||||
value: 'some host name',
|
||||
};
|
||||
|
||||
expect(formattedEntry).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#getDescriptionListContent', () => {
|
||||
test('it returns formatted description list with os if one is specified', () => {
|
||||
const payload = getExceptionListItemSchemaMock({ os_types: ['linux'] });
|
||||
payload.description = '';
|
||||
const result = getDescriptionListContent(payload);
|
||||
const os = result.find(({ title }) => title === 'OS');
|
||||
|
||||
expect(os).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"description": <EuiToolTip
|
||||
anchorClassName="eventFiltersDescriptionListDescription"
|
||||
content="Linux"
|
||||
delay="regular"
|
||||
display="inlineBlock"
|
||||
position="top"
|
||||
>
|
||||
<EuiDescriptionListDescription
|
||||
className="eui-fullWidth"
|
||||
>
|
||||
Linux
|
||||
</EuiDescriptionListDescription>
|
||||
</EuiToolTip>,
|
||||
"title": "OS",
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
test('it returns formatted description list with a description if one specified', () => {
|
||||
const payload = getExceptionListItemSchemaMock({ os_types: ['linux'] });
|
||||
payload.description = 'Im a description';
|
||||
const result = getDescriptionListContent(payload);
|
||||
const description = result.find(({ title }) => title === 'Description');
|
||||
|
||||
expect(description).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"description": <EuiToolTip
|
||||
anchorClassName="eventFiltersDescriptionListDescription"
|
||||
content="Im a description"
|
||||
delay="regular"
|
||||
display="inlineBlock"
|
||||
position="top"
|
||||
>
|
||||
<EuiDescriptionListDescription
|
||||
className="eui-fullWidth"
|
||||
>
|
||||
Im a description
|
||||
</EuiDescriptionListDescription>
|
||||
</EuiToolTip>,
|
||||
"title": "Description",
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
test('it returns scrolling element when description is longer than 75 charachters', () => {
|
||||
const payload = getExceptionListItemSchemaMock({ os_types: ['linux'] });
|
||||
payload.description =
|
||||
'Puppy kitty ipsum dolor sit good dog foot stick canary. Teeth Mittens grooming vaccine walk swimming nest good boy furry tongue heel furry treats fish. Cage run fast kitten dinnertime ball run foot park fleas throw house train licks stick dinnertime window. Yawn litter fish yawn toy pet gate throw Buddy kitty wag tail ball groom crate ferret heel wet nose Rover toys pet supplies. Bird Food treats tongue lick teeth ferret litter box slobbery litter box crate bird small animals yawn small animals shake slobber gimme five toys polydactyl meow. ';
|
||||
const result = getDescriptionListContent(payload);
|
||||
const description = result.find(({ title }) => title === 'Description');
|
||||
|
||||
expect(description).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"description": <EuiDescriptionListDescription
|
||||
style={
|
||||
Object {
|
||||
"height": 150,
|
||||
"overflowY": "hidden",
|
||||
}
|
||||
}
|
||||
>
|
||||
<EuiText
|
||||
aria-label=""
|
||||
className="eui-yScrollWithShadows"
|
||||
role="region"
|
||||
size="s"
|
||||
tabIndex={0}
|
||||
>
|
||||
Puppy kitty ipsum dolor sit good dog foot stick canary. Teeth Mittens grooming vaccine walk swimming nest good boy furry tongue heel furry treats fish. Cage run fast kitten dinnertime ball run foot park fleas throw house train licks stick dinnertime window. Yawn litter fish yawn toy pet gate throw Buddy kitty wag tail ball groom crate ferret heel wet nose Rover toys pet supplies. Bird Food treats tongue lick teeth ferret litter box slobbery litter box crate bird small animals yawn small animals shake slobber gimme five toys polydactyl meow.
|
||||
</EuiText>
|
||||
</EuiDescriptionListDescription>,
|
||||
"title": "Description",
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
test('it returns just user and date created if no other fields specified', () => {
|
||||
const payload = getExceptionListItemSchemaMock();
|
||||
payload.description = '';
|
||||
const result = getDescriptionListContent(payload);
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"description": <EuiToolTip
|
||||
anchorClassName="eventFiltersDescriptionListDescription"
|
||||
content="April 20th 2020 @ 15:25:31"
|
||||
delay="regular"
|
||||
display="inlineBlock"
|
||||
position="top"
|
||||
>
|
||||
<EuiDescriptionListDescription
|
||||
className="eui-fullWidth"
|
||||
>
|
||||
April 20th 2020 @ 15:25:31
|
||||
</EuiDescriptionListDescription>
|
||||
</EuiToolTip>,
|
||||
"title": "Date created",
|
||||
},
|
||||
Object {
|
||||
"description": <EuiToolTip
|
||||
anchorClassName="eventFiltersDescriptionListDescription"
|
||||
content="some user"
|
||||
delay="regular"
|
||||
display="inlineBlock"
|
||||
position="top"
|
||||
>
|
||||
<EuiDescriptionListDescription
|
||||
className="eui-fullWidth"
|
||||
>
|
||||
some user
|
||||
</EuiDescriptionListDescription>
|
||||
</EuiToolTip>,
|
||||
"title": "Created by",
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
test('it returns Modified By/On info when `includeModified` is true', () => {
|
||||
const result = getDescriptionListContent(
|
||||
getExceptionListItemSchemaMock({ os_types: ['linux'] }),
|
||||
true
|
||||
);
|
||||
const dateModified = result.find(({ title }) => title === 'Date modified');
|
||||
const modifiedBy = result.find(({ title }) => title === 'Modified by');
|
||||
expect(modifiedBy).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"description": <EuiToolTip
|
||||
anchorClassName="eventFiltersDescriptionListDescription"
|
||||
content="some user"
|
||||
delay="regular"
|
||||
display="inlineBlock"
|
||||
position="top"
|
||||
>
|
||||
<EuiDescriptionListDescription
|
||||
className="eui-fullWidth"
|
||||
>
|
||||
some user
|
||||
</EuiDescriptionListDescription>
|
||||
</EuiToolTip>,
|
||||
"title": "Modified by",
|
||||
}
|
||||
`);
|
||||
expect(dateModified).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"description": <EuiToolTip
|
||||
anchorClassName="eventFiltersDescriptionListDescription"
|
||||
content="April 20th 2020 @ 15:25:31"
|
||||
delay="regular"
|
||||
display="inlineBlock"
|
||||
position="top"
|
||||
>
|
||||
<EuiDescriptionListDescription
|
||||
className="eui-fullWidth"
|
||||
>
|
||||
April 20th 2020 @ 15:25:31
|
||||
</EuiDescriptionListDescription>
|
||||
</EuiToolTip>,
|
||||
"title": "Date modified",
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
test('it returns Name when `includeName` is true', () => {
|
||||
const result = getDescriptionListContent(
|
||||
getExceptionListItemSchemaMock({ os_types: ['linux'] }),
|
||||
false,
|
||||
true
|
||||
);
|
||||
const name = result.find(({ title }) => title === 'Name');
|
||||
expect(name).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"description": <EuiToolTip
|
||||
anchorClassName="eventFiltersDescriptionListDescription"
|
||||
content="some name"
|
||||
delay="regular"
|
||||
display="inlineBlock"
|
||||
position="top"
|
||||
>
|
||||
<EuiDescriptionListDescription
|
||||
className="eui-fullWidth"
|
||||
>
|
||||
some name
|
||||
</EuiDescriptionListDescription>
|
||||
</EuiToolTip>,
|
||||
"title": "Name",
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,166 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import moment from 'moment';
|
||||
|
||||
import { entriesNested, ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types';
|
||||
import {
|
||||
getEntryValue,
|
||||
getExceptionOperatorSelect,
|
||||
BuilderEntry,
|
||||
} from '@kbn/securitysolution-list-utils';
|
||||
|
||||
import React from 'react';
|
||||
import { EuiDescriptionListDescription, EuiText, EuiToolTip } from '@elastic/eui';
|
||||
import { formatOperatingSystems } from '../helpers';
|
||||
import type { FormattedEntry, DescriptionListItem } from '../types';
|
||||
import * as i18n from '../translations';
|
||||
|
||||
/**
|
||||
* Helper method for `getFormattedEntries`
|
||||
*/
|
||||
export const formatEntry = ({
|
||||
isNested,
|
||||
item,
|
||||
}: {
|
||||
isNested: boolean;
|
||||
item: BuilderEntry;
|
||||
}): FormattedEntry => {
|
||||
const operator = getExceptionOperatorSelect(item);
|
||||
const value = getEntryValue(item);
|
||||
|
||||
return {
|
||||
fieldName: item.field ?? '',
|
||||
operator: operator.message,
|
||||
value,
|
||||
isNested,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Formats ExceptionItem entries into simple field, operator, value
|
||||
* for use in rendering items in table
|
||||
*
|
||||
* @param entries an ExceptionItem's entries
|
||||
*/
|
||||
export const getFormattedEntries = (entries: BuilderEntry[]): FormattedEntry[] => {
|
||||
const formattedEntries = entries.map((item) => {
|
||||
if (entriesNested.is(item)) {
|
||||
const parent = {
|
||||
fieldName: item.field,
|
||||
operator: undefined,
|
||||
value: undefined,
|
||||
isNested: false,
|
||||
};
|
||||
return item.entries.reduce<FormattedEntry[]>(
|
||||
(acc, nestedEntry) => {
|
||||
const formattedEntry = formatEntry({
|
||||
isNested: true,
|
||||
item: nestedEntry,
|
||||
});
|
||||
return [...acc, { ...formattedEntry }];
|
||||
},
|
||||
[parent]
|
||||
);
|
||||
} else {
|
||||
return formatEntry({ isNested: false, item });
|
||||
}
|
||||
});
|
||||
|
||||
return formattedEntries.flat();
|
||||
};
|
||||
|
||||
/**
|
||||
* Formats ExceptionItem details for description list component
|
||||
*
|
||||
* @param exceptionItem an ExceptionItem
|
||||
* @param includeModified if modified information should be included
|
||||
* @param includeName if the Name should be included
|
||||
*/
|
||||
export const getDescriptionListContent = (
|
||||
exceptionItem: ExceptionListItemSchema,
|
||||
includeModified: boolean = false,
|
||||
includeName: boolean = false
|
||||
): DescriptionListItem[] => {
|
||||
const details = [
|
||||
...(includeName
|
||||
? [
|
||||
{
|
||||
title: i18n.NAME,
|
||||
value: exceptionItem.name,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
title: i18n.OPERATING_SYSTEM,
|
||||
value: formatOperatingSystems(exceptionItem.os_types),
|
||||
},
|
||||
{
|
||||
title: i18n.DATE_CREATED,
|
||||
value: moment(exceptionItem.created_at).format('MMMM Do YYYY @ HH:mm:ss'),
|
||||
},
|
||||
{
|
||||
title: i18n.CREATED_BY,
|
||||
value: exceptionItem.created_by,
|
||||
},
|
||||
...(includeModified
|
||||
? [
|
||||
{
|
||||
title: i18n.DATE_MODIFIED,
|
||||
value: moment(exceptionItem.updated_at).format('MMMM Do YYYY @ HH:mm:ss'),
|
||||
},
|
||||
{
|
||||
title: i18n.MODIFIED_BY,
|
||||
value: exceptionItem.updated_by,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
title: i18n.DESCRIPTION,
|
||||
value: exceptionItem.description,
|
||||
},
|
||||
];
|
||||
|
||||
return details.reduce<DescriptionListItem[]>((acc, { value, title }) => {
|
||||
if (value != null && value.trim() !== '') {
|
||||
const valueElement = (
|
||||
<EuiToolTip content={value} anchorClassName="eventFiltersDescriptionListDescription">
|
||||
<EuiDescriptionListDescription className="eui-fullWidth">
|
||||
{value}
|
||||
</EuiDescriptionListDescription>
|
||||
</EuiToolTip>
|
||||
);
|
||||
if (title === i18n.DESCRIPTION) {
|
||||
return [
|
||||
...acc,
|
||||
{
|
||||
title,
|
||||
description:
|
||||
value.length > 75 ? (
|
||||
<EuiDescriptionListDescription style={{ height: 150, overflowY: 'hidden' }}>
|
||||
<EuiText
|
||||
tabIndex={0}
|
||||
role="region"
|
||||
aria-label=""
|
||||
className="eui-yScrollWithShadows"
|
||||
size="s"
|
||||
>
|
||||
{value}
|
||||
</EuiText>
|
||||
</EuiDescriptionListDescription>
|
||||
) : (
|
||||
valueElement
|
||||
),
|
||||
},
|
||||
];
|
||||
}
|
||||
return [...acc, { title, description: valueElement }];
|
||||
} else {
|
||||
return acc;
|
||||
}
|
||||
}, []);
|
||||
};
|
|
@ -260,7 +260,6 @@ const ExceptionsViewerComponent = ({
|
|||
lists: exceptionListsMeta,
|
||||
exception,
|
||||
});
|
||||
|
||||
setCurrentModal('editException');
|
||||
},
|
||||
[setCurrentModal, exceptionListsMeta]
|
||||
|
@ -328,8 +327,7 @@ const ExceptionsViewerComponent = ({
|
|||
`security/detections/rules/id/${encodeURI(ruleId)}/edit`
|
||||
);
|
||||
|
||||
const showEmpty: boolean =
|
||||
!isInitLoading && !loadingList && totalEndpointItems === 0 && totalDetectionsItems === 0;
|
||||
const showEmpty: boolean = !isInitLoading && !loadingList && exceptions.length === 0;
|
||||
|
||||
const showNoResults: boolean =
|
||||
exceptions.length === 0 && (totalEndpointItems > 0 || totalDetectionsItems > 0);
|
||||
|
@ -396,7 +394,6 @@ const ExceptionsViewerComponent = ({
|
|||
isInitLoading={isInitLoading}
|
||||
exceptions={exceptions}
|
||||
loadingItemIds={loadingItemIds}
|
||||
commentsAccordionId={commentsAccordionId}
|
||||
onDeleteException={handleDeleteException}
|
||||
onEditExceptionItem={handleEditException}
|
||||
/>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue