mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[Security Solution] Refactor/host isolation exceptions list to use react-query (#118189)
This commit is contained in:
parent
fe2498c2dc
commit
937fd9863d
10 changed files with 115 additions and 331 deletions
|
@ -10,7 +10,7 @@ import { createMemoryHistory, MemoryHistory } from 'history';
|
|||
import { render as reactRender, RenderOptions, RenderResult } from '@testing-library/react';
|
||||
import { Action, Reducer, Store } from 'redux';
|
||||
import { AppDeepLink } from 'kibana/public';
|
||||
import { QueryClient, QueryClientProvider } from 'react-query';
|
||||
import { QueryClient, QueryClientProvider, setLogger } from 'react-query';
|
||||
import { coreMock } from '../../../../../../../src/core/public/mocks';
|
||||
import { StartPlugins, StartServices } from '../../../types';
|
||||
import { depsStartMock } from './dependencies_start_mock';
|
||||
|
@ -30,6 +30,15 @@ import { fleetGetPackageListHttpMock } from '../../../management/pages/mocks';
|
|||
|
||||
type UiRender = (ui: React.ReactElement, options?: RenderOptions) => RenderResult;
|
||||
|
||||
// hide react-query output in console
|
||||
setLogger({
|
||||
error: () => {},
|
||||
// eslint-disable-next-line no-console
|
||||
log: console.log,
|
||||
// eslint-disable-next-line no-console
|
||||
warn: console.warn,
|
||||
});
|
||||
|
||||
/**
|
||||
* Mocked app root context renderer
|
||||
*/
|
||||
|
@ -86,14 +95,6 @@ const experimentalFeaturesReducer: Reducer<State['app'], UpdateExperimentalFeatu
|
|||
}
|
||||
return state;
|
||||
};
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
// turns retries off
|
||||
retry: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Creates a mocked endpoint app context custom renderer that can be used to render
|
||||
|
@ -121,6 +122,17 @@ export const createAppRootMockRenderer = (): AppContextTestRender => {
|
|||
middlewareSpy.actionSpyMiddleware,
|
||||
]);
|
||||
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
// turns retries off
|
||||
retry: false,
|
||||
// prevent jest did not exit errors
|
||||
cacheTime: Infinity,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const AppWrapper: React.FC<{ children: React.ReactElement }> = ({ children }) => (
|
||||
<KibanaContextProvider services={startServices}>
|
||||
<AppRootProvider store={store} history={history} coreStart={coreStart} depsStart={depsStart}>
|
||||
|
|
|
@ -9,11 +9,6 @@ import { UpdateExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-
|
|||
import { Action } from 'redux';
|
||||
import { HostIsolationExceptionsPageState } from '../types';
|
||||
|
||||
export type HostIsolationExceptionsPageDataChanged =
|
||||
Action<'hostIsolationExceptionsPageDataChanged'> & {
|
||||
payload: HostIsolationExceptionsPageState['entries'];
|
||||
};
|
||||
|
||||
export type HostIsolationExceptionsFormStateChanged =
|
||||
Action<'hostIsolationExceptionsFormStateChanged'> & {
|
||||
payload: HostIsolationExceptionsPageState['form']['status'];
|
||||
|
@ -28,8 +23,6 @@ export type HostIsolationExceptionsCreateEntry = Action<'hostIsolationExceptions
|
|||
payload: HostIsolationExceptionsPageState['form']['entry'];
|
||||
};
|
||||
|
||||
export type HostIsolationExceptionsRefreshList = Action<'hostIsolationExceptionsRefreshList'>;
|
||||
|
||||
export type HostIsolationExceptionsMarkToEdit = Action<'hostIsolationExceptionsMarkToEdit'> & {
|
||||
payload: {
|
||||
id: string;
|
||||
|
@ -41,10 +34,8 @@ export type HostIsolationExceptionsSubmitEdit = Action<'hostIsolationExceptionsS
|
|||
};
|
||||
|
||||
export type HostIsolationExceptionsPageAction =
|
||||
| HostIsolationExceptionsPageDataChanged
|
||||
| HostIsolationExceptionsCreateEntry
|
||||
| HostIsolationExceptionsFormStateChanged
|
||||
| HostIsolationExceptionsRefreshList
|
||||
| HostIsolationExceptionsFormEntryChanged
|
||||
| HostIsolationExceptionsMarkToEdit
|
||||
| HostIsolationExceptionsSubmitEdit;
|
||||
|
|
|
@ -11,8 +11,6 @@ import {
|
|||
} from '@kbn/securitysolution-io-ts-list-types';
|
||||
import { applyMiddleware, createStore, Store } from 'redux';
|
||||
import { coreMock } from '../../../../../../../../src/core/public/mocks';
|
||||
import { getFoundExceptionListItemSchemaMock } from '../../../../../../lists/common/schemas/response/found_exception_list_item_schema.mock';
|
||||
import { HOST_ISOLATION_EXCEPTIONS_PATH } from '../../../../../common/constants';
|
||||
import { AppAction } from '../../../../common/store/actions';
|
||||
import {
|
||||
createSpyMiddleware,
|
||||
|
@ -25,7 +23,6 @@ import {
|
|||
} from '../../../state';
|
||||
import {
|
||||
createHostIsolationExceptionItem,
|
||||
getHostIsolationExceptionItems,
|
||||
getOneHostIsolationExceptionItem,
|
||||
updateOneHostIsolationExceptionItem,
|
||||
} from '../service';
|
||||
|
@ -34,10 +31,8 @@ import { createEmptyHostIsolationException } from '../utils';
|
|||
import { initialHostIsolationExceptionsPageState } from './builders';
|
||||
import { createHostIsolationExceptionsPageMiddleware } from './middleware';
|
||||
import { hostIsolationExceptionsPageReducer } from './reducer';
|
||||
import { getListFetchError } from './selector';
|
||||
|
||||
jest.mock('../service');
|
||||
const getHostIsolationExceptionItemsMock = getHostIsolationExceptionItems as jest.Mock;
|
||||
const createHostIsolationExceptionItemMock = createHostIsolationExceptionItem as jest.Mock;
|
||||
const getOneHostIsolationExceptionItemMock = getOneHostIsolationExceptionItem as jest.Mock;
|
||||
const updateOneHostIsolationExceptionItemMock = updateOneHostIsolationExceptionItem as jest.Mock;
|
||||
|
@ -79,84 +74,6 @@ describe('Host isolation exceptions middleware', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('when on the List page', () => {
|
||||
const changeUrl = (searchParams: string = '') => {
|
||||
store.dispatch({
|
||||
type: 'userChangedUrl',
|
||||
payload: {
|
||||
pathname: HOST_ISOLATION_EXCEPTIONS_PATH,
|
||||
search: searchParams,
|
||||
hash: '',
|
||||
key: 'miniMe',
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
getHostIsolationExceptionItemsMock.mockReset();
|
||||
getHostIsolationExceptionItemsMock.mockImplementation(getFoundExceptionListItemSchemaMock);
|
||||
});
|
||||
|
||||
it.each([
|
||||
[undefined, undefined],
|
||||
[3, 50],
|
||||
])(
|
||||
'should trigger api call to retrieve host isolation exceptions params page_index[%s] page_size[%s]',
|
||||
async (pageIndex, perPage) => {
|
||||
changeUrl((pageIndex && perPage && `?page_index=${pageIndex}&page_size=${perPage}`) || '');
|
||||
await spyMiddleware.waitForAction('hostIsolationExceptionsPageDataChanged', {
|
||||
validate({ payload }) {
|
||||
return isLoadedResourceState(payload);
|
||||
},
|
||||
});
|
||||
|
||||
expect(getHostIsolationExceptionItemsMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
page: (pageIndex ?? 0) + 1,
|
||||
perPage: perPage ?? 10,
|
||||
filter: undefined,
|
||||
})
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
it('should clear up previous page and apply a filter configuration when a filter is used', async () => {
|
||||
changeUrl('?filter=testMe');
|
||||
await spyMiddleware.waitForAction('hostIsolationExceptionsPageDataChanged', {
|
||||
validate({ payload }) {
|
||||
return isLoadedResourceState(payload);
|
||||
},
|
||||
});
|
||||
expect(getHostIsolationExceptionItemsMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
page: 1,
|
||||
perPage: 10,
|
||||
filter:
|
||||
'(exception-list-agnostic.attributes.name:(*testMe*) OR exception-list-agnostic.attributes.description:(*testMe*) OR exception-list-agnostic.attributes.entries.value:(*testMe*))',
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should dispatch a Failure if an API error was encountered', async () => {
|
||||
getHostIsolationExceptionItemsMock.mockRejectedValue({
|
||||
body: { message: 'error message', statusCode: 500, error: 'Internal Server Error' },
|
||||
});
|
||||
|
||||
changeUrl();
|
||||
await spyMiddleware.waitForAction('hostIsolationExceptionsPageDataChanged', {
|
||||
validate({ payload }) {
|
||||
return isFailedResourceState(payload);
|
||||
},
|
||||
});
|
||||
|
||||
expect(getListFetchError(store.getState())).toEqual({
|
||||
message: 'error message',
|
||||
statusCode: 500,
|
||||
error: 'Internal Server Error',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('When adding an item to host isolation exceptions', () => {
|
||||
let entry: CreateExceptionListItemSchema;
|
||||
beforeEach(() => {
|
||||
|
|
|
@ -8,31 +8,24 @@
|
|||
import {
|
||||
CreateExceptionListItemSchema,
|
||||
ExceptionListItemSchema,
|
||||
FoundExceptionListItemSchema,
|
||||
UpdateExceptionListItemSchema,
|
||||
} from '@kbn/securitysolution-io-ts-list-types';
|
||||
import { CoreStart, HttpSetup, HttpStart } from 'kibana/public';
|
||||
import { matchPath } from 'react-router-dom';
|
||||
import { transformNewItemOutput, transformOutput } from '@kbn/securitysolution-list-hooks';
|
||||
import { AppLocation, Immutable, ImmutableObject } from '../../../../../common/endpoint/types';
|
||||
import { ImmutableObject } from '../../../../../common/endpoint/types';
|
||||
import { ImmutableMiddleware, ImmutableMiddlewareAPI } from '../../../../common/store';
|
||||
import { AppAction } from '../../../../common/store/actions';
|
||||
import { MANAGEMENT_ROUTING_HOST_ISOLATION_EXCEPTIONS_PATH } from '../../../common/constants';
|
||||
import { parseQueryFilterToKQL } from '../../../common/utils';
|
||||
import {
|
||||
createFailedResourceState,
|
||||
createLoadedResourceState,
|
||||
createLoadingResourceState,
|
||||
asStaleResourceState,
|
||||
} from '../../../state/async_resource_builders';
|
||||
import {
|
||||
getHostIsolationExceptionItems,
|
||||
createHostIsolationExceptionItem,
|
||||
getOneHostIsolationExceptionItem,
|
||||
updateOneHostIsolationExceptionItem,
|
||||
} from '../service';
|
||||
import { HostIsolationExceptionsPageState } from '../types';
|
||||
import { getCurrentListPageDataState, getCurrentLocation } from './selector';
|
||||
import { HostIsolationExceptionsPageAction } from './action';
|
||||
|
||||
export const SEARCHABLE_FIELDS: Readonly<string[]> = [`name`, `description`, `entries.value`];
|
||||
|
@ -47,14 +40,6 @@ export const createHostIsolationExceptionsPageMiddleware = (
|
|||
return (store) => (next) => async (action) => {
|
||||
next(action);
|
||||
|
||||
if (action.type === 'userChangedUrl' && isHostIsolationExceptionsPage(action.payload)) {
|
||||
loadHostIsolationExceptionsList(store, coreStart.http);
|
||||
}
|
||||
|
||||
if (action.type === 'hostIsolationExceptionsRefreshList') {
|
||||
loadHostIsolationExceptionsList(store, coreStart.http);
|
||||
}
|
||||
|
||||
if (action.type === 'hostIsolationExceptionsCreateEntry') {
|
||||
createHostIsolationException(store, coreStart.http);
|
||||
}
|
||||
|
@ -104,57 +89,6 @@ async function createHostIsolationException(
|
|||
}
|
||||
}
|
||||
|
||||
async function loadHostIsolationExceptionsList(
|
||||
store: ImmutableMiddlewareAPI<
|
||||
HostIsolationExceptionsPageState,
|
||||
HostIsolationExceptionsPageAction
|
||||
>,
|
||||
http: HttpStart
|
||||
) {
|
||||
const { dispatch } = store;
|
||||
try {
|
||||
const {
|
||||
page_size: pageSize,
|
||||
page_index: pageIndex,
|
||||
filter,
|
||||
} = getCurrentLocation(store.getState());
|
||||
const query = {
|
||||
http,
|
||||
page: pageIndex + 1,
|
||||
perPage: pageSize,
|
||||
filter: parseQueryFilterToKQL(filter, SEARCHABLE_FIELDS) || undefined,
|
||||
};
|
||||
|
||||
dispatch({
|
||||
type: 'hostIsolationExceptionsPageDataChanged',
|
||||
payload: createLoadingResourceState(
|
||||
asStaleResourceState(getCurrentListPageDataState(store.getState()))
|
||||
),
|
||||
});
|
||||
|
||||
const entries = await getHostIsolationExceptionItems(query);
|
||||
|
||||
dispatch({
|
||||
type: 'hostIsolationExceptionsPageDataChanged',
|
||||
payload: createLoadedResourceState(entries),
|
||||
});
|
||||
} catch (error) {
|
||||
dispatch({
|
||||
type: 'hostIsolationExceptionsPageDataChanged',
|
||||
payload: createFailedResourceState<FoundExceptionListItemSchema>(error.body ?? error),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function isHostIsolationExceptionsPage(location: Immutable<AppLocation>) {
|
||||
return (
|
||||
matchPath(location.pathname ?? '', {
|
||||
path: MANAGEMENT_ROUTING_HOST_ISOLATION_EXCEPTIONS_PATH,
|
||||
exact: true,
|
||||
}) !== null
|
||||
);
|
||||
}
|
||||
|
||||
async function loadHostIsolationExceptionsItem(
|
||||
store: ImmutableMiddlewareAPI<
|
||||
HostIsolationExceptionsPageState,
|
||||
|
|
|
@ -65,12 +65,6 @@ export const hostIsolationExceptionsPageReducer: StateReducer = (
|
|||
},
|
||||
};
|
||||
}
|
||||
case 'hostIsolationExceptionsPageDataChanged': {
|
||||
return {
|
||||
...state,
|
||||
entries: action.payload,
|
||||
};
|
||||
}
|
||||
case 'userChangedUrl':
|
||||
return userChangedUrl(state, action);
|
||||
}
|
||||
|
|
|
@ -5,115 +5,20 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { Pagination } from '@elastic/eui';
|
||||
import {
|
||||
ExceptionListItemSchema,
|
||||
FoundExceptionListItemSchema,
|
||||
UpdateExceptionListItemSchema,
|
||||
} from '@kbn/securitysolution-io-ts-list-types';
|
||||
import { UpdateExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types';
|
||||
import { createSelector } from 'reselect';
|
||||
import { Immutable } from '../../../../../common/endpoint/types';
|
||||
import { ServerApiError } from '../../../../common/types';
|
||||
import {
|
||||
MANAGEMENT_DEFAULT_PAGE_SIZE,
|
||||
MANAGEMENT_PAGE_SIZE_OPTIONS,
|
||||
} from '../../../common/constants';
|
||||
import {
|
||||
getLastLoadedResourceState,
|
||||
isFailedResourceState,
|
||||
isLoadedResourceState,
|
||||
isLoadingResourceState,
|
||||
} from '../../../state/async_resource_state';
|
||||
import { isFailedResourceState } from '../../../state/async_resource_state';
|
||||
import { HostIsolationExceptionsPageState } from '../types';
|
||||
|
||||
type StoreState = Immutable<HostIsolationExceptionsPageState>;
|
||||
type HostIsolationExceptionsSelector<T> = (state: StoreState) => T;
|
||||
|
||||
export const getCurrentListPageState: HostIsolationExceptionsSelector<StoreState> = (state) => {
|
||||
return state;
|
||||
};
|
||||
|
||||
export const getCurrentListPageDataState: HostIsolationExceptionsSelector<StoreState['entries']> = (
|
||||
state
|
||||
) => state.entries;
|
||||
|
||||
const getListApiSuccessResponse: HostIsolationExceptionsSelector<
|
||||
Immutable<FoundExceptionListItemSchema> | undefined
|
||||
> = createSelector(getCurrentListPageDataState, (listPageData) => {
|
||||
return getLastLoadedResourceState(listPageData)?.data;
|
||||
});
|
||||
|
||||
export const getListItems: HostIsolationExceptionsSelector<Immutable<ExceptionListItemSchema[]>> =
|
||||
createSelector(getListApiSuccessResponse, (apiResponseData) => {
|
||||
return apiResponseData?.data || [];
|
||||
});
|
||||
|
||||
export const getTotalListItems: HostIsolationExceptionsSelector<Immutable<number>> = createSelector(
|
||||
getListApiSuccessResponse,
|
||||
(apiResponseData) => {
|
||||
return apiResponseData?.total || 0;
|
||||
}
|
||||
);
|
||||
|
||||
export const getListPagination: HostIsolationExceptionsSelector<Pagination> = createSelector(
|
||||
getListApiSuccessResponse,
|
||||
// memoized via `reselect` until the API response changes
|
||||
(response) => {
|
||||
return {
|
||||
totalItemCount: response?.total ?? 0,
|
||||
pageSize: response?.per_page ?? MANAGEMENT_DEFAULT_PAGE_SIZE,
|
||||
pageSizeOptions: [...MANAGEMENT_PAGE_SIZE_OPTIONS],
|
||||
pageIndex: (response?.page ?? 1) - 1,
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
export const getListIsLoading: HostIsolationExceptionsSelector<boolean> = createSelector(
|
||||
getCurrentListPageDataState,
|
||||
(listDataState) => isLoadingResourceState(listDataState)
|
||||
);
|
||||
|
||||
export const getListFetchError: HostIsolationExceptionsSelector<
|
||||
Immutable<ServerApiError> | undefined
|
||||
> = createSelector(getCurrentListPageDataState, (listPageDataState) => {
|
||||
return (isFailedResourceState(listPageDataState) && listPageDataState.error) || undefined;
|
||||
});
|
||||
|
||||
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 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;
|
||||
}
|
||||
});
|
||||
|
||||
const getFormState: HostIsolationExceptionsSelector<StoreState['form']> = (state) => {
|
||||
return state.form;
|
||||
};
|
||||
|
|
|
@ -26,6 +26,7 @@ import React, { memo, useCallback, useEffect, useMemo, useState } from 'react';
|
|||
import { useDispatch } from 'react-redux';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { Dispatch } from 'redux';
|
||||
import { useQueryClient } from 'react-query';
|
||||
import { Loader } from '../../../../../common/components/loader';
|
||||
import { useToasts } from '../../../../../common/lib/kibana';
|
||||
import { getHostIsolationExceptionsListPath } from '../../../../common/routing';
|
||||
|
@ -62,6 +63,7 @@ export const HostIsolationExceptionsFormFlyout: React.FC<{}> = memo(() => {
|
|||
const creationFailure = useHostIsolationExceptionsSelector(getFormStatusFailure);
|
||||
const exceptionToEdit = useHostIsolationExceptionsSelector(getExceptionToEdit);
|
||||
const navigateCallback = useHostIsolationExceptionsNavigateCallback();
|
||||
const queryClient = useQueryClient();
|
||||
const history = useHistory();
|
||||
|
||||
const [formHasError, setFormHasError] = useState(true);
|
||||
|
@ -115,8 +117,17 @@ export const HostIsolationExceptionsFormFlyout: React.FC<{}> = memo(() => {
|
|||
} else {
|
||||
toasts.addSuccess(getCreationSuccessMessage(exception.name));
|
||||
}
|
||||
queryClient.invalidateQueries('hostIsolationExceptions');
|
||||
}
|
||||
}, [creationSuccessful, dispatch, exception?.item_id, exception?.name, onCancel, toasts]);
|
||||
}, [
|
||||
creationSuccessful,
|
||||
dispatch,
|
||||
exception?.item_id,
|
||||
exception?.name,
|
||||
onCancel,
|
||||
queryClient,
|
||||
toasts,
|
||||
]);
|
||||
|
||||
// handle load item to edit error
|
||||
useEffect(() => {
|
||||
|
|
|
@ -7,6 +7,9 @@
|
|||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { FoundExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types';
|
||||
import { QueryObserverResult, useQuery } from 'react-query';
|
||||
import { ServerApiError } from '../../../../common/types';
|
||||
import { useHttp } from '../../../../common/lib/kibana/hooks';
|
||||
import { useEndpointPrivileges } from '../../../../common/components/user_privileges/endpoint';
|
||||
import { State } from '../../../../common/store';
|
||||
|
@ -15,9 +18,10 @@ import {
|
|||
MANAGEMENT_STORE_HOST_ISOLATION_EXCEPTIONS_NAMESPACE,
|
||||
} from '../../../common/constants';
|
||||
import { getHostIsolationExceptionsListPath } from '../../../common/routing';
|
||||
import { getHostIsolationExceptionSummary } from '../service';
|
||||
import { getHostIsolationExceptionItems, getHostIsolationExceptionSummary } from '../service';
|
||||
import { getCurrentLocation } from '../store/selector';
|
||||
import { HostIsolationExceptionsPageLocation, HostIsolationExceptionsPageState } from '../types';
|
||||
import { parseQueryFilterToKQL } from '../../../common/utils';
|
||||
|
||||
export function useHostIsolationExceptionsSelector<R>(
|
||||
selector: (state: HostIsolationExceptionsPageState) => R
|
||||
|
@ -69,3 +73,25 @@ export function useCanSeeHostIsolationExceptionsMenu() {
|
|||
|
||||
return canSeeMenu;
|
||||
}
|
||||
|
||||
const SEARCHABLE_FIELDS: Readonly<string[]> = [`name`, `description`, `entries.value`];
|
||||
|
||||
export function useFetchHostIsolationExceptionsList(): QueryObserverResult<
|
||||
FoundExceptionListItemSchema,
|
||||
ServerApiError
|
||||
> {
|
||||
const http = useHttp();
|
||||
const location = useHostIsolationExceptionsSelector(getCurrentLocation);
|
||||
|
||||
return useQuery<FoundExceptionListItemSchema, ServerApiError>(
|
||||
['hostIsolationExceptions', 'list', location.filter, location.page_size, location.page_index],
|
||||
() => {
|
||||
return getHostIsolationExceptionItems({
|
||||
http,
|
||||
page: location.page_index + 1,
|
||||
perPage: location.page_size,
|
||||
filter: parseQueryFilterToKQL(location.filter, SEARCHABLE_FIELDS) || undefined,
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
|
|
@ -5,13 +5,12 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { act } from '@testing-library/react';
|
||||
import { act, waitFor, waitForElementToBeRemoved } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import React from 'react';
|
||||
import { getFoundExceptionListItemSchemaMock } from '../../../../../../lists/common/schemas/response/found_exception_list_item_schema.mock';
|
||||
import { HOST_ISOLATION_EXCEPTIONS_PATH } from '../../../../../common/constants';
|
||||
import { AppContextTestRender, createAppRootMockRenderer } from '../../../../common/mock/endpoint';
|
||||
import { isFailedResourceState, isLoadedResourceState } from '../../../state';
|
||||
import { getHostIsolationExceptionItems } from '../service';
|
||||
import { HostIsolationExceptionsList } from './host_isolation_exceptions_list';
|
||||
import { useEndpointPrivileges } from '../../../../common/components/user_privileges/endpoint';
|
||||
|
@ -26,31 +25,25 @@ describe('When on the host isolation exceptions page', () => {
|
|||
let render: () => ReturnType<AppContextTestRender['render']>;
|
||||
let renderResult: ReturnType<typeof render>;
|
||||
let history: AppContextTestRender['history'];
|
||||
let waitForAction: AppContextTestRender['middlewareSpy']['waitForAction'];
|
||||
let mockedContext: AppContextTestRender;
|
||||
|
||||
const useEndpointPrivilegesMock = useEndpointPrivileges as jest.Mock;
|
||||
const waitForApiCall = () => {
|
||||
return waitFor(() => expect(getHostIsolationExceptionItemsMock).toHaveBeenCalled());
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
getHostIsolationExceptionItemsMock.mockReset();
|
||||
getHostIsolationExceptionItemsMock.mockClear();
|
||||
mockedContext = createAppRootMockRenderer();
|
||||
({ history } = mockedContext);
|
||||
render = () => (renderResult = mockedContext.render(<HostIsolationExceptionsList />));
|
||||
waitForAction = mockedContext.middlewareSpy.waitForAction;
|
||||
|
||||
act(() => {
|
||||
history.push(HOST_ISOLATION_EXCEPTIONS_PATH);
|
||||
});
|
||||
});
|
||||
|
||||
describe('When on the host isolation list page', () => {
|
||||
const dataReceived = () =>
|
||||
act(async () => {
|
||||
await waitForAction('hostIsolationExceptionsPageDataChanged', {
|
||||
validate(action) {
|
||||
return isLoadedResourceState(action.payload);
|
||||
},
|
||||
});
|
||||
});
|
||||
describe('And no data exists', () => {
|
||||
beforeEach(async () => {
|
||||
getHostIsolationExceptionItemsMock.mockReturnValue({
|
||||
|
@ -63,40 +56,47 @@ describe('When on the host isolation exceptions page', () => {
|
|||
|
||||
it('should show the Empty message', async () => {
|
||||
render();
|
||||
await dataReceived();
|
||||
await waitForApiCall();
|
||||
expect(renderResult.getByTestId('hostIsolationExceptionsEmpty')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should not display the search bar', async () => {
|
||||
render();
|
||||
await dataReceived();
|
||||
await waitForApiCall();
|
||||
expect(renderResult.queryByTestId('searchExceptions')).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('And data exists', () => {
|
||||
beforeEach(async () => {
|
||||
getHostIsolationExceptionItemsMock.mockImplementation(getFoundExceptionListItemSchemaMock);
|
||||
});
|
||||
|
||||
it('should show loading indicator while retrieving data', async () => {
|
||||
it('should show loading indicator while retrieving data and hide it when it gets it', async () => {
|
||||
let releaseApiResponse: (value?: unknown) => void;
|
||||
|
||||
// make the request wait
|
||||
getHostIsolationExceptionItemsMock.mockReturnValue(
|
||||
new Promise((resolve) => (releaseApiResponse = resolve))
|
||||
);
|
||||
render();
|
||||
await waitForApiCall();
|
||||
|
||||
// see if loader is present
|
||||
expect(renderResult.getByTestId('hostIsolationExceptionsContent-loader')).toBeTruthy();
|
||||
|
||||
const wasReceived = dataReceived();
|
||||
releaseApiResponse!();
|
||||
await wasReceived;
|
||||
expect(renderResult.container.querySelector('.euiProgress')).toBeNull();
|
||||
// release the request
|
||||
releaseApiResponse!(getFoundExceptionListItemSchemaMock());
|
||||
|
||||
// check the loader is gone
|
||||
await waitForElementToBeRemoved(
|
||||
renderResult.getByTestId('hostIsolationExceptionsContent-loader')
|
||||
);
|
||||
});
|
||||
|
||||
it('should display the search bar and item count', async () => {
|
||||
render();
|
||||
await dataReceived();
|
||||
await waitForApiCall();
|
||||
expect(renderResult.getByTestId('searchExceptions')).toBeTruthy();
|
||||
expect(renderResult.getByTestId('hostIsolationExceptions-totalCount').textContent).toBe(
|
||||
'Showing 1 exception'
|
||||
|
@ -105,7 +105,7 @@ describe('When on the host isolation exceptions page', () => {
|
|||
|
||||
it('should show items on the list', async () => {
|
||||
render();
|
||||
await dataReceived();
|
||||
await waitForApiCall();
|
||||
|
||||
expect(renderResult.getByTestId('hostIsolationExceptionsCard')).toBeTruthy();
|
||||
});
|
||||
|
@ -114,15 +114,8 @@ describe('When on the host isolation exceptions page', () => {
|
|||
getHostIsolationExceptionItemsMock.mockImplementation(() => {
|
||||
throw new Error('Server is too far away');
|
||||
});
|
||||
const errorDispatched = act(async () => {
|
||||
await waitForAction('hostIsolationExceptionsPageDataChanged', {
|
||||
validate(action) {
|
||||
return isFailedResourceState(action.payload);
|
||||
},
|
||||
});
|
||||
});
|
||||
render();
|
||||
await errorDispatched;
|
||||
await waitForApiCall();
|
||||
expect(
|
||||
renderResult.getByTestId('hostIsolationExceptionsContent-error').textContent
|
||||
).toEqual(' Server is too far away');
|
||||
|
@ -131,7 +124,7 @@ describe('When on the host isolation exceptions page', () => {
|
|||
it('should show the searchbar when no results from search', async () => {
|
||||
// render the page with data
|
||||
render();
|
||||
await dataReceived();
|
||||
await waitForApiCall();
|
||||
|
||||
// check if the searchbar is there
|
||||
expect(renderResult.getByTestId('searchExceptions')).toBeTruthy();
|
||||
|
@ -149,7 +142,7 @@ describe('When on the host isolation exceptions page', () => {
|
|||
userEvent.click(renderResult.getByTestId('searchButton'));
|
||||
|
||||
// wait for the page render
|
||||
await dataReceived();
|
||||
await waitForApiCall();
|
||||
|
||||
// check the url changed
|
||||
expect(mockedContext.history.location.search).toBe('?filter=this%20does%20not%20exists');
|
||||
|
@ -167,16 +160,16 @@ describe('When on the host isolation exceptions page', () => {
|
|||
|
||||
it('should show the create flyout when the add button is pressed', async () => {
|
||||
render();
|
||||
await dataReceived();
|
||||
await waitForApiCall();
|
||||
userEvent.click(renderResult.getByTestId('hostIsolationExceptionsListAddButton'));
|
||||
await dataReceived();
|
||||
await waitForApiCall();
|
||||
expect(renderResult.getByTestId('hostIsolationExceptionsCreateEditFlyout')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should show the create flyout when the show location is create', async () => {
|
||||
history.push(`${HOST_ISOLATION_EXCEPTIONS_PATH}?show=create`);
|
||||
render();
|
||||
await dataReceived();
|
||||
await waitForApiCall();
|
||||
expect(renderResult.getByTestId('hostIsolationExceptionsCreateEditFlyout')).toBeTruthy();
|
||||
expect(renderResult.queryByTestId('hostIsolationExceptionsCreateEditFlyout')).toBeTruthy();
|
||||
});
|
||||
|
|
|
@ -7,21 +7,14 @@
|
|||
|
||||
import { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React, { Dispatch, useCallback, useEffect, useState } from 'react';
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { EuiButton, EuiText, EuiSpacer } from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { ExceptionItem } from '../../../../common/components/exceptions/viewer/exception_item';
|
||||
import { getCurrentLocation } from '../store/selector';
|
||||
import {
|
||||
getCurrentLocation,
|
||||
getListFetchError,
|
||||
getListIsLoading,
|
||||
getListItems,
|
||||
getListPagination,
|
||||
getTotalListItems,
|
||||
} from '../store/selector';
|
||||
import {
|
||||
useFetchHostIsolationExceptionsList,
|
||||
useHostIsolationExceptionsNavigateCallback,
|
||||
useHostIsolationExceptionsSelector,
|
||||
} from './hooks';
|
||||
|
@ -39,7 +32,10 @@ import {
|
|||
} from './components/translations';
|
||||
import { getEndpointListPath } from '../../../common/routing';
|
||||
import { useEndpointPrivileges } from '../../../../common/components/user_privileges/endpoint';
|
||||
import { HostIsolationExceptionsPageAction } from '../store/action';
|
||||
import {
|
||||
MANAGEMENT_DEFAULT_PAGE_SIZE,
|
||||
MANAGEMENT_PAGE_SIZE_OPTIONS,
|
||||
} from '../../../common/constants';
|
||||
|
||||
type HostIsolationExceptionPaginatedContent = PaginatedContentProps<
|
||||
Immutable<ExceptionListItemSchema>,
|
||||
|
@ -47,19 +43,26 @@ type HostIsolationExceptionPaginatedContent = PaginatedContentProps<
|
|||
>;
|
||||
|
||||
export const HostIsolationExceptionsList = () => {
|
||||
const listItems = useHostIsolationExceptionsSelector(getListItems);
|
||||
const totalCountListItems = useHostIsolationExceptionsSelector(getTotalListItems);
|
||||
const pagination = useHostIsolationExceptionsSelector(getListPagination);
|
||||
const isLoading = useHostIsolationExceptionsSelector(getListIsLoading);
|
||||
const fetchError = useHostIsolationExceptionsSelector(getListFetchError);
|
||||
const history = useHistory();
|
||||
const privileges = useEndpointPrivileges();
|
||||
|
||||
const location = useHostIsolationExceptionsSelector(getCurrentLocation);
|
||||
const dispatch = useDispatch<Dispatch<HostIsolationExceptionsPageAction>>();
|
||||
const navigateCallback = useHostIsolationExceptionsNavigateCallback();
|
||||
|
||||
const [itemToDelete, setItemToDelete] = useState<ExceptionListItemSchema | null>(null);
|
||||
|
||||
const history = useHistory();
|
||||
const privileges = useEndpointPrivileges();
|
||||
const { isLoading, data, error, refetch } = useFetchHostIsolationExceptionsList();
|
||||
|
||||
const pagination = {
|
||||
totalItemCount: data?.total ?? 0,
|
||||
pageSize: data?.per_page ?? MANAGEMENT_DEFAULT_PAGE_SIZE,
|
||||
pageSizeOptions: [...MANAGEMENT_PAGE_SIZE_OPTIONS],
|
||||
pageIndex: (data?.page ?? 1) - 1,
|
||||
};
|
||||
|
||||
const listItems = data?.data || [];
|
||||
const totalCountListItems = data?.total || 0;
|
||||
|
||||
const showFlyout = privileges.canIsolateHost && !!location.show;
|
||||
const hasDataToShow = !!location.filter || listItems.length > 0;
|
||||
|
||||
|
@ -125,9 +128,7 @@ export const HostIsolationExceptionsList = () => {
|
|||
|
||||
const handleCloseDeleteDialog = (forceRefresh: boolean = false) => {
|
||||
if (forceRefresh) {
|
||||
dispatch({
|
||||
type: 'hostIsolationExceptionsRefreshList',
|
||||
});
|
||||
refetch();
|
||||
}
|
||||
setItemToDelete(null);
|
||||
};
|
||||
|
@ -201,7 +202,7 @@ export const HostIsolationExceptionsList = () => {
|
|||
ItemComponent={ArtifactEntryCard}
|
||||
itemComponentProps={handleItemComponentProps}
|
||||
onChange={handlePaginatedContentChange}
|
||||
error={fetchError?.message}
|
||||
error={error?.message}
|
||||
loading={isLoading}
|
||||
pagination={pagination}
|
||||
contentClassName="host-isolation-exceptions-container"
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue