From 35dc121c950992ed4ee6067c7f48155a25da1794 Mon Sep 17 00:00:00 2001 From: Khristinin Nikita Date: Mon, 15 Apr 2024 19:45:52 +0200 Subject: [PATCH] Value lists modal window (#179339) ## Value List Items Modal Window Experimental flag: **valueListItemsModal** it's temporarily true and will be set to false, before merge https://github.com/elastic/kibana/assets/7609147/2aafe007-734f-4c0e-b542-5eca229e310d Added a new modal window that lets users interact with value lists directly. This modal supports viewing, searching, adding, deleting, updating, and uploading items. Where to find the modal window: - Value list popup - Exception editing and viewing on both the rule page and shared exceptions management page Permissions: Add, edit, delete, and upload buttons are hidden for read-only users. The modal link is not shown to users without read permissions. ### Changes to API I added optional **refresh** flag for list items API, because we want to see the changes in UI, when editing list items ### How to review All new components is here - `x-pack/plugins/security_solution/public/value_list/` New hooks - `packages/kbn-securitysolution-list-hooks/` API calls - `packages/kbn-securitysolution-list-api` Actual API - `x-pack/plugins/lists/server/routes` There also some props drilling into exceptions package, which is not ideal, please let me know if you have better ideas [test plan](https://github.com/elastic/security-team/pull/9128) --------- Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../src/field_value_lists/index.test.tsx | 38 ++- .../src/field_value_lists/index.tsx | 49 ++-- .../src/translations/index.ts | 4 + .../conditions/conditions.test.tsx | 4 + .../__snapshots__/entry_content.test.tsx.snap | 12 +- .../entry_content.helper.test.tsx | 57 ++++- .../entry_content/entry_content.helper.tsx | 17 +- .../entry_content/entry_content.test.tsx | 16 +- .../conditions/entry_content/index.tsx | 19 +- .../exception_item_card/conditions/index.tsx | 4 +- .../exception_item_card/conditions/types.ts | 2 + .../exception_item_card.test.tsx | 7 + .../exception_item_card.tsx | 3 + .../exception_items/exception_items.test.tsx | 9 + .../src/exception_items/index.tsx | 4 + .../src/mocks/value_list_modal.mock.tsx | 15 ++ .../src/common/index.ts | 1 + .../src/common/refresh/index.ts | 18 ++ .../request/create_list_item_schema/index.ts | 3 +- .../request/delete_list_item_schema/index.ts | 3 +- .../index.mock.ts | 1 + .../import_list_item_query_schema/index.ts | 3 +- .../request/patch_list_item_schema/index.ts | 3 +- .../kbn-securitysolution-list-api/index.ts | 2 + .../src/list_api/index.test.ts | 2 +- .../src/list_api/index.ts | 44 +++- .../src/list_item_api/index.test.ts | 172 +++++++++++++ .../src/list_item_api/index.ts | 226 ++++++++++++++++++ .../src/list_item_api/mocks/response/index.ts | 23 ++ .../mocks/response/list_item_schema.mock.ts | 26 ++ .../src/{list_api => }/types.ts | 35 +++ .../kbn-securitysolution-list-hooks/index.ts | 5 + .../src/index.ts | 5 + .../src/use_create_list_item/index.ts | 42 ++++ .../src/use_delete_list_item/index.ts | 40 ++++ .../src/use_find_list_items/index.ts | 74 ++++++ .../src/use_get_list_by_id/index.ts | 27 +++ .../src/use_patch_list_item/index.ts | 41 ++++ .../tsconfig.json | 2 +- .../__mock__/show_value_list_modal.mock.tsx | 9 + .../builder/entry_renderer.test.tsx | 29 +++ .../components/builder/entry_renderer.tsx | 5 +- .../builder/exception_item_renderer.test.tsx | 10 + .../builder/exception_item_renderer.tsx | 5 +- .../builder/exception_items_renderer.test.tsx | 12 + .../builder/exception_items_renderer.tsx | 5 +- .../routes/list/import_list_item_route.ts | 4 +- .../list_item/create_list_item_route.ts | 3 +- .../list_item/delete_list_item_route.ts | 6 +- .../routes/list_item/patch_list_item_route.ts | 4 +- .../server/services/items/create_list_item.ts | 5 +- .../services/items/create_list_items_bulk.ts | 5 +- .../server/services/items/delete_list_item.ts | 4 +- .../items/delete_list_item_by_value.ts | 4 +- .../server/services/items/update_list_item.ts | 4 +- .../items/write_lines_to_bulk_list_items.ts | 8 + .../server/services/lists/list_client.ts | 16 +- .../services/lists/list_client_types.ts | 7 + .../common/experimental_features.ts | 5 + .../exception_item_card/conditions.tsx | 7 + .../item_conditions/index.tsx | 2 + .../table_helpers.tsx | 6 + .../components/list_exception_items/index.tsx | 2 + .../event_filters/view/components/form.tsx | 2 + .../components/add_list_item_popover.tsx | 119 +++++++++ .../components/delete_list_item.tsx | 47 ++++ .../public/value_list/components/info.tsx | 24 ++ .../inline_edit_list_item_value.tsx | 67 ++++++ .../value_list/components/list_item_table.tsx | 85 +++++++ .../components/show_value_list_modal.tsx | 52 ++++ .../components/upload_list_item.tsx | 106 ++++++++ .../components/value_list_modal.tsx | 181 ++++++++++++++ .../public/value_list/translations.ts | 168 +++++++++++++ .../public/value_list/types.ts | 43 ++++ .../value_lists/value_list_items.cy.ts | 152 ++++++++++++ .../value_lists/value_lists.cy.ts | 12 +- .../cypress/fixtures/value_list.txt | 6 + .../cypress/screens/common/controls.ts | 2 + .../cypress/screens/lists.ts | 20 ++ .../cypress/tasks/lists.ts | 120 +++++++++- 80 files changed, 2346 insertions(+), 85 deletions(-) create mode 100644 packages/kbn-securitysolution-exception-list-components/src/mocks/value_list_modal.mock.tsx create mode 100644 packages/kbn-securitysolution-io-ts-list-types/src/common/refresh/index.ts create mode 100644 packages/kbn-securitysolution-list-api/src/list_item_api/index.test.ts create mode 100644 packages/kbn-securitysolution-list-api/src/list_item_api/index.ts create mode 100644 packages/kbn-securitysolution-list-api/src/list_item_api/mocks/response/index.ts create mode 100644 packages/kbn-securitysolution-list-api/src/list_item_api/mocks/response/list_item_schema.mock.ts rename packages/kbn-securitysolution-list-api/src/{list_api => }/types.ts (66%) create mode 100644 packages/kbn-securitysolution-list-hooks/src/use_create_list_item/index.ts create mode 100644 packages/kbn-securitysolution-list-hooks/src/use_delete_list_item/index.ts create mode 100644 packages/kbn-securitysolution-list-hooks/src/use_find_list_items/index.ts create mode 100644 packages/kbn-securitysolution-list-hooks/src/use_get_list_by_id/index.ts create mode 100644 packages/kbn-securitysolution-list-hooks/src/use_patch_list_item/index.ts create mode 100644 x-pack/plugins/lists/public/exceptions/components/__mock__/show_value_list_modal.mock.tsx create mode 100644 x-pack/plugins/security_solution/public/value_list/components/add_list_item_popover.tsx create mode 100644 x-pack/plugins/security_solution/public/value_list/components/delete_list_item.tsx create mode 100644 x-pack/plugins/security_solution/public/value_list/components/info.tsx create mode 100644 x-pack/plugins/security_solution/public/value_list/components/inline_edit_list_item_value.tsx create mode 100644 x-pack/plugins/security_solution/public/value_list/components/list_item_table.tsx create mode 100644 x-pack/plugins/security_solution/public/value_list/components/show_value_list_modal.tsx create mode 100644 x-pack/plugins/security_solution/public/value_list/components/upload_list_item.tsx create mode 100644 x-pack/plugins/security_solution/public/value_list/components/value_list_modal.tsx create mode 100644 x-pack/plugins/security_solution/public/value_list/translations.ts create mode 100644 x-pack/plugins/security_solution/public/value_list/types.ts create mode 100644 x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/value_lists/value_list_items.cy.ts diff --git a/packages/kbn-securitysolution-autocomplete/src/field_value_lists/index.test.tsx b/packages/kbn-securitysolution-autocomplete/src/field_value_lists/index.test.tsx index 733e111fb4dd5..daff429912047 100644 --- a/packages/kbn-securitysolution-autocomplete/src/field_value_lists/index.test.tsx +++ b/packages/kbn-securitysolution-autocomplete/src/field_value_lists/index.test.tsx @@ -26,7 +26,11 @@ import { // const mockKibanaHttpService = coreMock.createStart().http; // import { coreMock } from '../../../../../../../src/core/public/mocks'; const mockKibanaHttpService = jest.fn(); - +const mockShowValueListModal = jest.fn(); +const MockedShowValueListModal = (props: unknown) => { + mockShowValueListModal(props); + return <>; +}; const mockStart = jest.fn(); const mockKeywordList: ListSchema = { ...getListResponseMock(), @@ -63,6 +67,7 @@ describe('AutocompleteFieldListsComponent', () => { placeholder="Placeholder text" selectedField={getField('ip')} selectedValue="some-list-id" + showValueListModal={MockedShowValueListModal} /> ); @@ -84,6 +89,7 @@ describe('AutocompleteFieldListsComponent', () => { placeholder="Placeholder text" selectedField={getField('@tags')} selectedValue="" + showValueListModal={MockedShowValueListModal} /> ); @@ -111,6 +117,7 @@ describe('AutocompleteFieldListsComponent', () => { placeholder="Placeholder text" selectedField={getField('ip')} selectedValue="" + showValueListModal={MockedShowValueListModal} /> ); expect( @@ -131,6 +138,7 @@ describe('AutocompleteFieldListsComponent', () => { placeholder="Placeholder text" selectedField={getField('@tags')} selectedValue="" + showValueListModal={MockedShowValueListModal} /> ); @@ -154,6 +162,7 @@ describe('AutocompleteFieldListsComponent', () => { placeholder="Placeholder text" selectedField={getField('ip')} selectedValue="" + showValueListModal={MockedShowValueListModal} /> ); @@ -177,6 +186,7 @@ describe('AutocompleteFieldListsComponent', () => { placeholder="Placeholder text" selectedField={getField('ip')} selectedValue="some-list-id" + showValueListModal={MockedShowValueListModal} /> ); @@ -200,6 +210,7 @@ describe('AutocompleteFieldListsComponent', () => { placeholder="Placeholder text" selectedField={getField('ip')} selectedValue="" + showValueListModal={MockedShowValueListModal} /> ); @@ -230,4 +241,29 @@ describe('AutocompleteFieldListsComponent', () => { }); }); }); + + test('it render the value list modal', async () => { + mockShowValueListModal.mockReset(); + mount( + + ); + + expect(mockShowValueListModal).toHaveBeenCalledWith( + expect.objectContaining({ + children: 'Show value list', + listId: 'some-list-id', + shouldShowContentIfModalNotAvailable: false, + }) + ); + }); }); diff --git a/packages/kbn-securitysolution-autocomplete/src/field_value_lists/index.tsx b/packages/kbn-securitysolution-autocomplete/src/field_value_lists/index.tsx index e7c79042e455d..75c7001c462d5 100644 --- a/packages/kbn-securitysolution-autocomplete/src/field_value_lists/index.tsx +++ b/packages/kbn-securitysolution-autocomplete/src/field_value_lists/index.tsx @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import React, { ElementType, useCallback, useEffect, useMemo, useState } from 'react'; import { EuiComboBox, EuiComboBoxOptionOption, EuiFormRow, EuiLink, EuiText } from '@elastic/eui'; import type { ListSchema } from '@kbn/securitysolution-io-ts-list-types'; import { useFindListsBySize } from '@kbn/securitysolution-list-hooks'; @@ -36,6 +36,7 @@ interface AutocompleteFieldListsProps { selectedValue: string | undefined; allowLargeValueLists?: boolean; 'aria-label'?: string; + showValueListModal: ElementType; } export interface AutocompleteListsData { @@ -55,6 +56,7 @@ export const AutocompleteFieldListsComponent: React.FC { const [error, setError] = useState(undefined); const [listData, setListData] = useState({ @@ -118,29 +120,36 @@ export const AutocompleteFieldListsComponent: React.FC isLoading || loading, [isLoading, loading]); + const ShowValueListModal = showValueListModal; const helpText = useMemo(() => { return ( - !allowLargeValueLists && ( - - {i18n.LISTS_TOOLTIP_INFO}{' '} - - {i18n.SEE_DOCUMENTATION} - - - ) + <> + {selectedValue && ( + + {i18n.SHOW_VALUE_LIST_MODAL} + + )} + {!allowLargeValueLists && ( + + {i18n.LISTS_TOOLTIP_INFO}{' '} + + {i18n.SEE_DOCUMENTATION} + + + )} + ); - }, [allowLargeValueLists]); - + }, [allowLargeValueLists, selectedValue, ShowValueListModal]); return ( { }, ]} dataTestSubj="exceptionItemConditions" + showValueListModal={MockedShowValueListModal} /> ); expect(wrapper.getByTestId('exceptionItemConditionsOs')).toHaveTextContent('OSIS Linux'); @@ -243,6 +245,7 @@ describe('ExceptionItemCardConditions', () => { type: 'nested', }, ]} + showValueListModal={MockedShowValueListModal} dataTestSubj="exceptionItemConditions" /> ); @@ -331,6 +334,7 @@ describe('ExceptionItemCardConditions', () => { type: 'list', }, ]} + showValueListModal={MockedShowValueListModal} dataTestSubj="exceptionItemConditions" /> ); diff --git a/packages/kbn-securitysolution-exception-list-components/src/exception_item_card/conditions/entry_content/__snapshots__/entry_content.test.tsx.snap b/packages/kbn-securitysolution-exception-list-components/src/exception_item_card/conditions/entry_content/__snapshots__/entry_content.test.tsx.snap index f2b8ce9c2ec22..bd4792360e0c9 100644 --- a/packages/kbn-securitysolution-exception-list-components/src/exception_item_card/conditions/entry_content/__snapshots__/entry_content.test.tsx.snap +++ b/packages/kbn-securitysolution-exception-list-components/src/exception_item_card/conditions/entry_content/__snapshots__/entry_content.test.tsx.snap @@ -48,11 +48,7 @@ Object { - - list_id - + list_id @@ -105,11 +101,7 @@ Object { - - list_id - + list_id diff --git a/packages/kbn-securitysolution-exception-list-components/src/exception_item_card/conditions/entry_content/entry_content.helper.test.tsx b/packages/kbn-securitysolution-exception-list-components/src/exception_item_card/conditions/entry_content/entry_content.helper.test.tsx index d30cf9fa2f1d2..0c111c5f2410f 100644 --- a/packages/kbn-securitysolution-exception-list-components/src/exception_item_card/conditions/entry_content/entry_content.helper.test.tsx +++ b/packages/kbn-securitysolution-exception-list-components/src/exception_item_card/conditions/entry_content/entry_content.helper.test.tsx @@ -14,6 +14,10 @@ import { includedListTypeEntry, includedMatchTypeEntry, } from '../../../mocks/entry.mock'; +import { + MockedShowValueListModal, + mockShowValueListModal, +} from '../../../mocks/value_list_modal.mock'; describe('entry_content.helper', () => { describe('getEntryOperator', () => { @@ -62,36 +66,79 @@ describe('entry_content.helper', () => { describe('getValueExpression', () => { it('should render multiple values in badges when operator type is match_any and values is Array', () => { const wrapper = render( - getValueExpression(ListOperatorTypeEnum.MATCH_ANY, 'included', ['value 1', 'value 2']) + getValueExpression( + ListOperatorTypeEnum.MATCH_ANY, + 'included', + ['value 1', 'value 2'], + MockedShowValueListModal + ) ); expect(wrapper.getByTestId('matchAnyBadge0')).toHaveTextContent('value 1'); expect(wrapper.getByTestId('matchAnyBadge1')).toHaveTextContent('value 2'); }); it('should return one value when operator type is match_any and values is not Array', () => { const wrapper = render( - getValueExpression(ListOperatorTypeEnum.MATCH_ANY, 'included', 'value 1') + getValueExpression( + ListOperatorTypeEnum.MATCH_ANY, + 'included', + 'value 1', + MockedShowValueListModal + ) ); expect(wrapper.getByTestId('entryValueExpression')).toHaveTextContent('value 1'); }); it('should return one value when operator type is a single value', () => { const wrapper = render( - getValueExpression(ListOperatorTypeEnum.EXISTS, 'included', 'value 1') + getValueExpression( + ListOperatorTypeEnum.EXISTS, + 'included', + 'value 1', + MockedShowValueListModal + ) ); expect(wrapper.getByTestId('entryValueExpression')).toHaveTextContent('value 1'); }); it('should return value with warning icon when the value contains a leading or trailing space', () => { const wrapper = render( - getValueExpression(ListOperatorTypeEnum.EXISTS, 'included', ' value 1') + getValueExpression( + ListOperatorTypeEnum.EXISTS, + 'included', + ' value 1', + MockedShowValueListModal + ) ); expect(wrapper.getByTestId('entryValueExpression')).toHaveTextContent(' value 1'); expect(wrapper.getByTestId('valueWithSpaceWarningTooltip')).toBeInTheDocument(); }); it('should return value without warning icon when the value does not contain a leading or trailing space', () => { const wrapper = render( - getValueExpression(ListOperatorTypeEnum.EXISTS, 'included', 'value 1') + getValueExpression( + ListOperatorTypeEnum.EXISTS, + 'included', + 'value 1', + MockedShowValueListModal + ) ); expect(wrapper.getByTestId('entryValueExpression')).toHaveTextContent(' value 1'); expect(wrapper.queryByTestId('valueWithSpaceWarningTooltip')).not.toBeInTheDocument(); }); + it('should render value list modal when operator type is list', () => { + mockShowValueListModal.mockReset(); + render( + getValueExpression( + ListOperatorTypeEnum.LIST, + 'included', + 'value 1', + MockedShowValueListModal + ) + ); + expect(mockShowValueListModal).toHaveBeenCalledWith( + expect.objectContaining({ + children: 'value 1', + listId: 'value 1', + shouldShowContentIfModalNotAvailable: true, + }) + ); + }); }); }); diff --git a/packages/kbn-securitysolution-exception-list-components/src/exception_item_card/conditions/entry_content/entry_content.helper.tsx b/packages/kbn-securitysolution-exception-list-components/src/exception_item_card/conditions/entry_content/entry_content.helper.tsx index 260b361582ae3..cd894db1d2aaa 100644 --- a/packages/kbn-securitysolution-exception-list-components/src/exception_item_card/conditions/entry_content/entry_content.helper.tsx +++ b/packages/kbn-securitysolution-exception-list-components/src/exception_item_card/conditions/entry_content/entry_content.helper.tsx @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import React from 'react'; +import React, { ElementType } from 'react'; import { css } from '@emotion/css'; import { EuiExpression, EuiBadge } from '@elastic/eui'; import type { ListOperatorTypeEnum } from '@kbn/securitysolution-io-ts-list-types'; @@ -21,13 +21,21 @@ const entryValueWrapStyle = css` const EntryValueWrap = ({ children }: { children: React.ReactNode }) => ( {children} ); -const getEntryValue = (type: string, value?: string | string[]) => { + +const getEntryValue = (type: string, value: string | string[], showValueListModal: ElementType) => { + const ShowValueListModal = showValueListModal; if (type === 'match_any' && Array.isArray(value)) { return value.map((currentValue, index) => ( {currentValue} )); + } else if (type === 'list' && value) { + return ( + + {value} + + ); } return {value} ?? ''; }; @@ -48,12 +56,13 @@ export const getValue = (entry: Entry) => { export const getValueExpression = ( type: ListOperatorTypeEnum, operator: string, - value: string | string[] + value: string | string[], + showValueListModal: ElementType ) => ( <> diff --git a/packages/kbn-securitysolution-exception-list-components/src/exception_item_card/conditions/entry_content/entry_content.test.tsx b/packages/kbn-securitysolution-exception-list-components/src/exception_item_card/conditions/entry_content/entry_content.test.tsx index 7e02e19d61844..fd11f74979047 100644 --- a/packages/kbn-securitysolution-exception-list-components/src/exception_item_card/conditions/entry_content/entry_content.test.tsx +++ b/packages/kbn-securitysolution-exception-list-components/src/exception_item_card/conditions/entry_content/entry_content.test.tsx @@ -10,18 +10,29 @@ import { render } from '@testing-library/react'; import { includedListTypeEntry } from '../../../mocks/entry.mock'; import * as i18n from '../../translations'; import { EntryContent } from '.'; +import { MockedShowValueListModal } from '../../../mocks/value_list_modal.mock'; describe('EntryContent', () => { it('should render a single value without AND when index is 0', () => { const wrapper = render( - + ); expect(wrapper.getByTestId('EntryContentSingleEntry')).toBeInTheDocument(); expect(wrapper.getByTestId('entryValueExpression')).toHaveTextContent('list_id'); }); it('should render a single value with AND when index is 1', () => { const wrapper = render( - + ); expect(wrapper.getByTestId('EntryContentSingleEntry')).toBeInTheDocument(); expect(wrapper.getByTestId('entryValueExpression')).toHaveTextContent('list_id'); @@ -34,6 +45,7 @@ describe('EntryContent', () => { index={0} isNestedEntry dataTestSubj="EntryContent" + showValueListModal={MockedShowValueListModal} /> ); expect(wrapper.getByTestId('EntryContentNestedEntry')).toBeInTheDocument(); diff --git a/packages/kbn-securitysolution-exception-list-components/src/exception_item_card/conditions/entry_content/index.tsx b/packages/kbn-securitysolution-exception-list-components/src/exception_item_card/conditions/entry_content/index.tsx index 8a5fe0b998fa4..95cbb9e59797c 100644 --- a/packages/kbn-securitysolution-exception-list-components/src/exception_item_card/conditions/entry_content/index.tsx +++ b/packages/kbn-securitysolution-exception-list-components/src/exception_item_card/conditions/entry_content/index.tsx @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import React, { FC, memo } from 'react'; +import React, { ElementType, FC, memo } from 'react'; import { EuiExpression, EuiToken, EuiFlexGroup } from '@elastic/eui'; import { ListOperatorTypeEnum } from '@kbn/securitysolution-io-ts-list-types'; import { @@ -23,10 +23,11 @@ interface EntryContentProps { index: number; isNestedEntry?: boolean; dataTestSubj?: string; + showValueListModal: ElementType; } export const EntryContent: FC = memo( - ({ entry, index, isNestedEntry = false, dataTestSubj }) => { + ({ entry, index, isNestedEntry = false, dataTestSubj, showValueListModal }) => { const { field, type } = entry; const value = getValue(entry); const operator = 'operator' in entry ? entry.operator : ''; @@ -48,7 +49,12 @@ export const EntryContent: FC = memo(
- {getValueExpression(type as ListOperatorTypeEnum, operator, value)} + {getValueExpression( + type as ListOperatorTypeEnum, + operator, + value, + showValueListModal + )}
) : ( @@ -60,7 +66,12 @@ export const EntryContent: FC = memo( data-test-subj={`${dataTestSubj || ''}SingleEntry`} /> - {getValueExpression(type as ListOperatorTypeEnum, operator, value)} + {getValueExpression( + type as ListOperatorTypeEnum, + operator, + value, + showValueListModal + )} )} diff --git a/packages/kbn-securitysolution-exception-list-components/src/exception_item_card/conditions/index.tsx b/packages/kbn-securitysolution-exception-list-components/src/exception_item_card/conditions/index.tsx index 28d9b1d9b09d1..52a2249c3b2c6 100644 --- a/packages/kbn-securitysolution-exception-list-components/src/exception_item_card/conditions/index.tsx +++ b/packages/kbn-securitysolution-exception-list-components/src/exception_item_card/conditions/index.tsx @@ -15,7 +15,7 @@ import { OsCondition } from './os_conditions'; import type { CriteriaConditionsProps, Entry } from './types'; export const ExceptionItemCardConditions = memo( - ({ os, entries, dataTestSubj }) => { + ({ os, entries, dataTestSubj, showValueListModal }) => { return ( ( entry={entry} index={index} dataTestSubj={dataTestSubj} + showValueListModal={showValueListModal} /> {nestedEntries?.length ? nestedEntries.map((nestedEntry: Entry, nestedIndex: number) => ( @@ -43,6 +44,7 @@ export const ExceptionItemCardConditions = memo( index={nestedIndex} isNestedEntry={true} dataTestSubj={dataTestSubj} + showValueListModal={showValueListModal} /> )) : null} diff --git a/packages/kbn-securitysolution-exception-list-components/src/exception_item_card/conditions/types.ts b/packages/kbn-securitysolution-exception-list-components/src/exception_item_card/conditions/types.ts index 09067e84cafc7..d965dfddd4b46 100644 --- a/packages/kbn-securitysolution-exception-list-components/src/exception_item_card/conditions/types.ts +++ b/packages/kbn-securitysolution-exception-list-components/src/exception_item_card/conditions/types.ts @@ -15,6 +15,7 @@ import type { EntryNested, ExceptionListItemSchema, } from '@kbn/securitysolution-io-ts-list-types'; +import { ElementType } from 'react'; export type Entry = | EntryExists @@ -29,4 +30,5 @@ export interface CriteriaConditionsProps { entries: Entries; dataTestSubj: string; os?: ExceptionListItemSchema['os_types']; + showValueListModal: ElementType; } diff --git a/packages/kbn-securitysolution-exception-list-components/src/exception_item_card/exception_item_card.test.tsx b/packages/kbn-securitysolution-exception-list-components/src/exception_item_card/exception_item_card.test.tsx index 649748f593034..a6b3763cb5541 100644 --- a/packages/kbn-securitysolution-exception-list-components/src/exception_item_card/exception_item_card.test.tsx +++ b/packages/kbn-securitysolution-exception-list-components/src/exception_item_card/exception_item_card.test.tsx @@ -12,6 +12,7 @@ import { fireEvent, render } from '@testing-library/react'; import { ExceptionItemCard } from '.'; import { getExceptionListItemSchemaMock } from '../mocks/exception_list_item_schema.mock'; import { getCommentsArrayMock, mockGetFormattedComments } from '../mocks/comments.mock'; +import { MockedShowValueListModal } from '../mocks/value_list_modal.mock'; import { ExceptionListTypeEnum } from '@kbn/securitysolution-io-ts-list-types'; import { rules } from '../mocks/rule_references.mock'; @@ -30,6 +31,7 @@ describe('ExceptionItemCard', () => { securityLinkAnchorComponent={() => null} formattedDateComponent={() => null} getFormattedComments={() => []} + showValueListModal={MockedShowValueListModal} /> ); @@ -53,6 +55,7 @@ describe('ExceptionItemCard', () => { securityLinkAnchorComponent={() => null} formattedDateComponent={() => null} getFormattedComments={mockGetFormattedComments} + showValueListModal={MockedShowValueListModal} /> ); @@ -77,6 +80,7 @@ describe('ExceptionItemCard', () => { securityLinkAnchorComponent={() => null} formattedDateComponent={() => null} getFormattedComments={() => []} + showValueListModal={MockedShowValueListModal} /> ); expect(wrapper.queryByTestId('itemActionButton')).not.toBeInTheDocument(); @@ -97,6 +101,7 @@ describe('ExceptionItemCard', () => { securityLinkAnchorComponent={() => null} formattedDateComponent={() => null} getFormattedComments={() => []} + showValueListModal={MockedShowValueListModal} /> ); @@ -120,6 +125,7 @@ describe('ExceptionItemCard', () => { securityLinkAnchorComponent={() => null} formattedDateComponent={() => null} getFormattedComments={() => []} + showValueListModal={MockedShowValueListModal} /> ); fireEvent.click(wrapper.getByTestId('exceptionItemCardHeaderButtonIcon')); @@ -146,6 +152,7 @@ describe('ExceptionItemCard', () => { securityLinkAnchorComponent={() => null} formattedDateComponent={() => null} getFormattedComments={mockGetFormattedComments} + showValueListModal={MockedShowValueListModal} /> ); diff --git a/packages/kbn-securitysolution-exception-list-components/src/exception_item_card/exception_item_card.tsx b/packages/kbn-securitysolution-exception-list-components/src/exception_item_card/exception_item_card.tsx index a9aa5c7dedd87..c44ae14c34b09 100644 --- a/packages/kbn-securitysolution-exception-list-components/src/exception_item_card/exception_item_card.tsx +++ b/packages/kbn-securitysolution-exception-list-components/src/exception_item_card/exception_item_card.tsx @@ -37,6 +37,7 @@ export interface ExceptionItemProps { getFormattedComments: (comments: CommentsArray) => EuiCommentProps[]; // This property needs to be removed to avoid the Prop Drilling, once we move all the common components from x-pack/security-solution/common onDeleteException: (arg: ExceptionListItemIdentifiers) => void; onEditException: (item: ExceptionListItemSchema) => void; + showValueListModal: ElementType; } const ExceptionItemCardComponent: FC = ({ @@ -52,6 +53,7 @@ const ExceptionItemCardComponent: FC = ({ getFormattedComments, onDeleteException, onEditException, + showValueListModal, }) => { const { actions, formattedComments } = useExceptionItemCard({ listType, @@ -93,6 +95,7 @@ const ExceptionItemCardComponent: FC = ({ os={exceptionItem.os_types} entries={exceptionItem.entries} dataTestSubj="exceptionItemCardConditions" + showValueListModal={showValueListModal} /> {formattedComments.length > 0 && ( diff --git a/packages/kbn-securitysolution-exception-list-components/src/exception_items/exception_items.test.tsx b/packages/kbn-securitysolution-exception-list-components/src/exception_items/exception_items.test.tsx index da5df52996952..c4d937339d647 100644 --- a/packages/kbn-securitysolution-exception-list-components/src/exception_items/exception_items.test.tsx +++ b/packages/kbn-securitysolution-exception-list-components/src/exception_items/exception_items.test.tsx @@ -19,6 +19,7 @@ import { ruleReferences } from '../mocks/rule_references.mock'; import { Pagination } from '@elastic/eui'; import { mockGetFormattedComments } from '../mocks/comments.mock'; import { securityLinkAnchorComponentMock } from '../mocks/security_link_component.mock'; +import { MockedShowValueListModal } from '../mocks/value_list_modal.mock'; const onCreateExceptionListItem = jest.fn(); const onDeleteException = jest.fn(); @@ -47,6 +48,7 @@ describe('ExceptionsViewerItems', () => { formattedDateComponent={() => null} exceptionsUtilityComponent={() => null} getFormattedComments={() => []} + showValueListModal={MockedShowValueListModal} /> ); expect(wrapper.getByTestId('emptyViewerState')).toBeInTheDocument(); @@ -71,6 +73,7 @@ describe('ExceptionsViewerItems', () => { formattedDateComponent={() => null} exceptionsUtilityComponent={() => null} getFormattedComments={() => []} + showValueListModal={MockedShowValueListModal} /> ); expect(wrapper.getByTestId('emptySearchViewerState')).toBeInTheDocument(); @@ -96,6 +99,7 @@ describe('ExceptionsViewerItems', () => { formattedDateComponent={() => null} exceptionsUtilityComponent={() => null} getFormattedComments={() => []} + showValueListModal={MockedShowValueListModal} /> ); expect(wrapper.getByTestId('exceptionsContainer')).toBeInTheDocument(); @@ -124,6 +128,7 @@ describe('ExceptionsViewerItems', () => { formattedDateComponent={() => null} exceptionsUtilityComponent={() => null} getFormattedComments={() => []} + showValueListModal={MockedShowValueListModal} /> ); expect(wrapper.getByTestId('exceptionsContainer')).toBeTruthy(); @@ -151,6 +156,7 @@ describe('ExceptionsViewerItems', () => { formattedDateComponent={() => null} exceptionsUtilityComponent={() => null} getFormattedComments={() => []} + showValueListModal={MockedShowValueListModal} /> ); expect(wrapper.getByTestId('exceptionsContainer')).toBeInTheDocument(); @@ -187,6 +193,7 @@ describe('ExceptionsViewerItems', () => { formattedDateComponent={() => null} exceptionsUtilityComponent={exceptionsUtilityComponent} getFormattedComments={() => []} + showValueListModal={MockedShowValueListModal} /> ); expect(wrapper.getByTestId('exceptionsContainer')).toBeInTheDocument(); @@ -224,6 +231,7 @@ describe('ExceptionsViewerItems', () => { formattedDateComponent={formattedDateComponent} exceptionsUtilityComponent={() => null} getFormattedComments={() => []} + showValueListModal={MockedShowValueListModal} /> ); expect(wrapper.getByTestId('exceptionsContainer')).toBeInTheDocument(); @@ -255,6 +263,7 @@ describe('ExceptionsViewerItems', () => { formattedDateComponent={() => null} exceptionsUtilityComponent={() => null} getFormattedComments={mockGetFormattedComments} + showValueListModal={MockedShowValueListModal} /> ); expect(wrapper.getByTestId('exceptionsContainer')).toBeInTheDocument(); diff --git a/packages/kbn-securitysolution-exception-list-components/src/exception_items/index.tsx b/packages/kbn-securitysolution-exception-list-components/src/exception_items/index.tsx index d9085b08f536d..5ea742a8f62b8 100644 --- a/packages/kbn-securitysolution-exception-list-components/src/exception_items/index.tsx +++ b/packages/kbn-securitysolution-exception-list-components/src/exception_items/index.tsx @@ -56,6 +56,7 @@ interface ExceptionItemsProps { onDeleteException: (arg: ExceptionListItemIdentifiers) => void; onEditExceptionItem: (item: ExceptionListItemSchema) => void; onPaginationChange: (arg: GetExceptionItemProps) => void; + showValueListModal: ElementType; } const ExceptionItemsComponent: FC = ({ @@ -80,6 +81,7 @@ const ExceptionItemsComponent: FC = ({ onDeleteException, onEditExceptionItem, onCreateExceptionListItem, + showValueListModal, }) => { const ExceptionsUtility = exceptionsUtilityComponent; if (!exceptions.length || viewerStatus) @@ -93,6 +95,7 @@ const ExceptionItemsComponent: FC = ({ onEmptyButtonStateClick={onCreateExceptionListItem} /> ); + const ShowValueListModal = showValueListModal; return ( <> @@ -128,6 +131,7 @@ const ExceptionItemsComponent: FC = ({ securityLinkAnchorComponent={securityLinkAnchorComponent} formattedDateComponent={formattedDateComponent} getFormattedComments={getFormattedComments} + showValueListModal={ShowValueListModal} /> ))} diff --git a/packages/kbn-securitysolution-exception-list-components/src/mocks/value_list_modal.mock.tsx b/packages/kbn-securitysolution-exception-list-components/src/mocks/value_list_modal.mock.tsx new file mode 100644 index 0000000000000..4dd02b6a1ca08 --- /dev/null +++ b/packages/kbn-securitysolution-exception-list-components/src/mocks/value_list_modal.mock.tsx @@ -0,0 +1,15 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; + +export const mockShowValueListModal = jest.fn(); +export const MockedShowValueListModal = (props: { children: React.ReactNode }) => { + mockShowValueListModal(props); + return <>{props.children}; +}; diff --git a/packages/kbn-securitysolution-io-ts-list-types/src/common/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/common/index.ts index 9c7e1a4ad31b5..ccd938ba33877 100644 --- a/packages/kbn-securitysolution-io-ts-list-types/src/common/index.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/common/index.ts @@ -63,3 +63,4 @@ export * from './underscore_version'; export * from './update_comment'; export * from './updated_at'; export * from './updated_by'; +export * from './refresh'; diff --git a/packages/kbn-securitysolution-io-ts-list-types/src/common/refresh/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/common/refresh/index.ts new file mode 100644 index 0000000000000..13602cbcfd1f6 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-list-types/src/common/refresh/index.ts @@ -0,0 +1,18 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import * as t from 'io-ts'; + +export const refresh = t.union([t.literal('true'), t.literal('false')]); +export const refreshWithWaitFor = t.union([ + t.literal('true'), + t.literal('false'), + t.literal('wait_for'), +]); +export type Refresh = t.TypeOf; +export type RefreshWithWaitFor = t.TypeOf; diff --git a/packages/kbn-securitysolution-io-ts-list-types/src/request/create_list_item_schema/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/request/create_list_item_schema/index.ts index c99e7b059f479..afff3b5c789e9 100644 --- a/packages/kbn-securitysolution-io-ts-list-types/src/request/create_list_item_schema/index.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/request/create_list_item_schema/index.ts @@ -13,6 +13,7 @@ import { list_id } from '../../common/list_id'; import { value } from '../../common/value'; import { id } from '../../common/id'; import { meta } from '../../common/meta'; +import { refreshWithWaitFor } from '../../common/refresh'; export const createListItemSchema = t.intersection([ t.exact( @@ -21,7 +22,7 @@ export const createListItemSchema = t.intersection([ value, }) ), - t.exact(t.partial({ id, meta })), + t.exact(t.partial({ id, meta, refresh: refreshWithWaitFor })), ]); export type CreateListItemSchema = t.OutputOf; diff --git a/packages/kbn-securitysolution-io-ts-list-types/src/request/delete_list_item_schema/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/request/delete_list_item_schema/index.ts index 903a6abb1535a..ab24bf3f862d5 100644 --- a/packages/kbn-securitysolution-io-ts-list-types/src/request/delete_list_item_schema/index.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/request/delete_list_item_schema/index.ts @@ -12,6 +12,7 @@ import { RequiredKeepUndefined } from '../../common/required_keep_undefined'; import { id } from '../../common/id'; import { list_id } from '../../common/list_id'; import { valueOrUndefined } from '../../common/value'; +import { refresh } from '../../common/refresh'; export const deleteListItemSchema = t.intersection([ t.exact( @@ -19,7 +20,7 @@ export const deleteListItemSchema = t.intersection([ value: valueOrUndefined, }) ), - t.exact(t.partial({ id, list_id })), + t.exact(t.partial({ id, list_id, refresh })), ]); export type DeleteListItemSchema = t.OutputOf; diff --git a/packages/kbn-securitysolution-io-ts-list-types/src/request/import_list_item_query_schema/index.mock.ts b/packages/kbn-securitysolution-io-ts-list-types/src/request/import_list_item_query_schema/index.mock.ts index 622400e307811..4dbc5bd1472d3 100644 --- a/packages/kbn-securitysolution-io-ts-list-types/src/request/import_list_item_query_schema/index.mock.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/request/import_list_item_query_schema/index.mock.ts @@ -15,4 +15,5 @@ export const getImportListItemQuerySchemaMock = (): ImportListItemQuerySchema => list_id: LIST_ID, serializer: undefined, type: TYPE, + refresh: 'false', }); diff --git a/packages/kbn-securitysolution-io-ts-list-types/src/request/import_list_item_query_schema/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/request/import_list_item_query_schema/index.ts index 9d7b782c502b4..71d6f933daf39 100644 --- a/packages/kbn-securitysolution-io-ts-list-types/src/request/import_list_item_query_schema/index.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/request/import_list_item_query_schema/index.ts @@ -13,9 +13,10 @@ import { deserializer } from '../../common/deserializer'; import { list_id } from '../../common/list_id'; import { type } from '../../common/type'; import { serializer } from '../../common/serializer'; +import { refreshWithWaitFor } from '../../common/refresh'; export const importListItemQuerySchema = t.exact( - t.partial({ deserializer, list_id, serializer, type }) + t.partial({ deserializer, list_id, serializer, type, refresh: refreshWithWaitFor }) ); export type ImportListItemQuerySchema = RequiredKeepUndefined< diff --git a/packages/kbn-securitysolution-io-ts-list-types/src/request/patch_list_item_schema/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/request/patch_list_item_schema/index.ts index 7de6d75c74eb2..402d265e1b70a 100644 --- a/packages/kbn-securitysolution-io-ts-list-types/src/request/patch_list_item_schema/index.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/request/patch_list_item_schema/index.ts @@ -13,6 +13,7 @@ import { _version } from '../../common/underscore_version'; import { id } from '../../common/id'; import { meta } from '../../common/meta'; import { value } from '../../common/value'; +import { refresh } from '../../common/refresh'; export const patchListItemSchema = t.intersection([ t.exact( @@ -20,7 +21,7 @@ export const patchListItemSchema = t.intersection([ id, }) ), - t.exact(t.partial({ _version, meta, value })), + t.exact(t.partial({ _version, meta, value, refresh })), ]); export type PatchListItemSchema = t.OutputOf; diff --git a/packages/kbn-securitysolution-list-api/index.ts b/packages/kbn-securitysolution-list-api/index.ts index 7b4a33641befa..3a28bfa1a6ff3 100644 --- a/packages/kbn-securitysolution-list-api/index.ts +++ b/packages/kbn-securitysolution-list-api/index.ts @@ -9,3 +9,5 @@ export * from './src/api'; export * from './src/fp_utils'; export * from './src/list_api'; +export * from './src/list_item_api'; +export * from './src/types'; diff --git a/packages/kbn-securitysolution-list-api/src/list_api/index.test.ts b/packages/kbn-securitysolution-list-api/src/list_api/index.test.ts index 56e0e51dea0d9..c3869eebd454a 100644 --- a/packages/kbn-securitysolution-list-api/src/list_api/index.test.ts +++ b/packages/kbn-securitysolution-list-api/src/list_api/index.test.ts @@ -12,7 +12,7 @@ import { ExportListParams, FindListsParams, ImportListParams, -} from './types'; +} from '../types'; import { HttpFetchOptions } from '@kbn/core-http-browser'; import { httpServiceMock } from '@kbn/core-http-browser-mocks'; diff --git a/packages/kbn-securitysolution-list-api/src/list_api/index.ts b/packages/kbn-securitysolution-list-api/src/list_api/index.ts index 59303b9e8abaf..1354ddd6f35e4 100644 --- a/packages/kbn-securitysolution-list-api/src/list_api/index.ts +++ b/packages/kbn-securitysolution-list-api/src/list_api/index.ts @@ -20,8 +20,10 @@ import { ImportListItemSchemaEncoded, ListItemIndexExistSchema, ListSchema, + ReadListSchema, acknowledgeSchema, deleteListSchema, + readListSchema, exportListItemQuerySchema, findListSchema, foundListSchema, @@ -47,7 +49,8 @@ import { ExportListParams, FindListsParams, ImportListParams, -} from './types'; + GetListByIdParams, +} from '../types'; export type { ApiParams, @@ -55,7 +58,7 @@ export type { ExportListParams, FindListsParams, ImportListParams, -} from './types'; +} from '../types'; const version = '2023-10-31'; @@ -158,6 +161,7 @@ const importList = async ({ list_id, type, signal, + refresh, }: ApiParams & ImportListItemSchemaEncoded & ImportListItemQuerySchemaEncoded): Promise => { @@ -168,7 +172,7 @@ const importList = async ({ body: formData, headers: { 'Content-Type': undefined }, method: 'POST', - query: { list_id, type }, + query: { list_id, type, refresh }, signal, version, }); @@ -180,11 +184,13 @@ const importListWithValidation = async ({ listId, type, signal, + refresh, }: ImportListParams): Promise => pipe( { list_id: listId, type, + refresh, }, (query) => fromEither(validateEither(importListItemQuerySchema, query)), chain((query) => @@ -303,3 +309,35 @@ const createListIndexWithValidation = async ({ )(); export { createListIndexWithValidation as createListIndex }; + +const getListById = async ({ + http, + signal, + id, +}: ApiParams & ReadListSchema): Promise => { + return http.fetch(`${LIST_URL}`, { + method: 'GET', + query: { + id, + }, + signal, + version, + }); +}; + +const getListByIdWithValidation = async ({ + http, + signal, + id, +}: GetListByIdParams): Promise => + pipe( + { + id, + }, + (payload) => fromEither(validateEither(readListSchema, payload)), + chain((payload) => tryCatch(() => getListById({ http, signal, ...payload }), toError)), + chain((response) => fromEither(validateEither(listSchema, response))), + flow(toPromise) + ); + +export { getListByIdWithValidation as getListById }; diff --git a/packages/kbn-securitysolution-list-api/src/list_item_api/index.test.ts b/packages/kbn-securitysolution-list-api/src/list_item_api/index.test.ts new file mode 100644 index 0000000000000..9923f7f290742 --- /dev/null +++ b/packages/kbn-securitysolution-list-api/src/list_item_api/index.test.ts @@ -0,0 +1,172 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { createListItem, deleteListItem, findListItems, patchListItem } from '.'; +import { httpServiceMock } from '@kbn/core-http-browser-mocks'; +import { + getFoundListSchemaMock, + getCreateListItemResponseMock, + getUpdatedListItemResponseMock, + getDeletedListItemResponseMock, +} from './mocks/response'; + +describe('Value list item API', () => { + let httpMock: ReturnType; + beforeEach(() => { + httpMock = httpServiceMock.createStartContract(); + }); + + describe('findListItems', () => { + beforeEach(() => { + httpMock.fetch.mockResolvedValue(getFoundListSchemaMock()); + }); + + it('GETs from the lists endpoint with query params', async () => { + const abortCtrl = new AbortController(); + await findListItems({ + http: httpMock, + pageIndex: 1, + pageSize: 10, + signal: abortCtrl.signal, + filter: '*:*', + listId: 'list_id', + sortField: 'updated_at', + sortOrder: 'asc', + }); + + expect(httpMock.fetch).toHaveBeenCalledWith( + '/api/lists/items/_find', + expect.objectContaining({ + method: 'GET', + query: { + cursor: undefined, + filter: '*:*', + list_id: 'list_id', + page: 1, + per_page: 10, + sort_field: 'updated_at', + sort_order: 'asc', + }, + }) + ); + }); + }); + + describe('createListItem', () => { + beforeEach(() => { + httpMock.fetch.mockResolvedValue(getCreateListItemResponseMock()); + }); + + it('POSTs to the lists endpoint with the list item', async () => { + const abortCtrl = new AbortController(); + await createListItem({ + http: httpMock, + signal: abortCtrl.signal, + value: '123', + listId: 'list_id', + }); + + expect(httpMock.fetch).toHaveBeenCalledWith( + '/api/lists/items', + expect.objectContaining({ + method: 'POST', + body: JSON.stringify({ + value: '123', + list_id: 'list_id', + }), + }) + ); + }); + + it('returns the created list item', async () => { + const abortCtrl = new AbortController(); + const result = await createListItem({ + http: httpMock, + signal: abortCtrl.signal, + value: '123', + listId: 'list_id', + }); + + expect(result).toEqual(getCreateListItemResponseMock()); + }); + }); + + describe('patchListItem', () => { + beforeEach(() => { + httpMock.fetch.mockResolvedValue(getUpdatedListItemResponseMock()); + }); + + it('PATCH to the lists endpoint with the list item', async () => { + const abortCtrl = new AbortController(); + await patchListItem({ + http: httpMock, + signal: abortCtrl.signal, + id: 'item_id', + value: '123', + }); + + expect(httpMock.fetch).toHaveBeenCalledWith( + '/api/lists/items', + expect.objectContaining({ + method: 'PATCH', + body: JSON.stringify({ + id: 'item_id', + value: '123', + }), + }) + ); + }); + + it('returns the updated list item', async () => { + const abortCtrl = new AbortController(); + const result = await patchListItem({ + http: httpMock, + signal: abortCtrl.signal, + id: 'item_id', + value: '123', + }); + + expect(result).toEqual(getUpdatedListItemResponseMock()); + }); + }); + + describe('deleteListItem', () => { + beforeEach(() => { + httpMock.fetch.mockResolvedValue(getCreateListItemResponseMock()); + }); + + it('DELETE to the lists endpoint with the list item', async () => { + const abortCtrl = new AbortController(); + await deleteListItem({ + http: httpMock, + signal: abortCtrl.signal, + id: 'item_id', + refresh: 'true', + }); + + expect(httpMock.fetch).toHaveBeenCalledWith( + '/api/lists/items', + expect.objectContaining({ + method: 'DELETE', + query: { id: 'item_id', refresh: 'true' }, + }) + ); + }); + + it('returns the deleted list item', async () => { + const abortCtrl = new AbortController(); + const result = await deleteListItem({ + http: httpMock, + signal: abortCtrl.signal, + id: 'item_id', + }); + + expect(result).toEqual(getDeletedListItemResponseMock()); + }); + }); +}); diff --git a/packages/kbn-securitysolution-list-api/src/list_item_api/index.ts b/packages/kbn-securitysolution-list-api/src/list_item_api/index.ts new file mode 100644 index 0000000000000..21fbfb08d51c0 --- /dev/null +++ b/packages/kbn-securitysolution-list-api/src/list_item_api/index.ts @@ -0,0 +1,226 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { + FindListItemSchema, + ListItemSchema, + deleteListItemSchema, + patchListItemSchema, + createListItemSchema, + findListItemSchema, + foundListItemSchema, + listItemSchema, + FoundListItemSchema, + DeleteListItemSchema, + PatchListItemSchema, + CreateListItemSchema, +} from '@kbn/securitysolution-io-ts-list-types'; +import { chain, fromEither, tryCatch } from 'fp-ts/lib/TaskEither'; +import { flow } from 'fp-ts/lib/function'; +import { pipe } from 'fp-ts/lib/pipeable'; +import { validateEither } from '@kbn/securitysolution-io-ts-utils'; + +import { LIST_ITEM_URL } from '@kbn/securitysolution-list-constants'; +import { + ApiParams, + FindListItemsParams, + DeleteListItemParams, + PatchListItemParams, + CreateListItemParams, +} from '../types'; +import { toError, toPromise } from '../fp_utils'; + +const version = '2023-10-31'; + +/** + * Fetch list items + */ +const findListItems = async ({ + http, + cursor, + page, + // eslint-disable-next-line @typescript-eslint/naming-convention + list_id, + // eslint-disable-next-line @typescript-eslint/naming-convention + per_page, + signal, + // eslint-disable-next-line @typescript-eslint/naming-convention + sort_field, + // eslint-disable-next-line @typescript-eslint/naming-convention + sort_order, + filter, +}: ApiParams & FindListItemSchema): Promise => { + return http.fetch(`${LIST_ITEM_URL}/_find`, { + method: 'GET', + query: { + cursor, + page, + per_page, + sort_field, + sort_order, + list_id, + filter, + }, + signal, + version, + }); +}; + +const findListItemsWithValidation = async ({ + cursor, + http, + pageIndex, + pageSize, + signal, + sortField, + sortOrder, + filter, + listId, +}: FindListItemsParams): Promise => + pipe( + { + cursor: cursor != null ? cursor.toString() : undefined, + page: pageIndex != null ? pageIndex.toString() : undefined, + per_page: pageSize != null ? pageSize.toString() : undefined, + sort_field: sortField != null ? sortField.toString() : undefined, + filter: filter != null ? filter.toString() : undefined, + sort_order: sortOrder, + list_id: listId, + }, + (payload) => fromEither(validateEither(findListItemSchema, payload)), + chain((payload) => tryCatch(() => findListItems({ http, signal, ...payload }), toError)), + chain((response) => fromEither(validateEither(foundListItemSchema, response))), + flow(toPromise) + ); + +export { findListItemsWithValidation as findListItems }; + +const deleteListItem = async ({ + http, + id, + signal, + refresh, +}: ApiParams & DeleteListItemSchema): Promise => + http.fetch(LIST_ITEM_URL, { + method: 'DELETE', + query: { id, refresh }, + signal, + version, + }); + +const deleteListItemWithValidation = async ({ + http, + id, + signal, + refresh, +}: DeleteListItemParams): Promise => + pipe( + { id, refresh }, + (payload) => fromEither(validateEither(deleteListItemSchema, payload)), + chain((payload) => + tryCatch( + () => + deleteListItem({ + http, + signal, + ...payload, + value: undefined, + list_id: undefined, + }), + toError + ) + ), + chain((response) => fromEither(validateEither(listItemSchema, response))), + flow(toPromise) + ); + +export { deleteListItemWithValidation as deleteListItem }; + +const patchListItem = async ({ + http, + id, + signal, + value, + _version, +}: ApiParams & PatchListItemSchema): Promise => + http.fetch(LIST_ITEM_URL, { + method: 'PATCH', + body: JSON.stringify({ id, value, _version }), + signal, + version, + }); + +const patchListItemWithValidation = async ({ + http, + id, + signal, + value, + refresh, + _version, +}: PatchListItemParams): Promise => + pipe( + { id, value, _version, refresh }, + (payload) => fromEither(validateEither(patchListItemSchema, payload)), + chain((payload) => + tryCatch( + () => + patchListItem({ + http, + signal, + ...payload, + }), + toError + ) + ), + chain((response) => fromEither(validateEither(listItemSchema, response))), + flow(toPromise) + ); + +export { patchListItemWithValidation as patchListItem }; + +const createListItem = async ({ + http, + signal, + value, + // eslint-disable-next-line @typescript-eslint/naming-convention + list_id, + refresh, +}: ApiParams & CreateListItemSchema): Promise => + http.fetch(LIST_ITEM_URL, { + method: 'POST', + body: JSON.stringify({ value, list_id, refresh }), + signal, + version, + }); + +const createListItemWithValidation = async ({ + http, + signal, + value, + refresh, + listId, +}: CreateListItemParams): Promise => + pipe( + { list_id: listId, value, refresh }, + (payload) => fromEither(validateEither(createListItemSchema, payload)), + chain((payload) => + tryCatch( + () => + createListItem({ + http, + signal, + ...payload, + }), + toError + ) + ), + chain((response) => fromEither(validateEither(listItemSchema, response))), + flow(toPromise) + ); + +export { createListItemWithValidation as createListItem }; diff --git a/packages/kbn-securitysolution-list-api/src/list_item_api/mocks/response/index.ts b/packages/kbn-securitysolution-list-api/src/list_item_api/mocks/response/index.ts new file mode 100644 index 0000000000000..3660f65fae573 --- /dev/null +++ b/packages/kbn-securitysolution-list-api/src/list_item_api/mocks/response/index.ts @@ -0,0 +1,23 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type { FoundListItemSchema, ListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; + +import { getListItemResponseMock } from './list_item_schema.mock'; + +export const getFoundListSchemaMock = (): FoundListItemSchema => ({ + cursor: '123', + data: [getListItemResponseMock()], + page: 1, + per_page: 1, + total: 1, +}); + +export const getCreateListItemResponseMock = (): ListItemSchema => getListItemResponseMock(); +export const getUpdatedListItemResponseMock = (): ListItemSchema => getListItemResponseMock(); +export const getDeletedListItemResponseMock = (): ListItemSchema => getListItemResponseMock(); diff --git a/packages/kbn-securitysolution-list-api/src/list_item_api/mocks/response/list_item_schema.mock.ts b/packages/kbn-securitysolution-list-api/src/list_item_api/mocks/response/list_item_schema.mock.ts new file mode 100644 index 0000000000000..1faf5d52b4871 --- /dev/null +++ b/packages/kbn-securitysolution-list-api/src/list_item_api/mocks/response/list_item_schema.mock.ts @@ -0,0 +1,26 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type { ListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; + +export const getListItemResponseMock = (): ListItemSchema => ({ + _version: '1', + '@timestamp': '2020-08-11T11:22:13.670Z', + created_at: '2020-08-11T11:22:13.670Z', + created_by: 'elastic', + deserializer: 'some deserializer', + id: 'bpdB3XMBx7pemMHopQ6M', + list_id: 'list_id', + meta: {}, + serializer: 'some serializer', + tie_breaker_id: '17d3befb-dc22-4b3c-a286-b5504c4fbeeb', + type: 'keyword', + updated_at: '2020-08-11T11:22:13.670Z', + updated_by: 'elastic', + value: 'some keyword', +}); diff --git a/packages/kbn-securitysolution-list-api/src/list_api/types.ts b/packages/kbn-securitysolution-list-api/src/types.ts similarity index 66% rename from packages/kbn-securitysolution-list-api/src/list_api/types.ts rename to packages/kbn-securitysolution-list-api/src/types.ts index 5f63c06234bcb..76682ad5082f3 100644 --- a/packages/kbn-securitysolution-list-api/src/list_api/types.ts +++ b/packages/kbn-securitysolution-list-api/src/types.ts @@ -10,6 +10,8 @@ import type { SortFieldOrUndefined, SortOrderOrUndefined, Type, + Refresh, + RefreshWithWaitFor, } from '@kbn/securitysolution-io-ts-list-types'; // TODO: Replace these with kbn packaged versions once we have those available to us @@ -33,10 +35,21 @@ export interface FindListsParams extends ApiParams { sortField?: SortFieldOrUndefined; } +export interface FindListItemsParams extends ApiParams { + cursor?: string | undefined; + pageSize: number | undefined; + pageIndex: number | undefined; + sortOrder?: SortOrderOrUndefined; + sortField?: SortFieldOrUndefined; + filter: string | undefined; + listId: string; +} + export interface ImportListParams extends ApiParams { file: File; listId: string | undefined; type: Type | undefined; + refresh?: RefreshWithWaitFor; } export interface DeleteListParams extends ApiParams { @@ -45,6 +58,28 @@ export interface DeleteListParams extends ApiParams { ignoreReferences?: boolean; } +export interface DeleteListItemParams extends ApiParams { + refresh?: Refresh; + id: string; +} + +export interface PatchListItemParams extends ApiParams { + refresh?: Refresh; + id: string; + value: string; + _version?: string; +} + +export interface CreateListItemParams extends ApiParams { + refresh?: RefreshWithWaitFor; + value: string; + listId: string; +} + export interface ExportListParams extends ApiParams { listId: string; } + +export interface GetListByIdParams extends ApiParams { + id: string; +} diff --git a/packages/kbn-securitysolution-list-hooks/index.ts b/packages/kbn-securitysolution-list-hooks/index.ts index 2422d2767349a..78af3f55e7f5a 100644 --- a/packages/kbn-securitysolution-list-hooks/index.ts +++ b/packages/kbn-securitysolution-list-hooks/index.ts @@ -19,3 +19,8 @@ export * from './src/use_persist_exception_item'; export * from './src/use_persist_exception_list'; export * from './src/use_read_list_index'; export * from './src/use_read_list_privileges'; +export * from './src/use_find_list_items'; +export * from './src/use_create_list_item'; +export * from './src/use_delete_list_item'; +export * from './src/use_patch_list_item'; +export * from './src/use_get_list_by_id'; diff --git a/packages/kbn-securitysolution-list-hooks/src/index.ts b/packages/kbn-securitysolution-list-hooks/src/index.ts index e458abd0448ad..fa0aca962c69e 100644 --- a/packages/kbn-securitysolution-list-hooks/src/index.ts +++ b/packages/kbn-securitysolution-list-hooks/src/index.ts @@ -18,3 +18,8 @@ export * from './use_persist_exception_item'; export * from './use_persist_exception_list'; export * from './use_read_list_index'; export * from './use_read_list_privileges'; +export * from './use_find_list_items'; +export * from './use_create_list_item'; +export * from './use_delete_list_item'; +export * from './use_patch_list_item'; +export * from './use_get_list_by_id'; diff --git a/packages/kbn-securitysolution-list-hooks/src/use_create_list_item/index.ts b/packages/kbn-securitysolution-list-hooks/src/use_create_list_item/index.ts new file mode 100644 index 0000000000000..1bd218de4f6c7 --- /dev/null +++ b/packages/kbn-securitysolution-list-hooks/src/use_create_list_item/index.ts @@ -0,0 +1,42 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type { UseMutationOptions } from '@tanstack/react-query'; +import type { ListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; +import type { IHttpFetchError } from '@kbn/core-http-browser'; +import { useMutation } from '@tanstack/react-query'; +import { createListItem } from '@kbn/securitysolution-list-api'; +import type { CreateListItemParams } from '@kbn/securitysolution-list-api'; +import { withOptionalSignal } from '@kbn/securitysolution-hook-utils'; + +import { useInvalidateListItemQuery } from '../use_find_list_items'; + +const createListItemWithOptionalSignal = withOptionalSignal(createListItem); + +export const CREATE_LIST_ITEM_MUTATION_KEY = ['POST', 'LIST_ITEM_CREATE']; +type CreateListMutationParams = Omit; + +export const useCreateListItemMutation = ( + options?: UseMutationOptions, CreateListMutationParams> +) => { + const invalidateListItemQuery = useInvalidateListItemQuery(); + return useMutation, CreateListMutationParams>( + ({ listId, value, http }) => + createListItemWithOptionalSignal({ listId, value, http, refresh: 'true' }), + { + ...options, + mutationKey: CREATE_LIST_ITEM_MUTATION_KEY, + onSettled: (...args) => { + invalidateListItemQuery(); + if (options?.onSettled) { + options.onSettled(...args); + } + }, + } + ); +}; diff --git a/packages/kbn-securitysolution-list-hooks/src/use_delete_list_item/index.ts b/packages/kbn-securitysolution-list-hooks/src/use_delete_list_item/index.ts new file mode 100644 index 0000000000000..b1f9f3490480c --- /dev/null +++ b/packages/kbn-securitysolution-list-hooks/src/use_delete_list_item/index.ts @@ -0,0 +1,40 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type { UseMutationOptions } from '@tanstack/react-query'; +import type { ListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; +import { useMutation } from '@tanstack/react-query'; +import { deleteListItem } from '@kbn/securitysolution-list-api'; +import type { IHttpFetchError } from '@kbn/core-http-browser'; +import type { DeleteListItemParams } from '@kbn/securitysolution-list-api'; +import { withOptionalSignal } from '@kbn/securitysolution-hook-utils'; +import { useInvalidateListItemQuery } from '../use_find_list_items'; + +const deleteListItemWithOptionalSignal = withOptionalSignal(deleteListItem); + +export const DELETE_LIST_ITEM_MUTATION_KEY = ['POST', ' DELETE_LIST_ITEM_MUTATION']; +type DeleteListMutationParams = Omit; + +export const useDeleteListItemMutation = ( + options?: UseMutationOptions, DeleteListMutationParams> +) => { + const invalidateListItemQuery = useInvalidateListItemQuery(); + return useMutation, DeleteListMutationParams>( + ({ id, http }) => deleteListItemWithOptionalSignal({ id, http, refresh: 'true' }), + { + ...options, + mutationKey: DELETE_LIST_ITEM_MUTATION_KEY, + onSettled: (...args) => { + invalidateListItemQuery(); + if (options?.onSettled) { + options.onSettled(...args); + } + }, + } + ); +}; diff --git a/packages/kbn-securitysolution-list-hooks/src/use_find_list_items/index.ts b/packages/kbn-securitysolution-list-hooks/src/use_find_list_items/index.ts new file mode 100644 index 0000000000000..031fe0f1311a2 --- /dev/null +++ b/packages/kbn-securitysolution-list-hooks/src/use_find_list_items/index.ts @@ -0,0 +1,74 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { useCallback } from 'react'; +import { useQuery, useQueryClient } from '@tanstack/react-query'; +import { findListItems, ApiParams } from '@kbn/securitysolution-list-api'; +import { withOptionalSignal } from '@kbn/securitysolution-hook-utils'; +import { useCursor } from '../use_cursor'; + +const findListItemsWithOptionalSignal = withOptionalSignal(findListItems); + +const FIND_LIST_ITEMS_QUERY_KEY = 'FIND_LIST_ITEMS'; + +export const useInvalidateListItemQuery = () => { + const queryClient = useQueryClient(); + + return useCallback(() => { + queryClient.invalidateQueries([FIND_LIST_ITEMS_QUERY_KEY], { + refetchType: 'active', + }); + }, [queryClient]); +}; + +export const useFindListItems = ({ + pageIndex, + pageSize, + sortField, + sortOrder, + listId, + filter, + http, +}: { + pageIndex: number; + pageSize: number; + sortField: string; + sortOrder: 'asc' | 'desc'; + listId: string; + filter: string; + http: ApiParams['http']; +}) => { + const [cursor, setCursor] = useCursor({ pageIndex, pageSize }); + return useQuery( + [FIND_LIST_ITEMS_QUERY_KEY, pageIndex, pageSize, sortField, sortOrder, listId, filter], + async ({ signal }) => { + const response = await findListItemsWithOptionalSignal({ + http, + signal, + pageIndex, + pageSize, + sortField, + sortOrder, + listId, + cursor, + filter, + }); + return response; + }, + { + keepPreviousData: true, + refetchOnWindowFocus: false, + retry: false, + onSuccess: (data) => { + if (data?.cursor) { + setCursor(data?.cursor); + } + }, + } + ); +}; diff --git a/packages/kbn-securitysolution-list-hooks/src/use_get_list_by_id/index.ts b/packages/kbn-securitysolution-list-hooks/src/use_get_list_by_id/index.ts new file mode 100644 index 0000000000000..4c2dae01b0a72 --- /dev/null +++ b/packages/kbn-securitysolution-list-hooks/src/use_get_list_by_id/index.ts @@ -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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { useQuery } from '@tanstack/react-query'; +import { getListById, ApiParams } from '@kbn/securitysolution-list-api'; +import { withOptionalSignal } from '@kbn/securitysolution-hook-utils'; + +const getListByIdWithOptionalSignal = withOptionalSignal(getListById); + +const GET_LIST_BY_ID_QUERY_KEY = 'GET_LIST_BY_ID'; +export const useGetListById = ({ http, id }: { http: ApiParams['http']; id: string }) => { + return useQuery( + [GET_LIST_BY_ID_QUERY_KEY, id], + async ({ signal }) => { + const respone = await getListByIdWithOptionalSignal({ http, signal, id }); + return respone; + }, + { + refetchOnWindowFocus: false, + } + ); +}; diff --git a/packages/kbn-securitysolution-list-hooks/src/use_patch_list_item/index.ts b/packages/kbn-securitysolution-list-hooks/src/use_patch_list_item/index.ts new file mode 100644 index 0000000000000..976b60a5f16c6 --- /dev/null +++ b/packages/kbn-securitysolution-list-hooks/src/use_patch_list_item/index.ts @@ -0,0 +1,41 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type { UseMutationOptions } from '@tanstack/react-query'; +import type { ListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; +import { useMutation } from '@tanstack/react-query'; +import type { PatchListItemParams } from '@kbn/securitysolution-list-api'; +import { patchListItem } from '@kbn/securitysolution-list-api'; +import type { IHttpFetchError } from '@kbn/core-http-browser'; +import { withOptionalSignal } from '@kbn/securitysolution-hook-utils'; +import { useInvalidateListItemQuery } from '../use_find_list_items'; + +const patchListItemWithOptionalSignal = withOptionalSignal(patchListItem); + +export const PATCH_LIST_ITEM_MUTATION_KEY = ['PATCH', 'LIST_ITEM_MUTATION']; +type PatchListMutationParams = Omit; + +export const usePatchListItemMutation = ( + options?: UseMutationOptions, PatchListMutationParams> +) => { + const invalidateListItemQuery = useInvalidateListItemQuery(); + return useMutation, PatchListMutationParams>( + ({ id, value, _version, http }: PatchListMutationParams) => + patchListItemWithOptionalSignal({ id, value, http, refresh: 'true', _version }), + { + ...options, + mutationKey: PATCH_LIST_ITEM_MUTATION_KEY, + onSettled: (...args) => { + invalidateListItemQuery(); + if (options?.onSettled) { + options.onSettled(...args); + } + }, + } + ); +}; diff --git a/packages/kbn-securitysolution-list-hooks/tsconfig.json b/packages/kbn-securitysolution-list-hooks/tsconfig.json index 0ef5544da1898..53e105e609750 100644 --- a/packages/kbn-securitysolution-list-hooks/tsconfig.json +++ b/packages/kbn-securitysolution-list-hooks/tsconfig.json @@ -8,7 +8,7 @@ ] }, "include": [ - "**/*.ts" + "**/*.ts", ], "kbn_references": [ "@kbn/securitysolution-hook-utils", diff --git a/x-pack/plugins/lists/public/exceptions/components/__mock__/show_value_list_modal.mock.tsx b/x-pack/plugins/lists/public/exceptions/components/__mock__/show_value_list_modal.mock.tsx new file mode 100644 index 0000000000000..a01b52c269c43 --- /dev/null +++ b/x-pack/plugins/lists/public/exceptions/components/__mock__/show_value_list_modal.mock.tsx @@ -0,0 +1,9 @@ +/* + * 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'; + +export const MockedShowValueListModal: React.FC = () => <>; diff --git a/x-pack/plugins/lists/public/exceptions/components/builder/entry_renderer.test.tsx b/x-pack/plugins/lists/public/exceptions/components/builder/entry_renderer.test.tsx index ff8df21fd86fe..0aaa47b4a1265 100644 --- a/x-pack/plugins/lists/public/exceptions/components/builder/entry_renderer.test.tsx +++ b/x-pack/plugins/lists/public/exceptions/components/builder/entry_renderer.test.tsx @@ -28,6 +28,7 @@ import { unifiedSearchPluginMock } from '@kbn/unified-search-plugin/public/mocks import { waitFor } from '@testing-library/react'; import { ReactWrapper, mount } from 'enzyme'; +import { MockedShowValueListModal } from '../__mock__/show_value_list_modal.mock'; import { getFoundListsBySizeSchemaMock } from '../../../../common/schemas/response/found_lists_by_size_schema.mock'; import { BuilderEntryItem } from './entry_renderer'; @@ -84,6 +85,7 @@ describe('BuilderEntryItem', () => { setWarningsExist={jest.fn()} exceptionItemIndex={0} showLabel + showValueListModal={MockedShowValueListModal} /> ); @@ -118,6 +120,7 @@ describe('BuilderEntryItem', () => { showLabel allowCustomOptions exceptionItemIndex={0} + showValueListModal={MockedShowValueListModal} /> ); @@ -156,6 +159,7 @@ describe('BuilderEntryItem', () => { showLabel allowCustomOptions exceptionItemIndex={0} + showValueListModal={MockedShowValueListModal} /> ); @@ -192,6 +196,7 @@ describe('BuilderEntryItem', () => { showLabel allowCustomOptions={false} exceptionItemIndex={0} + showValueListModal={MockedShowValueListModal} /> ); @@ -230,6 +235,7 @@ describe('BuilderEntryItem', () => { allowCustomOptions getExtendedFields={(): Promise => Promise.resolve([field])} exceptionItemIndex={0} + showValueListModal={MockedShowValueListModal} /> ); @@ -267,6 +273,7 @@ describe('BuilderEntryItem', () => { setWarningsExist={jest.fn()} showLabel={false} exceptionItemIndex={0} + showValueListModal={MockedShowValueListModal} /> ); @@ -307,6 +314,7 @@ describe('BuilderEntryItem', () => { setWarningsExist={jest.fn()} showLabel={false} exceptionItemIndex={0} + showValueListModal={MockedShowValueListModal} /> ); @@ -347,6 +355,7 @@ describe('BuilderEntryItem', () => { setWarningsExist={jest.fn()} showLabel={false} exceptionItemIndex={0} + showValueListModal={MockedShowValueListModal} /> ); @@ -387,6 +396,7 @@ describe('BuilderEntryItem', () => { setWarningsExist={jest.fn()} showLabel={false} exceptionItemIndex={0} + showValueListModal={MockedShowValueListModal} /> ); @@ -428,6 +438,7 @@ describe('BuilderEntryItem', () => { setWarningsExist={jest.fn()} showLabel exceptionItemIndex={0} + showValueListModal={MockedShowValueListModal} /> ); @@ -470,6 +481,7 @@ describe('BuilderEntryItem', () => { setWarningsExist={jest.fn()} showLabel exceptionItemIndex={0} + showValueListModal={MockedShowValueListModal} /> ); @@ -511,6 +523,7 @@ describe('BuilderEntryItem', () => { setWarningsExist={jest.fn()} showLabel={false} exceptionItemIndex={0} + showValueListModal={MockedShowValueListModal} /> ); @@ -555,6 +568,7 @@ describe('BuilderEntryItem', () => { setWarningsExist={jest.fn()} showLabel={false} exceptionItemIndex={0} + showValueListModal={MockedShowValueListModal} /> ); @@ -599,6 +613,7 @@ describe('BuilderEntryItem', () => { setWarningsExist={jest.fn()} showLabel={false} exceptionItemIndex={0} + showValueListModal={MockedShowValueListModal} /> ); @@ -643,6 +658,7 @@ describe('BuilderEntryItem', () => { setWarningsExist={jest.fn()} showLabel={false} exceptionItemIndex={0} + showValueListModal={MockedShowValueListModal} /> ); @@ -678,6 +694,7 @@ describe('BuilderEntryItem', () => { setWarningsExist={jest.fn()} showLabel={false} exceptionItemIndex={0} + showValueListModal={MockedShowValueListModal} /> ); @@ -738,6 +755,7 @@ describe('BuilderEntryItem', () => { setWarningsExist={jest.fn()} showLabel={false} exceptionItemIndex={0} + showValueListModal={MockedShowValueListModal} /> ); @@ -782,6 +800,7 @@ describe('BuilderEntryItem', () => { setWarningsExist={jest.fn()} showLabel={false} exceptionItemIndex={0} + showValueListModal={MockedShowValueListModal} /> ); @@ -824,6 +843,7 @@ describe('BuilderEntryItem', () => { setWarningsExist={jest.fn()} showLabel={false} exceptionItemIndex={0} + showValueListModal={MockedShowValueListModal} /> ); @@ -866,6 +886,7 @@ describe('BuilderEntryItem', () => { setWarningsExist={jest.fn()} showLabel={false} exceptionItemIndex={0} + showValueListModal={MockedShowValueListModal} /> ); @@ -908,6 +929,7 @@ describe('BuilderEntryItem', () => { setWarningsExist={jest.fn()} showLabel={false} exceptionItemIndex={0} + showValueListModal={MockedShowValueListModal} /> ); @@ -950,6 +972,7 @@ describe('BuilderEntryItem', () => { setWarningsExist={jest.fn()} showLabel={false} exceptionItemIndex={0} + showValueListModal={MockedShowValueListModal} /> ); @@ -998,6 +1021,7 @@ describe('BuilderEntryItem', () => { setWarningsExist={jest.fn()} showLabel={false} exceptionItemIndex={0} + showValueListModal={MockedShowValueListModal} /> ); @@ -1040,6 +1064,7 @@ describe('BuilderEntryItem', () => { setWarningsExist={jest.fn()} showLabel={false} exceptionItemIndex={0} + showValueListModal={MockedShowValueListModal} /> ); @@ -1081,6 +1106,7 @@ describe('BuilderEntryItem', () => { setWarningsExist={jest.fn()} showLabel={false} exceptionItemIndex={0} + showValueListModal={MockedShowValueListModal} /> ); @@ -1131,6 +1157,7 @@ describe('BuilderEntryItem', () => { setWarningsExist={mockSetWarningsExists} showLabel={false} exceptionItemIndex={0} + showValueListModal={MockedShowValueListModal} /> ); @@ -1181,6 +1208,7 @@ describe('BuilderEntryItem', () => { setWarningsExist={mockSetWarningsExists} showLabel={false} exceptionItemIndex={0} + showValueListModal={MockedShowValueListModal} /> ); @@ -1230,6 +1258,7 @@ describe('BuilderEntryItem', () => { showLabel={false} isDisabled={true} exceptionItemIndex={0} + showValueListModal={MockedShowValueListModal} /> ); expect( diff --git a/x-pack/plugins/lists/public/exceptions/components/builder/entry_renderer.tsx b/x-pack/plugins/lists/public/exceptions/components/builder/entry_renderer.tsx index bbb88429b2b76..1f7f8380555a0 100644 --- a/x-pack/plugins/lists/public/exceptions/components/builder/entry_renderer.tsx +++ b/x-pack/plugins/lists/public/exceptions/components/builder/entry_renderer.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import React, { ElementType, useCallback, useEffect, useMemo, useState } from 'react'; import { FormattedMessage } from '@kbn/i18n-react'; import { EuiAccordion, @@ -86,6 +86,7 @@ export interface EntryItemProps { operatorsList?: OperatorOption[]; allowCustomOptions?: boolean; getExtendedFields?: (fields: string[]) => Promise; + showValueListModal: ElementType; } export const BuilderEntryItem: React.FC = ({ @@ -106,6 +107,7 @@ export const BuilderEntryItem: React.FC = ({ allowCustomOptions = false, getExtendedFields, exceptionItemIndex, + showValueListModal, }): JSX.Element => { const sPaddingSize = useEuiPaddingSize('s'); @@ -499,6 +501,7 @@ export const BuilderEntryItem: React.FC = ({ data-test-subj="exceptionBuilderEntryFieldList" allowLargeValueLists={allowLargeValueLists} aria-label={ariaLabel} + showValueListModal={showValueListModal} /> ); case OperatorTypeEnum.EXISTS: diff --git a/x-pack/plugins/lists/public/exceptions/components/builder/exception_item_renderer.test.tsx b/x-pack/plugins/lists/public/exceptions/components/builder/exception_item_renderer.test.tsx index 4041c8516ee27..66286232e0880 100644 --- a/x-pack/plugins/lists/public/exceptions/components/builder/exception_item_renderer.test.tsx +++ b/x-pack/plugins/lists/public/exceptions/components/builder/exception_item_renderer.test.tsx @@ -15,6 +15,7 @@ import { coreMock } from '@kbn/core/public/mocks'; import { getExceptionListItemSchemaMock } from '../../../../common/schemas/response/exception_list_item_schema.mock'; import { getEntryMatchMock } from '../../../../common/schemas/types/entry_match.mock'; import { getEntryMatchAnyMock } from '../../../../common/schemas/types/entry_match_any.mock'; +import { MockedShowValueListModal } from '../__mock__/show_value_list_modal.mock'; import { BuilderExceptionListItemComponent } from './exception_item_renderer'; @@ -54,6 +55,7 @@ describe('BuilderExceptionListItemComponent', () => { onDeleteExceptionItem={jest.fn()} setErrorsExist={jest.fn()} setWarningsExist={jest.fn()} + showValueListModal={MockedShowValueListModal} /> ); @@ -86,6 +88,7 @@ describe('BuilderExceptionListItemComponent', () => { onDeleteExceptionItem={jest.fn()} setErrorsExist={jest.fn()} setWarningsExist={jest.fn()} + showValueListModal={MockedShowValueListModal} /> ); @@ -116,6 +119,7 @@ describe('BuilderExceptionListItemComponent', () => { onDeleteExceptionItem={jest.fn()} setErrorsExist={jest.fn()} setWarningsExist={jest.fn()} + showValueListModal={MockedShowValueListModal} /> ); @@ -148,6 +152,7 @@ describe('BuilderExceptionListItemComponent', () => { onDeleteExceptionItem={jest.fn()} setErrorsExist={jest.fn()} setWarningsExist={jest.fn()} + showValueListModal={MockedShowValueListModal} /> ); @@ -187,6 +192,7 @@ describe('BuilderExceptionListItemComponent', () => { onDeleteExceptionItem={jest.fn()} setErrorsExist={jest.fn()} setWarningsExist={jest.fn()} + showValueListModal={MockedShowValueListModal} /> ); @@ -218,6 +224,7 @@ describe('BuilderExceptionListItemComponent', () => { onDeleteExceptionItem={jest.fn()} setErrorsExist={jest.fn()} setWarningsExist={jest.fn()} + showValueListModal={MockedShowValueListModal} /> ); @@ -250,6 +257,7 @@ describe('BuilderExceptionListItemComponent', () => { onDeleteExceptionItem={jest.fn()} setErrorsExist={jest.fn()} setWarningsExist={jest.fn()} + showValueListModal={MockedShowValueListModal} /> ); @@ -280,6 +288,7 @@ describe('BuilderExceptionListItemComponent', () => { onDeleteExceptionItem={jest.fn()} setErrorsExist={jest.fn()} setWarningsExist={jest.fn()} + showValueListModal={MockedShowValueListModal} /> ); @@ -312,6 +321,7 @@ describe('BuilderExceptionListItemComponent', () => { onDeleteExceptionItem={mockOnDeleteExceptionItem} setErrorsExist={jest.fn()} setWarningsExist={jest.fn()} + showValueListModal={MockedShowValueListModal} /> ); diff --git a/x-pack/plugins/lists/public/exceptions/components/builder/exception_item_renderer.tsx b/x-pack/plugins/lists/public/exceptions/components/builder/exception_item_renderer.tsx index 962b830b55cd1..2bf7f59770de5 100644 --- a/x-pack/plugins/lists/public/exceptions/components/builder/exception_item_renderer.tsx +++ b/x-pack/plugins/lists/public/exceptions/components/builder/exception_item_renderer.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { useCallback, useMemo } from 'react'; +import React, { ElementType, useCallback, useMemo } from 'react'; import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import styled from 'styled-components'; import type { AutocompleteStart } from '@kbn/unified-search-plugin/public'; @@ -62,6 +62,7 @@ interface BuilderExceptionListItemProps { operatorsList?: OperatorOption[]; allowCustomOptions?: boolean; getExtendedFields?: (fields: string[]) => Promise; + showValueListModal: ElementType; } export const BuilderExceptionListItemComponent = React.memo( @@ -85,6 +86,7 @@ export const BuilderExceptionListItemComponent = React.memo { const handleEntryChange = useCallback( (entry: BuilderEntry, entryIndex: number): void => { @@ -159,6 +161,7 @@ export const BuilderExceptionListItemComponent = React.memo { listType="detection" ruleName="Test rule" onChange={jest.fn()} + showValueListModal={MockedShowValueListModal} /> ); @@ -102,6 +104,7 @@ describe('ExceptionBuilderComponent', () => { listType="detection" ruleName="Test rule" onChange={jest.fn()} + showValueListModal={MockedShowValueListModal} /> ); @@ -148,6 +151,7 @@ describe('ExceptionBuilderComponent', () => { listType="detection" ruleName="Test rule" onChange={jest.fn()} + showValueListModal={MockedShowValueListModal} /> ); @@ -185,6 +189,7 @@ describe('ExceptionBuilderComponent', () => { listType="detection" ruleName="Test rule" onChange={jest.fn()} + showValueListModal={MockedShowValueListModal} /> ); @@ -216,6 +221,7 @@ describe('ExceptionBuilderComponent', () => { listNamespaceType="single" ruleName="Test rule" onChange={jest.fn()} + showValueListModal={MockedShowValueListModal} /> ); @@ -252,6 +258,7 @@ describe('ExceptionBuilderComponent', () => { listNamespaceType="single" ruleName="Test rule" onChange={jest.fn()} + showValueListModal={MockedShowValueListModal} /> ); @@ -311,6 +318,7 @@ describe('ExceptionBuilderComponent', () => { listNamespaceType="single" ruleName="Test rule" onChange={jest.fn()} + showValueListModal={MockedShowValueListModal} /> ); @@ -379,6 +387,7 @@ describe('ExceptionBuilderComponent', () => { listNamespaceType="single" ruleName="Test rule" onChange={jest.fn()} + showValueListModal={MockedShowValueListModal} /> ); @@ -427,6 +436,7 @@ describe('ExceptionBuilderComponent', () => { listNamespaceType="single" ruleName="Test rule" onChange={jest.fn()} + showValueListModal={MockedShowValueListModal} /> ); @@ -463,6 +473,7 @@ describe('ExceptionBuilderComponent', () => { listNamespaceType="single" ruleName="Test rule" onChange={jest.fn()} + showValueListModal={MockedShowValueListModal} /> ); @@ -502,6 +513,7 @@ describe('ExceptionBuilderComponent', () => { listNamespaceType="single" ruleName="Test rule" onChange={jest.fn()} + showValueListModal={MockedShowValueListModal} /> ); diff --git a/x-pack/plugins/lists/public/exceptions/components/builder/exception_items_renderer.tsx b/x-pack/plugins/lists/public/exceptions/components/builder/exception_items_renderer.tsx index b9b6ff98e8c71..ba51e50b70a3c 100644 --- a/x-pack/plugins/lists/public/exceptions/components/builder/exception_items_renderer.tsx +++ b/x-pack/plugins/lists/public/exceptions/components/builder/exception_items_renderer.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { useCallback, useEffect, useMemo, useReducer, useState } from 'react'; +import React, { ElementType, useCallback, useEffect, useMemo, useReducer, useState } from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; import styled from 'styled-components'; import { HttpStart } from '@kbn/core/public'; @@ -96,6 +96,7 @@ export interface ExceptionBuilderProps { exceptionItemName?: string; allowCustomFieldOptions?: boolean; getExtendedFields?: (fields: string[]) => Promise; + showValueListModal: ElementType; } export const ExceptionBuilderComponent = ({ @@ -119,6 +120,7 @@ export const ExceptionBuilderComponent = ({ operatorsList, allowCustomFieldOptions = false, getExtendedFields, + showValueListModal, }: ExceptionBuilderProps): JSX.Element => { const [state, dispatch] = useReducer(exceptionsBuilderReducer(), { ...initialState, @@ -466,6 +468,7 @@ export const ExceptionBuilderComponent = ({ operatorsList={operatorsList} allowCustomOptions={allowCustomFieldOptions} getExtendedFields={getExtendedFields} + showValueListModal={showValueListModal} /> diff --git a/x-pack/plugins/lists/server/routes/list/import_list_item_route.ts b/x-pack/plugins/lists/server/routes/list/import_list_item_route.ts index 9e64fe59404c6..d9d1bc5af1e6e 100644 --- a/x-pack/plugins/lists/server/routes/list/import_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/list/import_list_item_route.ts @@ -51,7 +51,7 @@ export const importListItemRoute = (router: ListsPluginRouter, config: ConfigTyp async (context, request, response) => { const siemResponse = buildSiemResponse(response); try { - const { deserializer, list_id: listId, serializer, type } = request.query; + const { deserializer, list_id: listId, serializer, type, refresh } = request.query; const lists = await getListClient(context); const filename = await lists.getImportFilename({ @@ -112,6 +112,7 @@ export const importListItemRoute = (router: ListsPluginRouter, config: ConfigTyp deserializer: list.deserializer, listId, meta: undefined, + refresh, serializer: list.serializer, stream, type: list.type, @@ -129,6 +130,7 @@ export const importListItemRoute = (router: ListsPluginRouter, config: ConfigTyp deserializer, listId: undefined, meta: undefined, + refresh, serializer, stream, type, diff --git a/x-pack/plugins/lists/server/routes/list_item/create_list_item_route.ts b/x-pack/plugins/lists/server/routes/list_item/create_list_item_route.ts index 1f9041930a00d..b3ab6f4e09eb7 100644 --- a/x-pack/plugins/lists/server/routes/list_item/create_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/list_item/create_list_item_route.ts @@ -35,7 +35,7 @@ export const createListItemRoute = (router: ListsPluginRouter): void => { async (context, request, response) => { const siemResponse = buildSiemResponse(response); try { - const { id, list_id: listId, value, meta } = request.body; + const { id, list_id: listId, value, meta, refresh } = request.body; const lists = await getListClient(context); const list = await lists.getList({ id: listId }); if (list == null) { @@ -58,6 +58,7 @@ export const createListItemRoute = (router: ListsPluginRouter): void => { id, listId, meta, + refresh, serializer: list.serializer, type: list.type, value, diff --git a/x-pack/plugins/lists/server/routes/list_item/delete_list_item_route.ts b/x-pack/plugins/lists/server/routes/list_item/delete_list_item_route.ts index 9b82eb3d5cdeb..644c8235bae06 100644 --- a/x-pack/plugins/lists/server/routes/list_item/delete_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/list_item/delete_list_item_route.ts @@ -39,10 +39,11 @@ export const deleteListItemRoute = (router: ListsPluginRouter): void => { async (context, request, response) => { const siemResponse = buildSiemResponse(response); try { - const { id, list_id: listId, value } = request.query; + const { id, list_id: listId, value, refresh } = request.query; + const shouldRefresh = refresh === 'true' ? true : false; const lists = await getListClient(context); if (id != null) { - const deleted = await lists.deleteListItem({ id }); + const deleted = await lists.deleteListItem({ id, refresh: shouldRefresh }); if (deleted == null) { return siemResponse.error({ body: `list item with id: "${id}" not found`, @@ -66,6 +67,7 @@ export const deleteListItemRoute = (router: ListsPluginRouter): void => { } else { const deleted = await lists.deleteListItemByValue({ listId, + refresh: shouldRefresh, type: list.type, value, }); diff --git a/x-pack/plugins/lists/server/routes/list_item/patch_list_item_route.ts b/x-pack/plugins/lists/server/routes/list_item/patch_list_item_route.ts index e378843064190..ff369aa9403e8 100644 --- a/x-pack/plugins/lists/server/routes/list_item/patch_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/list_item/patch_list_item_route.ts @@ -35,7 +35,8 @@ export const patchListItemRoute = (router: ListsPluginRouter): void => { async (context, request, response) => { const siemResponse = buildSiemResponse(response); try { - const { value, id, meta, _version } = request.body; + const { value, id, meta, _version, refresh } = request.body; + const shouldRefresh = refresh === 'true' ? true : false; const lists = await getListClient(context); const dataStreamExists = await lists.getListItemDataStreamExists(); @@ -51,6 +52,7 @@ export const patchListItemRoute = (router: ListsPluginRouter): void => { _version, id, meta, + refresh: shouldRefresh, value, }); if (listItem == null) { diff --git a/x-pack/plugins/lists/server/services/items/create_list_item.ts b/x-pack/plugins/lists/server/services/items/create_list_item.ts index c51d3e3b944ea..fb3788876a2b0 100644 --- a/x-pack/plugins/lists/server/services/items/create_list_item.ts +++ b/x-pack/plugins/lists/server/services/items/create_list_item.ts @@ -12,6 +12,7 @@ import { IdOrUndefined, ListItemSchema, MetaOrUndefined, + RefreshWithWaitFor, SerializerOrUndefined, Type, } from '@kbn/securitysolution-io-ts-list-types'; @@ -33,6 +34,7 @@ export interface CreateListItemOptions { meta: MetaOrUndefined; dateNow?: string; tieBreaker?: string; + refresh?: RefreshWithWaitFor; } export const createListItem = async ({ @@ -48,6 +50,7 @@ export const createListItem = async ({ meta, dateNow, tieBreaker, + refresh = 'wait_for', }: CreateListItemOptions): Promise => { const createdAt = dateNow ?? new Date().toISOString(); const tieBreakerId = tieBreaker ?? uuidv4(); @@ -73,7 +76,7 @@ export const createListItem = async ({ body, id: id ?? uuidv4(), index: listItemIndex, - refresh: 'wait_for', + refresh, }); return { diff --git a/x-pack/plugins/lists/server/services/items/create_list_items_bulk.ts b/x-pack/plugins/lists/server/services/items/create_list_items_bulk.ts index 54965751a058a..6877b4aabedeb 100644 --- a/x-pack/plugins/lists/server/services/items/create_list_items_bulk.ts +++ b/x-pack/plugins/lists/server/services/items/create_list_items_bulk.ts @@ -10,6 +10,7 @@ import { ElasticsearchClient } from '@kbn/core/server'; import type { DeserializerOrUndefined, MetaOrUndefined, + RefreshWithWaitFor, SerializerOrUndefined, Type, } from '@kbn/securitysolution-io-ts-list-types'; @@ -29,6 +30,7 @@ export interface CreateListItemsBulkOptions { meta: MetaOrUndefined; dateNow?: string; tieBreaker?: string[]; + refresh?: RefreshWithWaitFor; } export const createListItemsBulk = async ({ @@ -43,6 +45,7 @@ export const createListItemsBulk = async ({ meta, dateNow, tieBreaker, + refresh = 'wait_for', }: CreateListItemsBulkOptions): Promise => { // It causes errors if you try to add items to bulk that do not exist within ES if (!value.length) { @@ -85,7 +88,7 @@ export const createListItemsBulk = async ({ await esClient.bulk({ body, index: listItemIndex, - refresh: 'wait_for', + refresh, }); } catch (error) { // TODO: Log out the error with return values from the bulk insert into another index or saved object diff --git a/x-pack/plugins/lists/server/services/items/delete_list_item.ts b/x-pack/plugins/lists/server/services/items/delete_list_item.ts index dffc12091d0fb..751a56a543352 100644 --- a/x-pack/plugins/lists/server/services/items/delete_list_item.ts +++ b/x-pack/plugins/lists/server/services/items/delete_list_item.ts @@ -14,12 +14,14 @@ export interface DeleteListItemOptions { id: Id; esClient: ElasticsearchClient; listItemIndex: string; + refresh?: boolean; } export const deleteListItem = async ({ id, esClient, listItemIndex, + refresh = false, }: DeleteListItemOptions): Promise => { const listItem = await getListItem({ esClient, id, listItemIndex }); if (listItem == null) { @@ -32,7 +34,7 @@ export const deleteListItem = async ({ values: [id], }, }, - refresh: false, + refresh, }); } return listItem; diff --git a/x-pack/plugins/lists/server/services/items/delete_list_item_by_value.ts b/x-pack/plugins/lists/server/services/items/delete_list_item_by_value.ts index 133beae0e6b62..0efe99436dea5 100644 --- a/x-pack/plugins/lists/server/services/items/delete_list_item_by_value.ts +++ b/x-pack/plugins/lists/server/services/items/delete_list_item_by_value.ts @@ -18,6 +18,7 @@ export interface DeleteListItemByValueOptions { value: string; esClient: ElasticsearchClient; listItemIndex: string; + refresh?: boolean; } export const deleteListItemByValue = async ({ @@ -26,6 +27,7 @@ export const deleteListItemByValue = async ({ type, esClient, listItemIndex, + refresh = false, }: DeleteListItemByValueOptions): Promise => { const listItems = await getListItemByValues({ esClient, @@ -49,7 +51,7 @@ export const deleteListItemByValue = async ({ }, }, index: listItemIndex, - refresh: false, + refresh, }); return listItems; }; diff --git a/x-pack/plugins/lists/server/services/items/update_list_item.ts b/x-pack/plugins/lists/server/services/items/update_list_item.ts index 13132c525e9cc..3d98b838e6c72 100644 --- a/x-pack/plugins/lists/server/services/items/update_list_item.ts +++ b/x-pack/plugins/lists/server/services/items/update_list_item.ts @@ -31,6 +31,7 @@ export interface UpdateListItemOptions { meta: MetaOrUndefined; dateNow?: string; isPatch?: boolean; + refresh?: boolean; } export const updateListItem = async ({ @@ -43,6 +44,7 @@ export const updateListItem = async ({ meta, dateNow, isPatch = false, + refresh = false, }: UpdateListItemOptions): Promise => { const updatedAt = dateNow ?? new Date().toISOString(); const listItem = await getListItem({ esClient, id, listItemIndex }); @@ -81,7 +83,7 @@ export const updateListItem = async ({ values: [id], }, }, - refresh: false, + refresh, script: { lang: 'painless', params, diff --git a/x-pack/plugins/lists/server/services/items/write_lines_to_bulk_list_items.ts b/x-pack/plugins/lists/server/services/items/write_lines_to_bulk_list_items.ts index 9d18b78757943..bb1d0c3877e54 100644 --- a/x-pack/plugins/lists/server/services/items/write_lines_to_bulk_list_items.ts +++ b/x-pack/plugins/lists/server/services/items/write_lines_to_bulk_list_items.ts @@ -13,6 +13,7 @@ import type { ListIdOrUndefined, ListSchema, MetaOrUndefined, + RefreshWithWaitFor, SerializerOrUndefined, Type, } from '@kbn/securitysolution-io-ts-list-types'; @@ -38,6 +39,7 @@ export interface ImportListItemsToStreamOptions { user: string; meta: MetaOrUndefined; version: Version; + refresh?: RefreshWithWaitFor; } export const importListItemsToStream = ({ @@ -53,6 +55,7 @@ export const importListItemsToStream = ({ user, meta, version, + refresh, }: ImportListItemsToStreamOptions): Promise => { return new Promise((resolve, reject) => { const readBuffer = new BufferLines({ bufferSize: config.importBufferSize, input: stream }); @@ -97,6 +100,7 @@ export const importListItemsToStream = ({ listId, listItemIndex, meta, + refresh, serializer, type, user, @@ -109,6 +113,7 @@ export const importListItemsToStream = ({ listId: fileName, listItemIndex, meta, + refresh, serializer, type, user, @@ -135,6 +140,7 @@ export interface WriteBufferToItemsOptions { type: Type; user: string; meta: MetaOrUndefined; + refresh?: RefreshWithWaitFor; } export interface LinesResult { @@ -151,6 +157,7 @@ export const writeBufferToItems = async ({ type, user, meta, + refresh, }: WriteBufferToItemsOptions): Promise => { await createListItemsBulk({ deserializer, @@ -158,6 +165,7 @@ export const writeBufferToItems = async ({ listId, listItemIndex, meta, + refresh, serializer, type, user, diff --git a/x-pack/plugins/lists/server/services/lists/list_client.ts b/x-pack/plugins/lists/server/services/lists/list_client.ts index d1d5c585c3f85..c12b198a0a7f8 100644 --- a/x-pack/plugins/lists/server/services/lists/list_client.ts +++ b/x-pack/plugins/lists/server/services/lists/list_client.ts @@ -646,10 +646,13 @@ export class ListClient { * Given a list item id, this will delete the single list item * @returns The list item if found, otherwise null */ - public deleteListItem = async ({ id }: DeleteListItemOptions): Promise => { + public deleteListItem = async ({ + id, + refresh, + }: DeleteListItemOptions): Promise => { const { esClient } = this; const listItemName = this.getListItemName(); - return deleteListItem({ esClient, id, listItemIndex: listItemName }); + return deleteListItem({ esClient, id, listItemIndex: listItemName, refresh }); }; /** @@ -664,6 +667,7 @@ export class ListClient { listId, value, type, + refresh, }: DeleteListItemByValueOptions): Promise => { const { esClient } = this; const listItemName = this.getListItemName(); @@ -671,6 +675,7 @@ export class ListClient { esClient, listId, listItemIndex: listItemName, + refresh, type, value, }); @@ -756,6 +761,7 @@ export class ListClient { * @param options.stream The stream to pull the import from * @param options.meta Additional meta data to associate with the list items as an object of "key/value" pairs. You can set this to "undefined" for no meta values. * @param options.version Version number of the list, typically this should be 1 unless you are re-creating a list you deleted or something unusual. + * @param options.refresh If true, then refresh the index after importing the list items. */ public importListItemsToStream = async ({ deserializer, @@ -765,6 +771,7 @@ export class ListClient { stream, meta, version, + refresh, }: ImportListItemsToStreamOptions): Promise => { const { esClient, user, config } = this; const listItemName = this.getListItemName(); @@ -777,6 +784,7 @@ export class ListClient { listIndex: listName, listItemIndex: listItemName, meta, + refresh, serializer, stream, type, @@ -831,6 +839,7 @@ export class ListClient { value, type, meta, + refresh, }: CreateListItemOptions): Promise => { const { esClient, user } = this; const listItemName = this.getListItemName(); @@ -841,6 +850,7 @@ export class ListClient { listId, listItemIndex: listItemName, meta, + refresh, serializer, type, user, @@ -893,6 +903,7 @@ export class ListClient { id, value, meta, + refresh, }: UpdateListItemOptions): Promise => { const { esClient, user } = this; const listItemName = this.getListItemName(); @@ -903,6 +914,7 @@ export class ListClient { isPatch: true, listItemIndex: listItemName, meta, + refresh, user, value, }); diff --git a/x-pack/plugins/lists/server/services/lists/list_client_types.ts b/x-pack/plugins/lists/server/services/lists/list_client_types.ts index 4c64e8e940163..b3fe607bf0795 100644 --- a/x-pack/plugins/lists/server/services/lists/list_client_types.ts +++ b/x-pack/plugins/lists/server/services/lists/list_client_types.ts @@ -23,6 +23,7 @@ import type { NameOrUndefined, Page, PerPage, + RefreshWithWaitFor, SerializerOrUndefined, SortFieldOrUndefined, SortOrderOrUndefined, @@ -73,6 +74,7 @@ export interface DeleteListOptions { export interface DeleteListItemOptions { /** The id of the list to delete from */ id: Id; + refresh?: boolean; } /** @@ -135,6 +137,7 @@ export interface DeleteListItemByValueOptions { value: string; /** The type of list such as "boolean", "double", "text", "keyword", etc... */ type: Type; + refresh?: boolean; } /** @@ -181,6 +184,8 @@ export interface ImportListItemsToStreamOptions { meta: MetaOrUndefined; /** Version number of the list, typically this should be 1 unless you are re-creating a list you deleted or something unusual. */ version: Version; + /** If true refresh index after import list items */ + refresh?: RefreshWithWaitFor; } /** @@ -202,6 +207,7 @@ export interface CreateListItemOptions { value: string; /** Additional meta data to associate with the list items as an object of "key/value" pairs. You can set this to "undefined" for no meta values. */ meta: MetaOrUndefined; + refresh?: RefreshWithWaitFor; } /** @@ -217,6 +223,7 @@ export interface UpdateListItemOptions { value: string | null | undefined; /** Additional meta data to associate with the list items as an object of "key/value" pairs. You can set this to "undefined" to not update meta values. */ meta: MetaOrUndefined; + refresh?: boolean; } /** diff --git a/x-pack/plugins/security_solution/common/experimental_features.ts b/x-pack/plugins/security_solution/common/experimental_features.ts index 48e22944853a9..89d40bbb36fb2 100644 --- a/x-pack/plugins/security_solution/common/experimental_features.ts +++ b/x-pack/plugins/security_solution/common/experimental_features.ts @@ -232,6 +232,11 @@ export const allowedExperimentalValues = Object.freeze({ * Makes Elastic Defend integration's Malware On-Write Scan option available to edit. */ malwareOnWriteScanOptionAvailable: false, + + /** + * Enables the new modal for the value list items + */ + valueListItemsModalEnabled: false, }); type ExperimentalConfigKeys = Array; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/exception_item_card/conditions.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/exception_item_card/conditions.tsx index a75ef577c8c70..0d6210b546019 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/exception_item_card/conditions.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/exception_item_card/conditions.tsx @@ -26,6 +26,7 @@ import type { NonEmptyNestedEntriesArray, } from '@kbn/securitysolution-io-ts-list-types'; import { ListOperatorTypeEnum } from '@kbn/securitysolution-io-ts-list-types'; +import { ShowValueListModal } from '../../../../value_list/components/show_value_list_modal'; import * as i18n from './translations'; import { ValueWithSpaceWarning } from '../value_with_space_warning'; @@ -102,6 +103,12 @@ export const ExceptionItemCardConditions = memo( {currentValue} )); + } else if (type === 'list' && value) { + return ( + + {value} + + ); } return {value} ?? ''; }; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/flyout_components/item_conditions/index.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/flyout_components/item_conditions/index.tsx index 4467b18f0b10e..d42e50df25e35 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/flyout_components/item_conditions/index.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/flyout_components/item_conditions/index.tsx @@ -32,6 +32,7 @@ import type { Rule } from '../../../../rule_management/logic/types'; import { useKibana } from '../../../../../common/lib/kibana'; import * as i18n from './translations'; import * as sharedI18n from '../../../utils/translations'; +import { ShowValueListModal } from '../../../../../value_list/components/show_value_list_modal'; const OS_OPTIONS: Array> = [ { @@ -265,6 +266,7 @@ const ExceptionsConditionsComponent: React.FC ); diff --git a/x-pack/plugins/security_solution/public/detections/components/value_lists_management_flyout/table_helpers.tsx b/x-pack/plugins/security_solution/public/detections/components/value_lists_management_flyout/table_helpers.tsx index 5174e2fa6bccb..3e8cde7e00a23 100644 --- a/x-pack/plugins/security_solution/public/detections/components/value_lists_management_flyout/table_helpers.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/value_lists_management_flyout/table_helpers.tsx @@ -10,6 +10,7 @@ import styled from 'styled-components'; import { EuiButtonIcon, EuiLoadingSpinner, EuiToolTip } from '@elastic/eui'; import type { ListSchema } from '@kbn/securitysolution-io-ts-list-types'; +import { ShowValueListModal } from '../../../value_list/components/show_value_list_modal'; import { FormattedDate } from '../../../common/components/formatted_date'; import * as i18n from './translations'; import type { TableItemCallback, TableProps } from './types'; @@ -28,6 +29,11 @@ export const buildColumns = ( field: 'name', name: i18n.COLUMN_FILE_NAME, truncateText: false, + render: (name: ListSchema['name'], item: ListSchema) => ( + + {name} + + ), }, { field: 'type', diff --git a/x-pack/plugins/security_solution/public/exceptions/components/list_exception_items/index.tsx b/x-pack/plugins/security_solution/public/exceptions/components/list_exception_items/index.tsx index 990fd818505b7..a52743b2d471a 100644 --- a/x-pack/plugins/security_solution/public/exceptions/components/list_exception_items/index.tsx +++ b/x-pack/plugins/security_solution/public/exceptions/components/list_exception_items/index.tsx @@ -23,6 +23,7 @@ import { LinkToRuleDetails } from '../link_to_rule_details'; import { ExceptionsUtility } from '../exceptions_utility'; import * as i18n from '../../translations/list_exception_items'; import { useEndpointExceptionsCapability } from '../../hooks/use_endpoint_exceptions_capability'; +import { ShowValueListModal } from '../../../value_list/components/show_value_list_modal'; interface ListExceptionItemsProps { isReadOnly: boolean; @@ -104,6 +105,7 @@ const ListExceptionItemsComponent: FC = ({ /> ) } + showValueListModal={ShowValueListModal} /> ); diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/form.tsx b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/form.tsx index e4e1fa7e14638..97eb5de7954ff 100644 --- a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/form.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/form.tsx @@ -63,6 +63,7 @@ import { EffectedPolicySelect } from '../../../../components/effected_policy_sel import { isGlobalPolicyEffected } from '../../../../components/effected_policy_select/utils'; import { ExceptionItemComments } from '../../../../../detection_engine/rule_exceptions/components/item_comments'; import { EventFiltersApiClient } from '../../service/api_client'; +import { ShowValueListModal } from '../../../../../value_list/components/show_value_list_modal'; const OPERATING_SYSTEMS: readonly OperatingSystem[] = [ OperatingSystem.MAC, @@ -456,6 +457,7 @@ export const EventFiltersForm: React.FC { + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + const { addSuccess, addError } = useAppToasts(); + const http = useKibana().services.http; + const createListItemMutation = useCreateListItemMutation({ + onSuccess: () => { + addSuccess(SUCCESSFULLY_ADDED_ITEM); + }, + onError: (error) => { + addError(error, { + title: error.message, + toastMessage: error?.body?.message ?? error.message, + }); + }, + }); + const formik = useFormik({ + initialValues: { + value: '', + }, + validate: (values) => { + if (values.value.trim() === '') { + return { value: VALUE_REQUIRED }; + } + }, + onSubmit: async (values) => { + await createListItemMutation.mutateAsync({ listId, value: values.value, http }); + setIsPopoverOpen(false); + formik.resetForm(); + }, + }); + + return ( + setIsPopoverOpen(true)} + > + {ADD_LIST_ITEM} + + } + isOpen={isPopoverOpen} + closePopover={() => setIsPopoverOpen(false)} + > +
+ + + + + + + + + + + {createListItemMutation.isLoading + ? ADDING_LIST_ITEM_BUTTON + : ADD_LIST_ITEM_BUTTON} + + + + + +
+
+ ); +}; diff --git a/x-pack/plugins/security_solution/public/value_list/components/delete_list_item.tsx b/x-pack/plugins/security_solution/public/value_list/components/delete_list_item.tsx new file mode 100644 index 0000000000000..d9ad65a60cf0b --- /dev/null +++ b/x-pack/plugins/security_solution/public/value_list/components/delete_list_item.tsx @@ -0,0 +1,47 @@ +/* + * 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, { useCallback } from 'react'; +import { EuiButtonIcon } from '@elastic/eui'; +import { useDeleteListItemMutation } from '@kbn/securitysolution-list-hooks'; +import { useAppToasts } from '../../common/hooks/use_app_toasts'; +import { useKibana } from '../../common/lib/kibana'; +import { SUCCESSFULLY_DELETED_ITEM } from '../translations'; + +const toastOptions = { + toastLifeTimeMs: 5000, +}; + +export const DeleteListItem = ({ id, value }: { id: string; value: string }) => { + const { addSuccess, addError } = useAppToasts(); + const http = useKibana().services.http; + const deleteListItemMutation = useDeleteListItemMutation({ + onSuccess: () => { + addSuccess(SUCCESSFULLY_DELETED_ITEM, toastOptions); + }, + onError: (error) => { + addError(error, { + title: error.message, + toastMessage: error?.body?.message ?? error.message, + }); + }, + }); + + const deleteListItem = useCallback(() => { + deleteListItemMutation.mutate({ id, http }); + }, [deleteListItemMutation, id, http]); + + return ( + + ); +}; diff --git a/x-pack/plugins/security_solution/public/value_list/components/info.tsx b/x-pack/plugins/security_solution/public/value_list/components/info.tsx new file mode 100644 index 0000000000000..328c1f0ed1931 --- /dev/null +++ b/x-pack/plugins/security_solution/public/value_list/components/info.tsx @@ -0,0 +1,24 @@ +/* + * 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 { EuiText } from '@elastic/eui'; +import { css } from '@emotion/css'; +import { euiThemeVars } from '@kbn/ui-theme'; + +const info = css` + margin-right: ${euiThemeVars.euiSizeS}; +`; + +const infoLabel = css` + margin-right: ${euiThemeVars.euiSizeXS}; +`; + +export const Info = ({ label, value }: { value: React.ReactNode; label: string }) => ( + + {label} {value} + +); diff --git a/x-pack/plugins/security_solution/public/value_list/components/inline_edit_list_item_value.tsx b/x-pack/plugins/security_solution/public/value_list/components/inline_edit_list_item_value.tsx new file mode 100644 index 0000000000000..4ef639bcdbebb --- /dev/null +++ b/x-pack/plugins/security_solution/public/value_list/components/inline_edit_list_item_value.tsx @@ -0,0 +1,67 @@ +/* + * 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, { useCallback, useState } from 'react'; +import { EuiInlineEditText } from '@elastic/eui'; +import type { ListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; +import { usePatchListItemMutation } from '@kbn/securitysolution-list-hooks'; +import { useAppToasts } from '../../common/hooks/use_app_toasts'; +import { useKibana } from '../../common/lib/kibana/kibana_react'; +import { EDIT_TEXT_INLINE_LABEL, SUCCESSFULLY_UPDATED_LIST_ITEM } from '../translations'; + +const toastOptions = { + toastLifeTimeMs: 5000, +}; + +export const InlineEditListItemValue = ({ listItem }: { listItem: ListItemSchema }) => { + const [value, setValue] = useState(listItem.value); + const { addSuccess, addError } = useAppToasts(); + const http = useKibana().services.http; + const patchListItemMutation = usePatchListItemMutation({ + onSuccess: () => { + addSuccess(SUCCESSFULLY_UPDATED_LIST_ITEM, toastOptions); + }, + onError: (error) => { + addError(error, { + title: error.message, + toastMessage: error?.body?.message ?? error.message, + }); + }, + }); + const onChange = useCallback((e) => { + setValue(e.target.value); + }, []); + const onCancel = useCallback(() => { + setValue(listItem.value); + }, [listItem]); + + const onSave = useCallback( + async (newValue) => { + await patchListItemMutation.mutateAsync({ + id: listItem.id, + value: newValue, + _version: listItem._version, + http, + }); + return true; + }, + [listItem._version, listItem.id, patchListItemMutation, http] + ); + + return ( + + ); +}; diff --git a/x-pack/plugins/security_solution/public/value_list/components/list_item_table.tsx b/x-pack/plugins/security_solution/public/value_list/components/list_item_table.tsx new file mode 100644 index 0000000000000..50226ea047acc --- /dev/null +++ b/x-pack/plugins/security_solution/public/value_list/components/list_item_table.tsx @@ -0,0 +1,85 @@ +/* + * 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 } from '@elastic/eui'; +import React from 'react'; +import type { ListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; +import { InlineEditListItemValue } from './inline_edit_list_item_value'; +import { DeleteListItem } from './delete_list_item'; +import { FormattedDate } from '../../common/components/formatted_date'; +import { LIST_ITEM_FIELDS } from '../types'; +import type { ListItemTableProps, ListItemTableColumns } from '../types'; +import { + COLUMN_VALUE, + COLUMN_UPDATED_AT, + COLUMN_UPDATED_BY, + COLUMN_ACTIONS, + FAILED_TO_FETCH_LIST_ITEM, + DELETE_LIST_ITEM, + DELETE_LIST_ITEM_DESCRIPTION, +} from '../translations'; + +export const ListItemTable = ({ + canWriteIndex, + items, + pagination, + sorting, + loading, + onChange, + isError, + list, +}: ListItemTableProps) => { + const columns: ListItemTableColumns = [ + { + field: LIST_ITEM_FIELDS.value, + name: COLUMN_VALUE, + render: (value, item) => + canWriteIndex ? : value, + sortable: list.type !== 'text', + }, + { + field: LIST_ITEM_FIELDS.updatedAt, + name: COLUMN_UPDATED_AT, + render: (value: ListItemSchema[LIST_ITEM_FIELDS.updatedAt]) => ( + + ), + width: '25%', + sortable: true, + }, + { + field: LIST_ITEM_FIELDS.updatedBy, + name: COLUMN_UPDATED_BY, + width: '15%', + }, + ]; + if (canWriteIndex) { + columns.push({ + name: COLUMN_ACTIONS, + actions: [ + { + name: DELETE_LIST_ITEM, + description: DELETE_LIST_ITEM_DESCRIPTION, + isPrimary: true, + render: (item: ListItemSchema) => , + }, + ], + width: '10%', + }); + } + + return ( + + ); +}; diff --git a/x-pack/plugins/security_solution/public/value_list/components/show_value_list_modal.tsx b/x-pack/plugins/security_solution/public/value_list/components/show_value_list_modal.tsx new file mode 100644 index 0000000000000..0cf0608425b2f --- /dev/null +++ b/x-pack/plugins/security_solution/public/value_list/components/show_value_list_modal.tsx @@ -0,0 +1,52 @@ +/* + * 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 { EuiLink } from '@elastic/eui'; +import React, { useState, useCallback } from 'react'; +import { useListsPrivileges } from '../../detections/containers/detection_engine/lists/use_lists_privileges'; +import { useIsExperimentalFeatureEnabled } from '../../common/hooks/use_experimental_features'; +import { ValueListModal } from './value_list_modal'; + +export const ShowValueListModal = ({ + listId, + children, + shouldShowContentIfModalNotAvailable, +}: { + listId: string; + children: React.ReactNode; + shouldShowContentIfModalNotAvailable: boolean; +}) => { + const [showModal, setShowModal] = useState(false); + const { canWriteIndex, canReadIndex, loading } = useListsPrivileges(); + const isValueItemsListModalEnabled = useIsExperimentalFeatureEnabled( + 'valueListItemsModalEnabled' + ); + + const onCloseModal = useCallback(() => setShowModal(false), []); + const onShowModal = useCallback(() => setShowModal(true), []); + + if (loading) return null; + + if (!canReadIndex || !isValueItemsListModalEnabled) { + return shouldShowContentIfModalNotAvailable ? <>{children} : null; + } + + return ( + <> + + {children} + + {showModal && ( + + )} + + ); +}; diff --git a/x-pack/plugins/security_solution/public/value_list/components/upload_list_item.tsx b/x-pack/plugins/security_solution/public/value_list/components/upload_list_item.tsx new file mode 100644 index 0000000000000..7e43629a6578c --- /dev/null +++ b/x-pack/plugins/security_solution/public/value_list/components/upload_list_item.tsx @@ -0,0 +1,106 @@ +/* + * 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, { useCallback, useEffect, useRef, useState } from 'react'; +import { EuiButton, EuiFilePicker, EuiToolTip } from '@elastic/eui'; +import { css } from '@emotion/css'; +import { useImportList, useInvalidateListItemQuery } from '@kbn/securitysolution-list-hooks'; +import type { Type as ListType } from '@kbn/securitysolution-io-ts-list-types'; +import { useKibana } from '../../common/lib/kibana'; +import { useAppToasts } from '../../common/hooks/use_app_toasts'; +import { + UPLOAD_LIST_ITEM, + UPLOAD_FILE_PICKER_INITAL_PROMT_TEXT, + UPLOAD_TOOLTIP, + FAILED_TO_UPLOAD_LIST_ITEM_TITLE, + FAILED_TO_UPLOAD_LIST_ITEM, + SUCCESSFULY_UPLOAD_LIST_ITEMS, +} from '../translations'; + +const validFileTypes = ['text/csv', 'text/plain']; + +const toastOptions = { + toastLifeTimeMs: 5000, +}; + +const uploadStyle = css` + min-width: 300px; +`; + +export const UploadListItem = ({ listId, type }: { listId: string; type: ListType }) => { + const [file, setFile] = useState(null); + const { http } = useKibana().services; + const ctrl = useRef(new AbortController()); + const { start: importList, ...importState } = useImportList(); + const invalidateListItemQuery = useInvalidateListItemQuery(); + const { addSuccess, addError } = useAppToasts(); + const handleFileChange = useCallback((files: FileList | null) => { + setFile(files?.item(0) ?? null); + }, []); + + const handleImport = useCallback(() => { + if (!importState.loading && file) { + ctrl.current = new AbortController(); + importList({ + file, + listId, + http, + signal: ctrl.current.signal, + type, + refresh: 'true', + }); + } + }, [importState.loading, file, importList, http, type, listId]); + + const fileIsValid = !file || validFileTypes.some((fileType) => file.type === fileType); + + useEffect(() => { + if (!importState.loading && importState.result) { + addSuccess(SUCCESSFULY_UPLOAD_LIST_ITEMS, toastOptions); + } else if (!importState.loading && importState.error) { + addError(FAILED_TO_UPLOAD_LIST_ITEM, { + title: FAILED_TO_UPLOAD_LIST_ITEM_TITLE, + ...toastOptions, + }); + } + invalidateListItemQuery(); + }, [ + importState.error, + importState.loading, + importState.result, + addSuccess, + addError, + invalidateListItemQuery, + ]); + + const isDisabled = file == null || !fileIsValid || importState.loading; + + return ( + <> + + + + + {UPLOAD_LIST_ITEM} + + + ); +}; diff --git a/x-pack/plugins/security_solution/public/value_list/components/value_list_modal.tsx b/x-pack/plugins/security_solution/public/value_list/components/value_list_modal.tsx new file mode 100644 index 0000000000000..6aa6b1b7d9b60 --- /dev/null +++ b/x-pack/plugins/security_solution/public/value_list/components/value_list_modal.tsx @@ -0,0 +1,181 @@ +/* + * 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, { useState, useCallback } from 'react'; +import { css } from '@emotion/css'; +import { + EuiModal, + EuiModalHeader, + EuiModalHeaderTitle, + EuiFlexItem, + EuiFlexGroup, + EuiText, + EuiLoadingSpinner, + EuiSpacer, + EuiSearchBar, +} from '@elastic/eui'; +import { useFindListItems, useGetListById } from '@kbn/securitysolution-list-hooks'; +import { euiThemeVars } from '@kbn/ui-theme'; +import { FormattedDate } from '../../common/components/formatted_date'; +import { useKibana } from '../../common/lib/kibana'; +import { AddListItemPopover } from './add_list_item_popover'; +import { UploadListItem } from './upload_list_item'; +import { ListItemTable } from './list_item_table'; +import { Info } from './info'; +import type { SortFields, ValueListModalProps, OnTableChange, Sorting } from '../types'; +import { + INFO_UPDATED_AT, + INFO_TYPE, + INFO_UPDATED_BY, + INFO_TOTAL_ITEMS, + getInfoTotalItems, +} from '../translations'; + +const modalBodyStyle = css` + overflow: hidden; + padding: ${euiThemeVars.euiSize}; +`; + +const modalWindow = css` + min-height: 90vh; + margin-top: 5vh; + max-width: 1400px; + min-width: 700px; +`; + +const tableStyle = css` + overflow: scroll; +`; + +export const ValueListModal = ({ listId, onCloseModal, canWriteIndex }: ValueListModalProps) => { + const [filter, setFilter] = useState(''); + const http = useKibana().services.http; + const [pageIndex, setPageIndex] = useState(0); + const [pageSize, setPageSize] = useState(10); + const [sortField, setSortField] = useState('updated_at'); + const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('desc'); + + const { + data: listItems, + isLoading: isListItemsLoading, + isError, + } = useFindListItems({ + listId, + pageIndex: pageIndex + 1, + pageSize, + sortField, + sortOrder, + filter, + http, + }); + + const { data: list, isLoading: isListLoading } = useGetListById({ http, id: listId }); + + const onTableChange: OnTableChange = ({ page, sort }) => { + if (page) { + setPageIndex(page.index); + setPageSize(page.size); + } + if (sort) { + setSortField(sort.field as SortFields); + setSortOrder(sort.direction); + } + }; + + const sorting: Sorting = { + sort: { + field: sortField, + direction: sortOrder, + }, + enableAllColumns: false, + }; + + const pagination = { + pageIndex, + pageSize, + totalItemCount: listItems?.total ?? 0, + pageSizeOptions: [5, 10, 25], + }; + + const onQueryChange = useCallback((params) => { + setFilter(params.queryText); + }, []); + + const isListExist = !isListLoading && !!list; + + return ( + + <> + + {isListExist && ( + + + + {list.id} + + + {list.description && ( + <> + {list.description} + + + )} + + + } + /> + + {listItems && } + + + + {canWriteIndex && ( + + + + + )} + + + )} + + + + + + + {!isListExist ? ( + + ) : ( + + )} + + + + + ); +}; diff --git a/x-pack/plugins/security_solution/public/value_list/translations.ts b/x-pack/plugins/security_solution/public/value_list/translations.ts new file mode 100644 index 0000000000000..62b81fa61cab2 --- /dev/null +++ b/x-pack/plugins/security_solution/public/value_list/translations.ts @@ -0,0 +1,168 @@ +/* + * 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 ADD_LIST_ITEM = i18n.translate('xpack.securitySolution.listItems.addListItem', { + defaultMessage: 'Add list item', +}); + +export const SUCCESSFULLY_ADDED_ITEM = i18n.translate( + 'xpack.securitySolution.listItems.successfullyAddedItem', + { + defaultMessage: 'Successfully added list item', + } +); + +export const VALUE_REQUIRED = i18n.translate('xpack.securitySolution.listItems.valueRequired', { + defaultMessage: 'Value is required', +}); + +export const VALUE_LABEL = i18n.translate('xpack.securitySolution.listItems.valueLabel', { + defaultMessage: 'Value', +}); + +export const ADD_VALUE_LIST_PLACEHOLDER = i18n.translate( + 'xpack.securitySolution.listItems.addValueListPlaceholder', + { + defaultMessage: 'Add list item..', + } +); + +export const ADD_LIST_ITEM_BUTTON = i18n.translate( + 'xpack.securitySolution.listItems.addListItemButton', + { + defaultMessage: 'Add', + } +); + +export const ADDING_LIST_ITEM_BUTTON = i18n.translate( + 'xpack.securitySolution.listItems.addingListItemButton', + { + defaultMessage: 'Adding...', + } +); + +export const SUCCESSFULLY_DELETED_ITEM = i18n.translate( + 'xpack.securitySolution.listItems.successfullyDeletedItem', + { + defaultMessage: 'Successfully deleted list item', + } +); + +export const EDIT_TEXT_INLINE_LABEL = i18n.translate( + 'xpack.securitySolution.listItems.editTextInlineLabel', + { + defaultMessage: 'Edit text inline', + } +); + +export const SUCCESSFULLY_UPDATED_LIST_ITEM = i18n.translate( + 'xpack.securitySolution.listItems.successfullyUpdatedListItem', + { + defaultMessage: 'Successfully updated list item', + } +); + +export const COLUMN_VALUE = i18n.translate('xpack.securitySolution.listItems.columnValue', { + defaultMessage: 'Value', +}); + +export const COLUMN_UPDATED_AT = i18n.translate( + 'xpack.securitySolution.listItems.columnUpdatedAt', + { + defaultMessage: 'Updated At', + } +); + +export const COLUMN_UPDATED_BY = i18n.translate( + 'xpack.securitySolution.listItems.columnUpdatedBy', + { + defaultMessage: 'Updated By', + } +); + +export const COLUMN_ACTIONS = i18n.translate('xpack.securitySolution.listItems.columnActions', { + defaultMessage: 'Actions', +}); + +export const FAILED_TO_FETCH_LIST_ITEM = i18n.translate( + 'xpack.securitySolution.listItems.failedToFetchListItem', + { + defaultMessage: + 'Failed to load list items. You can change the search query or contact your administrator', + } +); + +export const DELETE_LIST_ITEM = i18n.translate('xpack.securitySolution.listItems.deleteListItem', { + defaultMessage: 'Delete', +}); + +export const DELETE_LIST_ITEM_DESCRIPTION = i18n.translate( + 'xpack.securitySolution.listItems.deleteListItemDescription', + { + defaultMessage: 'Delete list item', + } +); + +export const SUCCESSFULY_UPLOAD_LIST_ITEMS = i18n.translate( + 'xpack.securitySolution.listItems.successfullyUploadListItems', + { + defaultMessage: 'Successfully uploaded list items', + } +); + +export const FAILED_TO_UPLOAD_LIST_ITEM = i18n.translate( + 'xpack.securitySolution.listItems.failedToUploadListItem', + { + defaultMessage: 'Failed to upload list items', + } +); + +export const FAILED_TO_UPLOAD_LIST_ITEM_TITLE = i18n.translate( + 'xpack.securitySolution.listItems.failedToUploadListItemTitle', + { + defaultMessage: 'Error', + } +); + +export const UPLOAD_TOOLTIP = i18n.translate('xpack.securitySolution.listItems.uploadTooltip', { + defaultMessage: 'All items from the file will be added as new items', +}); + +export const UPLOAD_FILE_PICKER_INITAL_PROMT_TEXT = i18n.translate( + 'xpack.securitySolution.listItems.uploadFilePickerInitialPromptText', + { + defaultMessage: 'Select or drag and drop a file', + } +); + +export const UPLOAD_LIST_ITEM = i18n.translate('xpack.securitySolution.listItems.uploadListItem', { + defaultMessage: 'Upload', +}); + +export const INFO_TYPE = i18n.translate('xpack.securitySolution.listItems.infoType', { + defaultMessage: 'Type:', +}); + +export const INFO_UPDATED_AT = i18n.translate('xpack.securitySolution.listItems.infoUpdatedAt', { + defaultMessage: 'Updated at:', +}); + +export const INFO_UPDATED_BY = i18n.translate('xpack.securitySolution.listItems.infoUpdatedBy', { + defaultMessage: 'Updated by:', +}); + +export const INFO_TOTAL_ITEMS = i18n.translate('xpack.securitySolution.listItems.infoTotalItems', { + defaultMessage: 'Total items:', +}); + +export const getInfoTotalItems = (listType: string) => + i18n.translate('xpack.securitySolution.listItems.searchBar', { + defaultMessage: 'Filter your data using KQL syntax - {listType}:*', + values: { listType }, + }); diff --git a/x-pack/plugins/security_solution/public/value_list/types.ts b/x-pack/plugins/security_solution/public/value_list/types.ts new file mode 100644 index 0000000000000..8681705d5b60a --- /dev/null +++ b/x-pack/plugins/security_solution/public/value_list/types.ts @@ -0,0 +1,43 @@ +/* + * 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 type { ListItemSchema, ListSchema } from '@kbn/securitysolution-io-ts-list-types'; +import type { + EuiBasicTableColumn, + Pagination, + EuiTableSortingType, + CriteriaWithPagination, +} from '@elastic/eui'; + +export type SortFields = 'updated_at' | 'updated_by'; + +export type OnTableChange = (params: CriteriaWithPagination) => void; + +export type Sorting = EuiTableSortingType>; + +export interface ListItemTableProps { + canWriteIndex: boolean; + items: ListItemSchema[]; + pagination: Pagination; + sorting: Sorting; + loading: boolean; + onChange: OnTableChange; + isError: boolean; + list: ListSchema; +} +export interface ValueListModalProps { + listId: string; + canWriteIndex: boolean; + onCloseModal: () => void; +} + +export type ListItemTableColumns = Array>; + +export enum LIST_ITEM_FIELDS { + value = 'value', + updatedAt = 'updated_at', + updatedBy = 'updated_by', +} diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/value_lists/value_list_items.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/value_lists/value_list_items.cy.ts new file mode 100644 index 0000000000000..9dc094153235a --- /dev/null +++ b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/value_lists/value_list_items.cy.ts @@ -0,0 +1,152 @@ +/* + * 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 { login } from '../../../../tasks/login'; +import { visit } from '../../../../tasks/navigation'; +import { TOASTER_BODY } from '../../../../screens/alerts_detection_rules'; +import { closeErrorToast } from '../../../../tasks/alerts_detection_rules'; +import { + createListsIndex, + waitForValueListsModalToBeLoaded, + openValueListsModal, + importValueList, + waitForListsIndex, + deleteValueLists, + KNOWN_VALUE_LIST_FILES, + openValueListItemsModal, + searchValueListItemsModal, + getValueListItemsTableRow, + checkTotalItems, + deleteListItem, + clearSearchValueListItemsModal, + sortValueListItemsTableByValue, + updateListItem, + addListItem, + selectValueListsItemsFile, + uploadValueListsItemsFile, + mockFetchListItemsError, + mockCreateListItemError, + mockUpdateListItemError, + mockDeleteListItemError, +} from '../../../../tasks/lists'; +import { + VALUE_LIST_ITEMS_MODAL_INFO, + VALUE_LIST_ITEMS_MODAL_TABLE, + VALUE_LIST_ITEMS_MODAL_TITLE, +} from '../../../../screens/lists'; +import { RULES_MANAGEMENT_URL } from '../../../../urls/rules_management'; + +describe( + 'Value list items', + { + tags: ['@ess', '@serverless', '@skipInServerless'], + env: { + ftrConfig: { + kbnServerArgs: [ + `--xpack.securitySolution.enableExperimental=${JSON.stringify([ + 'valueListItemsModalEnabled', + ])}`, + ], + }, + }, + }, + () => { + beforeEach(() => { + login(); + createListsIndex(); + visit(RULES_MANAGEMENT_URL); + waitForListsIndex(); + waitForValueListsModalToBeLoaded(); + importValueList(KNOWN_VALUE_LIST_FILES.TEXT, 'keyword'); + openValueListsModal(); + }); + + afterEach(() => { + deleteValueLists([ + KNOWN_VALUE_LIST_FILES.TEXT, + KNOWN_VALUE_LIST_FILES.IPs, + KNOWN_VALUE_LIST_FILES.CIDRs, + ]); + }); + + it('can CRUD value list items', () => { + openValueListItemsModal(KNOWN_VALUE_LIST_FILES.TEXT); + const totalItems = 12; + const perPage = 10; + + // check modal title and info + cy.get(VALUE_LIST_ITEMS_MODAL_TITLE).should('have.text', KNOWN_VALUE_LIST_FILES.TEXT); + cy.get(VALUE_LIST_ITEMS_MODAL_INFO).contains('Type: keyword'); + cy.get(VALUE_LIST_ITEMS_MODAL_INFO).contains('Updated by: system_indices_superuse'); + checkTotalItems(totalItems); + + // search working + getValueListItemsTableRow().should('have.length', perPage); + searchValueListItemsModal('keyword:not_exists'); + getValueListItemsTableRow().should('have.length', 1); + cy.get(VALUE_LIST_ITEMS_MODAL_TABLE).contains('No items found'); + + searchValueListItemsModal('keyword:*or*'); + getValueListItemsTableRow().should('have.length', 4); + + clearSearchValueListItemsModal(''); + getValueListItemsTableRow().should('have.length', perPage); + + // sort working + sortValueListItemsTableByValue(); + getValueListItemsTableRow().first().contains('a'); + + // delete item + deleteListItem('a'); + checkTotalItems(totalItems - 1); + + getValueListItemsTableRow().first().contains('are'); + + // update item + updateListItem('are', 'b'); + getValueListItemsTableRow().first().contains('b'); + + // add item + addListItem('a new item'); + getValueListItemsTableRow().first().contains('a new item'); + checkTotalItems(totalItems); + + // upload file just append all items from the file to the list + selectValueListsItemsFile(KNOWN_VALUE_LIST_FILES.TEXT); + uploadValueListsItemsFile(); + checkTotalItems(totalItems + totalItems); + }); + + it("displays an error message when list items can't be retrieved", () => { + mockFetchListItemsError(); + openValueListItemsModal(KNOWN_VALUE_LIST_FILES.TEXT); + cy.get(VALUE_LIST_ITEMS_MODAL_TABLE).contains( + 'Failed to load list items. You can change the search query or contact your administrator' + ); + }); + + it('displays a toaster error when list item actions fail', () => { + mockCreateListItemError(); + mockUpdateListItemError(); + mockDeleteListItemError(); + openValueListItemsModal(KNOWN_VALUE_LIST_FILES.TEXT); + addListItem('a new item'); + cy.get(TOASTER_BODY).contains('error to create list item'); + closeErrorToast(); + + sortValueListItemsTableByValue(); + + deleteListItem('a'); + cy.get(TOASTER_BODY).contains('error to delete list item'); + closeErrorToast(); + + updateListItem('a', 'b'); + cy.get(TOASTER_BODY).contains('error to update list item'); + closeErrorToast(); + }); + } +); diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/value_lists/value_lists.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/value_lists/value_lists.cy.ts index 59d9e433d0153..35f83e0ce18d2 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/value_lists/value_lists.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/value_lists/value_lists.cy.ts @@ -29,15 +29,19 @@ import { refreshIndex } from '../../../../tasks/api_calls/elasticsearch'; describe('value lists management modal', { tags: ['@ess', '@serverless'] }, () => { beforeEach(() => { login(); + + createListsIndex(); + visit(RULES_MANAGEMENT_URL); + waitForListsIndex(); + waitForValueListsModalToBeLoaded(); + }); + + afterEach(() => { deleteValueLists([ KNOWN_VALUE_LIST_FILES.TEXT, KNOWN_VALUE_LIST_FILES.IPs, KNOWN_VALUE_LIST_FILES.CIDRs, ]); - createListsIndex(); - visit(RULES_MANAGEMENT_URL); - waitForListsIndex(); - waitForValueListsModalToBeLoaded(); }); it('can open and close the modal', () => { diff --git a/x-pack/test/security_solution_cypress/cypress/fixtures/value_list.txt b/x-pack/test/security_solution_cypress/cypress/fixtures/value_list.txt index 2b40f036c62d2..086449e96c173 100644 --- a/x-pack/test/security_solution_cypress/cypress/fixtures/value_list.txt +++ b/x-pack/test/security_solution_cypress/cypress/fixtures/value_list.txt @@ -4,3 +4,9 @@ keywords for a list +with +more +words +to +check +pagination diff --git a/x-pack/test/security_solution_cypress/cypress/screens/common/controls.ts b/x-pack/test/security_solution_cypress/cypress/screens/common/controls.ts index 5c8a05e6cbce2..798d7e995711e 100644 --- a/x-pack/test/security_solution_cypress/cypress/screens/common/controls.ts +++ b/x-pack/test/security_solution_cypress/cypress/screens/common/controls.ts @@ -25,3 +25,5 @@ export const SELECT_EVENTS_ACTION_ADD_BULK_TO_TIMELINE = '[data-test-subj="investigate-bulk-in-timeline"]'; export const SELECT_ALL_EVENTS = '[data-test-subj="selectAllAlertsButton"]'; + +export const EUI_INLINE_SAVE_BUTTON = '[data-test-subj="euiInlineEditModeSaveButton"]'; diff --git a/x-pack/test/security_solution_cypress/cypress/screens/lists.ts b/x-pack/test/security_solution_cypress/cypress/screens/lists.ts index b125bb512548b..232949ed7b52b 100644 --- a/x-pack/test/security_solution_cypress/cypress/screens/lists.ts +++ b/x-pack/test/security_solution_cypress/cypress/screens/lists.ts @@ -16,3 +16,23 @@ export const VALUE_LIST_DELETE_BUTTON = (name: string) => export const VALUE_LIST_FILES = '[data-test-subj*="action-delete-value-list-"]'; export const VALUE_LIST_CLOSE_BUTTON = '[data-test-subj="value-lists-flyout-close-action"]'; export const VALUE_LIST_EXPORT_BUTTON = '[data-test-subj="action-export-value-list"]'; + +export const VALUE_LIST_ITEMS_MODAL_TITLE = '[data-test-subj="value-list-items-modal-title"]'; +export const VALUE_LIST_ITEMS_MODAL_INFO = '[data-test-subj="value-list-items-modal-info"]'; +export const VALUE_LIST_ITEMS_MODAL_TABLE = '[data-test-subj="value-list-items-modal-table"]'; +export const VALUE_LIST_ITEMS_MODAL_SEARCH_BAR = + '[data-test-subj="value-list-items-modal-search-bar"]'; +export const VALUE_LIST_ITEMS_MODAL_SEARCH_BAR_INPUT = + '[data-test-subj="value-list-items-modal-search-bar-input"]'; + +export const VALUE_LIST_ITEMS_ADD_BUTTON_SHOW_POPOVER = + '[data-test-subj="value-list-item-add-button-show-popover"]'; +export const VALUE_LIST_ITEMS_ADD_INPUT = '[data-test-subj="value-list-item-add-input"]'; +export const VALUE_LIST_ITEMS_ADD_BUTTON_SUBMIT = + '[data-test-subj="value-list-item-add-button-submit"]'; +export const VALUE_LIST_ITEMS_FILE_PICKER = '[data-test-subj="value-list-items-file-picker"]'; +export const VALUE_LIST_ITEMS_UPLOAD = '[data-test-subj="value-list-items-upload"]'; +export const getValueListDeleteItemButton = (name: string) => + `[data-test-subj="delete-list-item-${name}"]`; +export const getValueListUpdateItemButton = (name: string) => + `[data-test-subj="value-list-item-update-${name}"]`; diff --git a/x-pack/test/security_solution_cypress/cypress/tasks/lists.ts b/x-pack/test/security_solution_cypress/cypress/tasks/lists.ts index fcae40e1e8549..363e2a798ac55 100644 --- a/x-pack/test/security_solution_cypress/cypress/tasks/lists.ts +++ b/x-pack/test/security_solution_cypress/cypress/tasks/lists.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { LIST_ITEM_URL } from '@kbn/securitysolution-list-constants'; import { VALUE_LISTS_MODAL_ACTIVATOR, VALUE_LIST_CLOSE_BUTTON, @@ -13,7 +14,19 @@ import { VALUE_LIST_FILE_PICKER, VALUE_LIST_FILE_UPLOAD_BUTTON, VALUE_LIST_TYPE_SELECTOR, + VALUE_LIST_ITEMS_MODAL_SEARCH_BAR_INPUT, + VALUE_LIST_ITEMS_MODAL_TABLE, + VALUE_LIST_ITEMS_MODAL_INFO, + VALUE_LIST_ITEMS_ADD_BUTTON_SHOW_POPOVER, + VALUE_LIST_ITEMS_ADD_INPUT, + VALUE_LIST_ITEMS_ADD_BUTTON_SUBMIT, + VALUE_LIST_ITEMS_FILE_PICKER, + VALUE_LIST_ITEMS_UPLOAD, + getValueListUpdateItemButton, + getValueListDeleteItemButton, } from '../screens/lists'; +import { EUI_INLINE_SAVE_BUTTON } from '../screens/common/controls'; +import { rootRequest } from './api_calls/common'; export const KNOWN_VALUE_LIST_FILES = { TEXT: 'value_list.txt', @@ -22,18 +35,16 @@ export const KNOWN_VALUE_LIST_FILES = { }; export const createListsIndex = () => { - cy.request({ + rootRequest({ method: 'POST', url: '/api/lists/index', - headers: { 'kbn-xsrf': 'cypress-creds', 'x-elastic-internal-origin': 'security-solution' }, failOnStatusCode: false, }); }; export const waitForListsIndex = () => { - cy.request({ + rootRequest({ url: '/api/lists/index', - headers: { 'x-elastic-internal-origin': 'security-solution' }, retryOnStatusCodeFailure: true, }).then((response) => { if (response.status !== 200) { @@ -94,10 +105,9 @@ export const deleteValueLists = ( * Ref: https://www.elastic.co/guide/en/security/current/lists-api-delete-container.html */ const deleteValueList = (list: string): Cypress.Chainable> => { - return cy.request({ + return rootRequest({ method: 'DELETE', url: `api/lists?id=${list}`, - headers: { 'kbn-xsrf': 'delete-lists', 'x-elastic-internal-origin': 'security-solution' }, failOnStatusCode: false, }); }; @@ -123,13 +133,11 @@ const uploadListItemData = ( .filter((line) => line.trim() !== '') .join('\n'); - return cy.request({ + return rootRequest({ method: 'POST', - url: `api/lists/items/_import?type=${type}`, + url: `api/lists/items/_import?type=${type}&refresh=true`, encoding: 'binary', headers: { - 'kbn-xsrf': 'upload-value-lists', - 'x-elastic-internal-origin': 'security-solution', 'Content-Type': 'multipart/form-data; boundary=----WebKitFormBoundaryJLrRH89J8QVArZyv', }, body: `------WebKitFormBoundaryJLrRH89J8QVArZyv\nContent-Disposition: form-data; name="file"; filename="${file}"\n\n${removedEmptyLines}`, @@ -160,3 +168,95 @@ export const importValueList = ( ) => { return cy.fixture(file).then((data) => uploadListItemData(file, type, data)); }; + +export const openValueListItemsModal = (listId: string) => { + return cy.get(`[data-test-subj="show-value-list-modal-${listId}"]`).click(); +}; + +export const searchValueListItemsModal = (search: string) => { + cy.get(VALUE_LIST_ITEMS_MODAL_SEARCH_BAR_INPUT).clear(); + cy.get(VALUE_LIST_ITEMS_MODAL_SEARCH_BAR_INPUT).type(search); + return cy.get(VALUE_LIST_ITEMS_MODAL_SEARCH_BAR_INPUT).trigger('search'); +}; + +export const clearSearchValueListItemsModal = (search: string) => { + cy.get(VALUE_LIST_ITEMS_MODAL_SEARCH_BAR_INPUT).clear(); + return cy.get(VALUE_LIST_ITEMS_MODAL_SEARCH_BAR_INPUT).trigger('search'); +}; + +export const getValueListItemsTableRow = () => + cy.get(VALUE_LIST_ITEMS_MODAL_TABLE).find('tbody tr'); + +export const checkTotalItems = (totalItems: number) => { + cy.get(VALUE_LIST_ITEMS_MODAL_INFO).contains(`Total items: ${totalItems}`); +}; + +export const deleteListItem = (value: string) => { + return cy.get(getValueListDeleteItemButton(value)).click(); +}; + +export const sortValueListItemsTableByValue = () => { + cy.get(VALUE_LIST_ITEMS_MODAL_TABLE).find('thead tr th').eq(0).click(); +}; + +export const updateListItem = (oldValue: string, newValue: string) => { + const inlineEdit = getValueListUpdateItemButton(oldValue); + cy.get(inlineEdit).click(); + cy.get(inlineEdit).find('input').clear(); + cy.get(inlineEdit).find('input').type(newValue); + return cy.get(inlineEdit).find(EUI_INLINE_SAVE_BUTTON).click(); +}; + +export const addListItem = (value: string) => { + cy.get(VALUE_LIST_ITEMS_ADD_BUTTON_SHOW_POPOVER).click(); + cy.get(VALUE_LIST_ITEMS_ADD_INPUT).type(value); + return cy.get(VALUE_LIST_ITEMS_ADD_BUTTON_SUBMIT).click(); +}; + +export const selectValueListsItemsFile = (file: string) => { + return cy.get(VALUE_LIST_ITEMS_FILE_PICKER).attachFile(file).trigger('change'); +}; + +export const uploadValueListsItemsFile = () => { + return cy.get(VALUE_LIST_ITEMS_UPLOAD).click(); +}; + +export const mockFetchListItemsError = () => { + cy.intercept('GET', `${LIST_ITEM_URL}/_find*`, { + statusCode: 400, + body: { + message: 'search_phase_execution_exception: all shards failed', + status_code: 400, + }, + }); +}; + +export const mockCreateListItemError = () => { + cy.intercept('POST', `${LIST_ITEM_URL}`, { + statusCode: 400, + body: { + message: 'error to create list item', + status_code: 400, + }, + }); +}; + +export const mockUpdateListItemError = () => { + cy.intercept('PATCH', `${LIST_ITEM_URL}`, { + statusCode: 400, + body: { + message: 'error to update list item', + status_code: 400, + }, + }); +}; + +export const mockDeleteListItemError = () => { + cy.intercept('DELETE', `${LIST_ITEM_URL}*`, { + statusCode: 400, + body: { + message: 'error to delete list item', + status_code: 400, + }, + }); +};