= 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,
+ },
+ });
+};