diff --git a/x-pack/plugins/security_solution/public/common/components/conditions_table/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/common/components/conditions_table/__snapshots__/index.test.tsx.snap new file mode 100644 index 0000000000000..3d37aea431df3 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/conditions_table/__snapshots__/index.test.tsx.snap @@ -0,0 +1,174 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`conditions_table ConditionsTable should render multi item table with and badge correctly 1`] = ` + + + + + + + + + + +`; + +exports[`conditions_table ConditionsTable should render multi item table with or badge correctly 1`] = ` + + + + + + + + + + +`; + +exports[`conditions_table ConditionsTable should render single item table correctly 1`] = ` + + + + + +`; diff --git a/x-pack/plugins/security_solution/public/common/components/conditions_table/index.stories.tsx b/x-pack/plugins/security_solution/public/common/components/conditions_table/index.stories.tsx new file mode 100644 index 0000000000000..b179dbb6a405e --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/conditions_table/index.stories.tsx @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React from 'react'; +import { ThemeProvider } from 'styled-components'; +import { storiesOf, addDecorator } from '@storybook/react'; +import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; + +import { createItems, TEST_COLUMNS } from './test_utils'; +import { ConditionsTable } from '.'; + +addDecorator((storyFn) => ( + ({ eui: euiLightVars, darkMode: false })}>{storyFn()} +)); + +storiesOf('Components|ConditionsTable', module) + .add('single item', () => { + return ; + }) + .add('and', () => { + return ; + }) + .add('or', () => { + return ; + }); diff --git a/x-pack/plugins/security_solution/public/common/components/conditions_table/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/conditions_table/index.test.tsx new file mode 100644 index 0000000000000..e563e3604952a --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/conditions_table/index.test.tsx @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { shallow } from 'enzyme'; +import React from 'react'; + +import { ConditionsTable } from '.'; +import { createItems, TEST_COLUMNS } from './test_utils'; + +describe('conditions_table', () => { + describe('ConditionsTable', () => { + it('should render single item table correctly', () => { + const element = shallow( + + ); + + expect(element).toMatchSnapshot(); + }); + + it('should render multi item table with and badge correctly', () => { + const element = shallow( + + ); + + expect(element).toMatchSnapshot(); + }); + + it('should render multi item table with or badge correctly', () => { + const element = shallow( + + ); + + expect(element).toMatchSnapshot(); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/conditions_table/index.tsx b/x-pack/plugins/security_solution/public/common/components/conditions_table/index.tsx new file mode 100644 index 0000000000000..c90af8e619ff0 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/conditions_table/index.tsx @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import styled from 'styled-components'; +import { + EuiBasicTableProps, + EuiBasicTable, + EuiFlexGroup, + EuiFlexItem, + EuiHideFor, +} from '@elastic/eui'; + +import { AndOr, AndOrBadge } from '../and_or_badge'; + +const AndOrBadgeContainer = styled(EuiFlexItem)` + padding-top: ${({ theme }) => theme.eui.euiSizeXL}; + padding-bottom: ${({ theme }) => theme.eui.euiSizeS}; +`; + +type ConditionsTableProps = EuiBasicTableProps & { + badge: AndOr; +}; + +export const ConditionsTable = ({ badge, ...props }: ConditionsTableProps) => { + return ( + + {props.items.length > 1 && ( + + + + + + )} + + + + + ); +}; + +ConditionsTable.displayName = 'ConditionsTable'; diff --git a/x-pack/plugins/security_solution/public/common/components/conditions_table/test_utils.ts b/x-pack/plugins/security_solution/public/common/components/conditions_table/test_utils.ts new file mode 100644 index 0000000000000..eee61293ee7b3 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/conditions_table/test_utils.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiTableFieldDataColumnType } from '@elastic/eui'; + +export interface TestItem { + name: string; + value: string; +} + +export const TEST_COLUMNS: Array> = [ + { field: 'name', name: 'Name', textOnly: true, width: '50%' }, + { field: 'value', name: 'Value', textOnly: true, width: '50%' }, +]; + +export const createItems = (count: number): TestItem[] => + [...new Array(count).keys()].map((item) => ({ name: `item ${item}`, value: `value ${item}` })); diff --git a/x-pack/plugins/security_solution/public/common/components/item_details_card/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/common/components/item_details_card/__snapshots__/index.test.tsx.snap new file mode 100644 index 0000000000000..4bd2cd05d49d0 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/item_details_card/__snapshots__/index.test.tsx.snap @@ -0,0 +1,206 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`item_details_card ItemDetailsAction should render correctly 1`] = ` + + + + primary + + + +`; + +exports[`item_details_card ItemDetailsCard should render correctly with actions 1`] = ` + + + + + + + + + + + + + + + some text + + some node + + + + + + + primary + + + + + secondary + + + + + danger + + + + + + + + + + +`; + +exports[`item_details_card ItemDetailsCard should render correctly with no actions 1`] = ` + + + + + + + + + + + + + + + some text + + some node + + + + + + + + + + + +`; + +exports[`item_details_card ItemDetailsPropertySummary should render correctly 1`] = ` + + + name 1 + + + value 1 + + +`; diff --git a/x-pack/plugins/security_solution/public/common/components/item_details_card/index.stories.tsx b/x-pack/plugins/security_solution/public/common/components/item_details_card/index.stories.tsx new file mode 100644 index 0000000000000..b16f4be598866 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/item_details_card/index.stories.tsx @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React from 'react'; +import { ThemeProvider } from 'styled-components'; +import { storiesOf, addDecorator } from '@storybook/react'; +import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; + +import { ItemDetailsAction, ItemDetailsCard, ItemDetailsPropertySummary } from '.'; + +addDecorator((storyFn) => ( + ({ eui: euiLightVars, darkMode: false })}>{storyFn()} +)); + +storiesOf('Components|ItemDetailsCard', module).add('default', () => { + return ( + + + + + + {'content text'} + {'content node'} + + + {'primary'} + + + {'secondary'} + + + {'danger'} + + + ); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/item_details_card/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/item_details_card/index.test.tsx new file mode 100644 index 0000000000000..3f8578606aab9 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/item_details_card/index.test.tsx @@ -0,0 +1,73 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { shallow } from 'enzyme'; +import React from 'react'; + +import { ItemDetailsAction, ItemDetailsCard, ItemDetailsPropertySummary } from '.'; + +describe('item_details_card', () => { + describe('ItemDetailsPropertySummary', () => { + it('should render correctly', () => { + const element = shallow(); + + expect(element).toMatchSnapshot(); + }); + }); + + describe('ItemDetailsAction', () => { + it('should render correctly', () => { + const element = shallow( + + {'primary'} + + ); + + expect(element).toMatchSnapshot(); + }); + }); + + describe('ItemDetailsCard', () => { + it('should render correctly with no actions', () => { + const element = shallow( + + + + + + {'some text'} + {'some node'} + + ); + + expect(element).toMatchSnapshot(); + }); + + it('should render correctly with actions', () => { + const element = shallow( + + + + + + {'some text'} + {'some node'} + + + {'primary'} + + + {'secondary'} + + + {'danger'} + + + ); + + expect(element).toMatchSnapshot(); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/item_details_card/index.tsx b/x-pack/plugins/security_solution/public/common/components/item_details_card/index.tsx new file mode 100644 index 0000000000000..ee1c3e1bead1a --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/item_details_card/index.tsx @@ -0,0 +1,129 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC, isValidElement, memo, ReactElement, ReactNode, useMemo } from 'react'; +import styled from 'styled-components'; +import { + EuiPanel, + EuiFlexGroup, + EuiFlexItem, + EuiButton, + EuiDescriptionList, + EuiDescriptionListTitle, + EuiDescriptionListDescription, + EuiButtonProps, + PropsForButton, +} from '@elastic/eui'; + +const OTHER_NODES = {}; + +const groupChildrenByType = ( + children: ReactNode | ReactNode[], + types: Array +) => { + const result = new Map(); + + types.forEach((type) => result.set(type, [])); + result.set(OTHER_NODES, []); + + React.Children.toArray(children).forEach((child) => { + const key = isValidElement(child) ? child.type : OTHER_NODES; + + if (!result.has(key)) { + result.get(OTHER_NODES)?.push(child); + } else { + result.get(key)?.push(child); + } + }); + + return result; +}; + +const SummarySection = styled(EuiFlexItem)` + background-color: ${({ theme }) => theme.eui.euiColorLightestShade}; + padding: ${({ theme }) => theme.eui.euiSize}; +`; + +const DetailsSection = styled(EuiFlexItem)` + padding: ${({ theme }) => theme.eui.euiSize}; +`; + +const DescriptionListTitle = styled(EuiDescriptionListTitle)` + width: 40%; +`; + +const DescriptionListDescription = styled(EuiDescriptionListDescription)` + width: 60%; +`; + +interface ItemDetailsPropertySummaryProps { + name: ReactNode | ReactNode[]; + value: ReactNode | ReactNode[]; +} + +export const ItemDetailsPropertySummary: FC = memo( + ({ name, value }) => ( + <> + {name} + {value} + + ) +); + +ItemDetailsPropertySummary.displayName = 'ItemPropertySummary'; + +export const ItemDetailsAction: FC> = memo( + ({ children, ...rest }) => ( + + + {children} + + + ) +); + +ItemDetailsAction.displayName = 'ItemDetailsAction'; + +export const ItemDetailsCard: FC = memo(({ children }) => { + const childElements = useMemo( + () => groupChildrenByType(children, [ItemDetailsPropertySummary, ItemDetailsAction]), + [children] + ); + + return ( + + + + + + + {childElements.get(ItemDetailsPropertySummary)} + + + + + {childElements.get(OTHER_NODES)} + {childElements.has(ItemDetailsAction) && ( + + + {childElements.get(ItemDetailsAction)?.map((action, index) => ( + + {action} + + ))} + + + )} + + + + + + + ); +}); + +ItemDetailsCard.displayName = 'ItemDetailsCard'; diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/__snapshots__/trusted_apps_list.test.tsx.snap b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/__snapshots__/trusted_apps_list.test.tsx.snap index ccd94c63e96c8..d33c74a021f86 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/__snapshots__/trusted_apps_list.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/__snapshots__/trusted_apps_list.test.tsx.snap @@ -115,6 +115,21 @@ exports[`TrustedAppsList renders correctly initially 1`] = ` + +
+ +
+ @@ -123,7 +138,7 @@ exports[`TrustedAppsList renders correctly initially 1`] = ` >
+ +
+ +
+ @@ -266,7 +296,7 @@ exports[`TrustedAppsList renders correctly when failed loading data for the firs >
+ +
+ +
+ @@ -414,7 +459,7 @@ exports[`TrustedAppsList renders correctly when failed loading data for the seco >
`; -exports[`TrustedAppsList renders correctly when loaded data 1`] = ` +exports[`TrustedAppsList renders correctly when item details expanded 1`] = ` +.c0 { + background-color: #f5f7fa; + padding: 16px; +} + +.c3 { + padding: 16px; +} + +.c1 { + width: 40%; +} + +.c2 { + width: 60%; +} +
+ +
+ +
+ - Delete + Remove
+ +
+ +
+ + + + +
+
+
+
+
+
+
+
+ Name +
+
+ trusted app 0 +
+
+ OS +
+
+ Windows +
+
+ Date Created +
+
+ 1 minute ago +
+
+ Created By +
+
+ someone +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + + + + + + + + + + + + +
+
+
+ + Field + +
+
+
+ + Operator + +
+
+
+ + Value + +
+
+
+ + No items found + +
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+ - Delete + Remove
+ +
+ +
+ - Delete + Remove
+ +
+ +
+ - Delete + Remove
+ +
+ +
+ - Delete + Remove + +
+ +
+ - Delete + Remove + +
+ +
+ - Delete + Remove + +
+ +
+ - Delete + Remove + +
+ +
+ - Delete + Remove + +
+ +
+ - Delete + Remove + +
+ +
+ - Delete + Remove + +
+ +
+ - Delete + Remove + +
+ +
+ - Delete + Remove + +
+ +
+ - Delete + Remove + +
+ +
+ - Delete + Remove - -
- Name + +
+ + + + +
+ Name +
+
+ + trusted app 15 + +
+ + +
+ OS +
+
+ Windows +
+ + +
+ Date Created +
+
+ 1 minute ago +
+ + +
+ Created By +
+
+ + someone + +
+ + +
+ + + + Remove + + +
+ + +
+ +
+ + + + +
+ Name +
+
+ + trusted app 16 + +
+ + +
+ OS +
+
+ Mac OS +
+ + +
+ Date Created +
+
+ 1 minute ago +
+ + +
+ Created By +
+
+ + someone + +
+ + +
+ + + + Remove + + +
+ + +
+ +
+ + + + +
+ Name +
+
+ + trusted app 17 + +
+ + +
+ OS +
+
+ Linux +
+ + +
+ Date Created +
+
+ 1 minute ago +
+ + +
+ Created By +
+
+ + someone + +
+ + +
+ + + + Remove + + +
+ + +
+ +
+ + + + +
+ Name +
+
+ + trusted app 18 + +
+ + +
+ OS +
+
+ Windows +
+ + +
+ Date Created +
+
+ 1 minute ago +
+ + +
+ Created By +
+
+ + someone + +
+ + +
+ + + + Remove + + +
+ + +
+ +
+ + + + +
+ Name +
+
+ + trusted app 19 + +
+ + +
+ OS +
+
+ Mac OS +
+ + +
+ Date Created +
+
+ 1 minute ago +
+ + +
+ Created By +
+
+ + someone + +
+ + +
+ + + + Remove + + +
+ + +
+ +
+ + + + + +
+
+
+
+
+
+ +
+
+
+
+ +
+
+
+
+ +`; + +exports[`TrustedAppsList renders correctly when loaded data 1`] = ` +
+
+
+
+
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + Name + +
+
+
+ + OS + +
+
+
+ + Date Created + +
+
+
+ + Created By + +
+
+
+ + Actions + +
+
+
+ +
+
+
+ Name +
+
+ + trusted app 0 + +
+
+
+ OS +
+
+ Windows +
+
+
+ Date Created +
+
+ 1 minute ago +
+
+
+ Created By +
+
+ + someone + +
+
+
+ + + + Remove + + +
+
+
+ +
+
+
+ Name +
+
+ + trusted app 1 + +
+
+
+ OS +
+
+ Mac OS +
+
+
+ Date Created +
+
+ 1 minute ago +
+
+
+ Created By +
+
+ + someone + +
+
+
+ + + + Remove + + +
+
+
+ +
+
+
+ Name +
+
+ + trusted app 2 + +
+
+
+ OS +
+
+ Linux +
+
+
+ Date Created +
+
+ 1 minute ago +
+
+
+ Created By +
+
+ + someone + +
+
+
+ + + + Remove + + +
+
+
+ +
+
+
+ Name +
+
+ + trusted app 3 + +
+
+
+ OS +
+
+ Windows +
+
+
+ Date Created +
+
+ 1 minute ago +
+
+
+ Created By +
+
+ + someone + +
+
+
+ + + + Remove + + +
+
+
+ +
+
+
+ Name +
+
+ + trusted app 4 + +
+
+
+ OS +
+
+ Mac OS +
+
+
+ Date Created +
+
+ 1 minute ago +
+
+
+ Created By +
+
+ + someone + +
+
+
+ + + + Remove + + +
+
+
+ +
+
+
+ Name +
+
+ + trusted app 5 + +
+
+
+ OS +
+
+ Linux +
+
+
+ Date Created +
+
+ 1 minute ago +
+
+
+ Created By +
+
+ + someone + +
+
+
+ + + + Remove + + +
+
+
+ +
+
+
+ Name +
+
+ + trusted app 6 + +
+
+
+ OS +
+
+ Windows +
+
+
+ Date Created +
+
+ 1 minute ago +
+
+
+ Created By +
+
+ + someone + +
+
+
+ + + + Remove + + +
+
+
+ +
+
+
+ Name +
+
+ + trusted app 7 + +
+
+
+ OS +
+
+ Mac OS +
+
+
+ Date Created +
+
+ 1 minute ago +
+
+
+ Created By +
+
+ + someone + +
+
+
+ + + + Remove + + +
+
+
+ +
+
+
+ Name +
+
+ + trusted app 8 + +
+
+
+ OS +
+
+ Linux +
+
+
+ Date Created +
+
+ 1 minute ago +
+
+
+ Created By +
+
+ + someone + +
+
+
+ + + + Remove + + +
+
+
+ +
+
+
+ Name +
+
+ + trusted app 9 + +
+
+
+ OS +
+
+ Windows +
+
+
+ Date Created +
+
+ 1 minute ago +
+
+
+ Created By +
+
+ + someone + +
+
+
+ + + + Remove + + +
+
+
+ +
+
+
+ Name +
+
+ + trusted app 10 + +
+
+
+ OS +
+
+ Mac OS +
+
+
+ Date Created +
+
+ 1 minute ago +
+
+
+ Created By +
+
+ + someone + +
+
+
+ + + + Remove + + +
+
+
+ +
+
+
+ Name +
+
+ + trusted app 11 + +
+
+
+ OS +
+
+ Linux +
+
+
+ Date Created +
+
+ 1 minute ago +
+
+
+ Created By +
+
+ + someone + +
+
+
+ + + + Remove + + +
+
+
+ +
+
+
+ Name +
+
+ + trusted app 12 + +
+
+
+ OS +
+
+ Windows +
+
+
+ Date Created +
+
+ 1 minute ago +
+
+
+ Created By +
+
+ + someone + +
+
+
+ + + + Remove + + +
+
+
+ +
+
+
+ Name +
+
+ + trusted app 13 + +
+
+
+ OS +
+
+ Mac OS +
+
+
+ Date Created +
+
+ 1 minute ago +
+
+
+ Created By +
+
+ + someone + +
+
+
+ + + + Remove + + +
+
+
+ +
+
+
+ Name +
+
+ + trusted app 14 + +
+
+
+ OS +
+
+ Linux +
+
+
+ Date Created +
+
+ 1 minute ago +
+
+
+ Created By +
+
+ + someone + +
+
+
+ + + + Remove + + +
+
+
+ +
+
+
+ Name
- Delete + Remove
+
+ +
+
- Delete + Remove +
+ +
+
- Delete + Remove +
+ +
+
- Delete + Remove +
+ +
+
- Delete + Remove +
+ +
+
@@ -2859,6 +6333,21 @@ exports[`TrustedAppsList renders correctly when loading data for the first time
+ +
+ +
+ @@ -2867,7 +6356,7 @@ exports[`TrustedAppsList renders correctly when loading data for the first time >
+ +
+ +
+ - Delete + Remove
+ +
+ +
+ - Delete + Remove
+ +
+ +
+ - Delete + Remove
+ +
+ +
+ - Delete + Remove
+ +
+ +
+ - Delete + Remove + +
+ +
+ - Delete + Remove + +
+ +
+ - Delete + Remove + +
+ +
+ - Delete + Remove + +
+ +
+ - Delete + Remove + +
+ +
+ - Delete + Remove + +
+ +
+ - Delete + Remove + +
+ +
+ - Delete + Remove + +
+ +
+ - Delete + Remove + +
+ +
+ - Delete + Remove + +
+ +
+ - Delete + Remove + +
+ +
+ - Delete + Remove + +
+ +
+ - Delete + Remove + +
+ +
+ - Delete + Remove + +
+ +
+ - Delete + Remove + +
+ +
+ - Delete + Remove + +
+ +
+ @@ -5307,11 +9231,26 @@ exports[`TrustedAppsList renders correctly when new page and page size set (not + +
+ +
+ - Delete + Remove + +
+ +
+ - Delete + Remove + +
+ +
+ - Delete + Remove + +
+ +
+ - Delete + Remove + +
+ +
+ - Delete + Remove + +
+ +
+ - Delete + Remove + +
+ +
+ - Delete + Remove + +
+ +
+ - Delete + Remove + +
+ +
+ - Delete + Remove + +
+ +
+ - Delete + Remove + +
+ +
+ - Delete + Remove + +
+ +
+ - Delete + Remove + +
+ +
+ - Delete + Remove + +
+ +
+ - Delete + Remove + +
+ +
+ - Delete + Remove + +
+ +
+ - Delete + Remove + +
+ +
+ - Delete + Remove + +
+ +
+ - Delete + Remove + +
+ +
+ - Delete + Remove + +
+ +
+ - Delete + Remove + +
+ +
+ diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/create_trusted_app_form.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/create_trusted_app_form.tsx index 32170ed6fc4d8..08cb1835c5363 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/create_trusted_app_form.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/create_trusted_app_form.tsx @@ -23,7 +23,7 @@ import { TrustedApp, } from '../../../../../../common/endpoint/types'; import { LogicalConditionBuilderProps } from './logical_condition/logical_condition_builder'; -import { OS_TITLES } from '../constants'; +import { OS_TITLES } from '../translations'; import { isMacosLinuxTrustedAppCondition, isTrustedAppSupportedOs, diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_app_card/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_app_card/__snapshots__/index.test.tsx.snap new file mode 100644 index 0000000000000..3928f4ddec837 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_app_card/__snapshots__/index.test.tsx.snap @@ -0,0 +1,66 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`trusted_app_card TrustedAppCard should render correctly 1`] = ` + + + + + } + /> + + + + Remove + + +`; diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_app_card/index.stories.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_app_card/index.stories.tsx new file mode 100644 index 0000000000000..713e5e7095e12 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_app_card/index.stories.tsx @@ -0,0 +1,61 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React from 'react'; +import { ThemeProvider } from 'styled-components'; +import { storiesOf, addDecorator } from '@storybook/react'; +import { action } from '@storybook/addon-actions'; +import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; + +import { KibanaContextProvider } from '../../../../../../../../../../src/plugins/kibana_react/public'; +import { TrustedApp } from '../../../../../../../common/endpoint/types'; + +import { createSampleTrustedApp } from '../../../test_utils'; + +import { TrustedAppCard } from '.'; + +addDecorator((storyFn) => ( + 'MMM D, YYYY @ HH:mm:ss.SSS' } }}> + ({ eui: euiLightVars, darkMode: false })}> + {storyFn()} + + +)); + +storiesOf('TrustedApps|TrustedAppCard', module) + .add('default', () => { + const trustedApp: TrustedApp = createSampleTrustedApp(5); + trustedApp.created_at = '2020-09-17T14:52:33.899Z'; + trustedApp.entries = [ + { + field: 'process.path.text', + operator: 'included', + type: 'match', + value: '/some/path/on/file/system', + }, + ]; + + return ; + }) + .add('multiple entries', () => { + const trustedApp: TrustedApp = createSampleTrustedApp(5); + trustedApp.created_at = '2020-09-17T14:52:33.899Z'; + trustedApp.entries = [ + { + field: 'process.path.text', + operator: 'included', + type: 'match', + value: '/some/path/on/file/system', + }, + { + field: 'process.code_signature', + operator: 'included', + type: 'match', + value: 'Elastic', + }, + ]; + + return ; + }); diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_app_card/index.test.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_app_card/index.test.tsx new file mode 100644 index 0000000000000..163883b3dc3b8 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_app_card/index.test.tsx @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { shallow } from 'enzyme'; +import React from 'react'; + +import { TrustedAppCard } from '.'; +import { createSampleTrustedApp } from '../../../test_utils'; + +describe('trusted_app_card', () => { + describe('TrustedAppCard', () => { + it('should render correctly', () => { + const element = shallow( + {}} /> + ); + + expect(element).toMatchSnapshot(); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_app_card/index.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_app_card/index.tsx new file mode 100644 index 0000000000000..73dbe5482573a --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_app_card/index.tsx @@ -0,0 +1,95 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { memo, useCallback, useMemo } from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiTableFieldDataColumnType } from '@elastic/eui'; + +import { + Immutable, + TrustedApp, + MacosLinuxConditionEntry, + WindowsConditionEntry, +} from '../../../../../../../common/endpoint/types'; + +import { FormattedDate } from '../../../../../../common/components/formatted_date'; +import { ConditionsTable } from '../../../../../../common/components/conditions_table'; +import { + ItemDetailsAction, + ItemDetailsCard, + ItemDetailsPropertySummary, +} from '../../../../../../common/components/item_details_card'; + +import { OS_TITLES, PROPERTY_TITLES, ENTRY_PROPERTY_TITLES } from '../../translations'; + +type Entry = MacosLinuxConditionEntry | WindowsConditionEntry; + +const getEntriesColumnDefinitions = (): Array> => [ + { + field: 'field', + name: ENTRY_PROPERTY_TITLES.field, + sortable: false, + truncateText: true, + textOnly: true, + width: '30%', + }, + { + field: 'operator', + name: ENTRY_PROPERTY_TITLES.operator, + sortable: false, + truncateText: true, + width: '20%', + }, + { + field: 'value', + name: ENTRY_PROPERTY_TITLES.value, + sortable: false, + truncateText: true, + width: '60%', + }, +]; + +interface TrustedAppCardProps { + trustedApp: Immutable; + onDelete: (id: string) => void; +} + +export const TrustedAppCard = memo(({ trustedApp, onDelete }: TrustedAppCardProps) => { + const handleDelete = useCallback(() => onDelete(trustedApp.id), [onDelete, trustedApp.id]); + + return ( + + + + + } + /> + + + getEntriesColumnDefinitions(), [])} + items={useMemo(() => [...trustedApp.entries], [trustedApp.entries])} + badge="and" + responsive + /> + + + {i18n.translate('xpack.securitySolution.trustedapps.card.removeButtonLabel', { + defaultMessage: 'Remove', + })} + + + ); +}); + +TrustedAppCard.displayName = 'TrustedAppCard'; diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/constants.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/constants.ts deleted file mode 100644 index d5df8c528511a..0000000000000 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/constants.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { i18n } from '@kbn/i18n'; -import { TrustedApp } from '../../../../../common/endpoint/types'; - -export const OS_TITLES: Readonly<{ [K in TrustedApp['os']]: string }> = { - windows: i18n.translate('xpack.securitySolution.trustedapps.os.windows', { - defaultMessage: 'Windows', - }), - macos: i18n.translate('xpack.securitySolution.trustedapps.os.macos', { - defaultMessage: 'Mac OS', - }), - linux: i18n.translate('xpack.securitySolution.trustedapps.os.linux', { - defaultMessage: 'Linux', - }), -}; diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/translations.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/translations.ts new file mode 100644 index 0000000000000..e16155df6d2db --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/translations.ts @@ -0,0 +1,76 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; +import { + TrustedApp, + MacosLinuxConditionEntry, + WindowsConditionEntry, +} from '../../../../../common/endpoint/types'; + +export const OS_TITLES: Readonly<{ [K in TrustedApp['os']]: string }> = { + windows: i18n.translate('xpack.securitySolution.trustedapps.os.windows', { + defaultMessage: 'Windows', + }), + macos: i18n.translate('xpack.securitySolution.trustedapps.os.macos', { + defaultMessage: 'Mac OS', + }), + linux: i18n.translate('xpack.securitySolution.trustedapps.os.linux', { + defaultMessage: 'Linux', + }), +}; + +export const PROPERTY_TITLES: Readonly< + { [K in keyof Omit]: string } +> = { + name: i18n.translate('xpack.securitySolution.trustedapps.trustedapp.name', { + defaultMessage: 'Name', + }), + os: i18n.translate('xpack.securitySolution.trustedapps.trustedapp.os', { + defaultMessage: 'OS', + }), + created_at: i18n.translate('xpack.securitySolution.trustedapps.trustedapp.createdAt', { + defaultMessage: 'Date Created', + }), + created_by: i18n.translate('xpack.securitySolution.trustedapps.trustedapp.createdBy', { + defaultMessage: 'Created By', + }), +}; + +export const ENTRY_PROPERTY_TITLES: Readonly< + { [K in keyof Omit]: string } +> = { + field: i18n.translate('xpack.securitySolution.trustedapps.trustedapp.entry.field', { + defaultMessage: 'Field', + }), + operator: i18n.translate('xpack.securitySolution.trustedapps.trustedapp.entry.operator', { + defaultMessage: 'Operator', + }), + value: i18n.translate('xpack.securitySolution.trustedapps.trustedapp.entry.value', { + defaultMessage: 'Value', + }), +}; + +export const ACTIONS_COLUMN_TITLE = i18n.translate( + 'xpack.securitySolution.trustedapps.list.columns.actions', + { + defaultMessage: 'Actions', + } +); + +export const LIST_ACTIONS = { + delete: { + name: i18n.translate('xpack.securitySolution.trustedapps.list.actions.delete', { + defaultMessage: 'Remove', + }), + description: i18n.translate( + 'xpack.securitySolution.trustedapps.list.actions.delete.description', + { + defaultMessage: 'Remove this entry', + } + ), + }, +}; diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_list.test.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_list.test.tsx index a457ecd0d088f..1468871a655a5 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_list.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_list.test.tsx @@ -6,6 +6,8 @@ import { render } from '@testing-library/react'; import React from 'react'; import { Provider } from 'react-redux'; +import { ThemeProvider } from 'styled-components'; +import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; import { TrustedAppsList } from './trusted_apps_list'; import { @@ -25,7 +27,13 @@ jest.mock('@elastic/eui/lib/services/accessibility/html_id_generator', () => ({ const now = 111111; const renderList = (store: ReturnType) => { - const Wrapper: React.FC = ({ children }) => {children}; + const Wrapper: React.FC = ({ children }) => ( + + ({ eui: euiLightVars, darkMode: false })}> + {children} + + + ); return render(, { wrapper: Wrapper }); }; @@ -111,6 +119,22 @@ describe('TrustedAppsList', () => { expect(renderList(store).container).toMatchSnapshot(); }); + it('renders correctly when item details expanded', async () => { + const store = createGlobalNoMiddlewareStore(); + + store.dispatch( + createTrustedAppsListResourceStateChangedAction( + createListLoadedResourceState({ index: 0, size: 20 }, 200, now) + ) + ); + + const element = renderList(store); + + (await element.findAllByTestId('trustedAppsListItemExpandButton'))[0].click(); + + expect(element.container).toMatchSnapshot(); + }); + it('triggers deletion dialog when delete action clicked', async () => { const store = createGlobalNoMiddlewareStore(); diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_list.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_list.tsx index 5e5b83ccd8c03..d0c1fb477ea46 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_list.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_list.tsx @@ -5,11 +5,16 @@ */ import { Dispatch } from 'redux'; -import React, { memo, useCallback, useMemo } from 'react'; +import React, { memo, ReactNode, useCallback, useMemo, useState } from 'react'; import { useDispatch } from 'react-redux'; import { useHistory } from 'react-router-dom'; -import { EuiBasicTable, EuiBasicTableColumn, EuiTableActionsColumnType } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; +import { + EuiBasicTable, + EuiBasicTableColumn, + EuiButtonIcon, + EuiTableActionsColumnType, + RIGHT_ALIGNMENT, +} from '@elastic/eui'; import { Immutable } from '../../../../../common/endpoint/types'; import { AppAction } from '../../../../common/store/actions'; @@ -29,41 +34,51 @@ import { import { useTrustedAppsSelector } from './hooks'; import { FormattedDate } from '../../../../common/components/formatted_date'; -import { OS_TITLES } from './constants'; - -const COLUMN_TITLES: Readonly< - { [K in keyof Omit | 'actions']: string } -> = { - name: i18n.translate('xpack.securitySolution.trustedapps.list.columns.name', { - defaultMessage: 'Name', - }), - os: i18n.translate('xpack.securitySolution.trustedapps.list.columns.os', { - defaultMessage: 'OS', - }), - created_at: i18n.translate('xpack.securitySolution.trustedapps.list.columns.createdAt', { - defaultMessage: 'Date Created', - }), - created_by: i18n.translate('xpack.securitySolution.trustedapps.list.columns.createdBy', { - defaultMessage: 'Created By', - }), - actions: i18n.translate('xpack.securitySolution.trustedapps.list.columns.actions', { - defaultMessage: 'Actions', - }), -}; +import { ACTIONS_COLUMN_TITLE, LIST_ACTIONS, OS_TITLES, PROPERTY_TITLES } from './translations'; +import { TrustedAppCard } from './components/trusted_app_card'; + +interface DetailsMap { + [K: string]: ReactNode; +} + +interface TrustedAppsListContext { + dispatch: Dispatch>; + detailsMapState: [DetailsMap, (value: DetailsMap) => void]; +} +type ColumnsList = Array>>; type ActionsList = EuiTableActionsColumnType>['actions']; -const getActionDefinitions = (dispatch: Dispatch>): ActionsList => [ +const toggleItemDetailsInMap = ( + map: DetailsMap, + item: Immutable, + { dispatch }: TrustedAppsListContext +): DetailsMap => { + const changedMap = { ...map }; + + if (changedMap[item.id]) { + delete changedMap[item.id]; + } else { + changedMap[item.id] = ( + { + dispatch({ + type: 'trustedAppDeletionDialogStarted', + payload: { entry: item }, + }); + }} + /> + ); + } + + return changedMap; +}; + +const getActionDefinitions = ({ dispatch }: TrustedAppsListContext): ActionsList => [ { - name: i18n.translate('xpack.securitySolution.trustedapps.list.actions.delete', { - defaultMessage: 'Delete', - }), - description: i18n.translate( - 'xpack.securitySolution.trustedapps.list.actions.delete.description', - { - defaultMessage: 'Delete this entry', - } - ), + name: LIST_ACTIONS.delete.name, + description: LIST_ACTIONS.delete.description, 'data-test-subj': 'trustedAppDeleteAction', isPrimary: true, icon: 'trash', @@ -78,44 +93,62 @@ const getActionDefinitions = (dispatch: Dispatch>): Actions }, ]; -type ColumnsList = Array>>; +const getColumnDefinitions = (context: TrustedAppsListContext): ColumnsList => { + const [itemDetailsMap, setItemDetailsMap] = context.detailsMapState; -const getColumnDefinitions = (dispatch: Dispatch>): ColumnsList => [ - { - field: 'name', - name: COLUMN_TITLES.name, - }, - { - field: 'os', - name: COLUMN_TITLES.os, - render(value: TrustedApp['os'], record: Immutable) { - return OS_TITLES[value]; + return [ + { + field: 'name', + name: PROPERTY_TITLES.name, }, - }, - { - field: 'created_at', - name: COLUMN_TITLES.created_at, - render(value: TrustedApp['created_at'], record: Immutable) { - return ( - - ); + { + field: 'os', + name: PROPERTY_TITLES.os, + render(value: TrustedApp['os'], record: Immutable) { + return OS_TITLES[value]; + }, }, - }, - { - field: 'created_by', - name: COLUMN_TITLES.created_by, - }, - { - name: COLUMN_TITLES.actions, - actions: getActionDefinitions(dispatch), - }, -]; + { + field: 'created_at', + name: PROPERTY_TITLES.created_at, + render(value: TrustedApp['created_at'], record: Immutable) { + return ( + + ); + }, + }, + { + field: 'created_by', + name: PROPERTY_TITLES.created_by, + }, + { + name: ACTIONS_COLUMN_TITLE, + actions: getActionDefinitions(context), + }, + { + align: RIGHT_ALIGNMENT, + width: '40px', + isExpander: true, + render(item: Immutable) { + return ( + setItemDetailsMap(toggleItemDetailsInMap(itemDetailsMap, item, context))} + aria-label={itemDetailsMap[item.id] ? 'Collapse' : 'Expand'} + iconType={itemDetailsMap[item.id] ? 'arrowUp' : 'arrowDown'} + data-test-subj="trustedAppsListItemExpandButton" + /> + ); + }, + }, + ]; +}; export const TrustedAppsList = memo(() => { + const [detailsMap, setDetailsMap] = useState({}); const pageIndex = useTrustedAppsSelector(getListCurrentPageIndex); const pageSize = useTrustedAppsSelector(getListCurrentPageSize); const totalItemCount = useTrustedAppsSelector(getListTotalItemsCount); @@ -125,10 +158,16 @@ export const TrustedAppsList = memo(() => { return ( getColumnDefinitions(dispatch), [dispatch])} + columns={useMemo( + () => getColumnDefinitions({ dispatch, detailsMapState: [detailsMap, setDetailsMap] }), + [dispatch, detailsMap, setDetailsMap] + )} items={useMemo(() => [...listItems], [listItems])} error={useTrustedAppsSelector(getListErrorMessage)} loading={useTrustedAppsSelector(isListLoading)} + itemId="id" + itemIdToExpandedRowMap={detailsMap} + isExpandable={true} pagination={useMemo( () => ({ pageIndex,