From ad2b62a48e460f7a763ecd6c8d7899f9eda5d5a2 Mon Sep 17 00:00:00 2001 From: Esteban Beltran Date: Tue, 5 Oct 2021 15:50:44 +0200 Subject: [PATCH] [Security Solution] host isolation exceptions delete item UI (#113541) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: David Sánchez Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../host_isolation_exceptions/service.ts | 10 ++ .../host_isolation_exceptions/store/action.ts | 18 ++- .../store/builders.ts | 4 + .../store/middleware.test.ts | 74 ++++++++- .../store/middleware.ts | 48 +++++- .../store/reducer.ts | 18 +++ .../store/selector.ts | 35 +++++ .../pages/host_isolation_exceptions/types.ts | 9 +- .../view/components/delete_modal.test.tsx | 135 +++++++++++++++++ .../view/components/delete_modal.tsx | 141 ++++++++++++++++++ .../view/host_isolation_exceptions_list.tsx | 29 +++- 11 files changed, 512 insertions(+), 9 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/delete_modal.test.tsx create mode 100644 x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/delete_modal.tsx diff --git a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/service.ts b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/service.ts index 85545303c7df0..79ca595fbb61b 100644 --- a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/service.ts +++ b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/service.ts @@ -64,3 +64,13 @@ export async function getHostIsolationExceptionItems({ }); return entries; } + +export async function deleteHostIsolationExceptionItems(http: HttpStart, id: string) { + await ensureHostIsolationExceptionsListExists(http); + return http.delete(EXCEPTION_LIST_ITEM_URL, { + query: { + id, + namespace_type: 'agnostic', + }, + }); +} diff --git a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/store/action.ts b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/store/action.ts index 793c44ce79db2..0a9f776655371 100644 --- a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/store/action.ts +++ b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/store/action.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; import { Action } from 'redux'; import { HostIsolationExceptionsPageState } from '../types'; @@ -13,4 +14,19 @@ export type HostIsolationExceptionsPageDataChanged = payload: HostIsolationExceptionsPageState['entries']; }; -export type HostIsolationExceptionsPageAction = HostIsolationExceptionsPageDataChanged; +export type HostIsolationExceptionsDeleteItem = Action<'hostIsolationExceptionsMarkToDelete'> & { + payload?: ExceptionListItemSchema; +}; + +export type HostIsolationExceptionsSubmitDelete = Action<'hostIsolationExceptionsSubmitDelete'>; + +export type HostIsolationExceptionsDeleteStatusChanged = + Action<'hostIsolationExceptionsDeleteStatusChanged'> & { + payload: HostIsolationExceptionsPageState['deletion']['status']; + }; + +export type HostIsolationExceptionsPageAction = + | HostIsolationExceptionsPageDataChanged + | HostIsolationExceptionsDeleteItem + | HostIsolationExceptionsSubmitDelete + | HostIsolationExceptionsDeleteStatusChanged; diff --git a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/store/builders.ts b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/store/builders.ts index f5ea3c27bde7f..68a50f9c813f4 100644 --- a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/store/builders.ts +++ b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/store/builders.ts @@ -16,4 +16,8 @@ export const initialHostIsolationExceptionsPageState = (): HostIsolationExceptio page_size: MANAGEMENT_DEFAULT_PAGE_SIZE, filter: '', }, + deletion: { + item: undefined, + status: createUninitialisedResourceState(), + }, }); diff --git a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/store/middleware.test.ts b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/store/middleware.test.ts index cde9d89443903..984794e074ebb 100644 --- a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/store/middleware.test.ts +++ b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/store/middleware.test.ts @@ -14,8 +14,12 @@ import { createSpyMiddleware, MiddlewareActionSpyHelper, } from '../../../../common/store/test_utils'; -import { isFailedResourceState, isLoadedResourceState } from '../../../state'; -import { getHostIsolationExceptionItems } from '../service'; +import { + isFailedResourceState, + isLoadedResourceState, + isLoadingResourceState, +} from '../../../state'; +import { getHostIsolationExceptionItems, deleteHostIsolationExceptionItems } from '../service'; import { HostIsolationExceptionsPageState } from '../types'; import { initialHostIsolationExceptionsPageState } from './builders'; import { createHostIsolationExceptionsPageMiddleware } from './middleware'; @@ -24,6 +28,7 @@ import { getListFetchError } from './selector'; jest.mock('../service'); const getHostIsolationExceptionItemsMock = getHostIsolationExceptionItems as jest.Mock; +const deleteHostIsolationExceptionItemsMock = deleteHostIsolationExceptionItems as jest.Mock; const fakeCoreStart = coreMock.createStart({ basePath: '/mock' }); @@ -139,4 +144,69 @@ describe('Host isolation exceptions middleware', () => { }); }); }); + + describe('When deleting an item from host isolation exceptions', () => { + beforeEach(() => { + deleteHostIsolationExceptionItemsMock.mockClear(); + deleteHostIsolationExceptionItemsMock.mockReturnValue(undefined); + getHostIsolationExceptionItemsMock.mockClear(); + getHostIsolationExceptionItemsMock.mockImplementation(getFoundExceptionListItemSchemaMock); + store.dispatch({ + type: 'hostIsolationExceptionsMarkToDelete', + payload: { + id: '1', + }, + }); + }); + + it('should call the delete exception API when a delete is submitted and advertise a loading status', async () => { + const waiter = Promise.all([ + // delete loading action + spyMiddleware.waitForAction('hostIsolationExceptionsDeleteStatusChanged', { + validate({ payload }) { + return isLoadingResourceState(payload); + }, + }), + // delete finished action + spyMiddleware.waitForAction('hostIsolationExceptionsDeleteStatusChanged', { + validate({ payload }) { + return isLoadedResourceState(payload); + }, + }), + ]); + store.dispatch({ + type: 'hostIsolationExceptionsSubmitDelete', + }); + await waiter; + expect(deleteHostIsolationExceptionItemsMock).toHaveBeenLastCalledWith( + fakeCoreStart.http, + '1' + ); + }); + + it('should dispatch a failure if the API returns an error', async () => { + deleteHostIsolationExceptionItemsMock.mockRejectedValue({ + body: { message: 'error message', statusCode: 500, error: 'Internal Server Error' }, + }); + store.dispatch({ + type: 'hostIsolationExceptionsSubmitDelete', + }); + await spyMiddleware.waitForAction('hostIsolationExceptionsDeleteStatusChanged', { + validate({ payload }) { + return isFailedResourceState(payload); + }, + }); + }); + + it('should reload the host isolation exception lists after delete', async () => { + store.dispatch({ + type: 'hostIsolationExceptionsSubmitDelete', + }); + await spyMiddleware.waitForAction('hostIsolationExceptionsPageDataChanged', { + validate({ payload }) { + return isLoadingResourceState(payload); + }, + }); + }); + }); }); diff --git a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/store/middleware.ts b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/store/middleware.ts index 1df0ef229d2ef..4946cac488700 100644 --- a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/store/middleware.ts +++ b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/store/middleware.ts @@ -5,8 +5,11 @@ * 2.0. */ -import { FoundExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; -import { CoreStart, HttpStart } from 'kibana/public'; +import { + ExceptionListItemSchema, + FoundExceptionListItemSchema, +} from '@kbn/securitysolution-io-ts-list-types'; +import { CoreStart, HttpSetup, HttpStart } from 'kibana/public'; import { matchPath } from 'react-router-dom'; import { AppLocation, Immutable } from '../../../../../common/endpoint/types'; import { ImmutableMiddleware, ImmutableMiddlewareAPI } from '../../../../common/store'; @@ -17,9 +20,9 @@ import { createFailedResourceState, createLoadedResourceState, } from '../../../state/async_resource_builders'; -import { getHostIsolationExceptionItems } from '../service'; +import { deleteHostIsolationExceptionItems, getHostIsolationExceptionItems } from '../service'; import { HostIsolationExceptionsPageState } from '../types'; -import { getCurrentListPageDataState, getCurrentLocation } from './selector'; +import { getCurrentListPageDataState, getCurrentLocation, getItemToDelete } from './selector'; export const SEARCHABLE_FIELDS: Readonly = [`name`, `description`, `entries.value`]; @@ -36,6 +39,9 @@ export const createHostIsolationExceptionsPageMiddleware = ( if (action.type === 'userChangedUrl' && isHostIsolationExceptionsPage(action.payload)) { loadHostIsolationExceptionsList(store, coreStart.http); } + if (action.type === 'hostIsolationExceptionsSubmitDelete') { + deleteHostIsolationExceptionsItem(store, coreStart.http); + } }; }; @@ -88,3 +94,37 @@ function isHostIsolationExceptionsPage(location: Immutable) { }) !== null ); } + +async function deleteHostIsolationExceptionsItem( + store: ImmutableMiddlewareAPI, + http: HttpSetup +) { + const { dispatch } = store; + const itemToDelete = getItemToDelete(store.getState()); + if (itemToDelete === undefined) { + return; + } + try { + dispatch({ + type: 'hostIsolationExceptionsDeleteStatusChanged', + payload: { + type: 'LoadingResourceState', + // @ts-expect-error-next-line will be fixed with when AsyncResourceState is refactored (#830) + previousState: store.getState().deletion.status, + }, + }); + + await deleteHostIsolationExceptionItems(http, itemToDelete.id); + + dispatch({ + type: 'hostIsolationExceptionsDeleteStatusChanged', + payload: createLoadedResourceState(itemToDelete), + }); + loadHostIsolationExceptionsList(store, http); + } catch (error) { + dispatch({ + type: 'hostIsolationExceptionsDeleteStatusChanged', + payload: createFailedResourceState(error.body ?? error), + }); + } +} diff --git a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/store/reducer.ts b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/store/reducer.ts index 1bce76c1bfd06..09182661a80b3 100644 --- a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/store/reducer.ts +++ b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/store/reducer.ts @@ -16,6 +16,7 @@ import { HostIsolationExceptionsPageState } from '../types'; import { initialHostIsolationExceptionsPageState } from './builders'; import { MANAGEMENT_ROUTING_HOST_ISOLATION_EXCEPTIONS_PATH } from '../../../common/constants'; import { UserChangedUrl } from '../../../../common/store/routing/action'; +import { createUninitialisedResourceState } from '../../../state'; type StateReducer = ImmutableReducer; type CaseReducer = ( @@ -45,6 +46,23 @@ export const hostIsolationExceptionsPageReducer: StateReducer = ( } case 'userChangedUrl': return userChangedUrl(state, action); + case 'hostIsolationExceptionsMarkToDelete': { + return { + ...state, + deletion: { + item: action.payload, + status: createUninitialisedResourceState(), + }, + }; + } + case 'hostIsolationExceptionsDeleteStatusChanged': + return { + ...state, + deletion: { + ...state.deletion, + status: action.payload, + }, + }; } return state; }; diff --git a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/store/selector.ts b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/store/selector.ts index 0ddfc0953263c..4462864e90702 100644 --- a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/store/selector.ts +++ b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/store/selector.ts @@ -20,6 +20,7 @@ import { import { getLastLoadedResourceState, isFailedResourceState, + isLoadedResourceState, isLoadingResourceState, } from '../../../state/async_resource_state'; import { HostIsolationExceptionsPageState } from '../types'; @@ -73,3 +74,37 @@ export const getListFetchError: HostIsolationExceptionsSelector< export const getCurrentLocation: HostIsolationExceptionsSelector = ( state ) => state.location; + +export const getDeletionState: HostIsolationExceptionsSelector = + createSelector(getCurrentListPageState, (listState) => listState.deletion); + +export const showDeleteModal: HostIsolationExceptionsSelector = createSelector( + getDeletionState, + ({ item }) => { + return Boolean(item); + } +); + +export const getItemToDelete: HostIsolationExceptionsSelector = + createSelector(getDeletionState, ({ item }) => item); + +export const isDeletionInProgress: HostIsolationExceptionsSelector = createSelector( + getDeletionState, + ({ status }) => { + return isLoadingResourceState(status); + } +); + +export const wasDeletionSuccessful: HostIsolationExceptionsSelector = createSelector( + getDeletionState, + ({ status }) => { + return isLoadedResourceState(status); + } +); + +export const getDeleteError: HostIsolationExceptionsSelector = + createSelector(getDeletionState, ({ status }) => { + if (isFailedResourceState(status)) { + return status.error; + } + }); diff --git a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/types.ts b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/types.ts index 44f3d2a9df764..443a86fefab83 100644 --- a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/types.ts +++ b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/types.ts @@ -5,7 +5,10 @@ * 2.0. */ -import type { FoundExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; +import type { + ExceptionListItemSchema, + FoundExceptionListItemSchema, +} from '@kbn/securitysolution-io-ts-list-types'; import { AsyncResourceState } from '../../state/async_resource_state'; export interface HostIsolationExceptionsPageLocation { @@ -20,4 +23,8 @@ export interface HostIsolationExceptionsPageLocation { export interface HostIsolationExceptionsPageState { entries: AsyncResourceState; location: HostIsolationExceptionsPageLocation; + deletion: { + item?: ExceptionListItemSchema; + status: AsyncResourceState; + }; } diff --git a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/delete_modal.test.tsx b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/delete_modal.test.tsx new file mode 100644 index 0000000000000..0b09b4bfa14c4 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/delete_modal.test.tsx @@ -0,0 +1,135 @@ +/* + * 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 { act } from '@testing-library/react'; +import { + AppContextTestRender, + createAppRootMockRenderer, +} from '../../../../../common/mock/endpoint'; +import { HostIsolationExceptionDeleteModal } from './delete_modal'; +import { isFailedResourceState, isLoadedResourceState } from '../../../../state'; +import { getHostIsolationExceptionItems, deleteHostIsolationExceptionItems } from '../../service'; +import { getExceptionListItemSchemaMock } from '../../../../../../../lists/common/schemas/response/exception_list_item_schema.mock'; +import { fireEvent } from '@testing-library/dom'; + +jest.mock('../../service'); +const getHostIsolationExceptionItemsMock = getHostIsolationExceptionItems as jest.Mock; +const deleteHostIsolationExceptionItemsMock = deleteHostIsolationExceptionItems as jest.Mock; + +describe('When on the host isolation exceptions delete modal', () => { + let render: () => ReturnType; + let renderResult: ReturnType; + let waitForAction: AppContextTestRender['middlewareSpy']['waitForAction']; + let coreStart: AppContextTestRender['coreStart']; + + beforeEach(() => { + const itemToDelete = getExceptionListItemSchemaMock(); + getHostIsolationExceptionItemsMock.mockReset(); + deleteHostIsolationExceptionItemsMock.mockReset(); + const mockedContext = createAppRootMockRenderer(); + mockedContext.store.dispatch({ + type: 'hostIsolationExceptionsMarkToDelete', + payload: itemToDelete, + }); + render = () => (renderResult = mockedContext.render()); + waitForAction = mockedContext.middlewareSpy.waitForAction; + ({ coreStart } = mockedContext); + }); + + it('should render the delete modal with the cancel and submit buttons', () => { + render(); + expect(renderResult.getByTestId('hostIsolationExceptionsDeleteModalCancelButton')).toBeTruthy(); + expect( + renderResult.getByTestId('hostIsolationExceptionsDeleteModalConfirmButton') + ).toBeTruthy(); + }); + + it('should disable the buttons when confirm is pressed and show loading', async () => { + render(); + + const submitButton = renderResult.baseElement.querySelector( + '[data-test-subj="hostIsolationExceptionsDeleteModalConfirmButton"]' + )! as HTMLButtonElement; + + const cancelButton = renderResult.baseElement.querySelector( + '[data-test-subj="hostIsolationExceptionsDeleteModalConfirmButton"]' + )! as HTMLButtonElement; + + act(() => { + fireEvent.click(submitButton); + }); + + expect(submitButton.disabled).toBe(true); + expect(cancelButton.disabled).toBe(true); + expect(submitButton.querySelector('.euiLoadingSpinner')).not.toBeNull(); + }); + + it('should clear the item marked to delete when cancel is pressed', async () => { + render(); + const cancelButton = renderResult.baseElement.querySelector( + '[data-test-subj="hostIsolationExceptionsDeleteModalConfirmButton"]' + )! as HTMLButtonElement; + + const waiter = waitForAction('hostIsolationExceptionsMarkToDelete', { + validate: ({ payload }) => { + return payload === undefined; + }, + }); + + act(() => { + fireEvent.click(cancelButton); + }); + await waiter; + }); + + it('should show success toast after the delete is completed', async () => { + render(); + const updateCompleted = waitForAction('hostIsolationExceptionsDeleteStatusChanged', { + validate(action) { + return isLoadedResourceState(action.payload); + }, + }); + + const submitButton = renderResult.baseElement.querySelector( + '[data-test-subj="hostIsolationExceptionsDeleteModalConfirmButton"]' + )! as HTMLButtonElement; + + await act(async () => { + fireEvent.click(submitButton); + await updateCompleted; + }); + + expect(coreStart.notifications.toasts.addSuccess).toHaveBeenCalledWith( + '"some name" has been removed from the Host Isolation Exceptions list.' + ); + }); + + it('should show error toast if error is encountered', async () => { + deleteHostIsolationExceptionItemsMock.mockRejectedValue( + new Error("That's not true. That's impossible") + ); + render(); + const updateFailure = waitForAction('hostIsolationExceptionsDeleteStatusChanged', { + validate(action) { + return isFailedResourceState(action.payload); + }, + }); + + const submitButton = renderResult.baseElement.querySelector( + '[data-test-subj="hostIsolationExceptionsDeleteModalConfirmButton"]' + )! as HTMLButtonElement; + + await act(async () => { + fireEvent.click(submitButton); + await updateFailure; + }); + + expect(coreStart.notifications.toasts.addDanger).toHaveBeenCalledWith( + 'Unable to remove "some name" from the Host Isolation Exceptions list. Reason: That\'s not true. That\'s impossible' + ); + }); +}); diff --git a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/delete_modal.tsx b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/delete_modal.tsx new file mode 100644 index 0000000000000..61b0bb7f930c3 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/delete_modal.tsx @@ -0,0 +1,141 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { memo, useCallback, useEffect } from 'react'; +import { + EuiButton, + EuiButtonEmpty, + EuiModal, + EuiModalBody, + EuiModalFooter, + EuiModalHeader, + EuiModalHeaderTitle, + EuiText, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { useDispatch } from 'react-redux'; +import { Dispatch } from 'redux'; +import { i18n } from '@kbn/i18n'; +import { useToasts } from '../../../../../common/lib/kibana'; +import { useHostIsolationExceptionsSelector } from '../hooks'; +import { + getDeleteError, + getItemToDelete, + isDeletionInProgress, + wasDeletionSuccessful, +} from '../../store/selector'; +import { HostIsolationExceptionsPageAction } from '../../store/action'; + +export const HostIsolationExceptionDeleteModal = memo<{}>(() => { + const dispatch = useDispatch>(); + const toasts = useToasts(); + + const isDeleting = useHostIsolationExceptionsSelector(isDeletionInProgress); + const exception = useHostIsolationExceptionsSelector(getItemToDelete); + const wasDeleted = useHostIsolationExceptionsSelector(wasDeletionSuccessful); + const deleteError = useHostIsolationExceptionsSelector(getDeleteError); + + const onCancel = useCallback(() => { + dispatch({ type: 'hostIsolationExceptionsMarkToDelete', payload: undefined }); + }, [dispatch]); + + const onConfirm = useCallback(() => { + dispatch({ type: 'hostIsolationExceptionsSubmitDelete' }); + }, [dispatch]); + + // Show toast for success + useEffect(() => { + if (wasDeleted) { + toasts.addSuccess( + i18n.translate( + 'xpack.securitySolution.hostIsolationExceptions.deletionDialog.deleteSuccess', + { + defaultMessage: '"{name}" has been removed from the Host Isolation Exceptions list.', + values: { name: exception?.name }, + } + ) + ); + + dispatch({ type: 'hostIsolationExceptionsMarkToDelete', payload: undefined }); + } + }, [dispatch, exception?.name, toasts, wasDeleted]); + + // show toast for failures + useEffect(() => { + if (deleteError) { + toasts.addDanger( + i18n.translate( + 'xpack.securitySolution.hostIsolationExceptions.deletionDialog.deleteFailure', + { + defaultMessage: + 'Unable to remove "{name}" from the Host Isolation Exceptions list. Reason: {message}', + values: { name: exception?.name, message: deleteError.message }, + } + ) + ); + } + }, [deleteError, exception?.name, toasts]); + + return ( + + + + + + + + + +

+ {exception?.name} }} + /> +

+

+ +

+
+
+ + + + + + + + + + +
+ ); +}); + +HostIsolationExceptionDeleteModal.displayName = 'HostIsolationExceptionDeleteModal'; diff --git a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/host_isolation_exceptions_list.tsx b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/host_isolation_exceptions_list.tsx index f6198e4e1aa54..53fb74d5bd8f7 100644 --- a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/host_isolation_exceptions_list.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/host_isolation_exceptions_list.tsx @@ -7,12 +7,14 @@ import { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; import { i18n } from '@kbn/i18n'; -import React, { useCallback } from 'react'; +import React, { Dispatch, useCallback } from 'react'; import { EuiSpacer } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; +import { useDispatch } from 'react-redux'; import { ExceptionItem } from '../../../../common/components/exceptions/viewer/exception_item'; import { getCurrentLocation, + getItemToDelete, getListFetchError, getListIsLoading, getListItems, @@ -28,18 +30,29 @@ import { AdministrationListPage } from '../../../components/administration_list_ import { SearchExceptions } from '../../../components/search_exceptions'; import { ArtifactEntryCard, ArtifactEntryCardProps } from '../../../components/artifact_entry_card'; import { HostIsolationExceptionsEmptyState } from './components/empty'; +import { HostIsolationExceptionsPageAction } from '../store/action'; +import { HostIsolationExceptionDeleteModal } from './components/delete_modal'; type HostIsolationExceptionPaginatedContent = PaginatedContentProps< Immutable, typeof ExceptionItem >; +const DELETE_HOST_ISOLATION_EXCEPTION_LABEL = i18n.translate( + 'xpack.securitySolution.hostIsolationExceptions.list.actions.delete', + { + defaultMessage: 'Delete Exception', + } +); + export const HostIsolationExceptionsList = () => { const listItems = useHostIsolationExceptionsSelector(getListItems); const pagination = useHostIsolationExceptionsSelector(getListPagination); const isLoading = useHostIsolationExceptionsSelector(getListIsLoading); const fetchError = useHostIsolationExceptionsSelector(getListFetchError); const location = useHostIsolationExceptionsSelector(getCurrentLocation); + const dispatch = useDispatch>(); + const itemToDelete = useHostIsolationExceptionsSelector(getItemToDelete); const navigateCallback = useHostIsolationExceptionsNavigateCallback(); @@ -53,6 +66,19 @@ export const HostIsolationExceptionsList = () => { const handleItemComponentProps = (element: ExceptionListItemSchema): ArtifactEntryCardProps => ({ item: element, 'data-test-subj': `hostIsolationExceptionsCard`, + actions: [ + { + icon: 'trash', + onClick: () => { + dispatch({ + type: 'hostIsolationExceptionsMarkToDelete', + payload: element, + }); + }, + 'data-test-subj': 'deleteHostIsolationException', + children: DELETE_HOST_ISOLATION_EXCEPTION_LABEL, + }, + ], }); const handlePaginatedContentChange: HostIsolationExceptionPaginatedContent['onChange'] = @@ -87,6 +113,7 @@ export const HostIsolationExceptionsList = () => { )} /> + {itemToDelete ? : null} items={listItems} ItemComponent={ArtifactEntryCard}