mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[Security Solution] host isolation exceptions delete item UI (#113541)
Co-authored-by: David Sánchez <davidsansol92@gmail.com> Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
a4f209e6f0
commit
9902cbdc07
11 changed files with 512 additions and 9 deletions
|
@ -64,3 +64,13 @@ export async function getHostIsolationExceptionItems({
|
|||
});
|
||||
return entries;
|
||||
}
|
||||
|
||||
export async function deleteHostIsolationExceptionItems(http: HttpStart, id: string) {
|
||||
await ensureHostIsolationExceptionsListExists(http);
|
||||
return http.delete<ExceptionListItemSchema>(EXCEPTION_LIST_ITEM_URL, {
|
||||
query: {
|
||||
id,
|
||||
namespace_type: 'agnostic',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -16,4 +16,8 @@ export const initialHostIsolationExceptionsPageState = (): HostIsolationExceptio
|
|||
page_size: MANAGEMENT_DEFAULT_PAGE_SIZE,
|
||||
filter: '',
|
||||
},
|
||||
deletion: {
|
||||
item: undefined,
|
||||
status: createUninitialisedResourceState(),
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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<string[]> = [`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<AppLocation>) {
|
|||
}) !== null
|
||||
);
|
||||
}
|
||||
|
||||
async function deleteHostIsolationExceptionsItem(
|
||||
store: ImmutableMiddlewareAPI<HostIsolationExceptionsPageState, AppAction>,
|
||||
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<ExceptionListItemSchema>(error.body ?? error),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<HostIsolationExceptionsPageState, AppAction>;
|
||||
type CaseReducer<T extends AppAction> = (
|
||||
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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<StoreState['location']> = (
|
||||
state
|
||||
) => state.location;
|
||||
|
||||
export const getDeletionState: HostIsolationExceptionsSelector<StoreState['deletion']> =
|
||||
createSelector(getCurrentListPageState, (listState) => listState.deletion);
|
||||
|
||||
export const showDeleteModal: HostIsolationExceptionsSelector<boolean> = createSelector(
|
||||
getDeletionState,
|
||||
({ item }) => {
|
||||
return Boolean(item);
|
||||
}
|
||||
);
|
||||
|
||||
export const getItemToDelete: HostIsolationExceptionsSelector<StoreState['deletion']['item']> =
|
||||
createSelector(getDeletionState, ({ item }) => item);
|
||||
|
||||
export const isDeletionInProgress: HostIsolationExceptionsSelector<boolean> = createSelector(
|
||||
getDeletionState,
|
||||
({ status }) => {
|
||||
return isLoadingResourceState(status);
|
||||
}
|
||||
);
|
||||
|
||||
export const wasDeletionSuccessful: HostIsolationExceptionsSelector<boolean> = createSelector(
|
||||
getDeletionState,
|
||||
({ status }) => {
|
||||
return isLoadedResourceState(status);
|
||||
}
|
||||
);
|
||||
|
||||
export const getDeleteError: HostIsolationExceptionsSelector<ServerApiError | undefined> =
|
||||
createSelector(getDeletionState, ({ status }) => {
|
||||
if (isFailedResourceState(status)) {
|
||||
return status.error;
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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<FoundExceptionListItemSchema>;
|
||||
location: HostIsolationExceptionsPageLocation;
|
||||
deletion: {
|
||||
item?: ExceptionListItemSchema;
|
||||
status: AsyncResourceState<ExceptionListItemSchema>;
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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<AppContextTestRender['render']>;
|
||||
let renderResult: ReturnType<typeof render>;
|
||||
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(<HostIsolationExceptionDeleteModal />));
|
||||
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'
|
||||
);
|
||||
});
|
||||
});
|
|
@ -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<Dispatch<HostIsolationExceptionsPageAction>>();
|
||||
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 (
|
||||
<EuiModal onClose={onCancel}>
|
||||
<EuiModalHeader data-test-subj="hostIsolationExceptionsDeleteModalHeader">
|
||||
<EuiModalHeaderTitle>
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.hostIsolationExceptions.deletionDialog.title"
|
||||
defaultMessage="Delete Host Isolation Exception"
|
||||
/>
|
||||
</EuiModalHeaderTitle>
|
||||
</EuiModalHeader>
|
||||
|
||||
<EuiModalBody data-test-subj="hostIsolationExceptionsFilterDeleteModalBody">
|
||||
<EuiText>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.hostIsolationExceptions.deletionDialog.subtitle"
|
||||
defaultMessage='You are deleting exception "{name}".'
|
||||
values={{ name: <b className="eui-textBreakWord">{exception?.name}</b> }}
|
||||
/>
|
||||
</p>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.hostIsolationExceptions.deletionDialog.confirmation"
|
||||
defaultMessage="This action cannot be undone. Are you sure you wish to continue?"
|
||||
/>
|
||||
</p>
|
||||
</EuiText>
|
||||
</EuiModalBody>
|
||||
|
||||
<EuiModalFooter>
|
||||
<EuiButtonEmpty
|
||||
onClick={onCancel}
|
||||
isDisabled={isDeleting}
|
||||
data-test-subj="hostIsolationExceptionsDeleteModalCancelButton"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.hostIsolationExceptions.deletionDialog.cancel"
|
||||
defaultMessage="Cancel"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
|
||||
<EuiButton
|
||||
fill
|
||||
color="danger"
|
||||
onClick={onConfirm}
|
||||
isLoading={isDeleting}
|
||||
data-test-subj="hostIsolationExceptionsDeleteModalConfirmButton"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.hostIsolationExceptions.deletionDialog.confirmButton"
|
||||
defaultMessage="Remove exception"
|
||||
/>
|
||||
</EuiButton>
|
||||
</EuiModalFooter>
|
||||
</EuiModal>
|
||||
);
|
||||
});
|
||||
|
||||
HostIsolationExceptionDeleteModal.displayName = 'HostIsolationExceptionDeleteModal';
|
|
@ -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<ExceptionListItemSchema>,
|
||||
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<Dispatch<HostIsolationExceptionsPageAction>>();
|
||||
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 = () => {
|
|||
)}
|
||||
/>
|
||||
<EuiSpacer size="l" />
|
||||
{itemToDelete ? <HostIsolationExceptionDeleteModal /> : null}
|
||||
<PaginatedContent<ExceptionListItemSchema, typeof ArtifactEntryCard>
|
||||
items={listItems}
|
||||
ItemComponent={ArtifactEntryCard}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue