Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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