[Security Solution] Refactor/host isolation exceptions list to use react-query (#118189)

This commit is contained in:
Esteban Beltran 2021-11-15 10:12:36 -05:00 committed by GitHub
parent fe2498c2dc
commit 937fd9863d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 115 additions and 331 deletions

View file

@ -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}>

View file

@ -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;

View file

@ -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(() => {

View file

@ -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,

View file

@ -65,12 +65,6 @@ export const hostIsolationExceptionsPageReducer: StateReducer = (
},
};
}
case 'hostIsolationExceptionsPageDataChanged': {
return {
...state,
entries: action.payload,
};
}
case 'userChangedUrl':
return userChangedUrl(state, action);
}

View file

@ -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;
};

View file

@ -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(() => {

View file

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

View file

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

View file

@ -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"