Skip to content

Commit

Permalink
[Security Solution] host isolation exceptions delete item UI (#113541) (
Browse files Browse the repository at this point in the history
#114435)

Co-authored-by: David Sánchez <davidsansol92@gmail.com>
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>

Co-authored-by: David Sánchez <davidsansol92@gmail.com>
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
  • Loading branch information
3 people authored Oct 11, 2021
1 parent 7f83f29 commit b528572
Show file tree
Hide file tree
Showing 11 changed files with 512 additions and 9 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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',
},
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
* 2.0.
*/

import { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types';
import { Action } from 'redux';
import { HostIsolationExceptionsPageState } from '../types';

Expand All @@ -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;
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,8 @@ export const initialHostIsolationExceptionsPageState = (): HostIsolationExceptio
page_size: MANAGEMENT_DEFAULT_PAGE_SIZE,
filter: '',
},
deletion: {
item: undefined,
status: createUninitialisedResourceState(),
},
});
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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' });

Expand Down Expand Up @@ -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);
},
});
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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`];

Expand All @@ -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);
}
};
};

Expand Down Expand Up @@ -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),
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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> = (
Expand Down Expand Up @@ -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;
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
import {
getLastLoadedResourceState,
isFailedResourceState,
isLoadedResourceState,
isLoadingResourceState,
} from '../../../state/async_resource_state';
import { HostIsolationExceptionsPageState } from '../types';
Expand Down Expand Up @@ -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;
}
});
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -20,4 +23,8 @@ export interface HostIsolationExceptionsPageLocation {
export interface HostIsolationExceptionsPageState {
entries: AsyncResourceState<FoundExceptionListItemSchema>;
location: HostIsolationExceptionsPageLocation;
deletion: {
item?: ExceptionListItemSchema;
status: AsyncResourceState<ExceptionListItemSchema>;
};
}
Loading

0 comments on commit b528572

Please sign in to comment.