diff --git a/x-pack/plugins/security_solution/cypress/integration/exceptions/add_exception.spec.ts b/x-pack/plugins/security_solution/cypress/integration/exceptions/add_exception.spec.ts index d41e86fb9c96d..814f29622f51a 100644 --- a/x-pack/plugins/security_solution/cypress/integration/exceptions/add_exception.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/exceptions/add_exception.spec.ts @@ -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(() => { diff --git a/x-pack/plugins/security_solution/cypress/integration/exceptions/exceptions_flyout.spec.ts b/x-pack/plugins/security_solution/cypress/integration/exceptions/exceptions_flyout.spec.ts index 75d7696140368..9e08313e7e73f 100644 --- a/x-pack/plugins/security_solution/cypress/integration/exceptions/exceptions_flyout.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/exceptions/exceptions_flyout.spec.ts @@ -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); diff --git a/x-pack/plugins/security_solution/cypress/screens/exceptions.ts b/x-pack/plugins/security_solution/cypress/screens/exceptions.ts index 58c070b194002..4a98d59bc5a0f 100644 --- a/x-pack/plugins/security_solution/cypress/screens/exceptions.ts +++ b/x-pack/plugins/security_solution/cypress/screens/exceptions.ts @@ -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 = diff --git a/x-pack/plugins/security_solution/cypress/screens/rule_details.ts b/x-pack/plugins/security_solution/cypress/screens/rule_details.ts index d58451f6f1520..e8fd18c4449b1 100644 --- a/x-pack/plugins/security_solution/cypress/screens/rule_details.ts +++ b/x-pack/plugins/security_solution/cypress/screens/rule_details.ts @@ -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"]'; diff --git a/x-pack/plugins/security_solution/cypress/tasks/rule_details.ts b/x-pack/plugins/security_solution/cypress/tasks/rule_details.ts index 7fda21016205a..159f62778f74e 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/rule_details.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/rule_details.ts @@ -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(); }; diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/exception_details.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/exception_details.test.tsx deleted file mode 100644 index 20c58985344f8..0000000000000 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/exception_details.test.tsx +++ /dev/null @@ -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( - - - - ); - - 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( - - - - ); - - 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( - - - - ); - - 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( - - - - ); - - 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( - - - - ); - - 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( - - - - ); - - 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( - - - - ); - 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( - - - - ); - - 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( - - - - ); - - 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( - - - - ); - - 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( - - - - ); - - 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( - - - - ); - - 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'); - }); -}); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/exception_details.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/exception_details.tsx deleted file mode 100644 index 429f9672aece5..0000000000000 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/exception_details.tsx +++ /dev/null @@ -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 ( - - {!showComments - ? i18n.COMMENTS_SHOW(comments.length) - : i18n.COMMENTS_HIDE(comments.length)} - - ); - } else { - return <>; - } - }, [showComments, onCommentsClick, exceptionItem]); - - return ( - - - - - {descriptionListItems.map((item) => ( - - - - {item.title} - - - {item.description} - - ))} - - - {commentsSection} - - - ); -}; - -ExceptionDetailsComponent.displayName = 'ExceptionDetailsComponent'; - -export const ExceptionDetails = React.memo(ExceptionDetailsComponent); - -ExceptionDetails.displayName = 'ExceptionDetails'; diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/exception_entries.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/exception_entries.test.tsx deleted file mode 100644 index a53be08380698..0000000000000 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/exception_entries.test.tsx +++ /dev/null @@ -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( - - - - ); - - 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( - - - - ); - - expect(wrapper.find('[data-test-subj="exceptionsViewerAndBadge"]')).toHaveLength(1); - }); - - test('it invokes "onEdit" when edit button clicked', () => { - const mockOnEdit = jest.fn(); - const wrapper = mount( - - - - ); - 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( - - - - ); - 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( - - - - ); - 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( - - - - ); - 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( - - - - ); - - 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( - - - - ); - - 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'); - }); -}); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/exception_entries.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/exception_entries.tsx deleted file mode 100644 index 4db00bea5c932..0000000000000 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/exception_entries.tsx +++ /dev/null @@ -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> => [ - { - 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 ( - - - {value} - - ); - } 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 ( - - {values.map((value) => { - return ( - - {value} - - ); - })} - - ); - } else { - return values ? ( - - {values} - - ) : ( - getEmptyValue() - ); - } - }, - }, - ], - [] - ); - - return ( - - - - - {entries.length > 1 && ( - - - - - - )} - - - - - - {!disableActions && ( - - - - - {i18n.EDIT} - - - - - {i18n.REMOVE} - - - - - )} - - - ); -}; - -ExceptionEntriesComponent.displayName = 'ExceptionEntriesComponent'; - -export const ExceptionEntries = React.memo(ExceptionEntriesComponent); - -ExceptionEntries.displayName = 'ExceptionEntries'; diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/index.stories.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/index.stories.tsx deleted file mode 100644 index 30ca4428aa008..0000000000000 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/index.stories.tsx +++ /dev/null @@ -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) => ( - ({ eui: euiLightVars, darkMode: false })}>{storyFn()} -)); - -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 ( - - ); - }) - .add('with description', () => { - const payload = getExceptionListItemSchemaMock(); - payload.comments = []; - payload.entries = [ - { - field: 'actingProcess.file.signer', - type: 'match', - operator: 'included', - value: 'Elastic, N.V.', - }, - ]; - - return ( - - ); - }) - .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 ( - - ); - }) - .add('with nested entries', () => { - const payload = getExceptionListItemSchemaMock(); - payload.description = ''; - payload.comments = []; - - return ( - - ); - }) - .add('with everything', () => { - const payload = getExceptionListItemSchemaMock(); - payload.comments = getCommentsArrayMock(); - return ( - - ); - }) - .add('with loadingItemIds', () => { - // eslint-disable-next-line @typescript-eslint/naming-convention - const { id, namespace_type, ...rest } = getExceptionListItemSchemaMock(); - - return ( - - ); - }) - .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 ( - - ); - }); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/index.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/index.tsx deleted file mode 100644 index f47f802e558ca..0000000000000 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/index.tsx +++ /dev/null @@ -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([]); - 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 ( - - - - - - - - - - - - - - - - ); -}; - -ExceptionItemComponent.displayName = 'ExceptionItemComponent'; - -export const ExceptionItem = React.memo(ExceptionItemComponent); - -ExceptionItem.displayName = 'ExceptionItem'; diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item_card/exception_item_card_conditions.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item_card/exception_item_card_conditions.test.tsx new file mode 100644 index 0000000000000..dd0249958949f --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item_card/exception_item_card_conditions.test.tsx @@ -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( + + + + ); + + // 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( + + + + ); + + // 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'); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item_card/exception_item_card_conditions.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item_card/exception_item_card_conditions.tsx new file mode 100644 index 0000000000000..24cbdd5061943 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item_card/exception_item_card_conditions.tsx @@ -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( + ({ 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) => {currentValue}); + } + 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 ( + + + + + + + + + + + + ); + }); + } + }, + [dataTestSubj] + ); + + return ( +
+ {osLabel != null && ( +
+ + + + +
+ )} + {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 ( +
+
+ {i18n.CONDITION_AND} + } + value={field} + color={index === 0 ? 'primary' : 'subdued'} + /> + +
+ {nestedEntries != null && getNestedEntriesContent(type, nestedEntries)} +
+ ); + })} +
+ ); + } +); +ExceptionItemCardConditions.displayName = 'ExceptionItemCardConditions'; diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item_card/exception_item_card_header.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item_card/exception_item_card_header.test.tsx new file mode 100644 index 0000000000000..fe8811152e2e1 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item_card/exception_item_card_header.test.tsx @@ -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( + + + + ); + + 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( + + + + ); + + // 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( + + + + ); + + expect( + wrapper.find('button[data-test-subj="exceptionItemHeader-actionButton"]').at(0).props() + .disabled + ).toBeTruthy(); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item_card/exception_item_card_header.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item_card/exception_item_card_header.tsx new file mode 100644 index 0000000000000..3389bd0cb29b9 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item_card/exception_item_card_header.tsx @@ -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( + ({ 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) => ( + { + onClosePopover(); + action.onClick(); + }} + > + {action.label} + + )); + }, [dataTestSubj, actions]); + + return ( + + + +

{item.name}

+
+
+ + + } + panelPaddingSize="none" + isOpen={isPopoverOpen} + closePopover={onClosePopover} + data-test-subj={`${dataTestSubj}-items`} + > + + + +
+ ); + } +); + +ExceptionItemCardHeader.displayName = 'ExceptionItemCardHeader'; diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item_card/exception_item_card_meta.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item_card/exception_item_card_meta.test.tsx new file mode 100644 index 0000000000000..b5a24ef3e472d --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item_card/exception_item_card_meta.test.tsx @@ -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( + + + + ); + + 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( + + + + ); + + 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'); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item_card/exception_item_card_meta.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item_card/exception_item_card_meta.tsx new file mode 100644 index 0000000000000..4e3ae24900246 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item_card/exception_item_card_meta.tsx @@ -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( + ({ item, dataTestSubj }) => { + return ( + + + } + value2={item.created_by} + dataTestSubj={`${dataTestSubj}-createdBy`} + /> + + + + + + } + value2={item.updated_by} + dataTestSubj={`${dataTestSubj}-updatedBy`} + /> + + + ); + } +); +ExceptionItemCardMetaInfo.displayName = 'ExceptionItemCardMetaInfo'; + +interface MetaInfoDetailsProps { + fieldName: string; + label: string; + value1: JSX.Element | string; + value2: string; + dataTestSubj: string; +} + +const MetaInfoDetails = memo(({ label, value1, value2, dataTestSubj }) => { + return ( + + + + {label} + + + + + {value1} + + + + + {i18n.EXCEPTION_ITEM_META_BY} + + + + + + + + + + {value2} + + + + + + ); +}); + +MetaInfoDetails.displayName = 'MetaInfoDetails'; diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item_card/index.test.tsx similarity index 57% rename from x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/index.test.tsx rename to x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item_card/index.test.tsx index e1afc8f44b354..46a0f74642c08 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item_card/index.test.tsx @@ -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( - ); - 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( - ); - 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( - ); - 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( - ); - 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( - ); - 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( - ); 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( - - - - ); - - const commentsBtn = wrapper - .find('.euiButtonEmpty[data-test-subj="exceptionsViewerItemCommentsBtn"]') - .at(0); - commentsBtn.simulate('click'); - - expect(wrapper.find('.euiAccordion-isOpen')).toHaveLength(1); - }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item_card/index.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item_card/index.tsx new file mode 100644 index 0000000000000..13e1d679a44f9 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item_card/index.tsx @@ -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 ( + + + + + + + + + + + + {formattedComments.length > 0 && ( + + + {i18n.exceptionItemCommentsAccordion(formattedComments.length)} + + } + arrowDisplay="none" + data-test-subj="exceptionsViewerCommentAccordion" + > + + + + + + )} + + + ); +}; + +ExceptionItemCardComponent.displayName = 'ExceptionItemCardComponent'; + +export const ExceptionItemCard = React.memo(ExceptionItemCardComponent); + +ExceptionItemCard.displayName = 'ExceptionItemCard'; diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item_card/translations.ts b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item_card/translations.ts new file mode 100644 index 0000000000000..8d345c23fbf09 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item_card/translations.ts @@ -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', + } +); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_items.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_items.test.tsx index 90a06a732a283..22c6e7dbf8ecf 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_items.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_items.test.tsx @@ -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( - + + + ); 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( - - - + + + + + ); 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( - - - + + + + + ); 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( - - - + + + + + ); 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( - - - - ); - - 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( - - - - ); - - 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( - - - - ); - - wrapper.find('[data-test-subj="exceptionsViewerDeleteBtn"] button').at(0).simulate('click'); - - expect(mockOnDeleteException).toHaveBeenCalledWith({ - id: '1', - namespaceType: 'single', - }); - }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_items.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_items.tsx index 30b5b3e4d1339..e1d91ed0a0580 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_items.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_items.tsx @@ -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 = ({ isInitLoading, exceptions, loadingItemIds, - commentsAccordionId, onDeleteException, onEditExceptionItem, disableActions, @@ -79,23 +76,15 @@ const ExceptionsViewerItemsComponent: React.FC = ({ > {!isInitLoading && exceptions.length > 0 && - exceptions.map((exception, index) => ( + exceptions.map((exception) => ( - {index !== 0 ? ( - <> - - - - ) : ( - - )} - ))} diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/helpers.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/helpers.test.tsx deleted file mode 100644 index aee9ca3d87611..0000000000000 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/helpers.test.tsx +++ /dev/null @@ -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": - - Linux - - , - "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": - - Im a description - - , - "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": - - 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. - - , - "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": - - April 20th 2020 @ 15:25:31 - - , - "title": "Date created", - }, - Object { - "description": - - some user - - , - "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": - - some user - - , - "title": "Modified by", - } - `); - expect(dateModified).toMatchInlineSnapshot(` - Object { - "description": - - April 20th 2020 @ 15:25:31 - - , - "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": - - some name - - , - "title": "Name", - } - `); - }); - }); -}); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/helpers.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/helpers.tsx deleted file mode 100644 index 37bfeb6166405..0000000000000 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/helpers.tsx +++ /dev/null @@ -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( - (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((acc, { value, title }) => { - if (value != null && value.trim() !== '') { - const valueElement = ( - - - {value} - - - ); - if (title === i18n.DESCRIPTION) { - return [ - ...acc, - { - title, - description: - value.length > 75 ? ( - - - {value} - - - ) : ( - valueElement - ), - }, - ]; - } - return [...acc, { title, description: valueElement }]; - } else { - return acc; - } - }, []); -}; diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/index.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/index.tsx index 63093c06a9450..6e5d6a1c21fbd 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/index.tsx @@ -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} />