mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[Security Solution][Endpoint] User can edit existing event filters from the list (#98898)
* Makes width 100% to allow multilang * Removes state/index types and move those types into the parent types file * Allows fill form from existing exception by id. Adds unit tests. Fixes wrong comments display when there is more than one comment. * Allows user update an existing event filter. Adds unit tests. Fixes some wrong behaviours when opening the flyout after create/update action * Fixes typo * Fixes wrong entry type * Uses selectors when it's possible instead of accessing directly to state object * Fixes typechecks * Allows edit from the card edit button. Removes unused imports and fixes some types * Reverts type name * Changes reducer to don't add entry to the list manually after creation, list will be reloaded with api call. Also check always if data exists to display the add new entry button at the first time
This commit is contained in:
parent
beaa4e78fb
commit
35f4be4387
24 changed files with 844 additions and 226 deletions
|
@ -39,4 +39,4 @@ export {
|
|||
UseExceptionListsSuccess,
|
||||
} from './exceptions/types';
|
||||
export * as ExceptionBuilder from './exceptions/components/builder/index';
|
||||
export { transformNewItemOutput } from './exceptions/transforms';
|
||||
export { transformNewItemOutput, transformOutput } from './exceptions/transforms';
|
||||
|
|
|
@ -24,8 +24,7 @@ import { AdministrationSubTab } from '../types';
|
|||
import { appendSearch } from '../../common/components/link_to/helpers';
|
||||
import { EndpointIndexUIQueryParams } from '../pages/endpoint_hosts/types';
|
||||
import { TrustedAppsListPageLocation } from '../pages/trusted_apps/state';
|
||||
import { EventFiltersPageLocation } from '../pages/event_filters/state';
|
||||
import { EventFiltersListPageUrlSearchParams } from '../pages/event_filters/types';
|
||||
import { EventFiltersPageLocation } from '../pages/event_filters/types';
|
||||
|
||||
// Taken from: https://github.com/microsoft/TypeScript/issues/12936#issuecomment-559034150
|
||||
type ExactKeys<T1, T2> = Exclude<keyof T1, keyof T2> extends never ? T1 : never;
|
||||
|
@ -215,9 +214,7 @@ export const extractEventFiltetrsPageLocation = (
|
|||
};
|
||||
};
|
||||
|
||||
export const getEventFiltersListPath = (
|
||||
location?: Partial<EventFiltersListPageUrlSearchParams>
|
||||
): string => {
|
||||
export const getEventFiltersListPath = (location?: Partial<EventFiltersPageLocation>): string => {
|
||||
const path = generatePath(MANAGEMENT_ROUTING_EVENT_FILTERS_PATH, {
|
||||
tabName: AdministrationSubTab.eventFilters,
|
||||
});
|
||||
|
|
|
@ -7,10 +7,13 @@
|
|||
|
||||
import { HttpStart } from 'kibana/public';
|
||||
import {
|
||||
ExceptionListItemSchema,
|
||||
CreateExceptionListItemSchema,
|
||||
ENDPOINT_EVENT_FILTERS_LIST_ID,
|
||||
ExceptionListItemSchema,
|
||||
UpdateExceptionListItemSchema,
|
||||
} from '../../../../shared_imports';
|
||||
import { Immutable } from '../../../../../common/endpoint/types';
|
||||
|
||||
import { EVENT_FILTER_LIST, EXCEPTION_LIST_ITEM_URL, EXCEPTION_LIST_URL } from '../constants';
|
||||
import { FoundExceptionListItemSchema } from '../../../../../../lists/common/schemas';
|
||||
import { EventFiltersService } from '../types';
|
||||
|
@ -69,4 +72,21 @@ export class EventFiltersHttpService implements EventFiltersService {
|
|||
body: JSON.stringify(exception),
|
||||
});
|
||||
}
|
||||
|
||||
async getOne(id: string) {
|
||||
return (await this.httpWrapper()).get<ExceptionListItemSchema>(EXCEPTION_LIST_ITEM_URL, {
|
||||
query: {
|
||||
id,
|
||||
namespace_type: 'agnostic',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async updateOne(
|
||||
exception: Immutable<UpdateExceptionListItemSchema>
|
||||
): Promise<ExceptionListItemSchema> {
|
||||
return (await this.httpWrapper()).put<ExceptionListItemSchema>(EXCEPTION_LIST_ITEM_URL, {
|
||||
body: JSON.stringify(exception),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,29 +5,18 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { ExceptionListItemSchema, CreateExceptionListItemSchema } from '../../../../shared_imports';
|
||||
import { ExceptionListItemSchema } from '../../../../shared_imports';
|
||||
import { AsyncResourceState } from '../../../state/async_resource_state';
|
||||
import { FoundExceptionListItemSchema } from '../../../../../../lists/common/schemas';
|
||||
import { EventFiltersServiceGetListOptions } from '../types';
|
||||
|
||||
export interface EventFiltersPageLocation {
|
||||
page_index: number;
|
||||
page_size: number;
|
||||
show?: 'create' | 'edit';
|
||||
/** Used for editing. The ID of the selected event filter */
|
||||
id?: string;
|
||||
filter: string;
|
||||
}
|
||||
import {
|
||||
EventFiltersForm,
|
||||
EventFiltersPageLocation,
|
||||
EventFiltersServiceGetListOptions,
|
||||
} from '../types';
|
||||
|
||||
export interface EventFiltersListPageState {
|
||||
entries: ExceptionListItemSchema[];
|
||||
form: {
|
||||
entry: CreateExceptionListItemSchema | ExceptionListItemSchema | undefined;
|
||||
hasNameError: boolean;
|
||||
hasItemsError: boolean;
|
||||
hasOSError: boolean;
|
||||
submissionResourceState: AsyncResourceState<ExceptionListItemSchema>;
|
||||
};
|
||||
form: EventFiltersForm;
|
||||
location: EventFiltersPageLocation;
|
||||
/** State for the Event Filters List page */
|
||||
listPage: {
|
||||
|
|
|
@ -6,7 +6,11 @@
|
|||
*/
|
||||
|
||||
import { Action } from 'redux';
|
||||
import { ExceptionListItemSchema, CreateExceptionListItemSchema } from '../../../../shared_imports';
|
||||
import {
|
||||
ExceptionListItemSchema,
|
||||
CreateExceptionListItemSchema,
|
||||
UpdateExceptionListItemSchema,
|
||||
} from '../../../../shared_imports';
|
||||
import { AsyncResourceState } from '../../../state/async_resource_state';
|
||||
import { EventFiltersListPageState } from '../state';
|
||||
|
||||
|
@ -24,25 +28,30 @@ export type EventFiltersListPageDataExistsChanged = Action<'eventFiltersListPage
|
|||
|
||||
export type EventFiltersInitForm = Action<'eventFiltersInitForm'> & {
|
||||
payload: {
|
||||
entry: ExceptionListItemSchema | CreateExceptionListItemSchema;
|
||||
entry: UpdateExceptionListItemSchema | CreateExceptionListItemSchema;
|
||||
};
|
||||
};
|
||||
|
||||
export type EventFiltersInitFromId = Action<'eventFiltersInitFromId'> & {
|
||||
payload: {
|
||||
id: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type EventFiltersChangeForm = Action<'eventFiltersChangeForm'> & {
|
||||
payload: {
|
||||
entry: ExceptionListItemSchema | CreateExceptionListItemSchema;
|
||||
entry: UpdateExceptionListItemSchema | CreateExceptionListItemSchema;
|
||||
hasNameError?: boolean;
|
||||
hasItemsError?: boolean;
|
||||
hasOSError?: boolean;
|
||||
newComment?: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type EventFiltersUpdateStart = Action<'eventFiltersUpdateStart'>;
|
||||
export type EventFiltersUpdateSuccess = Action<'eventFiltersUpdateSuccess'>;
|
||||
export type EventFiltersCreateStart = Action<'eventFiltersCreateStart'>;
|
||||
export type EventFiltersCreateSuccess = Action<'eventFiltersCreateSuccess'> & {
|
||||
payload: {
|
||||
exception: ExceptionListItemSchema;
|
||||
};
|
||||
};
|
||||
export type EventFiltersCreateSuccess = Action<'eventFiltersCreateSuccess'>;
|
||||
export type EventFiltersCreateError = Action<'eventFiltersCreateError'>;
|
||||
|
||||
export type EventFiltersFormStateChanged = Action<'eventFiltersFormStateChanged'> & {
|
||||
|
@ -53,9 +62,12 @@ export type EventFiltersPageAction =
|
|||
| EventFiltersListPageStateChanged
|
||||
| EventFiltersListPageDataChanged
|
||||
| EventFiltersListPageDataExistsChanged
|
||||
| EventFiltersCreateStart
|
||||
| EventFiltersInitForm
|
||||
| EventFiltersInitFromId
|
||||
| EventFiltersChangeForm
|
||||
| EventFiltersUpdateStart
|
||||
| EventFiltersUpdateSuccess
|
||||
| EventFiltersCreateStart
|
||||
| EventFiltersCreateSuccess
|
||||
| EventFiltersCreateError
|
||||
| EventFiltersFormStateChanged;
|
||||
|
|
|
@ -15,6 +15,7 @@ export const initialEventFiltersPageState = (): EventFiltersListPageState => ({
|
|||
hasNameError: false,
|
||||
hasItemsError: false,
|
||||
hasOSError: false,
|
||||
newComment: '',
|
||||
submissionResourceState: { type: 'UninitialisedResourceState' },
|
||||
},
|
||||
location: {
|
||||
|
|
|
@ -14,6 +14,7 @@ import {
|
|||
import { AppAction } from '../../../../common/store/actions';
|
||||
import { createEventFiltersPageMiddleware } from './middleware';
|
||||
import { eventFiltersPageReducer } from './reducer';
|
||||
|
||||
import { EventFiltersListPageState } from '../state';
|
||||
import { initialEventFiltersPageState } from './builders';
|
||||
import { getInitialExceptionFromEvent } from './utils';
|
||||
|
@ -25,6 +26,8 @@ const initialState: EventFiltersListPageState = initialEventFiltersPageState();
|
|||
const createEventFiltersServiceMock = (): jest.Mocked<EventFiltersService> => ({
|
||||
addEventFilters: jest.fn(),
|
||||
getList: jest.fn(),
|
||||
getOne: jest.fn(),
|
||||
updateOne: jest.fn(),
|
||||
});
|
||||
|
||||
const createStoreSetup = (eventFiltersService: EventFiltersService) => {
|
||||
|
@ -51,18 +54,18 @@ describe('middleware', () => {
|
|||
});
|
||||
});
|
||||
|
||||
let service: jest.Mocked<EventFiltersService>;
|
||||
let store: Store<EventFiltersListPageState>;
|
||||
let spyMiddleware: MiddlewareActionSpyHelper<EventFiltersListPageState, AppAction>;
|
||||
|
||||
beforeEach(() => {
|
||||
service = createEventFiltersServiceMock();
|
||||
const storeSetup = createStoreSetup(service);
|
||||
store = storeSetup.store as Store<EventFiltersListPageState>;
|
||||
spyMiddleware = storeSetup.spyMiddleware;
|
||||
});
|
||||
|
||||
describe('submit creation event filter', () => {
|
||||
let service: jest.Mocked<EventFiltersService>;
|
||||
let store: Store<EventFiltersListPageState>;
|
||||
let spyMiddleware: MiddlewareActionSpyHelper<EventFiltersListPageState, AppAction>;
|
||||
|
||||
beforeEach(() => {
|
||||
service = createEventFiltersServiceMock();
|
||||
const storeSetup = createStoreSetup(service);
|
||||
store = storeSetup.store as Store<EventFiltersListPageState>;
|
||||
spyMiddleware = storeSetup.spyMiddleware;
|
||||
});
|
||||
|
||||
it('does not submit when entry is undefined', async () => {
|
||||
store.dispatch({ type: 'eventFiltersCreateStart' });
|
||||
expect(store.getState()).toStrictEqual({
|
||||
|
@ -87,7 +90,6 @@ describe('middleware', () => {
|
|||
await spyMiddleware.waitForAction('eventFiltersFormStateChanged');
|
||||
expect(store.getState()).toStrictEqual({
|
||||
...initialState,
|
||||
entries: [createdEventFilterEntryMock()],
|
||||
form: {
|
||||
...store.getState().form,
|
||||
submissionResourceState: {
|
||||
|
@ -110,6 +112,108 @@ describe('middleware', () => {
|
|||
|
||||
store.dispatch({ type: 'eventFiltersCreateStart' });
|
||||
|
||||
await spyMiddleware.waitForAction('eventFiltersFormStateChanged');
|
||||
expect(store.getState()).toStrictEqual({
|
||||
...initialState,
|
||||
form: {
|
||||
...store.getState().form,
|
||||
submissionResourceState: {
|
||||
type: 'FailedResourceState',
|
||||
lastLoadedState: undefined,
|
||||
error: {
|
||||
error: 'Internal Server Error',
|
||||
message: 'error message',
|
||||
statusCode: 500,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('load event filterby id', () => {
|
||||
it('init form with an entry loaded by id from API', async () => {
|
||||
service.getOne.mockResolvedValue(createdEventFilterEntryMock());
|
||||
store.dispatch({ type: 'eventFiltersInitFromId', payload: { id: 'id' } });
|
||||
await spyMiddleware.waitForAction('eventFiltersInitForm');
|
||||
expect(store.getState()).toStrictEqual({
|
||||
...initialState,
|
||||
form: {
|
||||
...store.getState().form,
|
||||
entry: createdEventFilterEntryMock(),
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('does throw error when getting by id', async () => {
|
||||
service.getOne.mockRejectedValue({
|
||||
body: { message: 'error message', statusCode: 500, error: 'Internal Server Error' },
|
||||
});
|
||||
store.dispatch({ type: 'eventFiltersInitFromId', payload: { id: 'id' } });
|
||||
await spyMiddleware.waitForAction('eventFiltersFormStateChanged');
|
||||
expect(store.getState()).toStrictEqual({
|
||||
...initialState,
|
||||
form: {
|
||||
...store.getState().form,
|
||||
submissionResourceState: {
|
||||
type: 'FailedResourceState',
|
||||
lastLoadedState: undefined,
|
||||
error: {
|
||||
error: 'Internal Server Error',
|
||||
message: 'error message',
|
||||
statusCode: 500,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('submit update event filter', () => {
|
||||
it('does not submit when entry is undefined', async () => {
|
||||
store.dispatch({ type: 'eventFiltersUpdateStart' });
|
||||
expect(store.getState()).toStrictEqual({
|
||||
...initialState,
|
||||
form: {
|
||||
...store.getState().form,
|
||||
submissionResourceState: { type: 'UninitialisedResourceState' },
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('does submit when entry is not undefined', async () => {
|
||||
service.updateOne.mockResolvedValue(createdEventFilterEntryMock());
|
||||
|
||||
store.dispatch({
|
||||
type: 'eventFiltersInitForm',
|
||||
payload: { entry: createdEventFilterEntryMock() },
|
||||
});
|
||||
|
||||
store.dispatch({ type: 'eventFiltersUpdateStart' });
|
||||
|
||||
await spyMiddleware.waitForAction('eventFiltersFormStateChanged');
|
||||
expect(store.getState()).toStrictEqual({
|
||||
...initialState,
|
||||
form: {
|
||||
...store.getState().form,
|
||||
submissionResourceState: {
|
||||
type: 'LoadedResourceState',
|
||||
data: createdEventFilterEntryMock(),
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('does throw error when creating', async () => {
|
||||
service.updateOne.mockRejectedValue({
|
||||
body: { message: 'error message', statusCode: 500, error: 'Internal Server Error' },
|
||||
});
|
||||
const entry = getInitialExceptionFromEvent(ecsEventMock());
|
||||
store.dispatch({
|
||||
type: 'eventFiltersInitForm',
|
||||
payload: { entry },
|
||||
});
|
||||
|
||||
store.dispatch({ type: 'eventFiltersUpdateStart' });
|
||||
|
||||
await spyMiddleware.waitForAction('eventFiltersFormStateChanged');
|
||||
expect(store.getState()).toStrictEqual({
|
||||
...initialState,
|
||||
|
|
|
@ -16,7 +16,13 @@ import { EventFiltersHttpService } from '../service';
|
|||
|
||||
import { EventFiltersListPageState } from '../state';
|
||||
import { getLastLoadedResourceState } from '../../../state/async_resource_state';
|
||||
import { CreateExceptionListItemSchema, transformNewItemOutput } from '../../../../shared_imports';
|
||||
|
||||
import {
|
||||
CreateExceptionListItemSchema,
|
||||
transformNewItemOutput,
|
||||
transformOutput,
|
||||
UpdateExceptionListItemSchema,
|
||||
} from '../../../../shared_imports';
|
||||
import {
|
||||
getCurrentListPageDataState,
|
||||
getCurrentLocation,
|
||||
|
@ -24,9 +30,23 @@ import {
|
|||
getListPageDataExistsState,
|
||||
getListPageIsActive,
|
||||
listDataNeedsRefresh,
|
||||
getFormEntry,
|
||||
getSubmissionResource,
|
||||
getNewComment,
|
||||
} from './selector';
|
||||
import { EventFiltersService, EventFiltersServiceGetListOptions } from '../types';
|
||||
|
||||
const addNewComments = (
|
||||
entry: UpdateExceptionListItemSchema | CreateExceptionListItemSchema,
|
||||
newComment: string
|
||||
): UpdateExceptionListItemSchema | CreateExceptionListItemSchema => {
|
||||
if (newComment) {
|
||||
if (!entry.comments) entry.comments = [];
|
||||
entry.comments.push({ comment: newComment });
|
||||
}
|
||||
return entry;
|
||||
};
|
||||
|
||||
type MiddlewareActionHandler = (
|
||||
store: ImmutableMiddlewareAPI<EventFiltersListPageState, AppAction>,
|
||||
eventFiltersService: EventFiltersService
|
||||
|
@ -35,7 +55,7 @@ type MiddlewareActionHandler = (
|
|||
const eventFiltersCreate: MiddlewareActionHandler = async (store, eventFiltersService) => {
|
||||
const submissionResourceState = store.getState().form.submissionResourceState;
|
||||
try {
|
||||
const formEntry = store.getState().form.entry;
|
||||
const formEntry = getFormEntry(store.getState());
|
||||
if (!formEntry) return;
|
||||
store.dispatch({
|
||||
type: 'eventFiltersFormStateChanged',
|
||||
|
@ -46,14 +66,83 @@ const eventFiltersCreate: MiddlewareActionHandler = async (store, eventFiltersSe
|
|||
});
|
||||
|
||||
const sanitizedEntry = transformNewItemOutput(formEntry as CreateExceptionListItemSchema);
|
||||
const updatedCommentsEntry = addNewComments(
|
||||
sanitizedEntry,
|
||||
getNewComment(store.getState())
|
||||
) as CreateExceptionListItemSchema;
|
||||
|
||||
const exception = await eventFiltersService.addEventFilters(updatedCommentsEntry);
|
||||
|
||||
const exception = await eventFiltersService.addEventFilters(sanitizedEntry);
|
||||
store.dispatch({
|
||||
type: 'eventFiltersCreateSuccess',
|
||||
});
|
||||
|
||||
store.dispatch({
|
||||
type: 'eventFiltersFormStateChanged',
|
||||
payload: {
|
||||
exception,
|
||||
type: 'LoadedResourceState',
|
||||
data: exception,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
store.dispatch({
|
||||
type: 'eventFiltersFormStateChanged',
|
||||
payload: {
|
||||
type: 'FailedResourceState',
|
||||
error: error.body || error,
|
||||
lastLoadedState: getLastLoadedResourceState(submissionResourceState),
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const eventFiltersUpdate = async (
|
||||
store: ImmutableMiddlewareAPI<EventFiltersListPageState, AppAction>,
|
||||
eventFiltersService: EventFiltersService
|
||||
) => {
|
||||
const submissionResourceState = getSubmissionResource(store.getState());
|
||||
try {
|
||||
const formEntry = getFormEntry(store.getState());
|
||||
if (!formEntry) return;
|
||||
store.dispatch({
|
||||
type: 'eventFiltersFormStateChanged',
|
||||
payload: {
|
||||
type: 'LoadingResourceState',
|
||||
previousState: { type: 'UninitialisedResourceState' },
|
||||
},
|
||||
});
|
||||
|
||||
const sanitizedEntry: UpdateExceptionListItemSchema = transformOutput(
|
||||
formEntry as UpdateExceptionListItemSchema
|
||||
);
|
||||
const updatedCommentsEntry = addNewComments(
|
||||
sanitizedEntry,
|
||||
getNewComment(store.getState())
|
||||
) as UpdateExceptionListItemSchema;
|
||||
|
||||
// Clean unnecessary fields for update action
|
||||
[
|
||||
'created_at',
|
||||
'created_by',
|
||||
'created_at',
|
||||
'created_by',
|
||||
'list_id',
|
||||
'tie_breaker_id',
|
||||
'updated_at',
|
||||
'updated_by',
|
||||
].forEach((field) => {
|
||||
delete updatedCommentsEntry[field as keyof UpdateExceptionListItemSchema];
|
||||
});
|
||||
|
||||
updatedCommentsEntry.comments = updatedCommentsEntry.comments?.map((comment) => ({
|
||||
comment: comment.comment,
|
||||
id: comment.id,
|
||||
}));
|
||||
|
||||
const exception = await eventFiltersService.updateOne(updatedCommentsEntry);
|
||||
store.dispatch({
|
||||
type: 'eventFiltersUpdateSuccess',
|
||||
});
|
||||
store.dispatch({
|
||||
type: 'eventFiltersFormStateChanged',
|
||||
payload: {
|
||||
|
@ -73,6 +162,30 @@ const eventFiltersCreate: MiddlewareActionHandler = async (store, eventFiltersSe
|
|||
}
|
||||
};
|
||||
|
||||
const eventFiltersLoadById = async (
|
||||
store: ImmutableMiddlewareAPI<EventFiltersListPageState, AppAction>,
|
||||
eventFiltersService: EventFiltersService,
|
||||
id: string
|
||||
) => {
|
||||
const submissionResourceState = getSubmissionResource(store.getState());
|
||||
try {
|
||||
const entry = await eventFiltersService.getOne(id);
|
||||
store.dispatch({
|
||||
type: 'eventFiltersInitForm',
|
||||
payload: { entry },
|
||||
});
|
||||
} catch (error) {
|
||||
store.dispatch({
|
||||
type: 'eventFiltersFormStateChanged',
|
||||
payload: {
|
||||
type: 'FailedResourceState',
|
||||
error: error.body || error,
|
||||
lastLoadedState: getLastLoadedResourceState(submissionResourceState),
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const checkIfEventFilterDataExist: MiddlewareActionHandler = async (
|
||||
{ dispatch, getState },
|
||||
eventFiltersService: EventFiltersService
|
||||
|
@ -146,6 +259,14 @@ const refreshListDataIfNeeded: MiddlewareActionHandler = async (store, eventFilt
|
|||
},
|
||||
});
|
||||
|
||||
dispatch({
|
||||
type: 'eventFiltersListPageDataExistsChanged',
|
||||
payload: {
|
||||
type: 'LoadedResourceState',
|
||||
data: Boolean(results.total),
|
||||
},
|
||||
});
|
||||
|
||||
// If no results were returned, then just check to make sure data actually exists for
|
||||
// event filters. This is used to drive the UI between showing "empty state" and "no items found"
|
||||
// messages to the user
|
||||
|
@ -172,11 +293,19 @@ export const createEventFiltersPageMiddleware = (
|
|||
|
||||
if (action.type === 'eventFiltersCreateStart') {
|
||||
await eventFiltersCreate(store, eventFiltersService);
|
||||
} else if (action.type === 'eventFiltersInitFromId') {
|
||||
await eventFiltersLoadById(store, eventFiltersService, action.payload.id);
|
||||
} else if (action.type === 'eventFiltersUpdateStart') {
|
||||
await eventFiltersUpdate(store, eventFiltersService);
|
||||
}
|
||||
|
||||
// Middleware that only applies to the List Page for Event Filters
|
||||
if (getListPageIsActive(store.getState())) {
|
||||
if (action.type === 'userChangedUrl' || action.type === 'eventFiltersCreateSuccess') {
|
||||
if (
|
||||
action.type === 'userChangedUrl' ||
|
||||
action.type === 'eventFiltersCreateSuccess' ||
|
||||
action.type === 'eventFiltersUpdateSuccess'
|
||||
) {
|
||||
refreshListDataIfNeeded(store, eventFiltersService);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -37,9 +37,10 @@ describe('reducer', () => {
|
|||
it('change form values', () => {
|
||||
const entry = getInitialExceptionFromEvent(ecsEventMock());
|
||||
const nameChanged = 'name changed';
|
||||
const newComment = 'new comment';
|
||||
const result = eventFiltersPageReducer(initialState, {
|
||||
type: 'eventFiltersChangeForm',
|
||||
payload: { entry: { ...entry, name: nameChanged } },
|
||||
payload: { entry: { ...entry, name: nameChanged }, newComment },
|
||||
});
|
||||
|
||||
expect(result).toStrictEqual({
|
||||
|
@ -50,6 +51,7 @@ describe('reducer', () => {
|
|||
...entry,
|
||||
name: nameChanged,
|
||||
},
|
||||
newComment,
|
||||
hasNameError: false,
|
||||
submissionResourceState: {
|
||||
type: 'UninitialisedResourceState',
|
||||
|
@ -79,34 +81,22 @@ describe('reducer', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('create is success when there is no entry on entries list', () => {
|
||||
const result = eventFiltersPageReducer(initialState, {
|
||||
it('create is success and force list refresh', () => {
|
||||
const initialStateWithListPageActive = {
|
||||
...initialState,
|
||||
listPage: { ...initialState.listPage, active: true },
|
||||
};
|
||||
const result = eventFiltersPageReducer(initialStateWithListPageActive, {
|
||||
type: 'eventFiltersCreateSuccess',
|
||||
payload: {
|
||||
exception: createdEventFilterEntryMock(),
|
||||
},
|
||||
});
|
||||
|
||||
expect(result).toStrictEqual({
|
||||
...initialState,
|
||||
entries: [createdEventFilterEntryMock()],
|
||||
});
|
||||
});
|
||||
|
||||
it('create is success when there there are entries on entries list', () => {
|
||||
const customizedInitialState = {
|
||||
...initialState,
|
||||
entries: [createdEventFilterEntryMock(), createdEventFilterEntryMock()],
|
||||
};
|
||||
const result = eventFiltersPageReducer(customizedInitialState, {
|
||||
type: 'eventFiltersCreateSuccess',
|
||||
payload: {
|
||||
exception: { ...createdEventFilterEntryMock(), meta: {} },
|
||||
...initialStateWithListPageActive,
|
||||
listPage: {
|
||||
...initialStateWithListPageActive.listPage,
|
||||
forceRefresh: true,
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.entries).toHaveLength(3);
|
||||
expect(result.entries[0]!.meta).not.toBeUndefined();
|
||||
});
|
||||
});
|
||||
describe('UserChangedUrl', () => {
|
||||
|
|
|
@ -14,12 +14,14 @@ import { AppLocation, Immutable } from '../../../../../common/endpoint/types';
|
|||
import { UserChangedUrl } from '../../../../common/store/routing/action';
|
||||
import { MANAGEMENT_ROUTING_EVENT_FILTERS_PATH } from '../../../common/constants';
|
||||
import { extractEventFiltetrsPageLocation } from '../../../common/routing';
|
||||
import { isUninitialisedResourceState } from '../../../state/async_resource_state';
|
||||
|
||||
import {
|
||||
EventFiltersInitForm,
|
||||
EventFiltersChangeForm,
|
||||
EventFiltersFormStateChanged,
|
||||
EventFiltersCreateSuccess,
|
||||
EventFiltersUpdateSuccess,
|
||||
EventFiltersListPageStateChanged,
|
||||
EventFiltersListPageDataChanged,
|
||||
EventFiltersListPageDataExistsChanged,
|
||||
|
@ -113,6 +115,8 @@ const eventFiltersChangeForm: CaseReducer<EventFiltersChangeForm> = (state, acti
|
|||
: state.form.hasNameError,
|
||||
hasOSError:
|
||||
action.payload.hasOSError !== undefined ? action.payload.hasOSError : state.form.hasOSError,
|
||||
newComment:
|
||||
action.payload.newComment !== undefined ? action.payload.newComment : state.form.newComment,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
@ -122,6 +126,8 @@ const eventFiltersFormStateChanged: CaseReducer<EventFiltersFormStateChanged> =
|
|||
...state,
|
||||
form: {
|
||||
...state.form,
|
||||
entry: isUninitialisedResourceState(action.payload) ? undefined : state.form.entry,
|
||||
newComment: isUninitialisedResourceState(action.payload) ? '' : state.form.newComment,
|
||||
submissionResourceState: action.payload,
|
||||
},
|
||||
};
|
||||
|
@ -130,7 +136,19 @@ const eventFiltersFormStateChanged: CaseReducer<EventFiltersFormStateChanged> =
|
|||
const eventFiltersCreateSuccess: CaseReducer<EventFiltersCreateSuccess> = (state, action) => {
|
||||
return {
|
||||
...state,
|
||||
entries: [action.payload.exception, ...state.entries],
|
||||
// If we are on the List page, then force a refresh of data
|
||||
listPage: getListPageIsActive(state)
|
||||
? {
|
||||
...state.listPage,
|
||||
forceRefresh: true,
|
||||
}
|
||||
: state.listPage,
|
||||
};
|
||||
};
|
||||
|
||||
const eventFiltersUpdateSuccess: CaseReducer<EventFiltersUpdateSuccess> = (state, action) => {
|
||||
return {
|
||||
...state,
|
||||
// If we are on the List page, then force a refresh of data
|
||||
listPage: getListPageIsActive(state)
|
||||
? {
|
||||
|
@ -180,6 +198,8 @@ export const eventFiltersPageReducer: StateReducer = (
|
|||
return eventFiltersFormStateChanged(state, action);
|
||||
case 'eventFiltersCreateSuccess':
|
||||
return eventFiltersCreateSuccess(state, action);
|
||||
case 'eventFiltersUpdateSuccess':
|
||||
return eventFiltersUpdateSuccess(state, action);
|
||||
case 'userChangedUrl':
|
||||
return userChangedUrl(state, action);
|
||||
}
|
||||
|
|
|
@ -4,16 +4,19 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { createSelector } from 'reselect';
|
||||
import { Pagination } from '@elastic/eui';
|
||||
|
||||
import { EventFiltersServiceGetListOptions } from '../types';
|
||||
|
||||
import { EventFiltersListPageState } from '../state';
|
||||
import { ExceptionListItemSchema, CreateExceptionListItemSchema } from '../../../../shared_imports';
|
||||
import { ExceptionListItemSchema } from '../../../../shared_imports';
|
||||
import { ServerApiError } from '../../../../common/types';
|
||||
import {
|
||||
isLoadingResourceState,
|
||||
isLoadedResourceState,
|
||||
isFailedResourceState,
|
||||
isUninitialisedResourceState,
|
||||
} from '../../../state/async_resource_state';
|
||||
import { FoundExceptionListItemSchema } from '../../../../../../lists/common/schemas';
|
||||
import {
|
||||
|
@ -21,7 +24,6 @@ import {
|
|||
MANAGEMENT_PAGE_SIZE_OPTIONS,
|
||||
} from '../../../common/constants';
|
||||
import { Immutable } from '../../../../../common/endpoint/types';
|
||||
import { EventFiltersServiceGetListOptions } from '../types';
|
||||
|
||||
type StoreState = Immutable<EventFiltersListPageState>;
|
||||
type EventFiltersSelector<T> = (state: StoreState) => T;
|
||||
|
@ -121,11 +123,29 @@ export const getListPageDoesDataExist: EventFiltersSelector<boolean> = createSel
|
|||
}
|
||||
);
|
||||
|
||||
export const getFormEntry = (
|
||||
state: EventFiltersListPageState
|
||||
): CreateExceptionListItemSchema | ExceptionListItemSchema | undefined => {
|
||||
export const getFormEntryState: EventFiltersSelector<StoreState['form']['entry']> = (state) => {
|
||||
return state.form.entry;
|
||||
};
|
||||
// Needed for form component as we modify the existing entry on exceptuionBuilder component
|
||||
export const getFormEntryStateMutable = (
|
||||
state: EventFiltersListPageState
|
||||
): EventFiltersListPageState['form']['entry'] => {
|
||||
return state.form.entry;
|
||||
};
|
||||
|
||||
export const getFormEntry = createSelector(getFormEntryState, (entry) => entry);
|
||||
|
||||
export const getNewCommentState: EventFiltersSelector<StoreState['form']['newComment']> = (
|
||||
state
|
||||
) => {
|
||||
return state.form.newComment;
|
||||
};
|
||||
|
||||
export const getNewComment = createSelector(getNewCommentState, (newComment) => newComment);
|
||||
|
||||
export const getHasNameError = (state: EventFiltersListPageState): boolean => {
|
||||
return state.form.hasNameError;
|
||||
};
|
||||
|
||||
export const getFormHasError = (state: EventFiltersListPageState): boolean => {
|
||||
return state.form.hasItemsError || state.form.hasNameError || state.form.hasOSError;
|
||||
|
@ -139,12 +159,27 @@ export const isCreationSuccessful = (state: EventFiltersListPageState): boolean
|
|||
return isLoadedResourceState(state.form.submissionResourceState);
|
||||
};
|
||||
|
||||
export const getCreationError = (state: EventFiltersListPageState): ServerApiError | undefined => {
|
||||
export const isUninitialisedForm = (state: EventFiltersListPageState): boolean => {
|
||||
return isUninitialisedResourceState(state.form.submissionResourceState);
|
||||
};
|
||||
|
||||
export const getActionError = (state: EventFiltersListPageState): ServerApiError | undefined => {
|
||||
const submissionResourceState = state.form.submissionResourceState;
|
||||
|
||||
return isFailedResourceState(submissionResourceState) ? submissionResourceState.error : undefined;
|
||||
};
|
||||
|
||||
export const getSubmissionResourceState: EventFiltersSelector<
|
||||
StoreState['form']['submissionResourceState']
|
||||
> = (state) => {
|
||||
return state.form.submissionResourceState;
|
||||
};
|
||||
|
||||
export const getSubmissionResource = createSelector(
|
||||
getSubmissionResourceState,
|
||||
(submissionResourceState) => submissionResourceState
|
||||
);
|
||||
|
||||
export const getCurrentLocation: EventFiltersSelector<StoreState['location']> = (state) =>
|
||||
state.location;
|
||||
|
||||
|
|
|
@ -10,6 +10,8 @@ import {
|
|||
getFormEntry,
|
||||
getFormHasError,
|
||||
getCurrentLocation,
|
||||
getNewComment,
|
||||
getHasNameError,
|
||||
getCurrentListPageState,
|
||||
getListPageIsActive,
|
||||
getCurrentListPageDataState,
|
||||
|
@ -24,7 +26,8 @@ import {
|
|||
} from './selector';
|
||||
import { ecsEventMock } from '../test_utils';
|
||||
import { getInitialExceptionFromEvent } from './utils';
|
||||
import { EventFiltersListPageState, EventFiltersPageLocation } from '../state';
|
||||
import { EventFiltersPageLocation } from '../types';
|
||||
import { EventFiltersListPageState } from '../state';
|
||||
import { MANAGEMENT_DEFAULT_PAGE, MANAGEMENT_DEFAULT_PAGE_SIZE } from '../../../common/constants';
|
||||
import { getFoundExceptionListItemSchemaMock } from '../../../../../../lists/common/schemas/response/found_exception_list_item_schema.mock';
|
||||
import {
|
||||
|
@ -252,6 +255,31 @@ describe('event filters selectors', () => {
|
|||
expect(getFormEntry(state)).toBe(entry);
|
||||
});
|
||||
});
|
||||
describe('getHasNameError()', () => {
|
||||
it('returns false when there is no entry', () => {
|
||||
expect(getHasNameError(initialState)).toBeFalsy();
|
||||
});
|
||||
it('returns true when entry with name error', () => {
|
||||
const state = {
|
||||
...initialState,
|
||||
form: {
|
||||
...initialState.form,
|
||||
hasNameError: true,
|
||||
},
|
||||
};
|
||||
expect(getHasNameError(state)).toBeTruthy();
|
||||
});
|
||||
it('returns false when entry with no name error', () => {
|
||||
const state = {
|
||||
...initialState,
|
||||
form: {
|
||||
...initialState.form,
|
||||
hasNameError: false,
|
||||
},
|
||||
};
|
||||
expect(getHasNameError(state)).toBeFalsy();
|
||||
});
|
||||
});
|
||||
describe('getFormHasError()', () => {
|
||||
it('returns false when there is no entry', () => {
|
||||
expect(getFormHasError(initialState)).toBeFalsy();
|
||||
|
@ -327,4 +355,23 @@ describe('event filters selectors', () => {
|
|||
expect(getCurrentLocation(state)).toBe(expectedLocation);
|
||||
});
|
||||
});
|
||||
describe('getNewComment()', () => {
|
||||
it('returns new comment', () => {
|
||||
const newComment = 'this is a new comment';
|
||||
const state = {
|
||||
...initialState,
|
||||
form: {
|
||||
...initialState.form,
|
||||
newComment,
|
||||
},
|
||||
};
|
||||
expect(getNewComment(state)).toBe(newComment);
|
||||
});
|
||||
it('returns empty comment', () => {
|
||||
const state = {
|
||||
...initialState,
|
||||
};
|
||||
expect(getNewComment(state)).toBe('');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -83,7 +83,7 @@ export const createdEventFilterEntryMock = (): ExceptionListItemSchema => ({
|
|||
name: 'Test',
|
||||
namespace_type: 'agnostic',
|
||||
os_types: ['windows'],
|
||||
tags: [],
|
||||
tags: ['policy:all'],
|
||||
tie_breaker_id: 'c42f3dbd-292f-49e8-83ab-158d024a4d8b',
|
||||
type: 'simple',
|
||||
updated_at: '2021-04-19T10:30:36.428Z',
|
||||
|
|
|
@ -5,16 +5,31 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { Immutable } from '../../../../common/endpoint/types';
|
||||
import {
|
||||
UpdateExceptionListItemSchema,
|
||||
CreateExceptionListItemSchema,
|
||||
ExceptionListItemSchema,
|
||||
} from '../../../../../lists/common';
|
||||
} from '../../../shared_imports';
|
||||
import { AsyncResourceState } from '../../state/async_resource_state';
|
||||
import { Immutable } from '../../../../common/endpoint/types';
|
||||
import { FoundExceptionListItemSchema } from '../../../../../lists/common/schemas';
|
||||
|
||||
export interface EventFiltersListPageUrlSearchParams {
|
||||
export interface EventFiltersPageLocation {
|
||||
page_index: number;
|
||||
page_size: number;
|
||||
show?: 'create' | 'edit';
|
||||
/** Used for editing. The ID of the selected event filter */
|
||||
id?: string;
|
||||
filter: string;
|
||||
}
|
||||
|
||||
export interface EventFiltersForm {
|
||||
entry: UpdateExceptionListItemSchema | CreateExceptionListItemSchema | undefined;
|
||||
newComment: string;
|
||||
hasNameError: boolean;
|
||||
hasItemsError: boolean;
|
||||
hasOSError: boolean;
|
||||
submissionResourceState: AsyncResourceState<ExceptionListItemSchema>;
|
||||
}
|
||||
|
||||
export type EventFiltersServiceGetListOptions = Partial<{
|
||||
|
@ -30,4 +45,6 @@ export interface EventFiltersService {
|
|||
): Promise<ExceptionListItemSchema>;
|
||||
|
||||
getList(options?: EventFiltersServiceGetListOptions): Promise<FoundExceptionListItemSchema>;
|
||||
getOne(id: string): Promise<ExceptionListItemSchema>;
|
||||
updateOne(exception: Immutable<UpdateExceptionListItemSchema>): Promise<ExceptionListItemSchema>;
|
||||
}
|
||||
|
|
|
@ -11,8 +11,8 @@ import { EuiButton, EuiEmptyPrompt } from '@elastic/eui';
|
|||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
|
||||
const EmptyPrompt = styled(EuiEmptyPrompt)`
|
||||
${({ theme }) => css`
|
||||
max-width: ${theme.eui.euiBreakpoints.m};
|
||||
${() => css`
|
||||
max-width: 100%;
|
||||
`}
|
||||
`;
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { EventFiltersFlyout } from '.';
|
||||
import { EventFiltersFlyout, EventFiltersFlyoutProps } from '.';
|
||||
import * as reactTestingLibrary from '@testing-library/react';
|
||||
import { fireEvent } from '@testing-library/dom';
|
||||
import {
|
||||
|
@ -18,8 +18,14 @@ import {
|
|||
CreateExceptionListItemSchema,
|
||||
ExceptionListItemSchema,
|
||||
} from '../../../../../../shared_imports';
|
||||
import { EventFiltersHttpService } from '../../../service';
|
||||
import { createdEventFilterEntryMock } from '../../../test_utils';
|
||||
import { getFormEntryState, isUninitialisedForm } from '../../../store/selector';
|
||||
import { EventFiltersListPageState } from '../../../state';
|
||||
|
||||
jest.mock('../form');
|
||||
jest.mock('../../../service');
|
||||
|
||||
jest.mock('../../hooks', () => {
|
||||
const originalModule = jest.requireActual('../../hooks');
|
||||
const useEventFiltersNotification = jest.fn().mockImplementation(() => {});
|
||||
|
@ -32,16 +38,31 @@ jest.mock('../../hooks', () => {
|
|||
|
||||
let mockedContext: AppContextTestRender;
|
||||
let waitForAction: MiddlewareActionSpyHelper['waitForAction'];
|
||||
let render: () => ReturnType<AppContextTestRender['render']>;
|
||||
let render: (
|
||||
props?: Partial<EventFiltersFlyoutProps>
|
||||
) => ReturnType<AppContextTestRender['render']>;
|
||||
const act = reactTestingLibrary.act;
|
||||
let onCancelMock: jest.Mock;
|
||||
const EventFiltersHttpServiceMock = EventFiltersHttpService as jest.Mock;
|
||||
let getState: () => EventFiltersListPageState;
|
||||
|
||||
describe('Event filter flyout', () => {
|
||||
beforeAll(() => {
|
||||
EventFiltersHttpServiceMock.mockImplementation(() => {
|
||||
return {
|
||||
getOne: () => createdEventFilterEntryMock(),
|
||||
addEventFilters: () => createdEventFilterEntryMock(),
|
||||
updateOne: () => createdEventFilterEntryMock(),
|
||||
};
|
||||
});
|
||||
});
|
||||
beforeEach(() => {
|
||||
mockedContext = createAppRootMockRenderer();
|
||||
waitForAction = mockedContext.middlewareSpy.waitForAction;
|
||||
onCancelMock = jest.fn();
|
||||
render = () => mockedContext.render(<EventFiltersFlyout onCancel={onCancelMock} />);
|
||||
getState = () => mockedContext.store.getState().management.eventFilters;
|
||||
render = (props) =>
|
||||
mockedContext.render(<EventFiltersFlyout {...props} onCancel={onCancelMock} />);
|
||||
});
|
||||
|
||||
afterEach(() => reactTestingLibrary.cleanup());
|
||||
|
@ -59,10 +80,8 @@ describe('Event filter flyout', () => {
|
|||
await waitForAction('eventFiltersInitForm');
|
||||
});
|
||||
|
||||
expect(mockedContext.store.getState().management.eventFilters.form.entry).not.toBeUndefined();
|
||||
expect(
|
||||
mockedContext.store.getState().management.eventFilters.form.entry!.entries[0].field
|
||||
).toBe('');
|
||||
expect(getFormEntryState(getState())).not.toBeUndefined();
|
||||
expect(getFormEntryState(getState())!.entries[0].field).toBe('');
|
||||
});
|
||||
|
||||
it('should confirm form when button is disabled', () => {
|
||||
|
@ -71,9 +90,7 @@ describe('Event filter flyout', () => {
|
|||
act(() => {
|
||||
fireEvent.click(confirmButton);
|
||||
});
|
||||
expect(
|
||||
mockedContext.store.getState().management.eventFilters.form.submissionResourceState.type
|
||||
).toBe('UninitialisedResourceState');
|
||||
expect(isUninitialisedForm(getState())).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should confirm form when button is enabled', async () => {
|
||||
|
@ -82,8 +99,7 @@ describe('Event filter flyout', () => {
|
|||
type: 'eventFiltersChangeForm',
|
||||
payload: {
|
||||
entry: {
|
||||
...(mockedContext.store.getState().management.eventFilters.form!
|
||||
.entry as CreateExceptionListItemSchema),
|
||||
...(getState().form!.entry as CreateExceptionListItemSchema),
|
||||
name: 'test',
|
||||
os_types: ['windows'],
|
||||
},
|
||||
|
@ -97,9 +113,7 @@ describe('Event filter flyout', () => {
|
|||
fireEvent.click(confirmButton);
|
||||
await waitForAction('eventFiltersCreateSuccess');
|
||||
});
|
||||
expect(
|
||||
mockedContext.store.getState().management.eventFilters.form.submissionResourceState.type
|
||||
).toBe('UninitialisedResourceState');
|
||||
expect(isUninitialisedForm(getState())).toBeTruthy();
|
||||
expect(confirmButton.hasAttribute('disabled')).toBeFalsy();
|
||||
});
|
||||
|
||||
|
@ -112,8 +126,7 @@ describe('Event filter flyout', () => {
|
|||
type: 'eventFiltersFormStateChanged',
|
||||
payload: {
|
||||
type: 'LoadedResourceState',
|
||||
data: mockedContext.store.getState().management.eventFilters.form!
|
||||
.entry as ExceptionListItemSchema,
|
||||
data: getState().form!.entry as ExceptionListItemSchema,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
@ -166,4 +179,22 @@ describe('Event filter flyout', () => {
|
|||
|
||||
expect(onCancelMock).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it('should renders correctly when id and edit type', () => {
|
||||
const component = render({ id: 'fakeId', type: 'edit' });
|
||||
|
||||
expect(component.getAllByText('Update Endpoint Event Filter')).not.toBeNull();
|
||||
expect(component.getByText('cancel')).not.toBeNull();
|
||||
expect(component.getByText('Endpoint Security')).not.toBeNull();
|
||||
});
|
||||
|
||||
it('should dispatch action to init form store on mount with id', async () => {
|
||||
await act(async () => {
|
||||
render({ id: 'fakeId', type: 'edit' });
|
||||
await waitForAction('eventFiltersInitFromId');
|
||||
});
|
||||
|
||||
expect(getFormEntryState(getState())).not.toBeUndefined();
|
||||
expect(getFormEntryState(getState())!.item_id).toBe(createdEventFilterEntryMock().item_id);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -22,7 +22,6 @@ import {
|
|||
EuiFlexItem,
|
||||
} from '@elastic/eui';
|
||||
import { AppAction } from '../../../../../../common/store/actions';
|
||||
import { Ecs } from '../../../../../../../common/ecs';
|
||||
import { EventFiltersForm } from '../form';
|
||||
import { useEventFiltersSelector, useEventFiltersNotification } from '../../hooks';
|
||||
import {
|
||||
|
@ -33,97 +32,125 @@ import {
|
|||
import { getInitialExceptionFromEvent } from '../../../store/utils';
|
||||
|
||||
export interface EventFiltersFlyoutProps {
|
||||
data?: Ecs;
|
||||
type?: 'create' | 'edit';
|
||||
id?: string;
|
||||
onCancel(): void;
|
||||
}
|
||||
|
||||
export const EventFiltersFlyout: React.FC<EventFiltersFlyoutProps> = memo(({ data, onCancel }) => {
|
||||
useEventFiltersNotification();
|
||||
const dispatch = useDispatch<Dispatch<AppAction>>();
|
||||
const formHasError = useEventFiltersSelector(getFormHasError);
|
||||
const creationInProgress = useEventFiltersSelector(isCreationInProgress);
|
||||
const creationSuccessful = useEventFiltersSelector(isCreationSuccessful);
|
||||
export const EventFiltersFlyout: React.FC<EventFiltersFlyoutProps> = memo(
|
||||
({ onCancel, id, type = 'create' }) => {
|
||||
useEventFiltersNotification();
|
||||
const dispatch = useDispatch<Dispatch<AppAction>>();
|
||||
const formHasError = useEventFiltersSelector(getFormHasError);
|
||||
const creationInProgress = useEventFiltersSelector(isCreationInProgress);
|
||||
const creationSuccessful = useEventFiltersSelector(isCreationSuccessful);
|
||||
|
||||
useEffect(() => {
|
||||
if (creationSuccessful) {
|
||||
useEffect(() => {
|
||||
if (creationSuccessful) {
|
||||
onCancel();
|
||||
dispatch({
|
||||
type: 'eventFiltersFormStateChanged',
|
||||
payload: {
|
||||
type: 'UninitialisedResourceState',
|
||||
},
|
||||
});
|
||||
}
|
||||
}, [creationSuccessful, onCancel, dispatch]);
|
||||
|
||||
// Initialize the store with the id passed as prop to allow render the form. It acts as componentDidMount
|
||||
useEffect(() => {
|
||||
if (type === 'edit' && !!id) {
|
||||
dispatch({
|
||||
type: 'eventFiltersInitFromId',
|
||||
payload: { id },
|
||||
});
|
||||
} else {
|
||||
dispatch({
|
||||
type: 'eventFiltersInitForm',
|
||||
payload: { entry: getInitialExceptionFromEvent() },
|
||||
});
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const handleOnCancel = useCallback(() => {
|
||||
if (creationInProgress) return;
|
||||
onCancel();
|
||||
dispatch({
|
||||
type: 'eventFiltersFormStateChanged',
|
||||
payload: {
|
||||
type: 'UninitialisedResourceState',
|
||||
},
|
||||
});
|
||||
}
|
||||
}, [creationSuccessful, onCancel, dispatch]);
|
||||
}, [creationInProgress, onCancel]);
|
||||
|
||||
// Initialize the store with the event passed as prop to allow render the form. It acts as componentDidMount
|
||||
useEffect(() => {
|
||||
dispatch({
|
||||
type: 'eventFiltersInitForm',
|
||||
payload: { entry: getInitialExceptionFromEvent(data) },
|
||||
});
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const handleOnCancel = useCallback(() => {
|
||||
if (creationInProgress) return;
|
||||
onCancel();
|
||||
}, [creationInProgress, onCancel]);
|
||||
|
||||
const confirmButtonMemo = useMemo(
|
||||
() => (
|
||||
<EuiButton
|
||||
data-test-subj="add-exception-confirm-button"
|
||||
fill
|
||||
disabled={formHasError || creationInProgress}
|
||||
onClick={() => dispatch({ type: 'eventFiltersCreateStart' })}
|
||||
isLoading={creationInProgress}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.eventFilters.eventFiltersFlyout.actions.confirm"
|
||||
defaultMessage="Add Endpoint Event Filter"
|
||||
/>
|
||||
</EuiButton>
|
||||
),
|
||||
[dispatch, formHasError, creationInProgress]
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiFlyout size="l" onClose={handleOnCancel}>
|
||||
<EuiFlyoutHeader hasBorder>
|
||||
<EuiTitle size="m">
|
||||
<h2>
|
||||
const confirmButtonMemo = useMemo(
|
||||
() => (
|
||||
<EuiButton
|
||||
data-test-subj="add-exception-confirm-button"
|
||||
fill
|
||||
disabled={formHasError || creationInProgress}
|
||||
onClick={() =>
|
||||
id
|
||||
? dispatch({ type: 'eventFiltersUpdateStart' })
|
||||
: dispatch({ type: 'eventFiltersCreateStart' })
|
||||
}
|
||||
isLoading={creationInProgress}
|
||||
>
|
||||
{id ? (
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.eventFilters.eventFiltersFlyout.title"
|
||||
defaultMessage="Endpoint Security"
|
||||
id="xpack.securitySolution.eventFilters.eventFiltersFlyout.actions.confirm.update"
|
||||
defaultMessage="Update Endpoint Event Filter"
|
||||
/>
|
||||
</h2>
|
||||
</EuiTitle>
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.eventFilters.eventFiltersFlyout.subtitle"
|
||||
defaultMessage="Add Endpoint Event Filter"
|
||||
/>
|
||||
</EuiFlyoutHeader>
|
||||
) : (
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.eventFilters.eventFiltersFlyout.actions.confirm.create"
|
||||
defaultMessage="Add Endpoint Event Filter"
|
||||
/>
|
||||
)}
|
||||
</EuiButton>
|
||||
),
|
||||
[formHasError, creationInProgress, id, dispatch]
|
||||
);
|
||||
|
||||
<EuiFlyoutBody>
|
||||
<EventFiltersForm allowSelectOs />
|
||||
</EuiFlyoutBody>
|
||||
|
||||
<EuiFlyoutFooter>
|
||||
<EuiFlexGroup justifyContent="spaceBetween">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty data-test-subj="cancelExceptionAddButton" onClick={handleOnCancel}>
|
||||
return (
|
||||
<EuiFlyout size="l" onClose={handleOnCancel}>
|
||||
<EuiFlyoutHeader hasBorder>
|
||||
<EuiTitle size="m">
|
||||
<h2>
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.eventFilters.eventFiltersFlyout.actions.cancel"
|
||||
defaultMessage="cancel"
|
||||
id="xpack.securitySolution.eventFilters.eventFiltersFlyout.title"
|
||||
defaultMessage="Endpoint Security"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>{confirmButtonMemo}</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlyoutFooter>
|
||||
</EuiFlyout>
|
||||
);
|
||||
});
|
||||
</h2>
|
||||
</EuiTitle>
|
||||
{id ? (
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.eventFilters.eventFiltersFlyout.subtitle.update"
|
||||
defaultMessage="Update Endpoint Event Filter"
|
||||
/>
|
||||
) : (
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.eventFilters.eventFiltersFlyout.subtitle.create"
|
||||
defaultMessage="Add Endpoint Event Filter"
|
||||
/>
|
||||
)}
|
||||
</EuiFlyoutHeader>
|
||||
|
||||
<EuiFlyoutBody>
|
||||
<EventFiltersForm allowSelectOs />
|
||||
</EuiFlyoutBody>
|
||||
|
||||
<EuiFlyoutFooter>
|
||||
<EuiFlexGroup justifyContent="spaceBetween">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty data-test-subj="cancelExceptionAddButton" onClick={handleOnCancel}>
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.eventFilters.eventFiltersFlyout.actions.cancel"
|
||||
defaultMessage="cancel"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>{confirmButtonMemo}</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlyoutFooter>
|
||||
</EuiFlyout>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
EventFiltersFlyout.displayName = 'EventFiltersFlyout';
|
||||
|
|
|
@ -111,8 +111,6 @@ describe('Event filter form', () => {
|
|||
});
|
||||
});
|
||||
|
||||
expect(store.getState()!.management!.eventFilters!.form!.entry!.comments![0].comment).toBe(
|
||||
'Exception comment'
|
||||
);
|
||||
expect(store.getState()!.management!.eventFilters!.form!.newComment).toBe('Exception comment');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { memo, useMemo, useCallback, useState } from 'react';
|
||||
import React, { memo, useMemo, useCallback } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { Dispatch } from 'redux';
|
||||
import {
|
||||
|
@ -18,7 +18,7 @@ import {
|
|||
EuiText,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { isEmpty } from 'lodash';
|
||||
import { isEmpty } from 'lodash/fp';
|
||||
import { OperatingSystem } from '../../../../../../../common/endpoint/types';
|
||||
import { AddExceptionComments } from '../../../../../../common/components/exceptions/add_exception_comments';
|
||||
import { filterIndexPatterns } from '../../../../../../common/components/exceptions/helpers';
|
||||
|
@ -29,7 +29,7 @@ import { AppAction } from '../../../../../../common/store/actions';
|
|||
import { ExceptionListItemSchema, ExceptionBuilder } from '../../../../../../shared_imports';
|
||||
|
||||
import { useEventFiltersSelector } from '../../hooks';
|
||||
import { getFormEntry } from '../../../store/selector';
|
||||
import { getFormEntryStateMutable, getHasNameError, getNewComment } from '../../../store/selector';
|
||||
import {
|
||||
FORM_DESCRIPTION,
|
||||
NAME_LABEL,
|
||||
|
@ -54,7 +54,9 @@ export const EventFiltersForm: React.FC<EventFiltersFormProps> = memo(
|
|||
({ allowSelectOs = false }) => {
|
||||
const { http, data } = useKibana().services;
|
||||
const dispatch = useDispatch<Dispatch<AppAction>>();
|
||||
const exception = useEventFiltersSelector(getFormEntry);
|
||||
const exception = useEventFiltersSelector(getFormEntryStateMutable);
|
||||
const hasNameError = useEventFiltersSelector(getHasNameError);
|
||||
const newComment = useEventFiltersSelector(getNewComment);
|
||||
|
||||
// This value has to be memoized to avoid infinite useEffect loop on useFetchIndex
|
||||
const indexNames = useMemo(() => ['logs-endpoint.events.*'], []);
|
||||
|
@ -65,9 +67,6 @@ export const EventFiltersForm: React.FC<EventFiltersFormProps> = memo(
|
|||
[]
|
||||
);
|
||||
|
||||
const [hasNameError, setHasNameError] = useState(!exception || !exception.name);
|
||||
const [comment, setComment] = useState<string>('');
|
||||
|
||||
const handleOnBuilderChange = useCallback(
|
||||
(arg: ExceptionBuilder.OnChangeProps) => {
|
||||
if (isEmpty(arg.exceptionItems)) return;
|
||||
|
@ -90,7 +89,6 @@ export const EventFiltersForm: React.FC<EventFiltersFormProps> = memo(
|
|||
const handleOnChangeName = useCallback(
|
||||
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (!exception) return;
|
||||
setHasNameError(!e.target.value);
|
||||
dispatch({
|
||||
type: 'eventFiltersChangeForm',
|
||||
payload: {
|
||||
|
@ -104,16 +102,16 @@ export const EventFiltersForm: React.FC<EventFiltersFormProps> = memo(
|
|||
|
||||
const handleOnChangeComment = useCallback(
|
||||
(value: string) => {
|
||||
setComment(value);
|
||||
if (!exception) return;
|
||||
dispatch({
|
||||
type: 'eventFiltersChangeForm',
|
||||
payload: {
|
||||
entry: { ...exception, comments: [{ comment: value }] },
|
||||
entry: exception,
|
||||
newComment: value,
|
||||
},
|
||||
});
|
||||
},
|
||||
[dispatch, exception, setComment]
|
||||
[dispatch, exception]
|
||||
);
|
||||
|
||||
const exceptionBuilderComponentMemo = useMemo(
|
||||
|
@ -189,11 +187,12 @@ export const EventFiltersForm: React.FC<EventFiltersFormProps> = memo(
|
|||
const commentsInputMemo = useMemo(
|
||||
() => (
|
||||
<AddExceptionComments
|
||||
newCommentValue={comment}
|
||||
exceptionItemComments={(exception as ExceptionListItemSchema)?.comments}
|
||||
newCommentValue={newComment}
|
||||
newCommentOnChange={handleOnChangeComment}
|
||||
/>
|
||||
),
|
||||
[comment, handleOnChangeComment]
|
||||
[exception, handleOnChangeComment, newComment]
|
||||
);
|
||||
|
||||
return !isIndexPatternLoading && exception ? (
|
||||
|
|
|
@ -5,12 +5,19 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { memo, useCallback } from 'react';
|
||||
import React, { memo, useCallback, useEffect } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { Dispatch } from 'redux';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { EuiButton } from '@elastic/eui';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { AppAction } from '../../../../common/store/actions';
|
||||
import { getEventFiltersListPath } from '../../../common/routing';
|
||||
import { AdministrationListPage as _AdministrationListPage } from '../../../components/administration_list_page';
|
||||
|
||||
import { EventFiltersListEmptyState } from './components/empty';
|
||||
import { useEventFiltersNavigateCallback, useEventFiltersSelector } from './hooks';
|
||||
import { EventFiltersFlyout } from './components/flyout';
|
||||
|
@ -21,6 +28,8 @@ import {
|
|||
getListPagination,
|
||||
getCurrentLocation,
|
||||
getListPageDoesDataExist,
|
||||
getActionError,
|
||||
getFormEntry,
|
||||
} from '../store/selector';
|
||||
import { PaginatedContent, PaginatedContentProps } from '../../../components/paginated_content';
|
||||
import { ExceptionListItemSchema } from '../../../../../../lists/common';
|
||||
|
@ -46,6 +55,10 @@ const AdministrationListPage = styled(_AdministrationListPage)`
|
|||
`;
|
||||
|
||||
export const EventFiltersListPage = memo(() => {
|
||||
const history = useHistory();
|
||||
const dispatch = useDispatch<Dispatch<AppAction>>();
|
||||
const isActionError = useEventFiltersSelector(getActionError);
|
||||
const formEntry = useEventFiltersSelector(getFormEntry);
|
||||
const listItems = useEventFiltersSelector(getListItems);
|
||||
const pagination = useEventFiltersSelector(getListPagination);
|
||||
const isLoading = useEventFiltersSelector(getListIsLoading);
|
||||
|
@ -56,6 +69,35 @@ export const EventFiltersListPage = memo(() => {
|
|||
const navigateCallback = useEventFiltersNavigateCallback();
|
||||
const showFlyout = !!location.show;
|
||||
|
||||
// Clean url params if wrong
|
||||
useEffect(() => {
|
||||
if ((location.show === 'edit' && !location.id) || (location.show === 'create' && !!location.id))
|
||||
navigateCallback({
|
||||
show: 'create',
|
||||
id: undefined,
|
||||
});
|
||||
}, [location, navigateCallback]);
|
||||
|
||||
// Catch fetch error -> actionError + empty entry in form
|
||||
useEffect(() => {
|
||||
if (isActionError && !formEntry) {
|
||||
// Replace the current URL route so that user does not keep hitting this page via browser back/fwd buttons
|
||||
history.replace(
|
||||
getEventFiltersListPath({
|
||||
...location,
|
||||
show: undefined,
|
||||
id: undefined,
|
||||
})
|
||||
);
|
||||
dispatch({
|
||||
type: 'eventFiltersFormStateChanged',
|
||||
payload: {
|
||||
type: 'UninitialisedResourceState',
|
||||
},
|
||||
});
|
||||
}
|
||||
}, [dispatch, formEntry, history, isActionError, location, navigateCallback]);
|
||||
|
||||
const handleAddButtonClick = useCallback(
|
||||
() =>
|
||||
navigateCallback({
|
||||
|
@ -65,7 +107,7 @@ export const EventFiltersListPage = memo(() => {
|
|||
[navigateCallback]
|
||||
);
|
||||
|
||||
const handleAddCancelButtonClick = useCallback(
|
||||
const handleCancelButtonClick = useCallback(
|
||||
() =>
|
||||
navigateCallback({
|
||||
show: undefined,
|
||||
|
@ -74,9 +116,15 @@ export const EventFiltersListPage = memo(() => {
|
|||
[navigateCallback]
|
||||
);
|
||||
|
||||
const handleItemEdit: ExceptionItemProps['onEditException'] = useCallback((item) => {
|
||||
// TODO: implement edit item
|
||||
}, []);
|
||||
const handleItemEdit: ExceptionItemProps['onEditException'] = useCallback(
|
||||
(item: ExceptionListItemSchema) => {
|
||||
navigateCallback({
|
||||
show: 'edit',
|
||||
id: item.id,
|
||||
});
|
||||
},
|
||||
[navigateCallback]
|
||||
);
|
||||
|
||||
const handleItemDelete: ExceptionItemProps['onDeleteException'] = useCallback((args) => {
|
||||
// TODO: implement delete item
|
||||
|
@ -136,7 +184,13 @@ export const EventFiltersListPage = memo(() => {
|
|||
)
|
||||
}
|
||||
>
|
||||
{showFlyout && <EventFiltersFlyout onCancel={handleAddCancelButtonClick} />}
|
||||
{showFlyout && (
|
||||
<EventFiltersFlyout
|
||||
onCancel={handleCancelButtonClick}
|
||||
id={location.id}
|
||||
type={location.show}
|
||||
/>
|
||||
)}
|
||||
|
||||
<PaginatedContent<Immutable<ExceptionListItemSchema>, typeof ExceptionItem>
|
||||
items={listItems}
|
||||
|
|
|
@ -11,16 +11,23 @@ import { useHistory } from 'react-router-dom';
|
|||
|
||||
import {
|
||||
isCreationSuccessful,
|
||||
getFormEntry,
|
||||
getCreationError,
|
||||
getFormEntryStateMutable,
|
||||
getActionError,
|
||||
getCurrentLocation,
|
||||
} from '../store/selector';
|
||||
|
||||
import { useToasts } from '../../../../common/lib/kibana';
|
||||
import { getCreationSuccessMessage, getCreationErrorMessage } from './translations';
|
||||
import {
|
||||
getCreationSuccessMessage,
|
||||
getUpdateSuccessMessage,
|
||||
getCreationErrorMessage,
|
||||
getUpdateErrorMessage,
|
||||
getGetErrorMessage,
|
||||
} from './translations';
|
||||
|
||||
import { State } from '../../../../common/store';
|
||||
import { EventFiltersListPageState, EventFiltersPageLocation } from '../state';
|
||||
import { EventFiltersListPageState } from '../state';
|
||||
import { EventFiltersPageLocation } from '../types';
|
||||
import { getEventFiltersListPath } from '../../../common/routing';
|
||||
|
||||
import {
|
||||
|
@ -36,17 +43,27 @@ export function useEventFiltersSelector<R>(selector: (state: EventFiltersListPag
|
|||
|
||||
export const useEventFiltersNotification = () => {
|
||||
const creationSuccessful = useEventFiltersSelector(isCreationSuccessful);
|
||||
const creationError = useEventFiltersSelector(getCreationError);
|
||||
const formEntry = useEventFiltersSelector(getFormEntry);
|
||||
const actionError = useEventFiltersSelector(getActionError);
|
||||
const formEntry = useEventFiltersSelector(getFormEntryStateMutable);
|
||||
const toasts = useToasts();
|
||||
const [wasAlreadyHandled] = useState(new WeakSet());
|
||||
|
||||
if (creationSuccessful && formEntry && !wasAlreadyHandled.has(formEntry)) {
|
||||
wasAlreadyHandled.add(formEntry);
|
||||
toasts.addSuccess(getCreationSuccessMessage(formEntry));
|
||||
} else if (creationError && !wasAlreadyHandled.has(creationError)) {
|
||||
wasAlreadyHandled.add(creationError);
|
||||
toasts.addDanger(getCreationErrorMessage(creationError));
|
||||
if (formEntry.item_id) {
|
||||
toasts.addSuccess(getUpdateSuccessMessage(formEntry));
|
||||
} else {
|
||||
toasts.addSuccess(getCreationSuccessMessage(formEntry));
|
||||
}
|
||||
} else if (actionError && !wasAlreadyHandled.has(actionError)) {
|
||||
wasAlreadyHandled.add(actionError);
|
||||
if (formEntry && formEntry.item_id) {
|
||||
toasts.addDanger(getUpdateErrorMessage(actionError));
|
||||
} else if (formEntry) {
|
||||
toasts.addDanger(getCreationErrorMessage(actionError));
|
||||
} else {
|
||||
toasts.addWarning(getGetErrorMessage(actionError));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -7,21 +7,47 @@
|
|||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import { ExceptionListItemSchema, CreateExceptionListItemSchema } from '../../../../shared_imports';
|
||||
import {
|
||||
CreateExceptionListItemSchema,
|
||||
UpdateExceptionListItemSchema,
|
||||
} from '../../../../shared_imports';
|
||||
import { ServerApiError } from '../../../../common/types';
|
||||
|
||||
export const getCreationSuccessMessage = (
|
||||
entry: CreateExceptionListItemSchema | ExceptionListItemSchema | undefined
|
||||
entry: CreateExceptionListItemSchema | UpdateExceptionListItemSchema | undefined
|
||||
) => {
|
||||
return i18n.translate('xpack.securitySolution.eventFilter.form.successToastTitle', {
|
||||
return i18n.translate('xpack.securitySolution.eventFilter.form.creationSuccessToastTitle', {
|
||||
defaultMessage: '"{name}" has been added to the event exceptions list.',
|
||||
values: { name: entry?.name },
|
||||
});
|
||||
};
|
||||
|
||||
export const getUpdateSuccessMessage = (
|
||||
entry: CreateExceptionListItemSchema | UpdateExceptionListItemSchema | undefined
|
||||
) => {
|
||||
return i18n.translate('xpack.securitySolution.eventFilter.form.updateSuccessToastTitle', {
|
||||
defaultMessage: '"{name}" has been updated successfully.',
|
||||
values: { name: entry?.name },
|
||||
});
|
||||
};
|
||||
|
||||
export const getCreationErrorMessage = (creationError: ServerApiError) => {
|
||||
return i18n.translate('xpack.securitySolution.eventFilter.form.failedToastTitle', {
|
||||
return i18n.translate('xpack.securitySolution.eventFilter.form.failedToastTitle.create', {
|
||||
defaultMessage: 'There was an error creating the new exception: "{error}"',
|
||||
values: { error: creationError.message },
|
||||
});
|
||||
};
|
||||
|
||||
export const getUpdateErrorMessage = (updateError: ServerApiError) => {
|
||||
return i18n.translate('xpack.securitySolution.eventFilter.form.failedToastTitle.update', {
|
||||
defaultMessage: 'There was an error updating the exception: "{error}"',
|
||||
values: { error: updateError.message },
|
||||
});
|
||||
};
|
||||
|
||||
export const getGetErrorMessage = (getError: ServerApiError) => {
|
||||
return i18n.translate('xpack.securitySolution.eventFilter.form.failedToastTitle.get', {
|
||||
defaultMessage: 'Unable to edit trusted application: "{error}"',
|
||||
values: { error: getError.message },
|
||||
});
|
||||
};
|
||||
|
|
|
@ -14,9 +14,19 @@ import { coreMock } from '../../../../../../../../src/core/public/mocks';
|
|||
import { KibanaContextProvider } from '../../../../../../../../src/plugins/kibana_react/public/context';
|
||||
import { CreateExceptionListItemSchema, ExceptionListItemSchema } from '../../../../shared_imports';
|
||||
|
||||
import { createGlobalNoMiddlewareStore, ecsEventMock } from '../test_utils';
|
||||
import {
|
||||
createdEventFilterEntryMock,
|
||||
createGlobalNoMiddlewareStore,
|
||||
ecsEventMock,
|
||||
} from '../test_utils';
|
||||
import { useEventFiltersNotification } from './hooks';
|
||||
import { getCreationErrorMessage, getCreationSuccessMessage } from './translations';
|
||||
import {
|
||||
getCreationErrorMessage,
|
||||
getCreationSuccessMessage,
|
||||
getGetErrorMessage,
|
||||
getUpdateSuccessMessage,
|
||||
getUpdateErrorMessage,
|
||||
} from './translations';
|
||||
import { getInitialExceptionFromEvent } from '../store/utils';
|
||||
import {
|
||||
getLastLoadedResourceState,
|
||||
|
@ -79,6 +89,37 @@ describe('EventFiltersNotification', () => {
|
|||
expect(notifications.toasts.addDanger).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('shows success notification when update successful', () => {
|
||||
const store = createGlobalNoMiddlewareStore();
|
||||
const notifications = mockNotifications();
|
||||
|
||||
renderNotifications(store, notifications);
|
||||
|
||||
act(() => {
|
||||
store.dispatch({
|
||||
type: 'eventFiltersInitForm',
|
||||
payload: { entry: createdEventFilterEntryMock() },
|
||||
});
|
||||
});
|
||||
|
||||
act(() => {
|
||||
store.dispatch({
|
||||
type: 'eventFiltersFormStateChanged',
|
||||
payload: {
|
||||
type: 'LoadedResourceState',
|
||||
data: store.getState()!.management!.eventFilters!.form!.entry as ExceptionListItemSchema,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
expect(notifications.toasts.addSuccess).toBeCalledWith(
|
||||
getUpdateSuccessMessage(
|
||||
store.getState()!.management!.eventFilters!.form!.entry as CreateExceptionListItemSchema
|
||||
)
|
||||
);
|
||||
expect(notifications.toasts.addDanger).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('shows error notification when creation fails', () => {
|
||||
const store = createGlobalNoMiddlewareStore();
|
||||
const notifications = mockNotifications();
|
||||
|
@ -114,4 +155,67 @@ describe('EventFiltersNotification', () => {
|
|||
)
|
||||
);
|
||||
});
|
||||
|
||||
it('shows error notification when update fails', () => {
|
||||
const store = createGlobalNoMiddlewareStore();
|
||||
const notifications = mockNotifications();
|
||||
|
||||
renderNotifications(store, notifications);
|
||||
|
||||
act(() => {
|
||||
store.dispatch({
|
||||
type: 'eventFiltersInitForm',
|
||||
payload: { entry: createdEventFilterEntryMock() },
|
||||
});
|
||||
});
|
||||
|
||||
act(() => {
|
||||
store.dispatch({
|
||||
type: 'eventFiltersFormStateChanged',
|
||||
payload: {
|
||||
type: 'FailedResourceState',
|
||||
error: { message: 'error message', statusCode: 500, error: 'error' },
|
||||
lastLoadedState: getLastLoadedResourceState(
|
||||
store.getState()!.management!.eventFilters!.form!.submissionResourceState
|
||||
),
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
expect(notifications.toasts.addSuccess).not.toBeCalled();
|
||||
expect(notifications.toasts.addDanger).toBeCalledWith(
|
||||
getUpdateErrorMessage(
|
||||
(store.getState()!.management!.eventFilters!.form!
|
||||
.submissionResourceState as FailedResourceState).error
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
it('shows error notification when get fails', () => {
|
||||
const store = createGlobalNoMiddlewareStore();
|
||||
const notifications = mockNotifications();
|
||||
|
||||
renderNotifications(store, notifications);
|
||||
|
||||
act(() => {
|
||||
store.dispatch({
|
||||
type: 'eventFiltersFormStateChanged',
|
||||
payload: {
|
||||
type: 'FailedResourceState',
|
||||
error: { message: 'error message', statusCode: 500, error: 'error' },
|
||||
lastLoadedState: getLastLoadedResourceState(
|
||||
store.getState()!.management!.eventFilters!.form!.submissionResourceState
|
||||
),
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
expect(notifications.toasts.addSuccess).not.toBeCalled();
|
||||
expect(notifications.toasts.addWarning).toBeCalledWith(
|
||||
getGetErrorMessage(
|
||||
(store.getState()!.management!.eventFilters!.form!
|
||||
.submissionResourceState as FailedResourceState).error
|
||||
)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -60,4 +60,5 @@ export {
|
|||
withOptionalSignal,
|
||||
ExceptionBuilder,
|
||||
transformNewItemOutput,
|
||||
transformOutput,
|
||||
} from '../../lists/public';
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue