diff --git a/x-pack/plugins/security_solution/public/common/store/actions.ts b/x-pack/plugins/security_solution/public/common/store/actions.ts index 585fdb98a0323..f1d5e51e172ba 100644 --- a/x-pack/plugins/security_solution/public/common/store/actions.ts +++ b/x-pack/plugins/security_solution/public/common/store/actions.ts @@ -7,7 +7,6 @@ import { EndpointAction } from '../../management/pages/endpoint_hosts/store/action'; import { PolicyDetailsAction } from '../../management/pages/policy/store/policy_details'; -import { EventFiltersPageAction } from '../../management/pages/event_filters/store/action'; export { appActions } from './app'; export { dragAndDropActions } from './drag_and_drop'; @@ -15,8 +14,4 @@ export { inputsActions } from './inputs'; export { sourcererActions } from './sourcerer'; import { RoutingAction } from './routing'; -export type AppAction = - | EndpointAction - | RoutingAction - | PolicyDetailsAction - | EventFiltersPageAction; +export type AppAction = EndpointAction | RoutingAction | PolicyDetailsAction; diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx index 160252f4d11c1..05a91f094ed38 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx @@ -28,7 +28,7 @@ import { TimelineId } from '../../../../../common/types'; import { AlertData, EcsHit } from '../../../../common/components/exceptions/types'; import { useQueryAlerts } from '../../../containers/detection_engine/alerts/use_query'; import { useSignalIndex } from '../../../containers/detection_engine/alerts/use_signal_index'; -import { EventFiltersFlyout } from '../../../../management/pages/event_filters/view/components/flyout'; +import { EventFiltersFlyout } from '../../../../management/pages/event_filters/view/components/event_filters_flyout'; import { useAlertsActions } from './use_alerts_actions'; import { useExceptionFlyout } from './use_add_exception_flyout'; import { useExceptionActions } from './use_add_exception_actions'; diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/index.tsx b/x-pack/plugins/security_solution/public/management/pages/event_filters/index.tsx index 86c2f2364961d..54d18f85b739a 100644 --- a/x-pack/plugins/security_solution/public/management/pages/event_filters/index.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/index.tsx @@ -9,12 +9,12 @@ import { Route, Switch } from 'react-router-dom'; import React from 'react'; import { NotFoundPage } from '../../../app/404'; import { MANAGEMENT_ROUTING_EVENT_FILTERS_PATH } from '../../common/constants'; -import { EventFiltersListPage } from './view/event_filters_list_page'; +import { EventFiltersList } from './view/event_filters_list'; export const EventFiltersContainer = () => { return ( - + ); diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/service/event_filters_api_client.ts b/x-pack/plugins/security_solution/public/management/pages/event_filters/service/api_client.ts similarity index 100% rename from x-pack/plugins/security_solution/public/management/pages/event_filters/service/event_filters_api_client.ts rename to x-pack/plugins/security_solution/public/management/pages/event_filters/service/api_client.ts diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/store/action.ts b/x-pack/plugins/security_solution/public/management/pages/event_filters/store/action.ts deleted file mode 100644 index 4325c4d90951a..0000000000000 --- a/x-pack/plugins/security_solution/public/management/pages/event_filters/store/action.ts +++ /dev/null @@ -1,92 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { Action } from 'redux'; -import type { - ExceptionListItemSchema, - CreateExceptionListItemSchema, - UpdateExceptionListItemSchema, -} from '@kbn/securitysolution-io-ts-list-types'; -import { AsyncResourceState } from '../../../state/async_resource_state'; -import { EventFiltersListPageState } from '../types'; - -export type EventFiltersListPageDataChanged = Action<'eventFiltersListPageDataChanged'> & { - payload: EventFiltersListPageState['listPage']['data']; -}; - -export type EventFiltersListPageDataExistsChanged = - Action<'eventFiltersListPageDataExistsChanged'> & { - payload: EventFiltersListPageState['listPage']['dataExist']; - }; - -export type EventFilterForDeletion = Action<'eventFilterForDeletion'> & { - payload: ExceptionListItemSchema; -}; - -export type EventFilterDeletionReset = Action<'eventFilterDeletionReset'>; - -export type EventFilterDeleteSubmit = Action<'eventFilterDeleteSubmit'>; - -export type EventFilterDeleteStatusChanged = Action<'eventFilterDeleteStatusChanged'> & { - payload: EventFiltersListPageState['listPage']['deletion']['status']; -}; - -export type EventFiltersInitForm = Action<'eventFiltersInitForm'> & { - payload: { - entry: UpdateExceptionListItemSchema | CreateExceptionListItemSchema; - }; -}; - -export type EventFiltersInitFromId = Action<'eventFiltersInitFromId'> & { - payload: { - id: string; - }; -}; - -export type EventFiltersChangeForm = Action<'eventFiltersChangeForm'> & { - payload: { - 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'>; -export type EventFiltersCreateError = Action<'eventFiltersCreateError'>; - -export type EventFiltersFormStateChanged = Action<'eventFiltersFormStateChanged'> & { - payload: AsyncResourceState; -}; - -export type EventFiltersForceRefresh = Action<'eventFiltersForceRefresh'> & { - payload: { - forceRefresh: boolean; - }; -}; - -export type EventFiltersPageAction = - | EventFiltersListPageDataChanged - | EventFiltersListPageDataExistsChanged - | EventFiltersInitForm - | EventFiltersInitFromId - | EventFiltersChangeForm - | EventFiltersUpdateStart - | EventFiltersUpdateSuccess - | EventFiltersCreateStart - | EventFiltersCreateSuccess - | EventFiltersCreateError - | EventFiltersFormStateChanged - | EventFilterForDeletion - | EventFilterDeletionReset - | EventFilterDeleteSubmit - | EventFilterDeleteStatusChanged - | EventFiltersForceRefresh; diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/store/builders.ts b/x-pack/plugins/security_solution/public/management/pages/event_filters/store/builders.ts deleted file mode 100644 index 397a7c2ae1e79..0000000000000 --- a/x-pack/plugins/security_solution/public/management/pages/event_filters/store/builders.ts +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { MANAGEMENT_DEFAULT_PAGE, MANAGEMENT_DEFAULT_PAGE_SIZE } from '../../../common/constants'; -import { EventFiltersListPageState } from '../types'; -import { createUninitialisedResourceState } from '../../../state'; - -export const initialEventFiltersPageState = (): EventFiltersListPageState => ({ - entries: [], - form: { - entry: undefined, - hasNameError: false, - hasItemsError: false, - hasOSError: false, - newComment: '', - submissionResourceState: createUninitialisedResourceState(), - }, - location: { - page_index: MANAGEMENT_DEFAULT_PAGE, - page_size: MANAGEMENT_DEFAULT_PAGE_SIZE, - filter: '', - included_policies: '', - }, - listPage: { - active: false, - forceRefresh: false, - data: createUninitialisedResourceState(), - dataExist: createUninitialisedResourceState(), - deletion: { - item: undefined, - status: createUninitialisedResourceState(), - }, - }, -}); diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/store/middleware.test.ts b/x-pack/plugins/security_solution/public/management/pages/event_filters/store/middleware.test.ts deleted file mode 100644 index 9ec7e84d693fd..0000000000000 --- a/x-pack/plugins/security_solution/public/management/pages/event_filters/store/middleware.test.ts +++ /dev/null @@ -1,387 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { applyMiddleware, createStore, Store } from 'redux'; - -import { - createSpyMiddleware, - MiddlewareActionSpyHelper, -} from '../../../../common/store/test_utils'; -import { AppAction } from '../../../../common/store/actions'; -import { createEventFiltersPageMiddleware } from './middleware'; -import { eventFiltersPageReducer } from './reducer'; - -import { initialEventFiltersPageState } from './builders'; -import { getInitialExceptionFromEvent } from './utils'; -import { createdEventFilterEntryMock, ecsEventMock } from '../test_utils'; -import { EventFiltersListPageState, EventFiltersService } from '../types'; -import { getFoundExceptionListItemSchemaMock } from '@kbn/lists-plugin/common/schemas/response/found_exception_list_item_schema.mock'; -import { isFailedResourceState, isLoadedResourceState } from '../../../state'; -import { getListFetchError } from './selector'; -import type { - ExceptionListItemSchema, - CreateExceptionListItemSchema, -} from '@kbn/securitysolution-io-ts-list-types'; -import { Immutable } from '../../../../../common/endpoint/types'; -import { parsePoliciesAndFilterToKql } from '../../../common/utils'; - -const createEventFiltersServiceMock = (): jest.Mocked => ({ - addEventFilters: jest.fn(), - getList: jest.fn(), - getOne: jest.fn(), - updateOne: jest.fn(), - deleteOne: jest.fn(), - getSummary: jest.fn(), -}); - -const createStoreSetup = (eventFiltersService: EventFiltersService) => { - const spyMiddleware = createSpyMiddleware(); - - return { - spyMiddleware, - store: createStore( - eventFiltersPageReducer, - applyMiddleware( - createEventFiltersPageMiddleware(eventFiltersService), - spyMiddleware.actionSpyMiddleware - ) - ), - }; -}; - -describe('Event filters middleware', () => { - let service: jest.Mocked; - let store: Store; - let spyMiddleware: MiddlewareActionSpyHelper; - let initialState: EventFiltersListPageState; - - beforeEach(() => { - initialState = initialEventFiltersPageState(); - service = createEventFiltersServiceMock(); - - const storeSetup = createStoreSetup(service); - - store = storeSetup.store as Store; - spyMiddleware = storeSetup.spyMiddleware; - }); - - describe('initial state', () => { - it('sets initial state properly', async () => { - expect(createStoreSetup(createEventFiltersServiceMock()).store.getState()).toStrictEqual( - initialState - ); - }); - }); - - describe('when on the List page', () => { - const changeUrl = (searchParams: string = '') => { - store.dispatch({ - type: 'userChangedUrl', - payload: { - pathname: '/administration/event_filters', - search: searchParams, - hash: '', - key: 'ylsd7h', - }, - }); - }; - - beforeEach(() => { - service.getList.mockResolvedValue(getFoundExceptionListItemSchemaMock()); - }); - - it.each([ - [undefined, undefined, undefined], - [3, 50, ['1', '2']], - ])( - 'should trigger api call to retrieve event filters with url params page_index[%s] page_size[%s] included_policies[%s]', - async (pageIndex, perPage, policies) => { - const dataLoaded = spyMiddleware.waitForAction('eventFiltersListPageDataChanged', { - validate({ payload }) { - return isLoadedResourceState(payload); - }, - }); - - changeUrl( - (pageIndex && - perPage && - `?page_index=${pageIndex}&page_size=${perPage}&included_policies=${policies}`) || - '' - ); - await dataLoaded; - - expect(service.getList).toHaveBeenCalledWith({ - page: (pageIndex ?? 0) + 1, - perPage: perPage ?? 10, - sortField: 'created_at', - sortOrder: 'desc', - filter: policies ? parsePoliciesAndFilterToKql({ policies }) : undefined, - }); - } - ); - - it('should not refresh the list if nothing in the query has changed', async () => { - const dataLoaded = spyMiddleware.waitForAction('eventFiltersListPageDataChanged', { - validate({ payload }) { - return isLoadedResourceState(payload); - }, - }); - - changeUrl(); - await dataLoaded; - const getListCallCount = service.getList.mock.calls.length; - changeUrl('&show=create'); - - expect(service.getList.mock.calls.length).toBe(getListCallCount); - }); - - it('should trigger second api call to check if data exists if first returned no records', async () => { - const dataLoaded = spyMiddleware.waitForAction('eventFiltersListPageDataExistsChanged', { - validate({ payload }) { - return isLoadedResourceState(payload); - }, - }); - - service.getList.mockResolvedValue({ - data: [], - total: 0, - page: 1, - per_page: 10, - }); - - changeUrl(); - await dataLoaded; - - expect(service.getList).toHaveBeenCalledTimes(2); - expect(service.getList).toHaveBeenNthCalledWith(2, { - page: 1, - perPage: 1, - }); - }); - - it('should dispatch a Failure if an API error was encountered', async () => { - const dataLoaded = spyMiddleware.waitForAction('eventFiltersListPageDataChanged', { - validate({ payload }) { - return isFailedResourceState(payload); - }, - }); - - service.getList.mockRejectedValue({ - body: { message: 'error message', statusCode: 500, error: 'Internal Server Error' }, - }); - - changeUrl(); - await dataLoaded; - - expect(getListFetchError(store.getState())).toEqual({ - message: 'error message', - statusCode: 500, - error: 'Internal Server Error', - }); - }); - }); - - describe('submit creation event filter', () => { - it('does not submit when entry is undefined', async () => { - store.dispatch({ type: 'eventFiltersCreateStart' }); - expect(store.getState()).toStrictEqual({ - ...initialState, - form: { - ...store.getState().form, - submissionResourceState: { type: 'UninitialisedResourceState' }, - }, - }); - }); - - it('does submit when entry is not undefined', async () => { - service.addEventFilters.mockResolvedValue(createdEventFilterEntryMock()); - const entry = getInitialExceptionFromEvent(ecsEventMock()); - store.dispatch({ - type: 'eventFiltersInitForm', - payload: { entry }, - }); - - store.dispatch({ type: 'eventFiltersCreateStart' }); - - await spyMiddleware.waitForAction('eventFiltersFormStateChanged'); - expect(store.getState()).toStrictEqual({ - ...initialState, - form: { - ...store.getState().form, - submissionResourceState: { - type: 'LoadedResourceState', - data: createdEventFilterEntryMock(), - }, - }, - }); - }); - - it('does submit when entry has empty comments with white spaces', async () => { - service.addEventFilters.mockImplementation( - async (exception: Immutable) => { - expect(exception.comments).toStrictEqual(createdEventFilterEntryMock().comments); - return createdEventFilterEntryMock(); - } - ); - const entry = getInitialExceptionFromEvent(ecsEventMock()); - store.dispatch({ - type: 'eventFiltersInitForm', - payload: { entry }, - }); - - store.dispatch({ - type: 'eventFiltersChangeForm', - payload: { newComment: ' ', entry }, - }); - - store.dispatch({ type: 'eventFiltersCreateStart' }); - 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.addEventFilters.mockRejectedValue({ - body: { message: 'error message', statusCode: 500, error: 'Internal Server Error' }, - }); - const entry = getInitialExceptionFromEvent(ecsEventMock()); - store.dispatch({ - type: 'eventFiltersInitForm', - payload: { entry }, - }); - - 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, - form: { - ...store.getState().form, - submissionResourceState: { - type: 'FailedResourceState', - lastLoadedState: undefined, - error: { - error: 'Internal Server Error', - message: 'error message', - statusCode: 500, - }, - }, - }, - }); - }); - }); -}); diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/store/middleware.ts b/x-pack/plugins/security_solution/public/management/pages/event_filters/store/middleware.ts deleted file mode 100644 index a8bf725e61b2a..0000000000000 --- a/x-pack/plugins/security_solution/public/management/pages/event_filters/store/middleware.ts +++ /dev/null @@ -1,342 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { - CreateExceptionListItemSchema, - ExceptionListItemSchema, - UpdateExceptionListItemSchema, -} from '@kbn/securitysolution-io-ts-list-types'; -import { transformNewItemOutput, transformOutput } from '@kbn/securitysolution-list-hooks'; -import { AppAction } from '../../../../common/store/actions'; -import { - ImmutableMiddleware, - ImmutableMiddlewareAPI, - ImmutableMiddlewareFactory, -} from '../../../../common/store'; - -import { EventFiltersHttpService } from '../service'; - -import { - getCurrentListPageDataState, - getCurrentLocation, - getListIsLoading, - getListPageDataExistsState, - getListPageIsActive, - listDataNeedsRefresh, - getFormEntry, - getSubmissionResource, - getNewComment, - isDeletionInProgress, - getItemToDelete, - getDeletionState, -} from './selector'; - -import { parseQueryFilterToKQL, parsePoliciesAndFilterToKql } from '../../../common/utils'; -import { SEARCHABLE_FIELDS } from '../constants'; -import { - EventFiltersListPageData, - EventFiltersListPageState, - EventFiltersService, - EventFiltersServiceGetListOptions, -} from '../types'; -import { - asStaleResourceState, - createFailedResourceState, - createLoadedResourceState, - createLoadingResourceState, - getLastLoadedResourceState, -} from '../../../state'; -import { ServerApiError } from '../../../../common/types'; - -const addNewComments = ( - entry: UpdateExceptionListItemSchema | CreateExceptionListItemSchema, - newComment: string -): UpdateExceptionListItemSchema | CreateExceptionListItemSchema => { - if (newComment) { - if (!entry.comments) entry.comments = []; - const trimmedComment = newComment.trim(); - if (trimmedComment) entry.comments.push({ comment: trimmedComment }); - } - return entry; -}; - -type MiddlewareActionHandler = ( - store: ImmutableMiddlewareAPI, - eventFiltersService: EventFiltersService -) => Promise; - -const eventFiltersCreate: MiddlewareActionHandler = async (store, eventFiltersService) => { - const submissionResourceState = store.getState().form.submissionResourceState; - try { - const formEntry = getFormEntry(store.getState()); - if (!formEntry) return; - store.dispatch({ - type: 'eventFiltersFormStateChanged', - payload: createLoadingResourceState({ - type: 'UninitialisedResourceState', - }), - }); - - const sanitizedEntry = transformNewItemOutput(formEntry as CreateExceptionListItemSchema); - const updatedCommentsEntry = addNewComments( - sanitizedEntry, - getNewComment(store.getState()) - ) as CreateExceptionListItemSchema; - - const exception = await eventFiltersService.addEventFilters(updatedCommentsEntry); - - store.dispatch({ - type: 'eventFiltersFormStateChanged', - payload: { - type: 'LoadedResourceState', - data: exception, - }, - }); - store.dispatch({ - type: 'eventFiltersCreateSuccess', - }); - } catch (error) { - store.dispatch({ - type: 'eventFiltersFormStateChanged', - payload: { - type: 'FailedResourceState', - error: error.body || error, - lastLoadedState: getLastLoadedResourceState(submissionResourceState), - }, - }); - } -}; - -const eventFiltersUpdate = async ( - store: ImmutableMiddlewareAPI, - 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; - - const exception = await eventFiltersService.updateOne(updatedCommentsEntry); - store.dispatch({ - type: 'eventFiltersUpdateSuccess', - }); - store.dispatch({ - type: 'eventFiltersFormStateChanged', - payload: createLoadedResourceState(exception), - }); - } catch (error) { - store.dispatch({ - type: 'eventFiltersFormStateChanged', - payload: createFailedResourceState( - error.body ?? error, - getLastLoadedResourceState(submissionResourceState) - ), - }); - } -}; - -const eventFiltersLoadById = async ( - store: ImmutableMiddlewareAPI, - 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 -) => { - dispatch({ - type: 'eventFiltersListPageDataExistsChanged', - payload: createLoadingResourceState( - asStaleResourceState(getListPageDataExistsState(getState())) - ), - }); - - try { - const anythingInListResults = await eventFiltersService.getList({ perPage: 1, page: 1 }); - - dispatch({ - type: 'eventFiltersListPageDataExistsChanged', - payload: createLoadedResourceState(Boolean(anythingInListResults.total)), - }); - } catch (error) { - dispatch({ - type: 'eventFiltersListPageDataExistsChanged', - payload: createFailedResourceState(error.body ?? error), - }); - } -}; - -const refreshListDataIfNeeded: MiddlewareActionHandler = async (store, eventFiltersService) => { - const { dispatch, getState } = store; - const state = getState(); - const isLoading = getListIsLoading(state); - - if (!isLoading && listDataNeedsRefresh(state)) { - dispatch({ - type: 'eventFiltersListPageDataChanged', - payload: { - type: 'LoadingResourceState', - previousState: asStaleResourceState(getCurrentListPageDataState(state)), - }, - }); - - const { - page_size: pageSize, - page_index: pageIndex, - filter, - included_policies: includedPolicies, - } = getCurrentLocation(state); - - const kuery = parseQueryFilterToKQL(filter, SEARCHABLE_FIELDS) || undefined; - - const query: EventFiltersServiceGetListOptions = { - page: pageIndex + 1, - perPage: pageSize, - sortField: 'created_at', - sortOrder: 'desc', - filter: parsePoliciesAndFilterToKql({ - kuery, - policies: includedPolicies ? includedPolicies.split(',') : [], - }), - }; - - try { - const results = await eventFiltersService.getList(query); - - dispatch({ - type: 'eventFiltersListPageDataChanged', - payload: createLoadedResourceState({ - query: { ...query, filter }, - content: results, - }), - }); - - // 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 - if (results.total === 0) { - await checkIfEventFilterDataExist(store, eventFiltersService); - } else { - dispatch({ - type: 'eventFiltersListPageDataExistsChanged', - payload: { - type: 'LoadedResourceState', - data: Boolean(results.total), - }, - }); - } - } catch (error) { - dispatch({ - type: 'eventFiltersListPageDataChanged', - payload: createFailedResourceState(error.body ?? error), - }); - } - } -}; - -const eventFilterDeleteEntry: MiddlewareActionHandler = async ( - { getState, dispatch }, - eventFiltersService -) => { - const state = getState(); - - if (isDeletionInProgress(state)) { - return; - } - - const itemId = getItemToDelete(state)?.id; - - if (!itemId) { - return; - } - - dispatch({ - type: 'eventFilterDeleteStatusChanged', - payload: createLoadingResourceState(asStaleResourceState(getDeletionState(state).status)), - }); - - try { - const response = await eventFiltersService.deleteOne(itemId); - dispatch({ - type: 'eventFilterDeleteStatusChanged', - payload: createLoadedResourceState(response), - }); - } catch (e) { - dispatch({ - type: 'eventFilterDeleteStatusChanged', - payload: createFailedResourceState(e.body ?? e), - }); - } -}; - -export const createEventFiltersPageMiddleware = ( - eventFiltersService: EventFiltersService -): ImmutableMiddleware => { - return (store) => (next) => async (action) => { - next(action); - - 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' || - action.type === 'eventFiltersUpdateSuccess' || - action.type === 'eventFilterDeleteStatusChanged' - ) { - refreshListDataIfNeeded(store, eventFiltersService); - } else if (action.type === 'eventFilterDeleteSubmit') { - eventFilterDeleteEntry(store, eventFiltersService); - } - } - }; -}; - -export const eventFiltersPageMiddlewareFactory: ImmutableMiddlewareFactory< - EventFiltersListPageState -> = (coreStart) => createEventFiltersPageMiddleware(new EventFiltersHttpService(coreStart.http)); diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/store/reducer.test.ts b/x-pack/plugins/security_solution/public/management/pages/event_filters/store/reducer.test.ts deleted file mode 100644 index 0deb7cb51c850..0000000000000 --- a/x-pack/plugins/security_solution/public/management/pages/event_filters/store/reducer.test.ts +++ /dev/null @@ -1,221 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { initialEventFiltersPageState } from './builders'; -import { eventFiltersPageReducer } from './reducer'; -import { getInitialExceptionFromEvent } from './utils'; -import { createdEventFilterEntryMock, ecsEventMock } from '../test_utils'; -import { UserChangedUrl } from '../../../../common/store/routing/action'; -import { getListPageIsActive } from './selector'; -import { EventFiltersListPageState } from '../types'; - -describe('event filters reducer', () => { - let initialState: EventFiltersListPageState; - - beforeEach(() => { - initialState = initialEventFiltersPageState(); - }); - - describe('EventFiltersForm', () => { - it('sets the initial form values', () => { - const entry = getInitialExceptionFromEvent(ecsEventMock()); - const result = eventFiltersPageReducer(initialState, { - type: 'eventFiltersInitForm', - payload: { entry }, - }); - - expect(result).toStrictEqual({ - ...initialState, - form: { - ...initialState.form, - entry, - hasNameError: !entry.name, - submissionResourceState: { - type: 'UninitialisedResourceState', - }, - }, - }); - }); - - 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 }, newComment }, - }); - - expect(result).toStrictEqual({ - ...initialState, - form: { - ...initialState.form, - entry: { - ...entry, - name: nameChanged, - }, - newComment, - hasNameError: false, - submissionResourceState: { - type: 'UninitialisedResourceState', - }, - }, - }); - }); - - it('change form values without entry', () => { - const newComment = 'new comment'; - const result = eventFiltersPageReducer(initialState, { - type: 'eventFiltersChangeForm', - payload: { newComment }, - }); - - expect(result).toStrictEqual({ - ...initialState, - form: { - ...initialState.form, - newComment, - submissionResourceState: { - type: 'UninitialisedResourceState', - }, - }, - }); - }); - - it('change form status', () => { - const result = eventFiltersPageReducer(initialState, { - type: 'eventFiltersFormStateChanged', - payload: { - type: 'LoadedResourceState', - data: createdEventFilterEntryMock(), - }, - }); - - expect(result).toStrictEqual({ - ...initialState, - form: { - ...initialState.form, - submissionResourceState: { - type: 'LoadedResourceState', - data: createdEventFilterEntryMock(), - }, - }, - }); - }); - - it('clean form after change form status', () => { - const entry = getInitialExceptionFromEvent(ecsEventMock()); - const nameChanged = 'name changed'; - const newComment = 'new comment'; - const result = eventFiltersPageReducer(initialState, { - type: 'eventFiltersChangeForm', - payload: { entry: { ...entry, name: nameChanged }, newComment }, - }); - const cleanState = eventFiltersPageReducer(result, { - type: 'eventFiltersInitForm', - payload: { entry }, - }); - - expect(cleanState).toStrictEqual({ - ...initialState, - form: { ...initialState.form, entry, hasNameError: true, newComment: '' }, - }); - }); - - it('create is success and force list refresh', () => { - const initialStateWithListPageActive = { - ...initialState, - listPage: { ...initialState.listPage, active: true }, - }; - const result = eventFiltersPageReducer(initialStateWithListPageActive, { - type: 'eventFiltersCreateSuccess', - }); - - expect(result).toStrictEqual({ - ...initialStateWithListPageActive, - listPage: { - ...initialStateWithListPageActive.listPage, - forceRefresh: true, - }, - }); - }); - }); - describe('UserChangedUrl', () => { - const userChangedUrlAction = ( - search: string = '', - pathname = '/administration/event_filters' - ): UserChangedUrl => ({ - type: 'userChangedUrl', - payload: { search, pathname, hash: '' }, - }); - - describe('When url is the Event List page', () => { - it('should mark page active when on the list url', () => { - const result = eventFiltersPageReducer(initialState, userChangedUrlAction()); - expect(getListPageIsActive(result)).toBe(true); - }); - - it('should mark page not active when not on the list url', () => { - const result = eventFiltersPageReducer( - initialState, - userChangedUrlAction('', '/some-other-page') - ); - expect(getListPageIsActive(result)).toBe(false); - }); - }); - - describe('When `show=create`', () => { - it('receives a url change with show=create', () => { - const result = eventFiltersPageReducer(initialState, userChangedUrlAction('?show=create')); - - expect(result).toStrictEqual({ - ...initialState, - location: { - ...initialState.location, - id: undefined, - show: 'create', - }, - listPage: { - ...initialState.listPage, - active: true, - }, - }); - }); - }); - }); - - describe('ForceRefresh', () => { - it('sets the force refresh state to true', () => { - const result = eventFiltersPageReducer( - { - ...initialState, - listPage: { ...initialState.listPage, forceRefresh: false }, - }, - { type: 'eventFiltersForceRefresh', payload: { forceRefresh: true } } - ); - - expect(result).toStrictEqual({ - ...initialState, - listPage: { ...initialState.listPage, forceRefresh: true }, - }); - }); - it('sets the force refresh state to false', () => { - const result = eventFiltersPageReducer( - { - ...initialState, - listPage: { ...initialState.listPage, forceRefresh: true }, - }, - { type: 'eventFiltersForceRefresh', payload: { forceRefresh: false } } - ); - - expect(result).toStrictEqual({ - ...initialState, - listPage: { ...initialState.listPage, forceRefresh: false }, - }); - }); - }); -}); diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/store/reducer.ts b/x-pack/plugins/security_solution/public/management/pages/event_filters/store/reducer.ts deleted file mode 100644 index 95b0078f80f8b..0000000000000 --- a/x-pack/plugins/security_solution/public/management/pages/event_filters/store/reducer.ts +++ /dev/null @@ -1,271 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -// eslint-disable-next-line import/no-nodejs-modules -import { parse } from 'querystring'; -import { matchPath } from 'react-router-dom'; -import { ImmutableReducer } from '../../../../common/store'; -import { AppAction } from '../../../../common/store/actions'; -import { AppLocation, Immutable } from '../../../../../common/endpoint/types'; -import { UserChangedUrl } from '../../../../common/store/routing/action'; -import { MANAGEMENT_ROUTING_EVENT_FILTERS_PATH } from '../../../common/constants'; -import { extractEventFiltersPageLocation } from '../../../common/routing'; -import { - isLoadedResourceState, - isUninitialisedResourceState, -} from '../../../state/async_resource_state'; - -import { - EventFiltersInitForm, - EventFiltersChangeForm, - EventFiltersFormStateChanged, - EventFiltersCreateSuccess, - EventFiltersUpdateSuccess, - EventFiltersListPageDataChanged, - EventFiltersListPageDataExistsChanged, - EventFilterForDeletion, - EventFilterDeletionReset, - EventFilterDeleteStatusChanged, - EventFiltersForceRefresh, -} from './action'; - -import { initialEventFiltersPageState } from './builders'; -import { getListPageIsActive } from './selector'; -import { EventFiltersListPageState } from '../types'; - -type StateReducer = ImmutableReducer; -type CaseReducer = ( - state: Immutable, - action: Immutable -) => Immutable; - -const isEventFiltersPageLocation = (location: Immutable) => { - return ( - matchPath(location.pathname ?? '', { - path: MANAGEMENT_ROUTING_EVENT_FILTERS_PATH, - exact: true, - }) !== null - ); -}; - -const handleEventFiltersListPageDataChanges: CaseReducer = ( - state, - action -) => { - return { - ...state, - listPage: { - ...state.listPage, - forceRefresh: false, - data: action.payload, - }, - }; -}; - -const handleEventFiltersListPageDataExistChanges: CaseReducer< - EventFiltersListPageDataExistsChanged -> = (state, action) => { - return { - ...state, - listPage: { - ...state.listPage, - dataExist: action.payload, - }, - }; -}; - -const eventFiltersInitForm: CaseReducer = (state, action) => { - return { - ...state, - form: { - ...state.form, - entry: action.payload.entry, - hasNameError: !action.payload.entry.name, - hasOSError: !action.payload.entry.os_types?.length, - newComment: '', - submissionResourceState: { - type: 'UninitialisedResourceState', - }, - }, - }; -}; - -const eventFiltersChangeForm: CaseReducer = (state, action) => { - return { - ...state, - form: { - ...state.form, - entry: action.payload.entry !== undefined ? action.payload.entry : state.form.entry, - hasItemsError: - action.payload.hasItemsError !== undefined - ? action.payload.hasItemsError - : state.form.hasItemsError, - hasNameError: - action.payload.hasNameError !== undefined - ? action.payload.hasNameError - : 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, - }, - }; -}; - -const eventFiltersFormStateChanged: CaseReducer = (state, action) => { - return { - ...state, - form: { - ...state.form, - entry: isUninitialisedResourceState(action.payload) ? undefined : state.form.entry, - newComment: isUninitialisedResourceState(action.payload) ? '' : state.form.newComment, - submissionResourceState: action.payload, - }, - }; -}; - -const eventFiltersCreateSuccess: CaseReducer = (state, action) => { - return { - ...state, - // 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 = (state, action) => { - return { - ...state, - // If we are on the List page, then force a refresh of data - listPage: getListPageIsActive(state) - ? { - ...state.listPage, - forceRefresh: true, - } - : state.listPage, - }; -}; - -const userChangedUrl: CaseReducer = (state, action) => { - if (isEventFiltersPageLocation(action.payload)) { - const location = extractEventFiltersPageLocation(parse(action.payload.search.slice(1))); - return { - ...state, - location, - listPage: { - ...state.listPage, - active: true, - }, - }; - } else { - // Reset the list page state if needed - if (state.listPage.active) { - const { listPage } = initialEventFiltersPageState(); - - return { - ...state, - listPage, - }; - } - - return state; - } -}; - -const handleEventFilterForDeletion: CaseReducer = (state, action) => { - return { - ...state, - listPage: { - ...state.listPage, - deletion: { - ...state.listPage.deletion, - item: action.payload, - }, - }, - }; -}; - -const handleEventFilterDeletionReset: CaseReducer = (state) => { - return { - ...state, - listPage: { - ...state.listPage, - deletion: initialEventFiltersPageState().listPage.deletion, - }, - }; -}; - -const handleEventFilterDeleteStatusChanges: CaseReducer = ( - state, - action -) => { - return { - ...state, - listPage: { - ...state.listPage, - forceRefresh: isLoadedResourceState(action.payload) ? true : state.listPage.forceRefresh, - deletion: { - ...state.listPage.deletion, - status: action.payload, - }, - }, - }; -}; - -const handleEventFilterForceRefresh: CaseReducer = (state, action) => { - return { - ...state, - listPage: { - ...state.listPage, - forceRefresh: action.payload.forceRefresh, - }, - }; -}; - -export const eventFiltersPageReducer: StateReducer = ( - state = initialEventFiltersPageState(), - action -) => { - switch (action.type) { - case 'eventFiltersInitForm': - return eventFiltersInitForm(state, action); - case 'eventFiltersChangeForm': - return eventFiltersChangeForm(state, action); - case 'eventFiltersFormStateChanged': - return eventFiltersFormStateChanged(state, action); - case 'eventFiltersCreateSuccess': - return eventFiltersCreateSuccess(state, action); - case 'eventFiltersUpdateSuccess': - return eventFiltersUpdateSuccess(state, action); - case 'userChangedUrl': - return userChangedUrl(state, action); - case 'eventFiltersForceRefresh': - return handleEventFilterForceRefresh(state, action); - } - - // actions only handled if we're on the List Page - if (getListPageIsActive(state)) { - switch (action.type) { - case 'eventFiltersListPageDataChanged': - return handleEventFiltersListPageDataChanges(state, action); - case 'eventFiltersListPageDataExistsChanged': - return handleEventFiltersListPageDataExistChanges(state, action); - case 'eventFilterForDeletion': - return handleEventFilterForDeletion(state, action); - case 'eventFilterDeletionReset': - return handleEventFilterDeletionReset(state, action); - case 'eventFilterDeleteStatusChanged': - return handleEventFilterDeleteStatusChanges(state, action); - } - } - - return state; -}; diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/store/selector.ts b/x-pack/plugins/security_solution/public/management/pages/event_filters/store/selector.ts deleted file mode 100644 index 9e5eb5c531b6e..0000000000000 --- a/x-pack/plugins/security_solution/public/management/pages/event_filters/store/selector.ts +++ /dev/null @@ -1,224 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 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 type { - ExceptionListItemSchema, - FoundExceptionListItemSchema, -} from '@kbn/securitysolution-io-ts-list-types'; -import { EventFiltersListPageState, EventFiltersServiceGetListOptions } from '../types'; - -import { ServerApiError } from '../../../../common/types'; -import { - isLoadingResourceState, - isLoadedResourceState, - isFailedResourceState, - isUninitialisedResourceState, - getLastLoadedResourceState, -} from '../../../state/async_resource_state'; -import { - MANAGEMENT_DEFAULT_PAGE_SIZE, - MANAGEMENT_PAGE_SIZE_OPTIONS, -} from '../../../common/constants'; -import { Immutable } from '../../../../../common/endpoint/types'; - -type StoreState = Immutable; -type EventFiltersSelector = (state: StoreState) => T; - -export const getCurrentListPageState: EventFiltersSelector = (state) => { - return state.listPage; -}; - -export const getListPageIsActive: EventFiltersSelector = createSelector( - getCurrentListPageState, - (listPage) => listPage.active -); - -export const getCurrentListPageDataState: EventFiltersSelector = ( - state -) => state.listPage.data; - -/** - * Will return the API response with event filters. If the current state is attempting to load a new - * page of content, then return the previous API response if we have one - */ -export const getListApiSuccessResponse: EventFiltersSelector< - Immutable | undefined -> = createSelector(getCurrentListPageDataState, (listPageData) => { - return getLastLoadedResourceState(listPageData)?.data.content; -}); - -export const getListItems: EventFiltersSelector> = - createSelector(getListApiSuccessResponse, (apiResponseData) => { - return apiResponseData?.data || []; - }); - -export const getTotalCountListItems: EventFiltersSelector> = createSelector( - getListApiSuccessResponse, - (apiResponseData) => { - return apiResponseData?.total || 0; - } -); - -/** - * Will return the query that was used with the currently displayed list of content. If a new page - * of content is being loaded, this selector will then attempt to use the previousState to return - * the query used. - */ -export const getCurrentListItemsQuery: EventFiltersSelector = - createSelector(getCurrentListPageDataState, (pageDataState) => { - return getLastLoadedResourceState(pageDataState)?.data.query ?? {}; - }); - -export const getListPagination: EventFiltersSelector = 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 getListFetchError: EventFiltersSelector | undefined> = - createSelector(getCurrentListPageDataState, (listPageDataState) => { - return (isFailedResourceState(listPageDataState) && listPageDataState.error) || undefined; - }); - -export const getListPageDataExistsState: EventFiltersSelector< - StoreState['listPage']['dataExist'] -> = ({ listPage: { dataExist } }) => dataExist; - -export const getListIsLoading: EventFiltersSelector = createSelector( - getCurrentListPageDataState, - getListPageDataExistsState, - (listDataState, dataExists) => - isLoadingResourceState(listDataState) || isLoadingResourceState(dataExists) -); - -export const getListPageDoesDataExist: EventFiltersSelector = createSelector( - getListPageDataExistsState, - (dataExistsState) => { - return !!getLastLoadedResourceState(dataExistsState)?.data; - } -); - -export const getFormEntryState: EventFiltersSelector = (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 = ( - 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; -}; - -export const isCreationInProgress = (state: EventFiltersListPageState): boolean => { - return isLoadingResourceState(state.form.submissionResourceState); -}; - -export const isCreationSuccessful = (state: EventFiltersListPageState): boolean => { - return isLoadedResourceState(state.form.submissionResourceState); -}; - -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 = (state) => - state.location; - -/** Compares the URL param values to the values used in the last data query */ -export const listDataNeedsRefresh: EventFiltersSelector = createSelector( - getCurrentLocation, - getCurrentListItemsQuery, - (state) => state.listPage.forceRefresh, - (location, currentQuery, forceRefresh) => { - return ( - forceRefresh || - location.page_index + 1 !== currentQuery.page || - location.page_size !== currentQuery.perPage - ); - } -); - -export const getDeletionState = createSelector( - getCurrentListPageState, - (listState) => listState.deletion -); - -export const showDeleteModal: EventFiltersSelector = createSelector( - getDeletionState, - ({ item }) => { - return Boolean(item); - } -); - -export const getItemToDelete: EventFiltersSelector = - createSelector(getDeletionState, ({ item }) => item); - -export const isDeletionInProgress: EventFiltersSelector = createSelector( - getDeletionState, - ({ status }) => { - return isLoadingResourceState(status); - } -); - -export const wasDeletionSuccessful: EventFiltersSelector = createSelector( - getDeletionState, - ({ status }) => { - return isLoadedResourceState(status); - } -); - -export const getDeleteError: EventFiltersSelector = createSelector( - getDeletionState, - ({ status }) => { - if (isFailedResourceState(status)) { - return status.error; - } - } -); diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/store/selectors.test.ts b/x-pack/plugins/security_solution/public/management/pages/event_filters/store/selectors.test.ts deleted file mode 100644 index fa3a519bc1908..0000000000000 --- a/x-pack/plugins/security_solution/public/management/pages/event_filters/store/selectors.test.ts +++ /dev/null @@ -1,391 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { initialEventFiltersPageState } from './builders'; -import { - getFormEntry, - getFormHasError, - getCurrentLocation, - getNewComment, - getHasNameError, - getCurrentListPageState, - getListPageIsActive, - getCurrentListPageDataState, - getListApiSuccessResponse, - getListItems, - getTotalCountListItems, - getCurrentListItemsQuery, - getListPagination, - getListFetchError, - getListIsLoading, - getListPageDoesDataExist, - listDataNeedsRefresh, -} from './selector'; -import { ecsEventMock } from '../test_utils'; -import { getInitialExceptionFromEvent } from './utils'; -import { EventFiltersListPageState, EventFiltersPageLocation } from '../types'; -import { MANAGEMENT_DEFAULT_PAGE, MANAGEMENT_DEFAULT_PAGE_SIZE } from '../../../common/constants'; -import { getFoundExceptionListItemSchemaMock } from '@kbn/lists-plugin/common/schemas/response/found_exception_list_item_schema.mock'; -import { - asStaleResourceState, - createFailedResourceState, - createLoadedResourceState, - createLoadingResourceState, - createUninitialisedResourceState, - getLastLoadedResourceState, -} from '../../../state'; - -describe('event filters selectors', () => { - let initialState: EventFiltersListPageState; - - // When `setToLoadingState()` is called, this variable will hold the prevousState in order to - // avoid ts-ignores due to know issues (#830) around the LoadingResourceState - let previousStateWhileLoading: EventFiltersListPageState['listPage']['data'] | undefined; - - const setToLoadedState = () => { - initialState.listPage.data = createLoadedResourceState({ - query: { page: 2, perPage: 10, filter: '' }, - content: getFoundExceptionListItemSchemaMock(), - }); - }; - - const setToLoadingState = ( - previousState: EventFiltersListPageState['listPage']['data'] = createLoadedResourceState({ - query: { page: 5, perPage: 50, filter: '' }, - content: getFoundExceptionListItemSchemaMock(), - }) - ) => { - previousStateWhileLoading = previousState; - - initialState.listPage.data = createLoadingResourceState(asStaleResourceState(previousState)); - }; - - beforeEach(() => { - initialState = initialEventFiltersPageState(); - }); - - describe('getCurrentListPageState()', () => { - it('should retrieve list page state', () => { - expect(getCurrentListPageState(initialState)).toEqual(initialState.listPage); - }); - }); - - describe('getListPageIsActive()', () => { - it('should return active state', () => { - expect(getListPageIsActive(initialState)).toBe(false); - }); - }); - - describe('getCurrentListPageDataState()', () => { - it('should return list data state', () => { - expect(getCurrentListPageDataState(initialState)).toEqual(initialState.listPage.data); - }); - }); - - describe('getListApiSuccessResponse()', () => { - it('should return api response', () => { - setToLoadedState(); - expect(getListApiSuccessResponse(initialState)).toEqual( - getLastLoadedResourceState(initialState.listPage.data)?.data.content - ); - }); - - it('should return undefined if not available', () => { - setToLoadingState(createUninitialisedResourceState()); - expect(getListApiSuccessResponse(initialState)).toBeUndefined(); - }); - - it('should return previous success response if currently loading', () => { - setToLoadingState(); - expect(getListApiSuccessResponse(initialState)).toEqual( - getLastLoadedResourceState(previousStateWhileLoading!)?.data.content - ); - }); - }); - - describe('getListItems()', () => { - it('should return the list items from api response', () => { - setToLoadedState(); - expect(getListItems(initialState)).toEqual( - getLastLoadedResourceState(initialState.listPage.data)?.data.content.data - ); - }); - - it('should return empty array if no api response', () => { - expect(getListItems(initialState)).toEqual([]); - }); - }); - - describe('getTotalCountListItems()', () => { - it('should return the list items from api response', () => { - setToLoadedState(); - expect(getTotalCountListItems(initialState)).toEqual( - getLastLoadedResourceState(initialState.listPage.data)?.data.content.total - ); - }); - - it('should return empty array if no api response', () => { - expect(getTotalCountListItems(initialState)).toEqual(0); - }); - }); - - describe('getCurrentListItemsQuery()', () => { - it('should return empty object if Uninitialized', () => { - expect(getCurrentListItemsQuery(initialState)).toEqual({}); - }); - - it('should return query from current loaded state', () => { - setToLoadedState(); - expect(getCurrentListItemsQuery(initialState)).toEqual({ page: 2, perPage: 10, filter: '' }); - }); - - it('should return query from previous state while Loading new page', () => { - setToLoadingState(); - expect(getCurrentListItemsQuery(initialState)).toEqual({ page: 5, perPage: 50, filter: '' }); - }); - }); - - describe('getListPagination()', () => { - it('should return pagination defaults if no API response is available', () => { - expect(getListPagination(initialState)).toEqual({ - totalItemCount: 0, - pageSize: 10, - pageSizeOptions: [10, 20, 50], - pageIndex: 0, - }); - }); - - it('should return pagination based on API response', () => { - setToLoadedState(); - expect(getListPagination(initialState)).toEqual({ - totalItemCount: 1, - pageSize: 1, - pageSizeOptions: [10, 20, 50], - pageIndex: 0, - }); - }); - }); - - describe('getListFetchError()', () => { - it('should return undefined if no error exists', () => { - expect(getListFetchError(initialState)).toBeUndefined(); - }); - - it('should return the API error', () => { - const error = { - statusCode: 500, - error: 'Internal Server Error', - message: 'Something is not right', - }; - - initialState.listPage.data = createFailedResourceState(error); - expect(getListFetchError(initialState)).toBe(error); - }); - }); - - describe('getListIsLoading()', () => { - it('should return false if not in a Loading state', () => { - expect(getListIsLoading(initialState)).toBe(false); - }); - - it('should return true if in a Loading state', () => { - setToLoadingState(); - expect(getListIsLoading(initialState)).toBe(true); - }); - }); - - describe('getListPageDoesDataExist()', () => { - it('should return false (default) until we get a Loaded Resource state', () => { - expect(getListPageDoesDataExist(initialState)).toBe(false); - - // Set DataExists to Loading - initialState.listPage.dataExist = createLoadingResourceState( - asStaleResourceState(initialState.listPage.dataExist) - ); - expect(getListPageDoesDataExist(initialState)).toBe(false); - - // Set DataExists to Failure - initialState.listPage.dataExist = createFailedResourceState({ - statusCode: 500, - error: 'Internal Server Error', - message: 'Something is not right', - }); - expect(getListPageDoesDataExist(initialState)).toBe(false); - }); - - it('should return false if no data exists', () => { - initialState.listPage.dataExist = createLoadedResourceState(false); - expect(getListPageDoesDataExist(initialState)).toBe(false); - }); - }); - - describe('listDataNeedsRefresh()', () => { - beforeEach(() => { - setToLoadedState(); - - initialState.location = { - page_index: 1, - page_size: 10, - filter: '', - id: '', - show: undefined, - included_policies: '', - }; - }); - - it('should return false if location url params match those that were used in api call', () => { - expect(listDataNeedsRefresh(initialState)).toBe(false); - }); - - it('should return true if `forceRefresh` is set', () => { - initialState.listPage.forceRefresh = true; - expect(listDataNeedsRefresh(initialState)).toBe(true); - }); - - it('should should return true if any of the url params differ from last api call', () => { - initialState.location.page_index = 10; - expect(listDataNeedsRefresh(initialState)).toBe(true); - }); - }); - - describe('getFormEntry()', () => { - it('returns undefined when there is no entry', () => { - expect(getFormEntry(initialState)).toBe(undefined); - }); - it('returns entry when there is an entry on form', () => { - const entry = getInitialExceptionFromEvent(ecsEventMock()); - const state = { - ...initialState, - form: { - ...initialState.form, - entry, - }, - }; - 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(); - }); - it('returns true when entry with name error', () => { - const state = { - ...initialState, - form: { - ...initialState.form, - hasNameError: true, - }, - }; - expect(getFormHasError(state)).toBeTruthy(); - }); - it('returns true when entry with item error', () => { - const state = { - ...initialState, - form: { - ...initialState.form, - hasItemsError: true, - }, - }; - expect(getFormHasError(state)).toBeTruthy(); - }); - it('returns true when entry with os error', () => { - const state = { - ...initialState, - form: { - ...initialState.form, - hasOSError: true, - }, - }; - expect(getFormHasError(state)).toBeTruthy(); - }); - it('returns true when entry with item error, name error and os error', () => { - const state = { - ...initialState, - form: { - ...initialState.form, - hasItemsError: true, - hasNameError: true, - hasOSError: true, - }, - }; - expect(getFormHasError(state)).toBeTruthy(); - }); - - it('returns false when entry without errors', () => { - const state = { - ...initialState, - form: { - ...initialState.form, - hasItemsError: false, - hasNameError: false, - hasOSError: false, - }, - }; - expect(getFormHasError(state)).toBeFalsy(); - }); - }); - describe('getCurrentLocation()', () => { - it('returns current locations', () => { - const expectedLocation: EventFiltersPageLocation = { - show: 'create', - page_index: MANAGEMENT_DEFAULT_PAGE, - page_size: MANAGEMENT_DEFAULT_PAGE_SIZE, - filter: 'filter', - included_policies: '1', - }; - const state = { - ...initialState, - location: expectedLocation, - }; - 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(''); - }); - }); -}); diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/test_utils/index.ts b/x-pack/plugins/security_solution/public/management/pages/event_filters/test_utils/index.ts index 398b3d9fa6d37..6edff2d89c416 100644 --- a/x-pack/plugins/security_solution/public/management/pages/event_filters/test_utils/index.ts +++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/test_utils/index.ts @@ -5,7 +5,6 @@ * 2.0. */ -import { combineReducers, createStore } from 'redux'; import type { FoundExceptionListItemSchema, ExceptionListItemSchema, @@ -17,27 +16,11 @@ import { getExceptionListItemSchemaMock } from '@kbn/lists-plugin/common/schemas import { getSummaryExceptionListSchemaMock } from '@kbn/lists-plugin/common/schemas/response/exception_list_summary_schema.mock'; import { Ecs } from '../../../../../common/ecs'; -import { - MANAGEMENT_STORE_GLOBAL_NAMESPACE, - MANAGEMENT_STORE_EVENT_FILTERS_NAMESPACE, -} from '../../../common/constants'; - -import { eventFiltersPageReducer } from '../store/reducer'; import { httpHandlerMockFactory, ResponseProvidersInterface, } from '../../../../common/mock/endpoint/http_handler_mock_factory'; -export const createGlobalNoMiddlewareStore = () => { - return createStore( - combineReducers({ - [MANAGEMENT_STORE_GLOBAL_NAMESPACE]: combineReducers({ - [MANAGEMENT_STORE_EVENT_FILTERS_NAMESPACE]: eventFiltersPageReducer, - }), - }) - ); -}; - export const ecsEventMock = (): Ecs => ({ _id: 'unLfz3gB2mJZsMY3ytx3', timestamp: '2021-04-14T15:34:15.330Z', @@ -206,6 +189,8 @@ export const esResponseData = () => ({ ], }, }, + indexFields: [], + indicesExist: [], isPartial: false, isRunning: false, total: 1, diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/types.ts b/x-pack/plugins/security_solution/public/management/pages/event_filters/types.ts index f15bd47e0f3e7..b6a7c3b555daa 100644 --- a/x-pack/plugins/security_solution/public/management/pages/event_filters/types.ts +++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/types.ts @@ -12,7 +12,6 @@ import type { UpdateExceptionListItemSchema, ExceptionListSummarySchema, } from '@kbn/securitysolution-io-ts-list-types'; -import { AsyncResourceState } from '../../state/async_resource_state'; import { Immutable } from '../../../../common/endpoint/types'; export interface EventFiltersPageLocation { @@ -25,15 +24,6 @@ export interface EventFiltersPageLocation { included_policies: string; } -export interface EventFiltersForm { - entry: UpdateExceptionListItemSchema | CreateExceptionListItemSchema | undefined; - newComment: string; - hasNameError: boolean; - hasItemsError: boolean; - hasOSError: boolean; - submissionResourceState: AsyncResourceState; -} - export type EventFiltersServiceGetListOptions = Partial<{ page: number; perPage: number; @@ -60,22 +50,3 @@ export interface EventFiltersListPageData { /** The data retrieved from the API */ content: FoundExceptionListItemSchema; } - -export interface EventFiltersListPageState { - entries: ExceptionListItemSchema[]; - form: EventFiltersForm; - location: EventFiltersPageLocation; - /** State for the Event Filters List page */ - listPage: { - active: boolean; - forceRefresh: boolean; - data: AsyncResourceState; - /** tracks if the overall list (not filtered or with invalid page numbers) contains data */ - dataExist: AsyncResourceState; - /** state for deletion of items from the list */ - deletion: { - item: ExceptionListItemSchema | undefined; - status: AsyncResourceState; - }; - }; -} diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/empty/index.tsx b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/empty/index.tsx deleted file mode 100644 index e48d4f8fb4d21..0000000000000 --- a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/empty/index.tsx +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { memo } from 'react'; -import styled, { css } from 'styled-components'; -import { EuiButton, EuiEmptyPrompt } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n-react'; -import { ManagementEmptyStateWrapper } from '../../../../../components/management_empty_state_wrapper'; - -const EmptyPrompt = styled(EuiEmptyPrompt)` - ${() => css` - max-width: 100%; - `} -`; - -export const EventFiltersListEmptyState = memo<{ - onAdd: () => void; - /** Should the Add button be disabled */ - isAddDisabled?: boolean; - backComponent?: React.ReactNode; -}>(({ onAdd, isAddDisabled = false, backComponent }) => { - return ( - - - - - } - body={ - - } - actions={[ - - - , - ...(backComponent ? [backComponent] : []), - ]} - /> - - ); -}); - -EventFiltersListEmptyState.displayName = 'EventFiltersListEmptyState'; diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/event_filter_delete_modal.test.tsx b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/event_filter_delete_modal.test.tsx deleted file mode 100644 index 9e245e5c8214e..0000000000000 --- a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/event_filter_delete_modal.test.tsx +++ /dev/null @@ -1,177 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { - AppContextTestRender, - createAppRootMockRenderer, -} from '../../../../../common/mock/endpoint'; -import { act } from '@testing-library/react'; -import { fireEvent } from '@testing-library/dom'; -import { EventFilterDeleteModal } from './event_filter_delete_modal'; -import { getExceptionListItemSchemaMock } from '@kbn/lists-plugin/common/schemas/response/exception_list_item_schema.mock'; -import { showDeleteModal } from '../../store/selector'; -import { isFailedResourceState, isLoadedResourceState } from '../../../../state'; -import { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; - -describe('When event filters delete modal is shown', () => { - let renderAndSetup: ( - customEventFilterProps?: Partial - ) => Promise>; - let renderResult: ReturnType; - let coreStart: AppContextTestRender['coreStart']; - let history: AppContextTestRender['history']; - let waitForAction: AppContextTestRender['middlewareSpy']['waitForAction']; - let store: AppContextTestRender['store']; - - const getConfirmButton = () => - renderResult.baseElement.querySelector( - '[data-test-subj="eventFilterDeleteModalConfirmButton"]' - ) as HTMLButtonElement; - - const getCancelButton = () => - renderResult.baseElement.querySelector( - '[data-test-subj="eventFilterDeleteModalCancelButton"]' - ) as HTMLButtonElement; - - const getCurrentState = () => store.getState().management.eventFilters; - - beforeEach(() => { - const mockedContext = createAppRootMockRenderer(); - - ({ history, store, coreStart } = mockedContext); - renderAndSetup = async (customEventFilterProps) => { - renderResult = mockedContext.render(); - - await act(async () => { - history.push('/administration/event_filters'); - - await waitForAction('userChangedUrl'); - - mockedContext.store.dispatch({ - type: 'eventFilterForDeletion', - payload: getExceptionListItemSchemaMock({ - id: '123', - name: 'tic-tac-toe', - tags: [], - ...(customEventFilterProps ? customEventFilterProps : {}), - }), - }); - }); - - return renderResult; - }; - - waitForAction = mockedContext.middlewareSpy.waitForAction; - }); - - it("should display calllout when it's assigned to one policy", async () => { - await renderAndSetup({ tags: ['policy:1'] }); - expect(renderResult.getByTestId('eventFilterDeleteModalCalloutMessage').textContent).toMatch( - /Deleting this entry will remove it from 1 associated policy./ - ); - }); - - it("should display calllout when it's assigned to more than one policy", async () => { - await renderAndSetup({ tags: ['policy:1', 'policy:2', 'policy:3'] }); - expect(renderResult.getByTestId('eventFilterDeleteModalCalloutMessage').textContent).toMatch( - /Deleting this entry will remove it from 3 associated policies./ - ); - }); - - it("should display calllout when it's assigned globally", async () => { - await renderAndSetup({ tags: ['policy:all'] }); - expect(renderResult.getByTestId('eventFilterDeleteModalCalloutMessage').textContent).toMatch( - /Deleting this entry will remove it from all associated policies./ - ); - }); - - it("should display calllout when it's unassigned", async () => { - await renderAndSetup({ tags: [] }); - expect(renderResult.getByTestId('eventFilterDeleteModalCalloutMessage').textContent).toMatch( - /Deleting this entry will remove it from 0 associated policies./ - ); - }); - - it('should close dialog if cancel button is clicked', async () => { - await renderAndSetup(); - act(() => { - fireEvent.click(getCancelButton()); - }); - - expect(showDeleteModal(getCurrentState())).toBe(false); - }); - - it('should close dialog if the close X button is clicked', async () => { - await renderAndSetup(); - const dialogCloseButton = renderResult.baseElement.querySelector( - '[aria-label="Closes this modal window"]' - )!; - act(() => { - fireEvent.click(dialogCloseButton); - }); - - expect(showDeleteModal(getCurrentState())).toBe(false); - }); - - it('should disable action buttons when confirmed', async () => { - await renderAndSetup(); - act(() => { - fireEvent.click(getConfirmButton()); - }); - - expect(getCancelButton().disabled).toBe(true); - expect(getConfirmButton().disabled).toBe(true); - }); - - it('should set confirm button to loading', async () => { - await renderAndSetup(); - act(() => { - fireEvent.click(getConfirmButton()); - }); - - expect(getConfirmButton().querySelector('.euiLoadingSpinner')).not.toBeNull(); - }); - - it('should show success toast', async () => { - await renderAndSetup(); - const updateCompleted = waitForAction('eventFilterDeleteStatusChanged', { - validate(action) { - return isLoadedResourceState(action.payload); - }, - }); - - await act(async () => { - fireEvent.click(getConfirmButton()); - await updateCompleted; - }); - - expect(coreStart.notifications.toasts.addSuccess).toHaveBeenCalledWith( - '"tic-tac-toe" has been removed from the event filters list.' - ); - }); - - it('should show error toast if error is countered', async () => { - coreStart.http.delete.mockRejectedValue(new Error('oh oh')); - await renderAndSetup(); - const updateFailure = waitForAction('eventFilterDeleteStatusChanged', { - validate(action) { - return isFailedResourceState(action.payload); - }, - }); - - await act(async () => { - fireEvent.click(getConfirmButton()); - await updateFailure; - }); - - expect(coreStart.notifications.toasts.addDanger).toHaveBeenCalledWith( - 'Unable to remove "tic-tac-toe" from the event filters list. Reason: oh oh' - ); - expect(showDeleteModal(getCurrentState())).toBe(true); - }); -}); diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/event_filter_delete_modal.tsx b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/event_filter_delete_modal.tsx deleted file mode 100644 index 75e49bf270bab..0000000000000 --- a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/event_filter_delete_modal.tsx +++ /dev/null @@ -1,159 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { - EuiButtonEmpty, - EuiCallOut, - EuiModal, - EuiModalBody, - EuiModalFooter, - EuiModalHeader, - EuiModalHeaderTitle, - EuiSpacer, - EuiText, -} from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n-react'; -import React, { memo, useCallback, useEffect } from 'react'; -import { useDispatch } from 'react-redux'; -import { Dispatch } from 'redux'; -import { AutoFocusButton } from '../../../../../common/components/autofocus_button/autofocus_button'; -import { useToasts } from '../../../../../common/lib/kibana'; -import { AppAction } from '../../../../../common/store/actions'; -import { - getArtifactPoliciesIdByTag, - isGlobalPolicyEffected, -} from '../../../../components/effected_policy_select/utils'; -import { - getDeleteError, - getItemToDelete, - isDeletionInProgress, - wasDeletionSuccessful, -} from '../../store/selector'; -import { useEventFiltersSelector } from '../hooks'; - -export const EventFilterDeleteModal = memo<{}>(() => { - const dispatch = useDispatch>(); - const toasts = useToasts(); - - const isDeleting = useEventFiltersSelector(isDeletionInProgress); - const eventFilter = useEventFiltersSelector(getItemToDelete); - const wasDeleted = useEventFiltersSelector(wasDeletionSuccessful); - const deleteError = useEventFiltersSelector(getDeleteError); - - const onCancel = useCallback(() => { - dispatch({ type: 'eventFilterDeletionReset' }); - }, [dispatch]); - - const onConfirm = useCallback(() => { - dispatch({ type: 'eventFilterDeleteSubmit' }); - }, [dispatch]); - - // Show toast for success - useEffect(() => { - if (wasDeleted) { - toasts.addSuccess( - i18n.translate('xpack.securitySolution.eventFilters.deletionDialog.deleteSuccess', { - defaultMessage: '"{name}" has been removed from the event filters list.', - values: { name: eventFilter?.name }, - }) - ); - - dispatch({ type: 'eventFilterDeletionReset' }); - } - }, [dispatch, eventFilter?.name, toasts, wasDeleted]); - - // show toast for failures - useEffect(() => { - if (deleteError) { - toasts.addDanger( - i18n.translate('xpack.securitySolution.eventFilters.deletionDialog.deleteFailure', { - defaultMessage: - 'Unable to remove "{name}" from the event filters list. Reason: {message}', - values: { name: eventFilter?.name, message: deleteError.message }, - }) - ); - } - }, [deleteError, eventFilter?.name, toasts]); - - return ( - - - - {eventFilter?.name ?? ''} }} - /> - - - - - - - - - - - - - - - - - - - - - - - - - - - - ); -}); - -EventFilterDeleteModal.displayName = 'EventFilterDeleteModal'; diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/event_filters_flyout.test.tsx b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/event_filters_flyout.test.tsx new file mode 100644 index 0000000000000..21bd1fa655c2e --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/event_filters_flyout.test.tsx @@ -0,0 +1,222 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EventFiltersFlyout, EventFiltersFlyoutProps } from './event_filters_flyout'; +import { act, cleanup } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; + +import { + AppContextTestRender, + createAppRootMockRenderer, +} from '../../../../../common/mock/endpoint'; + +import { getInitialExceptionFromEvent } from '../utils'; +import { useCreateArtifact } from '../../../../hooks/artifacts/use_create_artifact'; +import { useGetEndpointSpecificPolicies } from '../../../../services/policies/hooks'; +import { ecsEventMock, esResponseData } from '../../test_utils'; + +import { useKibana, useToasts } from '../../../../../common/lib/kibana'; +import { of } from 'rxjs'; +import { ExceptionsListItemGenerator } from '../../../../../../common/endpoint/data_generators/exceptions_list_item_generator'; +import { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; + +// mocked modules +jest.mock('../../../../../common/lib/kibana'); +jest.mock('../../../../services/policies/hooks'); +jest.mock('../../../../services/policies/policies'); +jest.mock('../../../../hooks/artifacts/use_create_artifact'); +jest.mock('../utils'); + +let mockedContext: AppContextTestRender; +let render: ( + props?: Partial +) => ReturnType; +let renderResult: ReturnType; +let onCancelMock: jest.Mock; +const exceptionsGenerator = new ExceptionsListItemGenerator(); + +describe('Event filter flyout', () => { + beforeEach(async () => { + mockedContext = createAppRootMockRenderer(); + onCancelMock = jest.fn(); + + (useKibana as jest.Mock).mockReturnValue({ + services: { + docLinks: { + links: { + securitySolution: { + eventFilters: '', + }, + }, + }, + http: {}, + data: { + search: { + search: jest.fn().mockImplementation(() => of(esResponseData())), + }, + }, + notifications: {}, + unifiedSearch: {}, + }, + }); + (useToasts as jest.Mock).mockReturnValue({ + addSuccess: jest.fn(), + addError: jest.fn(), + addWarning: jest.fn(), + remove: jest.fn(), + }); + + (useCreateArtifact as jest.Mock).mockImplementation(() => { + return { + isLoading: false, + mutateAsync: jest.fn(), + }; + }); + + (useGetEndpointSpecificPolicies as jest.Mock).mockImplementation(() => { + return { isLoading: false, isRefetching: false }; + }); + + render = (props) => { + renderResult = mockedContext.render( + + ); + return renderResult; + }; + }); + + afterEach(() => { + cleanup(); + }); + + describe('On initial render', () => { + const exception = exceptionsGenerator.generateEventFilterForCreate({ + meta: {}, + entries: [ + { + field: 'event.category', + operator: 'included', + type: 'match', + value: 'a', + }, + { + field: 'process.executable', + operator: 'included', + type: 'match', + value: 'b', + }, + ], + name: '', + }); + beforeEach(() => { + (getInitialExceptionFromEvent as jest.Mock).mockImplementation(() => { + return exception; + }); + }); + it('should render correctly without data ', () => { + render(); + expect(renderResult.getAllByText('Add event filter')).not.toBeNull(); + expect(renderResult.getByText('Cancel')).not.toBeNull(); + }); + + it('should render correctly with data ', () => { + act(() => { + render({ data: ecsEventMock() }); + }); + expect(renderResult.getAllByText('Add endpoint event filter')).not.toBeNull(); + expect(renderResult.getByText('Cancel')).not.toBeNull(); + }); + + it('should start with "add event filter" button disabled', () => { + render(); + const confirmButton = renderResult.getByTestId('add-exception-confirm-button'); + expect(confirmButton.hasAttribute('disabled')).toBeTruthy(); + }); + + it('should close when click on cancel button', () => { + render(); + const cancelButton = renderResult.getByTestId('cancelExceptionAddButton'); + expect(onCancelMock).toHaveBeenCalledTimes(0); + + userEvent.click(cancelButton); + expect(onCancelMock).toHaveBeenCalledTimes(1); + }); + }); + + describe('When valid form state', () => { + const exceptionOptions: Partial = { + meta: {}, + entries: [ + { + field: 'event.category', + operator: 'included', + type: 'match', + value: 'a', + }, + { + field: 'process.executable', + operator: 'included', + type: 'match', + value: 'b', + }, + ], + name: 'some name', + }; + beforeEach(() => { + const exception = exceptionsGenerator.generateEventFilterForCreate(exceptionOptions); + (getInitialExceptionFromEvent as jest.Mock).mockImplementation(() => { + return exception; + }); + }); + it('should change to "add event filter" button enabled', () => { + render(); + const confirmButton = renderResult.getByTestId('add-exception-confirm-button'); + expect(confirmButton.hasAttribute('disabled')).toBeFalsy(); + }); + it('should prevent close when submitting data', () => { + (useCreateArtifact as jest.Mock).mockImplementation(() => { + return { isLoading: true, mutateAsync: jest.fn() }; + }); + render(); + const cancelButton = renderResult.getByTestId('cancelExceptionAddButton'); + expect(onCancelMock).toHaveBeenCalledTimes(0); + + userEvent.click(cancelButton); + expect(onCancelMock).toHaveBeenCalledTimes(0); + }); + + it('should close when exception has been submitted successfully and close flyout', () => { + // mock submit query + (useCreateArtifact as jest.Mock).mockImplementation(() => { + return { + isLoading: false, + mutateAsync: ( + _: Parameters['mutateAsync']>[0], + options: Parameters['mutateAsync']>[1] + ) => { + if (!options) return; + if (!options.onSuccess) return; + const exception = exceptionsGenerator.generateEventFilter(exceptionOptions); + + options.onSuccess(exception, exception, () => null); + }, + }; + }); + + render(); + + const confirmButton = renderResult.getByTestId('add-exception-confirm-button'); + expect(confirmButton.hasAttribute('disabled')).toBeFalsy(); + expect(onCancelMock).toHaveBeenCalledTimes(0); + userEvent.click(confirmButton); + + expect(useToasts().addSuccess).toHaveBeenCalled(); + expect(onCancelMock).toHaveBeenCalledTimes(1); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/event_filters_flyout.tsx b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/event_filters_flyout.tsx new file mode 100644 index 0000000000000..c370f548e6812 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/event_filters_flyout.tsx @@ -0,0 +1,239 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { memo, useMemo, useEffect, useCallback, useState } from 'react'; +import { FormattedMessage } from '@kbn/i18n-react'; + +import { + EuiFlyout, + EuiFlyoutHeader, + EuiTitle, + EuiFlyoutBody, + EuiFlyoutFooter, + EuiButton, + EuiButtonEmpty, + EuiFlexGroup, + EuiFlexItem, + EuiTextColor, +} from '@elastic/eui'; +import { lastValueFrom } from 'rxjs'; + +import { useWithArtifactSubmitData } from '../../../../components/artifact_list_page/hooks/use_with_artifact_submit_data'; +import { + ArtifactFormComponentOnChangeCallbackProps, + ArtifactFormComponentProps, +} from '../../../../components/artifact_list_page/types'; +import { EventFiltersForm } from './form'; + +import { getInitialExceptionFromEvent } from '../utils'; +import { Ecs } from '../../../../../../common/ecs'; +import { useHttp, useKibana, useToasts } from '../../../../../common/lib/kibana'; +import { useGetEndpointSpecificPolicies } from '../../../../services/policies/hooks'; +import { getLoadPoliciesError } from '../../../../common/translations'; + +import { EventFiltersApiClient } from '../../service/api_client'; +import { getCreationSuccessMessage, getCreationErrorMessage } from '../translations'; +export interface EventFiltersFlyoutProps { + data?: Ecs; + onCancel(): void; + maskProps?: { + style?: string; + }; +} + +export const EventFiltersFlyout: React.FC = memo( + ({ onCancel: onClose, data, ...flyoutProps }) => { + const toasts = useToasts(); + const http = useHttp(); + + const { isLoading: isSubmittingData, mutateAsync: submitData } = useWithArtifactSubmitData( + EventFiltersApiClient.getInstance(http), + 'create' + ); + + const [enrichedData, setEnrichedData] = useState(); + const [isFormValid, setIsFormValid] = useState(false); + const { + data: { search }, + } = useKibana().services; + + // load the list of policies> + const policiesRequest = useGetEndpointSpecificPolicies({ + perPage: 1000, + onError: (error) => { + toasts.addWarning(getLoadPoliciesError(error)); + }, + }); + + const [exception, setException] = useState( + getInitialExceptionFromEvent(data) + ); + + const policiesIsLoading = useMemo( + () => policiesRequest.isLoading || policiesRequest.isRefetching, + [policiesRequest] + ); + + useEffect(() => { + const enrichEvent = async () => { + if (!data || !data._index) return; + const searchResponse = await lastValueFrom( + search.search({ + params: { + index: data._index, + body: { + query: { + match: { + _id: data._id, + }, + }, + }, + }, + }) + ); + setEnrichedData({ + ...data, + host: { + ...data.host, + os: { + ...(data?.host?.os || {}), + name: [searchResponse.rawResponse.hits.hits[0]._source.host.os.name], + }, + }, + }); + }; + + if (data) { + enrichEvent(); + } + + return () => { + setException(getInitialExceptionFromEvent()); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + const handleOnClose = useCallback(() => { + if (policiesIsLoading || isSubmittingData) return; + onClose(); + }, [isSubmittingData, policiesIsLoading, onClose]); + + const handleOnSubmit = useCallback(() => { + return submitData(exception, { + onSuccess: (result) => { + toasts.addSuccess(getCreationSuccessMessage(result)); + onClose(); + }, + onError: (error) => { + toasts.addError(error, getCreationErrorMessage(error)); + }, + }); + }, [exception, onClose, submitData, toasts]); + + const confirmButtonMemo = useMemo( + () => ( + + {data ? ( + + ) : ( + + )} + + ), + [data, enrichedData, handleOnSubmit, isFormValid, isSubmittingData, policiesIsLoading] + ); + + // update flyout state with form state + const onChange = useCallback((formState?: ArtifactFormComponentOnChangeCallbackProps) => { + if (!formState) return; + setIsFormValid(formState.isValid); + setException(formState.item); + }, []); + + return ( + + + + + {data ? ( + + ) : ( + + )} + + + {data ? ( + + + + ) : null} + + + + + + + + + + + + + + {confirmButtonMemo} + + + + ); + } +); + +EventFiltersFlyout.displayName = 'EventFiltersFlyout'; diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/flyout/index.test.tsx b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/flyout/index.test.tsx deleted file mode 100644 index 0ba0a3385dcb6..0000000000000 --- a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/flyout/index.test.tsx +++ /dev/null @@ -1,287 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ -import React from 'react'; -import { EventFiltersFlyout, EventFiltersFlyoutProps } from '.'; -import * as reactTestingLibrary from '@testing-library/react'; -import { fireEvent } from '@testing-library/dom'; -import { - AppContextTestRender, - createAppRootMockRenderer, -} from '../../../../../../common/mock/endpoint'; -import { MiddlewareActionSpyHelper } from '../../../../../../common/store/test_utils'; -import { sendGetEndpointSpecificPackagePolicies } from '../../../../../services/policies/policies'; -import { sendGetEndpointSpecificPackagePoliciesMock } from '../../../../../services/policies/test_mock_utils'; -import type { - CreateExceptionListItemSchema, - ExceptionListItemSchema, -} from '@kbn/securitysolution-io-ts-list-types'; -import { ecsEventMock, esResponseData, eventFiltersListQueryHttpMock } from '../../../test_utils'; -import { getFormEntryState, isUninitialisedForm } from '../../../store/selector'; -import { EventFiltersListPageState } from '../../../types'; -import { useKibana } from '../../../../../../common/lib/kibana'; -import { licenseService } from '../../../../../../common/hooks/use_license'; -import { getExceptionListItemSchemaMock } from '@kbn/lists-plugin/common/schemas/response/exception_list_item_schema.mock'; -import { of } from 'rxjs'; - -jest.mock('../../../../../../common/lib/kibana'); -jest.mock('../form'); -jest.mock('../../../../../services/policies/policies'); - -jest.mock('../../hooks', () => { - const originalModule = jest.requireActual('../../hooks'); - const useEventFiltersNotification = jest.fn().mockImplementation(() => {}); - - return { - ...originalModule, - useEventFiltersNotification, - }; -}); - -jest.mock('../../../../../../common/hooks/use_license', () => { - const licenseServiceInstance = { - isPlatinumPlus: jest.fn(), - }; - return { - licenseService: licenseServiceInstance, - useLicense: () => { - return licenseServiceInstance; - }, - }; -}); - -(sendGetEndpointSpecificPackagePolicies as jest.Mock).mockImplementation( - sendGetEndpointSpecificPackagePoliciesMock -); - -let component: reactTestingLibrary.RenderResult; -let mockedContext: AppContextTestRender; -let waitForAction: MiddlewareActionSpyHelper['waitForAction']; -let render: ( - props?: Partial -) => ReturnType; -const act = reactTestingLibrary.act; -let onCancelMock: jest.Mock; -let getState: () => EventFiltersListPageState; -let mockedApi: ReturnType; - -describe('Event filter flyout', () => { - beforeEach(() => { - (licenseService.isPlatinumPlus as jest.Mock).mockReturnValue(true); - mockedContext = createAppRootMockRenderer(); - waitForAction = mockedContext.middlewareSpy.waitForAction; - onCancelMock = jest.fn(); - getState = () => mockedContext.store.getState().management.eventFilters; - mockedApi = eventFiltersListQueryHttpMock(mockedContext.coreStart.http); - - render = (props) => { - return mockedContext.render(); - }; - - (useKibana as jest.Mock).mockReturnValue({ - services: { - docLinks: { - links: { - securitySolution: { - eventFilters: '', - }, - }, - }, - http: {}, - data: { - search: { - search: jest.fn().mockImplementation(() => of(esResponseData())), - }, - }, - notifications: {}, - }, - }); - }); - - afterEach(() => reactTestingLibrary.cleanup()); - - it('should renders correctly', () => { - component = render(); - expect(component.getAllByText('Add event filter')).not.toBeNull(); - expect(component.getByText('Cancel')).not.toBeNull(); - }); - - it('should renders correctly with data ', async () => { - await act(async () => { - component = render({ data: ecsEventMock() }); - await waitForAction('eventFiltersInitForm'); - }); - expect(component.getAllByText('Add endpoint event filter')).not.toBeNull(); - expect(component.getByText('Cancel')).not.toBeNull(); - }); - - it('should dispatch action to init form store on mount', async () => { - await act(async () => { - render(); - await waitForAction('eventFiltersInitForm'); - }); - - expect(getFormEntryState(getState())).not.toBeUndefined(); - expect(getFormEntryState(getState())?.entries[0].field).toBe(''); - }); - - it('should confirm form when button is disabled', () => { - component = render(); - const confirmButton = component.getByTestId('add-exception-confirm-button'); - act(() => { - fireEvent.click(confirmButton); - }); - expect(isUninitialisedForm(getState())).toBeTruthy(); - }); - - it('should confirm form when button is enabled', async () => { - component = render(); - - mockedContext.store.dispatch({ - type: 'eventFiltersChangeForm', - payload: { - entry: { - ...(getState().form?.entry as CreateExceptionListItemSchema), - name: 'test', - os_types: ['windows'], - }, - hasNameError: false, - hasOSError: false, - }, - }); - await reactTestingLibrary.waitFor(() => { - expect(sendGetEndpointSpecificPackagePolicies).toHaveBeenCalled(); - }); - const confirmButton = component.getByTestId('add-exception-confirm-button'); - - await act(async () => { - fireEvent.click(confirmButton); - await waitForAction('eventFiltersCreateSuccess'); - }); - expect(isUninitialisedForm(getState())).toBeTruthy(); - expect(confirmButton.hasAttribute('disabled')).toBeFalsy(); - }); - - it('should close when exception has been submitted correctly', () => { - render(); - expect(onCancelMock).toHaveBeenCalledTimes(0); - - act(() => { - mockedContext.store.dispatch({ - type: 'eventFiltersFormStateChanged', - payload: { - type: 'LoadedResourceState', - data: getState().form?.entry as ExceptionListItemSchema, - }, - }); - }); - - expect(onCancelMock).toHaveBeenCalledTimes(1); - }); - - it('should close when click on cancel button', () => { - component = render(); - const cancelButton = component.getByText('Cancel'); - expect(onCancelMock).toHaveBeenCalledTimes(0); - - act(() => { - fireEvent.click(cancelButton); - }); - - expect(onCancelMock).toHaveBeenCalledTimes(1); - }); - - it('should close when close flyout', () => { - component = render(); - const flyoutCloseButton = component.getByTestId('euiFlyoutCloseButton'); - expect(onCancelMock).toHaveBeenCalledTimes(0); - - act(() => { - fireEvent.click(flyoutCloseButton); - }); - - expect(onCancelMock).toHaveBeenCalledTimes(1); - }); - - it('should prevent close when is loading action', () => { - component = render(); - act(() => { - mockedContext.store.dispatch({ - type: 'eventFiltersFormStateChanged', - payload: { - type: 'LoadingResourceState', - previousState: { type: 'UninitialisedResourceState' }, - }, - }); - }); - - const cancelButton = component.getByText('Cancel'); - expect(onCancelMock).toHaveBeenCalledTimes(0); - - act(() => { - fireEvent.click(cancelButton); - }); - - expect(onCancelMock).toHaveBeenCalledTimes(0); - }); - - it('should renders correctly when id and edit type', () => { - component = render({ id: 'fakeId', type: 'edit' }); - - expect(component.getAllByText('Update event filter')).not.toBeNull(); - expect(component.getByText('Cancel')).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( - mockedApi.responseProvider.eventFiltersGetOne.getMockImplementation()!().item_id - ); - }); - - it('should not display banner when platinum license', async () => { - await act(async () => { - component = render({ id: 'fakeId', type: 'edit' }); - await waitForAction('eventFiltersInitFromId'); - }); - - expect(component.queryByTestId('expired-license-callout')).toBeNull(); - }); - - it('should not display banner when under platinum license and create mode', async () => { - component = render(); - expect(component.queryByTestId('expired-license-callout')).toBeNull(); - }); - - it('should not display banner when under platinum license and edit mode with global assignment', async () => { - mockedApi.responseProvider.eventFiltersGetOne.mockReturnValue({ - ...getExceptionListItemSchemaMock(), - tags: ['policy:all'], - }); - (licenseService.isPlatinumPlus as jest.Mock).mockReturnValue(false); - await act(async () => { - component = render({ id: 'fakeId', type: 'edit' }); - await waitForAction('eventFiltersInitFromId'); - }); - - expect(component.queryByTestId('expired-license-callout')).toBeNull(); - }); - - it('should display banner when under platinum license and edit mode with by policy assignment', async () => { - (licenseService.isPlatinumPlus as jest.Mock).mockReturnValue(false); - await act(async () => { - component = render({ id: 'fakeId', type: 'edit' }); - await waitForAction('eventFiltersInitFromId'); - }); - - expect(component.queryByTestId('expired-license-callout')).not.toBeNull(); - }); -}); diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/flyout/index.tsx b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/flyout/index.tsx deleted file mode 100644 index ed4e0e11975c7..0000000000000 --- a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/flyout/index.tsx +++ /dev/null @@ -1,302 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { memo, useMemo, useEffect, useCallback, useState } from 'react'; -import { useDispatch } from 'react-redux'; -import { Dispatch } from 'redux'; - -import { FormattedMessage } from '@kbn/i18n-react'; -import { i18n } from '@kbn/i18n'; -import { - EuiFlyout, - EuiFlyoutHeader, - EuiTitle, - EuiFlyoutBody, - EuiFlyoutFooter, - EuiButton, - EuiButtonEmpty, - EuiFlexGroup, - EuiFlexItem, - EuiTextColor, - EuiCallOut, - EuiLink, -} from '@elastic/eui'; -import { lastValueFrom } from 'rxjs'; -import { AppAction } from '../../../../../../common/store/actions'; -import { EventFiltersForm } from '../form'; -import { useEventFiltersSelector, useEventFiltersNotification } from '../../hooks'; -import { - getFormEntryStateMutable, - getFormHasError, - isCreationInProgress, - isCreationSuccessful, -} from '../../../store/selector'; -import { getInitialExceptionFromEvent } from '../../../store/utils'; -import { Ecs } from '../../../../../../../common/ecs'; -import { useKibana, useToasts } from '../../../../../../common/lib/kibana'; -import { useGetEndpointSpecificPolicies } from '../../../../../services/policies/hooks'; -import { getLoadPoliciesError } from '../../../../../common/translations'; -import { useLicense } from '../../../../../../common/hooks/use_license'; -import { isGlobalPolicyEffected } from '../../../../../components/effected_policy_select/utils'; - -export interface EventFiltersFlyoutProps { - type?: 'create' | 'edit'; - id?: string; - data?: Ecs; - onCancel(): void; - maskProps?: { - style?: string; - }; -} - -export const EventFiltersFlyout: React.FC = memo( - ({ onCancel, id, type = 'create', data, ...flyoutProps }) => { - useEventFiltersNotification(); - const [enrichedData, setEnrichedData] = useState(); - const toasts = useToasts(); - const dispatch = useDispatch>(); - const formHasError = useEventFiltersSelector(getFormHasError); - const creationInProgress = useEventFiltersSelector(isCreationInProgress); - const creationSuccessful = useEventFiltersSelector(isCreationSuccessful); - const exception = useEventFiltersSelector(getFormEntryStateMutable); - const { - data: { search }, - docLinks, - } = useKibana().services; - - // load the list of policies> - const policiesRequest = useGetEndpointSpecificPolicies({ - perPage: 1000, - onError: (error) => { - toasts.addWarning(getLoadPoliciesError(error)); - }, - }); - - const isPlatinumPlus = useLicense().isPlatinumPlus(); - const isEditMode = useMemo(() => type === 'edit' && !!id, [type, id]); - const [wasByPolicy, setWasByPolicy] = useState(undefined); - - const showExpiredLicenseBanner = useMemo(() => { - return !isPlatinumPlus && isEditMode && wasByPolicy; - }, [isPlatinumPlus, isEditMode, wasByPolicy]); - - useEffect(() => { - if (exception && wasByPolicy === undefined) { - setWasByPolicy(!isGlobalPolicyEffected(exception?.tags)); - } - }, [exception, wasByPolicy]); - - 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(() => { - const enrichEvent = async () => { - if (!data || !data._index) return; - const searchResponse = await lastValueFrom( - search.search({ - params: { - index: data._index, - body: { - query: { - match: { - _id: data._id, - }, - }, - }, - }, - }) - ); - - setEnrichedData({ - ...data, - host: { - ...data.host, - os: { - ...(data?.host?.os || {}), - name: [searchResponse.rawResponse.hits.hits[0]._source.host.os.name], - }, - }, - }); - }; - - if (type === 'edit' && !!id) { - dispatch({ - type: 'eventFiltersInitFromId', - payload: { id }, - }); - } else if (data) { - enrichEvent(); - } else { - dispatch({ - type: 'eventFiltersInitForm', - payload: { entry: getInitialExceptionFromEvent() }, - }); - } - - return () => { - dispatch({ - type: 'eventFiltersFormStateChanged', - payload: { - type: 'UninitialisedResourceState', - }, - }); - }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - // Initialize the store with the enriched event to allow render the form - useEffect(() => { - if (enrichedData) { - dispatch({ - type: 'eventFiltersInitForm', - payload: { entry: getInitialExceptionFromEvent(enrichedData) }, - }); - } - }, [dispatch, enrichedData]); - - const handleOnCancel = useCallback(() => { - if (creationInProgress) return; - onCancel(); - }, [creationInProgress, onCancel]); - - const confirmButtonMemo = useMemo( - () => ( - - id - ? dispatch({ type: 'eventFiltersUpdateStart' }) - : dispatch({ type: 'eventFiltersCreateStart' }) - } - isLoading={creationInProgress} - > - {id ? ( - - ) : data ? ( - - ) : ( - - )} - - ), - [formHasError, creationInProgress, data, enrichedData, id, dispatch, policiesRequest] - ); - - return ( - - - - - {id ? ( - - ) : data ? ( - - ) : ( - - )} - - - {data ? ( - - - - ) : null} - - - {showExpiredLicenseBanner && ( - - - - - - - )} - - - - - - - - - - - - - {confirmButtonMemo} - - - - ); - } -); - -EventFiltersFlyout.displayName = 'EventFiltersFlyout'; diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/form.test.tsx b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/form.test.tsx new file mode 100644 index 0000000000000..e20abb2f93264 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/form.test.tsx @@ -0,0 +1,468 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React from 'react'; +import { act, cleanup } from '@testing-library/react'; +import { fireEvent } from '@testing-library/dom'; +import { stubIndexPattern } from '@kbn/data-plugin/common/stubs'; +import { useFetchIndex } from '../../../../../common/containers/source'; +import { NAME_ERROR } from '../event_filters_list'; +import { useCurrentUser, useKibana } from '../../../../../common/lib/kibana'; +import { licenseService } from '../../../../../common/hooks/use_license'; +import { + AppContextTestRender, + createAppRootMockRenderer, +} from '../../../../../common/mock/endpoint'; +import userEvent from '@testing-library/user-event'; +import { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; + +import { ENDPOINT_EVENT_FILTERS_LIST_ID } from '@kbn/securitysolution-list-constants'; +import { + ArtifactFormComponentOnChangeCallbackProps, + ArtifactFormComponentProps, +} from '../../../../components/artifact_list_page'; +import { OperatingSystem } from '@kbn/securitysolution-utils'; +import { EventFiltersForm } from './form'; +import { EndpointDocGenerator } from '../../../../../../common/endpoint/generate_data'; +import { PolicyData } from '../../../../../../common/endpoint/types'; + +jest.mock('../../../../../common/lib/kibana'); +jest.mock('../../../../../common/containers/source'); +jest.mock('../../../../../common/hooks/use_license', () => { + const licenseServiceInstance = { + isPlatinumPlus: jest.fn(), + isGoldPlus: jest.fn(), + }; + return { + licenseService: licenseServiceInstance, + useLicense: () => { + return licenseServiceInstance; + }, + }; +}); + +describe('Event filter form', () => { + const formPrefix = 'eventFilters-form'; + const generator = new EndpointDocGenerator('effected-policy-select'); + + let formProps: jest.Mocked; + let mockedContext: AppContextTestRender; + let renderResult: ReturnType; + let latestUpdatedItem: ArtifactFormComponentProps['item']; + + const getUI = () => ; + const render = () => { + return (renderResult = mockedContext.render(getUI())); + }; + const rerender = () => renderResult.rerender(getUI()); + const rerenderWithLatestProps = () => { + formProps.item = latestUpdatedItem; + rerender(); + }; + + function createEntry( + overrides?: ExceptionListItemSchema['entries'][number] + ): ExceptionListItemSchema['entries'][number] { + const defaultEntry: ExceptionListItemSchema['entries'][number] = { + field: '', + operator: 'included', + type: 'match', + value: '', + }; + + return { + ...defaultEntry, + ...overrides, + }; + } + + function createItem( + overrides: Partial = {} + ): ArtifactFormComponentProps['item'] { + const defaults: ArtifactFormComponentProps['item'] = { + id: 'some_item_id', + list_id: ENDPOINT_EVENT_FILTERS_LIST_ID, + name: '', + description: '', + os_types: [OperatingSystem.WINDOWS], + entries: [createEntry()], + type: 'simple', + tags: ['policy:all'], + }; + return { + ...defaults, + ...overrides, + }; + } + + function createOnChangeArgs( + overrides: Partial + ): ArtifactFormComponentOnChangeCallbackProps { + const defaults = { + item: createItem(), + isValid: false, + }; + return { + ...defaults, + ...overrides, + }; + } + + function createPolicies(): PolicyData[] { + const policies = [ + generator.generatePolicyPackagePolicy(), + generator.generatePolicyPackagePolicy(), + ]; + policies.map((p, i) => { + p.id = `id-${i}`; + p.name = `some-policy-${Math.random().toString(36).split('.').pop()}`; + return p; + }); + return policies; + } + + beforeEach(async () => { + (useCurrentUser as jest.Mock).mockReturnValue({ username: 'test-username' }); + (useKibana as jest.Mock).mockReturnValue({ + services: { + http: {}, + data: {}, + unifiedSearch: {}, + notifications: {}, + }, + }); + (licenseService.isPlatinumPlus as jest.Mock).mockReturnValue(true); + mockedContext = createAppRootMockRenderer(); + latestUpdatedItem = createItem(); + (useFetchIndex as jest.Mock).mockImplementation(() => [ + false, + { + indexPatterns: stubIndexPattern, + }, + ]); + + formProps = { + item: latestUpdatedItem, + mode: 'create', + disabled: false, + error: undefined, + policiesIsLoading: false, + onChange: jest.fn((updates) => { + latestUpdatedItem = updates.item; + }), + policies: [], + }; + }); + + afterEach(() => { + cleanup(); + }); + + describe('Details and Conditions', () => { + it('should render correctly without data', () => { + formProps.policies = createPolicies(); + formProps.policiesIsLoading = true; + formProps.item.tags = [formProps.policies.map((p) => `policy:${p.id}`)[0]]; + formProps.item.entries = []; + render(); + expect(renderResult.getByTestId('loading-spinner')).not.toBeNull(); + }); + + it('should render correctly with data', async () => { + formProps.policies = createPolicies(); + render(); + expect(renderResult.queryByTestId('loading-spinner')).toBeNull(); + expect(renderResult.getByTestId('exceptionsBuilderWrapper')).not.toBeNull(); + }); + + it('should display sections', async () => { + render(); + expect(renderResult.queryByText('Details')).not.toBeNull(); + expect(renderResult.queryByText('Conditions')).not.toBeNull(); + expect(renderResult.queryByText('Comments')).not.toBeNull(); + }); + + it('should display name error only when on blur and empty name', async () => { + render(); + expect(renderResult.queryByText(NAME_ERROR)).toBeNull(); + const nameInput = renderResult.getByTestId(`${formPrefix}-name-input`); + act(() => { + fireEvent.blur(nameInput); + }); + rerenderWithLatestProps(); + expect(renderResult.queryByText(NAME_ERROR)).not.toBeNull(); + }); + + it('should change name', async () => { + render(); + const nameInput = renderResult.getByTestId(`${formPrefix}-name-input`); + + act(() => { + fireEvent.change(nameInput, { + target: { + value: 'Exception name', + }, + }); + fireEvent.blur(nameInput); + }); + rerenderWithLatestProps(); + + expect(formProps.item?.name).toBe('Exception name'); + expect(renderResult.queryByText(NAME_ERROR)).toBeNull(); + }); + + it('should change name with a white space still shows an error', async () => { + render(); + const nameInput = renderResult.getByTestId(`${formPrefix}-name-input`); + + act(() => { + fireEvent.change(nameInput, { + target: { + value: ' ', + }, + }); + fireEvent.blur(nameInput); + }); + rerenderWithLatestProps(); + + expect(formProps.item.name).toBe(''); + expect(renderResult.queryByText(NAME_ERROR)).not.toBeNull(); + }); + + it('should change description', async () => { + render(); + const nameInput = renderResult.getByTestId(`${formPrefix}-description-input`); + + act(() => { + fireEvent.change(nameInput, { + target: { + value: 'Exception description', + }, + }); + fireEvent.blur(nameInput); + }); + rerenderWithLatestProps(); + + expect(formProps.item.description).toBe('Exception description'); + }); + + it('should change comments', async () => { + render(); + const commentInput = renderResult.getByLabelText('Comment Input'); + + act(() => { + fireEvent.change(commentInput, { + target: { + value: 'Exception comment', + }, + }); + fireEvent.blur(commentInput); + }); + rerenderWithLatestProps(); + + expect(formProps.item.comments).toEqual([{ comment: 'Exception comment' }]); + }); + }); + + describe('Policy section', () => { + beforeEach(() => { + formProps.policies = createPolicies(); + }); + + afterEach(() => { + cleanup(); + }); + + it('should display loader when policies are still loading', () => { + formProps.policiesIsLoading = true; + formProps.item.tags = [formProps.policies.map((p) => `policy:${p.id}`)[0]]; + render(); + expect(renderResult.getByTestId('loading-spinner')).not.toBeNull(); + }); + + it('should display the policy list when "per policy" is selected', async () => { + render(); + userEvent.click(renderResult.getByTestId('perPolicy')); + rerenderWithLatestProps(); + // policy selector should show up + expect(renderResult.getByTestId('effectedPolicies-select-policiesSelectable')).toBeTruthy(); + }); + + it('should call onChange when a policy is selected from the policy selection', async () => { + formProps.item.tags = [formProps.policies.map((p) => `policy:${p.id}`)[0]]; + render(); + const policyId = formProps.policies[0].id; + userEvent.click(renderResult.getByTestId('effectedPolicies-select-perPolicy')); + userEvent.click(renderResult.getByTestId(`policy-${policyId}`)); + formProps.item.tags = formProps.onChange.mock.calls[0][0].item.tags; + rerender(); + const expected = createOnChangeArgs({ + item: { + ...formProps.item, + tags: [`policy:${policyId}`], + }, + }); + expect(formProps.onChange).toHaveBeenCalledWith(expected); + }); + + it('should have global policy by default', async () => { + render(); + expect(renderResult.getByTestId('globalPolicy')).toBeChecked(); + expect(renderResult.getByTestId('perPolicy')).not.toBeChecked(); + }); + + it('should retain the previous policy selection when switching from per-policy to global', async () => { + formProps.item.tags = [formProps.policies.map((p) => `policy:${p.id}`)[0]]; + render(); + const policyId = formProps.policies[0].id; + + // move to per-policy and select the first + userEvent.click(renderResult.getByTestId('perPolicy')); + userEvent.click(renderResult.getByTestId(`policy-${policyId}`)); + formProps.item.tags = formProps.onChange.mock.calls[0][0].item.tags; + rerender(); + expect(renderResult.queryByTestId('effectedPolicies-select-policiesSelectable')).toBeTruthy(); + expect(formProps.item.tags).toEqual([`policy:${policyId}`]); + + // move back to global + userEvent.click(renderResult.getByTestId('globalPolicy')); + formProps.item.tags = ['policy:all']; + rerenderWithLatestProps(); + expect(formProps.item.tags).toEqual(['policy:all']); + expect(renderResult.queryByTestId('effectedPolicies-select-policiesSelectable')).toBeFalsy(); + + // move back to per-policy + userEvent.click(renderResult.getByTestId('perPolicy')); + formProps.item.tags = [`policy:${policyId}`]; + rerender(); + // on change called with the previous policy + expect(formProps.item.tags).toEqual([`policy:${policyId}`]); + // the previous selected policy should be selected + // expect(renderResult.getByTestId(`policy-${policyId}`)).toHaveAttribute( + // 'data-test-selected', + // 'true' + // ); + }); + }); + + describe('Policy section with downgraded license', () => { + beforeEach(() => { + const policies = createPolicies(); + formProps.policies = policies; + formProps.item.tags = [policies.map((p) => `policy:${p.id}`)[0]]; + formProps.mode = 'edit'; + // downgrade license + (licenseService.isPlatinumPlus as jest.Mock).mockReturnValue(false); + }); + + it('should hide assignment section when no license', () => { + render(); + formProps.item.tags = ['policy:all']; + rerender(); + expect(renderResult.queryByTestId('effectedPolicies-select')).toBeNull(); + }); + + it('should hide assignment section when create mode and no license even with by policy', () => { + render(); + formProps.mode = 'create'; + rerender(); + expect(renderResult.queryByTestId('effectedPolicies-select')).toBeNull(); + }); + + it('should show disabled assignment section when edit mode and no license with by policy', async () => { + render(); + formProps.item.tags = ['policy:id-0']; + rerender(); + + expect(renderResult.queryByTestId('perPolicy')).not.toBeNull(); + expect(renderResult.getByTestId('policy-id-0').getAttribute('aria-disabled')).toBe('true'); + }); + + it("allows the user to set the event filter entry to 'Global' in the edit option", () => { + render(); + const globalButtonInput = renderResult.getByTestId('globalPolicy') as HTMLButtonElement; + userEvent.click(globalButtonInput); + formProps.item.tags = ['policy:all']; + rerender(); + const expected = createOnChangeArgs({ + item: { + ...formProps.item, + tags: ['policy:all'], + }, + }); + expect(formProps.onChange).toHaveBeenCalledWith(expected); + + const policyItem = formProps.onChange.mock.calls[0][0].item.tags + ? formProps.onChange.mock.calls[0][0].item.tags[0] + : ''; + + expect(policyItem).toBe('policy:all'); + }); + }); + + describe('Warnings', () => { + beforeEach(() => { + render(); + }); + + it('should not show warning text when unique fields are added', async () => { + formProps.item.entries = [ + { + field: 'event.category', + operator: 'included', + type: 'match', + value: 'some value', + }, + { + field: 'file.name', + operator: 'excluded', + type: 'match', + value: 'some other value', + }, + ]; + rerender(); + expect(renderResult.queryByTestId('duplicate-fields-warning-message')).toBeNull(); + }); + + it('should not show warning text when field values are not added', async () => { + formProps.item.entries = [ + { + field: 'event.category', + operator: 'included', + type: 'match', + value: '', + }, + { + field: 'event.category', + operator: 'excluded', + type: 'match', + value: '', + }, + ]; + rerender(); + expect(renderResult.queryByTestId('duplicate-fields-warning-message')).toBeNull(); + }); + + it('should show warning text when duplicate fields are added with values', async () => { + formProps.item.entries = [ + { + field: 'event.category', + operator: 'included', + type: 'match', + value: 'some value', + }, + { + field: 'event.category', + operator: 'excluded', + type: 'match', + value: 'some other value', + }, + ]; + rerender(); + expect(renderResult.findByTestId('duplicate-fields-warning-message')).not.toBeNull(); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/form.tsx b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/form.tsx new file mode 100644 index 0000000000000..4e021d12dac36 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/form.tsx @@ -0,0 +1,558 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { memo, useMemo, useCallback, useState, useEffect } from 'react'; + +import { isEqual } from 'lodash'; +import { + EuiFieldText, + EuiSpacer, + EuiForm, + EuiFormRow, + EuiSuperSelect, + EuiSuperSelectOption, + EuiText, + EuiHorizontalRule, + EuiTextArea, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; + +import type { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; +import { EVENT_FILTERS_OPERATORS } from '@kbn/securitysolution-list-utils'; +import { OperatingSystem } from '@kbn/securitysolution-utils'; + +import { getExceptionBuilderComponentLazy } from '@kbn/lists-plugin/public'; +import type { OnChangeProps } from '@kbn/lists-plugin/public'; +import { useTestIdGenerator } from '../../../../components/hooks/use_test_id_generator'; +import { PolicyData } from '../../../../../../common/endpoint/types'; +import { AddExceptionComments } from '../../../../../common/components/exceptions/add_exception_comments'; +import { useFetchIndex } from '../../../../../common/containers/source'; +import { Loader } from '../../../../../common/components/loader'; +import { useLicense } from '../../../../../common/hooks/use_license'; +import { useKibana } from '../../../../../common/lib/kibana'; +import { ArtifactFormComponentProps } from '../../../../components/artifact_list_page'; +import { filterIndexPatterns } from '../../../../../common/components/exceptions/helpers'; +import { + isArtifactGlobal, + getPolicyIdsFromArtifact, + GLOBAL_ARTIFACT_TAG, + BY_POLICY_ARTIFACT_TAG_PREFIX, +} from '../../../../../../common/endpoint/service/artifacts'; + +import { + ABOUT_EVENT_FILTERS, + NAME_LABEL, + NAME_ERROR, + DESCRIPTION_LABEL, + OS_LABEL, + RULE_NAME, +} from '../event_filters_list'; +import { OS_TITLES } from '../../../../common/translations'; +import { ENDPOINT_EVENT_FILTERS_LIST_ID, EVENT_FILTER_LIST_TYPE } from '../../constants'; + +import { + EffectedPolicySelect, + EffectedPolicySelection, +} from '../../../../components/effected_policy_select'; +import { isGlobalPolicyEffected } from '../../../../components/effected_policy_select/utils'; + +const OPERATING_SYSTEMS: readonly OperatingSystem[] = [ + OperatingSystem.MAC, + OperatingSystem.WINDOWS, + OperatingSystem.LINUX, +]; + +// OS options +const osOptions: Array> = OPERATING_SYSTEMS.map((os) => ({ + value: os, + inputDisplay: OS_TITLES[os], +})); + +const getAddedFieldsCounts = (formFields: string[]): { [k: string]: number } => + formFields.reduce<{ [k: string]: number }>((allFields, field) => { + if (field in allFields) { + allFields[field]++; + } else { + allFields[field] = 1; + } + return allFields; + }, {}); + +const computeHasDuplicateFields = (formFieldsList: Record): boolean => + Object.values(formFieldsList).some((e) => e > 1); + +const defaultConditionEntry = (): ExceptionListItemSchema['entries'] => [ + { + field: '', + operator: 'included', + type: 'match', + value: '', + }, +]; + +const cleanupEntries = ( + item: ArtifactFormComponentProps['item'] +): ArtifactFormComponentProps['item']['entries'] => { + return item.entries.map( + (e: ArtifactFormComponentProps['item']['entries'][number] & { id?: string }) => { + delete e.id; + return e; + } + ); +}; + +type EventFilterItemEntries = Array<{ + field: string; + value: string; + operator: 'included' | 'excluded'; + type: Exclude; +}>; + +export const EventFiltersForm: React.FC = + memo(({ allowSelectOs = true, item: exception, policies, policiesIsLoading, onChange, mode }) => { + const getTestId = useTestIdGenerator('eventFilters-form'); + const { http, unifiedSearch } = useKibana().services; + + const [hasFormChanged, setHasFormChanged] = useState(false); + const [hasNameError, toggleHasNameError] = useState(!exception.name); + const [newComment, setNewComment] = useState(''); + const [hasBeenInputNameVisited, setHasBeenInputNameVisited] = useState(false); + const [selectedPolicies, setSelectedPolicies] = useState([]); + const isPlatinumPlus = useLicense().isPlatinumPlus(); + const isGlobal = useMemo( + () => isArtifactGlobal(exception as ExceptionListItemSchema), + [exception] + ); + const [wasByPolicy, setWasByPolicy] = useState(!isGlobalPolicyEffected(exception?.tags)); + + const [hasDuplicateFields, setHasDuplicateFields] = useState(false); + // This value has to be memoized to avoid infinite useEffect loop on useFetchIndex + const indexNames = useMemo(() => ['logs-endpoint.events.*'], []); + const [isIndexPatternLoading, { indexPatterns }] = useFetchIndex(indexNames); + const [areConditionsValid, setAreConditionsValid] = useState( + !!exception.entries.length || false + ); + // compute this for initial render only + const existingComments = useMemo( + () => (exception as ExceptionListItemSchema)?.comments, + // eslint-disable-next-line react-hooks/exhaustive-deps + [] + ); + + const showAssignmentSection = useMemo(() => { + return ( + isPlatinumPlus || + (mode === 'edit' && (!isGlobal || (wasByPolicy && isGlobal && hasFormChanged))) + ); + }, [mode, isGlobal, hasFormChanged, isPlatinumPlus, wasByPolicy]); + + const isFormValid = useMemo(() => { + // verify that it has legit entries + // and not just default entry without values + return ( + !hasNameError && + !!exception.entries.length && + (exception.entries as EventFilterItemEntries).some((e) => e.value !== '' || e.value.length) + ); + }, [hasNameError, exception.entries]); + + const processChanged = useCallback( + (updatedItem?: Partial) => { + const item = updatedItem + ? { + ...exception, + ...updatedItem, + } + : exception; + cleanupEntries(item); + onChange({ + item, + isValid: isFormValid && areConditionsValid, + }); + }, + [areConditionsValid, exception, isFormValid, onChange] + ); + + // set initial state of `wasByPolicy` that checks + // if the initial state of the exception was by policy or not + useEffect(() => { + if (!hasFormChanged && exception.tags) { + setWasByPolicy(!isGlobalPolicyEffected(exception.tags)); + } + }, [exception.tags, hasFormChanged]); + + // select policies if editing + useEffect(() => { + if (hasFormChanged) return; + const policyIds = exception.tags ? getPolicyIdsFromArtifact({ tags: exception.tags }) : []; + + if (!policyIds.length) return; + const policiesData = policies.filter((policy) => policyIds.includes(policy.id)); + setSelectedPolicies(policiesData); + }, [hasFormChanged, exception, policies]); + + const eventFilterItem = useMemo(() => { + const ef: ArtifactFormComponentProps['item'] = exception; + ef.entries = exception.entries.length + ? (exception.entries as ExceptionListItemSchema['entries']) + : defaultConditionEntry(); + + // TODO: `id` gets added to the exception.entries item + // Is there a simpler way to this? + cleanupEntries(ef); + + setAreConditionsValid(!!exception.entries.length); + return ef; + }, [exception]); + + // name and handler + const handleOnChangeName = useCallback( + (event: React.ChangeEvent) => { + if (!exception) return; + const name = event.target.value.trim(); + toggleHasNameError(!name); + processChanged({ name }); + if (!hasFormChanged) setHasFormChanged(true); + }, + [exception, hasFormChanged, processChanged] + ); + + const nameInputMemo = useMemo( + () => ( + + !hasBeenInputNameVisited && setHasBeenInputNameVisited(true)} + /> + + ), + [getTestId, hasNameError, handleOnChangeName, hasBeenInputNameVisited, exception?.name] + ); + + // description and handler + const handleOnDescriptionChange = useCallback( + (event: React.ChangeEvent) => { + if (!exception) return; + if (!hasFormChanged) setHasFormChanged(true); + processChanged({ description: event.target.value.toString().trim() }); + }, + [exception, hasFormChanged, processChanged] + ); + const descriptionInputMemo = useMemo( + () => ( + + + + ), + [exception?.description, getTestId, handleOnDescriptionChange] + ); + + // selected OS and handler + const selectedOs = useMemo((): OperatingSystem => { + if (!exception?.os_types?.length) { + return OperatingSystem.WINDOWS; + } + return exception.os_types[0] as OperatingSystem; + }, [exception?.os_types]); + + const handleOnOsChange = useCallback( + (os: OperatingSystem) => { + if (!exception) return; + processChanged({ + os_types: [os], + entries: exception.entries, + }); + if (!hasFormChanged) setHasFormChanged(true); + }, + [exception, hasFormChanged, processChanged] + ); + + const osInputMemo = useMemo( + () => ( + + + + ), + [handleOnOsChange, selectedOs] + ); + + // comments and handler + const handleOnChangeComment = useCallback( + (value: string) => { + if (!exception) return; + setNewComment(value); + processChanged({ comments: [{ comment: value }] }); + if (!hasFormChanged) setHasFormChanged(true); + }, + [exception, hasFormChanged, processChanged] + ); + const commentsInputMemo = useMemo( + () => ( + + ), + [existingComments, handleOnChangeComment, newComment] + ); + + // comments + const commentsSection = useMemo( + () => ( + <> + + + + + + + + + + + + + {commentsInputMemo} + > + ), + [commentsInputMemo] + ); + + // details + const detailsSection = useMemo( + () => ( + <> + + + + + + + + {ABOUT_EVENT_FILTERS} + + + {nameInputMemo} + {descriptionInputMemo} + > + ), + [nameInputMemo, descriptionInputMemo] + ); + + // conditions and handler + const handleOnBuilderChange = useCallback( + (arg: OnChangeProps) => { + const hasDuplicates = + (!hasFormChanged && arg.exceptionItems[0] === undefined) || + isEqual(arg.exceptionItems[0]?.entries, exception?.entries); + if (hasDuplicates) { + const addedFields = arg.exceptionItems[0]?.entries.map((e) => e.field) || ['']; + setHasDuplicateFields(computeHasDuplicateFields(getAddedFieldsCounts(addedFields))); + if (!hasFormChanged) setHasFormChanged(true); + return; + } + const updatedItem: Partial = + arg.exceptionItems[0] !== undefined + ? { + ...arg.exceptionItems[0], + name: exception?.name ?? '', + description: exception?.description ?? '', + comments: exception?.comments ?? [], + os_types: exception?.os_types ?? [OperatingSystem.WINDOWS], + tags: exception?.tags ?? [], + } + : exception; + const hasValidConditions = + arg.exceptionItems[0] !== undefined + ? !(arg.errorExists && !arg.exceptionItems[0]?.entries?.length) + : false; + + setAreConditionsValid(hasValidConditions); + processChanged(updatedItem); + if (!hasFormChanged) setHasFormChanged(true); + }, + [exception, hasFormChanged, processChanged] + ); + const exceptionBuilderComponentMemo = useMemo( + () => + getExceptionBuilderComponentLazy({ + allowLargeValueLists: false, + httpService: http, + autocompleteService: unifiedSearch.autocomplete, + exceptionListItems: [eventFilterItem as ExceptionListItemSchema], + listType: EVENT_FILTER_LIST_TYPE, + listId: ENDPOINT_EVENT_FILTERS_LIST_ID, + listNamespaceType: 'agnostic', + ruleName: RULE_NAME, + indexPatterns, + isOrDisabled: true, + isOrHidden: true, + isAndDisabled: false, + isNestedDisabled: false, + dataTestSubj: 'alert-exception-builder', + idAria: 'alert-exception-builder', + onChange: handleOnBuilderChange, + listTypeSpecificIndexPatternFilter: filterIndexPatterns, + operatorsList: EVENT_FILTERS_OPERATORS, + osTypes: exception.os_types, + }), + [unifiedSearch, handleOnBuilderChange, http, indexPatterns, exception, eventFilterItem] + ); + + // conditions + const criteriaSection = useMemo( + () => ( + <> + + + + + + + + + {allowSelectOs ? ( + + ) : ( + + )} + + + + {allowSelectOs ? ( + <> + {osInputMemo} + + > + ) : null} + {exceptionBuilderComponentMemo} + > + ), + [allowSelectOs, exceptionBuilderComponentMemo, osInputMemo] + ); + + // policy and handler + const handleOnPolicyChange = useCallback( + (change: EffectedPolicySelection) => { + const tags = change.isGlobal + ? [GLOBAL_ARTIFACT_TAG] + : change.selected.map((policy) => `${BY_POLICY_ARTIFACT_TAG_PREFIX}${policy.id}`); + + // Preserve old selected policies when switching to global + if (!change.isGlobal) { + setSelectedPolicies(change.selected); + } + processChanged({ tags }); + if (!hasFormChanged) setHasFormChanged(true); + }, + [processChanged, hasFormChanged, setSelectedPolicies] + ); + + const policiesSection = useMemo( + () => ( + + ), + [ + policies, + selectedPolicies, + isGlobal, + isPlatinumPlus, + handleOnPolicyChange, + policiesIsLoading, + ] + ); + + useEffect(() => { + processChanged(); + }, [processChanged]); + + if (isIndexPatternLoading || !exception) { + return ; + } + + return ( + + {detailsSection} + + {criteriaSection} + {hasDuplicateFields && ( + <> + + + + + > + )} + {showAssignmentSection && ( + <> + + {policiesSection} + > + )} + + {commentsSection} + + ); + }); + +EventFiltersForm.displayName = 'EventFiltersForm'; diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/form/index.test.tsx b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/form/index.test.tsx deleted file mode 100644 index f0589099a8077..0000000000000 --- a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/form/index.test.tsx +++ /dev/null @@ -1,338 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ -import React from 'react'; -import { EventFiltersForm } from '.'; -import { RenderResult, act } from '@testing-library/react'; -import { fireEvent, waitFor } from '@testing-library/dom'; -import { stubIndexPattern } from '@kbn/data-plugin/common/stubs'; -import { getInitialExceptionFromEvent } from '../../../store/utils'; -import { useFetchIndex } from '../../../../../../common/containers/source'; -import { ecsEventMock } from '../../../test_utils'; -import { NAME_ERROR, NAME_PLACEHOLDER } from './translations'; -import { useCurrentUser, useKibana } from '../../../../../../common/lib/kibana'; -import { licenseService } from '../../../../../../common/hooks/use_license'; -import { - AppContextTestRender, - createAppRootMockRenderer, -} from '../../../../../../common/mock/endpoint'; -import { EventFiltersListPageState } from '../../../types'; -import { sendGetEndpointSpecificPackagePoliciesMock } from '../../../../../services/policies/test_mock_utils'; -import { GetPolicyListResponse } from '../../../../policy/types'; -import userEvent from '@testing-library/user-event'; -import { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; - -jest.mock('../../../../../../common/lib/kibana'); -jest.mock('../../../../../../common/containers/source'); -jest.mock('../../../../../../common/hooks/use_license', () => { - const licenseServiceInstance = { - isPlatinumPlus: jest.fn(), - }; - return { - licenseService: licenseServiceInstance, - useLicense: () => { - return licenseServiceInstance; - }, - }; -}); - -describe('Event filter form', () => { - let component: RenderResult; - let mockedContext: AppContextTestRender; - let render: ( - props?: Partial> - ) => ReturnType; - let renderWithData: ( - customEventFilterProps?: Partial - ) => Promise>; - let getState: () => EventFiltersListPageState; - let policiesRequest: GetPolicyListResponse; - - beforeEach(async () => { - (licenseService.isPlatinumPlus as jest.Mock).mockReturnValue(true); - mockedContext = createAppRootMockRenderer(); - policiesRequest = await sendGetEndpointSpecificPackagePoliciesMock(); - getState = () => mockedContext.store.getState().management.eventFilters; - render = (props) => - mockedContext.render( - - ); - renderWithData = async (customEventFilterProps = {}) => { - const renderResult = render(); - const entry = getInitialExceptionFromEvent(ecsEventMock()); - - act(() => { - mockedContext.store.dispatch({ - type: 'eventFiltersInitForm', - payload: { entry: { ...entry, ...customEventFilterProps } }, - }); - }); - await waitFor(() => { - expect(renderResult.getByTestId('exceptionsBuilderWrapper')).toBeInTheDocument(); - }); - return renderResult; - }; - - (useFetchIndex as jest.Mock).mockImplementation(() => [ - false, - { - indexPatterns: stubIndexPattern, - }, - ]); - (useCurrentUser as jest.Mock).mockReturnValue({ username: 'test-username' }); - (useKibana as jest.Mock).mockReturnValue({ - services: { - http: {}, - data: {}, - unifiedSearch: {}, - notifications: {}, - }, - }); - }); - - it('should renders correctly without data', () => { - component = render(); - expect(component.getByTestId('loading-spinner')).not.toBeNull(); - }); - - it('should renders correctly with data', async () => { - component = await renderWithData(); - - expect(component.getByTestId('exceptionsBuilderWrapper')).not.toBeNull(); - }); - - it('should displays loader when policies are still loading', () => { - component = render({ arePoliciesLoading: true }); - - expect(component.queryByTestId('exceptionsBuilderWrapper')).toBeNull(); - expect(component.getByTestId('loading-spinner')).not.toBeNull(); - }); - - it('should display sections', async () => { - component = await renderWithData(); - - expect(component.queryByText('Details')).not.toBeNull(); - expect(component.queryByText('Conditions')).not.toBeNull(); - expect(component.queryByText('Comments')).not.toBeNull(); - }); - - it('should display name error only when on blur and empty name', async () => { - component = await renderWithData(); - expect(component.queryByText(NAME_ERROR)).toBeNull(); - const nameInput = component.getByPlaceholderText(NAME_PLACEHOLDER); - act(() => { - fireEvent.blur(nameInput); - }); - expect(component.queryByText(NAME_ERROR)).not.toBeNull(); - }); - - it('should change name', async () => { - component = await renderWithData(); - - const nameInput = component.getByPlaceholderText(NAME_PLACEHOLDER); - - act(() => { - fireEvent.change(nameInput, { - target: { - value: 'Exception name', - }, - }); - }); - - expect(getState().form.entry?.name).toBe('Exception name'); - expect(getState().form.hasNameError).toBeFalsy(); - }); - - it('should change name with a white space still shows an error', async () => { - component = await renderWithData(); - - const nameInput = component.getByPlaceholderText(NAME_PLACEHOLDER); - - act(() => { - fireEvent.change(nameInput, { - target: { - value: ' ', - }, - }); - }); - - expect(getState().form.entry?.name).toBe(''); - expect(getState().form.hasNameError).toBeTruthy(); - }); - - it('should change description', async () => { - component = await renderWithData(); - - const nameInput = component.getByTestId('eventFilters-form-description-input'); - - act(() => { - fireEvent.change(nameInput, { - target: { - value: 'Exception description', - }, - }); - }); - - expect(getState().form.entry?.description).toBe('Exception description'); - }); - - it('should change comments', async () => { - component = await renderWithData(); - - const commentInput = component.getByPlaceholderText('Add a new comment...'); - - act(() => { - fireEvent.change(commentInput, { - target: { - value: 'Exception comment', - }, - }); - }); - - expect(getState().form.newComment).toBe('Exception comment'); - }); - - it('should display the policy list when "per policy" is selected', async () => { - component = await renderWithData(); - userEvent.click(component.getByTestId('perPolicy')); - - // policy selector should show up - expect(component.getByTestId('effectedPolicies-select-policiesSelectable')).toBeTruthy(); - }); - - it('should call onChange when a policy is selected from the policy selection', async () => { - component = await renderWithData(); - - const policyId = policiesRequest.items[0].id; - userEvent.click(component.getByTestId('perPolicy')); - userEvent.click(component.getByTestId(`policy-${policyId}`)); - expect(getState().form.entry?.tags).toEqual([`policy:${policyId}`]); - }); - - it('should have global policy by default', async () => { - component = await renderWithData(); - - expect(component.getByTestId('globalPolicy')).toBeChecked(); - expect(component.getByTestId('perPolicy')).not.toBeChecked(); - }); - - it('should retain the previous policy selection when switching from per-policy to global', async () => { - const policyId = policiesRequest.items[0].id; - - component = await renderWithData(); - - // move to per-policy and select the first - userEvent.click(component.getByTestId('perPolicy')); - userEvent.click(component.getByTestId(`policy-${policyId}`)); - expect(component.queryByTestId('effectedPolicies-select-policiesSelectable')).toBeTruthy(); - expect(getState().form.entry?.tags).toEqual([`policy:${policyId}`]); - - // move back to global - userEvent.click(component.getByTestId('globalPolicy')); - expect(component.queryByTestId('effectedPolicies-select-policiesSelectable')).toBeFalsy(); - expect(getState().form.entry?.tags).toEqual([`policy:all`]); - - // move back to per-policy - userEvent.click(component.getByTestId('perPolicy')); - // the previous selected policy should be selected - expect(component.getByTestId(`policy-${policyId}`)).toHaveAttribute( - 'data-test-selected', - 'true' - ); - // on change called with the previous policy - expect(getState().form.entry?.tags).toEqual([`policy:${policyId}`]); - }); - - it('should hide assignment section when no license', async () => { - (licenseService.isPlatinumPlus as jest.Mock).mockReturnValue(false); - component = await renderWithData(); - expect(component.queryByTestId('perPolicy')).toBeNull(); - }); - - it('should hide assignment section when create mode and no license even with by policy', async () => { - const policyId = policiesRequest.items[0].id; - (licenseService.isPlatinumPlus as jest.Mock).mockReturnValue(false); - component = await renderWithData({ tags: [`policy:${policyId}`] }); - expect(component.queryByTestId('perPolicy')).toBeNull(); - }); - - it('should show disabled assignment section when edit mode and no license with by policy', async () => { - const policyId = policiesRequest.items[0].id; - (licenseService.isPlatinumPlus as jest.Mock).mockReturnValue(false); - component = await renderWithData({ tags: [`policy:${policyId}`], item_id: '1' }); - expect(component.queryByTestId('perPolicy')).not.toBeNull(); - expect(component.getByTestId(`policy-${policyId}`).getAttribute('aria-disabled')).toBe('true'); - }); - - it('should change from by policy to global when edit mode and no license with by policy', async () => { - const policyId = policiesRequest.items[0].id; - (licenseService.isPlatinumPlus as jest.Mock).mockReturnValue(false); - component = await renderWithData({ tags: [`policy:${policyId}`], item_id: '1' }); - userEvent.click(component.getByTestId('globalPolicy')); - expect(component.queryByTestId('effectedPolicies-select-policiesSelectable')).toBeFalsy(); - expect(getState().form.entry?.tags).toEqual([`policy:all`]); - }); - - it('should not show warning text when unique fields are added', async () => { - component = await renderWithData({ - entries: [ - { - field: 'event.category', - operator: 'included', - type: 'match', - value: 'some value', - }, - { - field: 'file.name', - operator: 'excluded', - type: 'match', - value: 'some other value', - }, - ], - }); - expect(component.queryByTestId('duplicate-fields-warning-message')).toBeNull(); - }); - - it('should not show warning text when field values are not added', async () => { - component = await renderWithData({ - entries: [ - { - field: 'event.category', - operator: 'included', - type: 'match', - value: '', - }, - { - field: 'event.category', - operator: 'excluded', - type: 'match', - value: '', - }, - ], - }); - expect(component.queryByTestId('duplicate-fields-warning-message')).toBeNull(); - }); - - it('should show warning text when duplicate fields are added with values', async () => { - component = await renderWithData({ - entries: [ - { - field: 'event.category', - operator: 'included', - type: 'match', - value: 'some value', - }, - { - field: 'event.category', - operator: 'excluded', - type: 'match', - value: 'some other value', - }, - ], - }); - expect(component.queryByTestId('duplicate-fields-warning-message')).not.toBeNull(); - }); -}); diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/form/index.tsx b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/form/index.tsx deleted file mode 100644 index 11d1af0a5a2e9..0000000000000 --- a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/form/index.tsx +++ /dev/null @@ -1,487 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { memo, useMemo, useCallback, useState, useEffect } from 'react'; -import { useDispatch } from 'react-redux'; -import { Dispatch } from 'redux'; - -import { isEqual } from 'lodash'; -import { - EuiFieldText, - EuiSpacer, - EuiForm, - EuiFormRow, - EuiSuperSelect, - EuiSuperSelectOption, - EuiText, - EuiHorizontalRule, - EuiTextArea, -} from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n-react'; - -import type { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; -import { EVENT_FILTERS_OPERATORS } from '@kbn/securitysolution-list-utils'; -import { OperatingSystem } from '@kbn/securitysolution-utils'; - -import { getExceptionBuilderComponentLazy } from '@kbn/lists-plugin/public'; -import type { OnChangeProps } from '@kbn/lists-plugin/public'; -import { PolicyData } from '../../../../../../../common/endpoint/types'; -import { AddExceptionComments } from '../../../../../../common/components/exceptions/add_exception_comments'; -import { filterIndexPatterns } from '../../../../../../common/components/exceptions/helpers'; -import { Loader } from '../../../../../../common/components/loader'; -import { useKibana } from '../../../../../../common/lib/kibana'; -import { useFetchIndex } from '../../../../../../common/containers/source'; -import { AppAction } from '../../../../../../common/store/actions'; -import { useEventFiltersSelector } from '../../hooks'; -import { getFormEntryStateMutable, getHasNameError, getNewComment } from '../../../store/selector'; -import { - NAME_LABEL, - NAME_ERROR, - DESCRIPTION_LABEL, - DESCRIPTION_PLACEHOLDER, - NAME_PLACEHOLDER, - OS_LABEL, - RULE_NAME, -} from './translations'; -import { OS_TITLES } from '../../../../../common/translations'; -import { ENDPOINT_EVENT_FILTERS_LIST_ID, EVENT_FILTER_LIST_TYPE } from '../../../constants'; -import { ABOUT_EVENT_FILTERS } from '../../translations'; -import { - EffectedPolicySelect, - EffectedPolicySelection, - EffectedPolicySelectProps, -} from '../../../../../components/effected_policy_select'; -import { - getArtifactTagsByEffectedPolicySelection, - getArtifactTagsWithoutPolicies, - getEffectedPolicySelectionByTags, - isGlobalPolicyEffected, -} from '../../../../../components/effected_policy_select/utils'; -import { useLicense } from '../../../../../../common/hooks/use_license'; - -const OPERATING_SYSTEMS: readonly OperatingSystem[] = [ - OperatingSystem.MAC, - OperatingSystem.WINDOWS, - OperatingSystem.LINUX, -]; - -const getAddedFieldsCounts = (formFields: string[]): { [k: string]: number } => - formFields.reduce<{ [k: string]: number }>((allFields, field) => { - if (field in allFields) { - allFields[field]++; - } else { - allFields[field] = 1; - } - return allFields; - }, {}); - -const computeHasDuplicateFields = (formFieldsList: Record): boolean => - Object.values(formFieldsList).some((e) => e > 1); -interface EventFiltersFormProps { - allowSelectOs?: boolean; - policies: PolicyData[]; - arePoliciesLoading: boolean; -} -export const EventFiltersForm: React.FC = memo( - ({ allowSelectOs = false, policies, arePoliciesLoading }) => { - const { http, unifiedSearch } = useKibana().services; - - const dispatch = useDispatch>(); - const exception = useEventFiltersSelector(getFormEntryStateMutable); - const hasNameError = useEventFiltersSelector(getHasNameError); - const newComment = useEventFiltersSelector(getNewComment); - const [hasBeenInputNameVisited, setHasBeenInputNameVisited] = useState(false); - const isPlatinumPlus = useLicense().isPlatinumPlus(); - const [hasFormChanged, setHasFormChanged] = useState(false); - const [hasDuplicateFields, setHasDuplicateFields] = useState(false); - - // This value has to be memoized to avoid infinite useEffect loop on useFetchIndex - const indexNames = useMemo(() => ['logs-endpoint.events.*'], []); - const [isIndexPatternLoading, { indexPatterns }] = useFetchIndex(indexNames); - - const [selection, setSelection] = useState({ - selected: [], - isGlobal: isGlobalPolicyEffected(exception?.tags), - }); - - const isEditMode = useMemo(() => !!exception?.item_id, [exception?.item_id]); - const [wasByPolicy, setWasByPolicy] = useState(!isGlobalPolicyEffected(exception?.tags)); - - const showAssignmentSection = useMemo(() => { - return ( - isPlatinumPlus || - (isEditMode && - (!selection.isGlobal || (wasByPolicy && selection.isGlobal && hasFormChanged))) - ); - }, [isEditMode, selection.isGlobal, hasFormChanged, isPlatinumPlus, wasByPolicy]); - - // set current policies if not previously selected - useEffect(() => { - if (selection.selected.length === 0 && exception?.tags) { - setSelection(getEffectedPolicySelectionByTags(exception.tags, policies)); - } - }, [exception?.tags, policies, selection.selected.length]); - - // set initial state of `wasByPolicy` that checks if the initial state of the exception was by policy or not - useEffect(() => { - if (!hasFormChanged && exception?.tags) { - setWasByPolicy(!isGlobalPolicyEffected(exception?.tags)); - } - }, [exception?.tags, hasFormChanged]); - - const osOptions: Array> = useMemo( - () => OPERATING_SYSTEMS.map((os) => ({ value: os, inputDisplay: OS_TITLES[os] })), - [] - ); - - const handleOnBuilderChange = useCallback( - (arg: OnChangeProps) => { - if ( - (!hasFormChanged && arg.exceptionItems[0] === undefined) || - isEqual(arg.exceptionItems[0]?.entries, exception?.entries) - ) { - const addedFields = arg.exceptionItems[0]?.entries.map((e) => e.field) || ['']; - setHasDuplicateFields(computeHasDuplicateFields(getAddedFieldsCounts(addedFields))); - setHasFormChanged(true); - return; - } - setHasFormChanged(true); - dispatch({ - type: 'eventFiltersChangeForm', - payload: { - ...(arg.exceptionItems[0] !== undefined - ? { - entry: { - ...arg.exceptionItems[0], - name: exception?.name ?? '', - description: exception?.description ?? '', - comments: exception?.comments ?? [], - os_types: exception?.os_types ?? [OperatingSystem.WINDOWS], - tags: exception?.tags ?? [], - }, - hasItemsError: arg.errorExists || !arg.exceptionItems[0]?.entries?.length, - } - : { - hasItemsError: true, - }), - }, - }); - }, - [dispatch, exception, hasFormChanged] - ); - - const handleOnChangeName = useCallback( - (e: React.ChangeEvent) => { - if (!exception) return; - setHasFormChanged(true); - const name = e.target.value.toString().trim(); - dispatch({ - type: 'eventFiltersChangeForm', - payload: { - entry: { ...exception, name }, - hasNameError: !name, - }, - }); - }, - [dispatch, exception] - ); - - const handleOnDescriptionChange = useCallback( - (e: React.ChangeEvent) => { - if (!exception) return; - setHasFormChanged(true); - const description = e.target.value.toString().trim(); - dispatch({ - type: 'eventFiltersChangeForm', - payload: { - entry: { ...exception, description }, - }, - }); - }, - [dispatch, exception] - ); - - const handleOnChangeComment = useCallback( - (value: string) => { - if (!exception) return; - setHasFormChanged(true); - dispatch({ - type: 'eventFiltersChangeForm', - payload: { - entry: exception, - newComment: value, - }, - }); - }, - [dispatch, exception] - ); - - const exceptionBuilderComponentMemo = useMemo( - () => - getExceptionBuilderComponentLazy({ - allowLargeValueLists: false, - httpService: http, - autocompleteService: unifiedSearch.autocomplete, - exceptionListItems: [exception as ExceptionListItemSchema], - listType: EVENT_FILTER_LIST_TYPE, - listId: ENDPOINT_EVENT_FILTERS_LIST_ID, - listNamespaceType: 'agnostic', - ruleName: RULE_NAME, - indexPatterns, - isOrDisabled: true, - isOrHidden: true, - isAndDisabled: false, - isNestedDisabled: false, - dataTestSubj: 'alert-exception-builder', - idAria: 'alert-exception-builder', - onChange: handleOnBuilderChange, - listTypeSpecificIndexPatternFilter: filterIndexPatterns, - operatorsList: EVENT_FILTERS_OPERATORS, - osTypes: exception?.os_types, - }), - [unifiedSearch, handleOnBuilderChange, http, indexPatterns, exception] - ); - - const nameInputMemo = useMemo( - () => ( - - !hasBeenInputNameVisited && setHasBeenInputNameVisited(true)} - /> - - ), - [hasNameError, exception?.name, handleOnChangeName, hasBeenInputNameVisited] - ); - - const descriptionInputMemo = useMemo( - () => ( - - - - ), - [exception?.description, handleOnDescriptionChange] - ); - - const osInputMemo = useMemo( - () => ( - - { - if (!exception) return; - dispatch({ - type: 'eventFiltersChangeForm', - payload: { - entry: { - ...exception, - os_types: [value as 'windows' | 'linux' | 'macos'], - }, - }, - }); - }} - /> - - ), - [dispatch, exception, osOptions] - ); - - const commentsInputMemo = useMemo( - () => ( - - ), - [exception, handleOnChangeComment, newComment] - ); - - const detailsSection = useMemo( - () => ( - <> - - - - - - - - {ABOUT_EVENT_FILTERS} - - - {nameInputMemo} - {descriptionInputMemo} - > - ), - [nameInputMemo, descriptionInputMemo] - ); - - const criteriaSection = useMemo( - () => ( - <> - - - - - - - - - - - - - {allowSelectOs ? ( - <> - {osInputMemo} - - > - ) : null} - {exceptionBuilderComponentMemo} - > - ), - [allowSelectOs, exceptionBuilderComponentMemo, osInputMemo] - ); - - const handleOnChangeEffectScope: EffectedPolicySelectProps['onChange'] = useCallback( - (currentSelection) => { - if (currentSelection.isGlobal) { - // Preserve last selection inputs - setSelection({ ...selection, isGlobal: true }); - } else { - setSelection(currentSelection); - } - - if (!exception) return; - setHasFormChanged(true); - - dispatch({ - type: 'eventFiltersChangeForm', - payload: { - entry: { - ...exception, - tags: getArtifactTagsByEffectedPolicySelection( - currentSelection, - getArtifactTagsWithoutPolicies(exception?.tags ?? []) - ), - }, - }, - }); - }, - [dispatch, exception, selection] - ); - const policiesSection = useMemo( - () => ( - - ), - [policies, selection, isPlatinumPlus, handleOnChangeEffectScope, arePoliciesLoading] - ); - - const commentsSection = useMemo( - () => ( - <> - - - - - - - - - - - - - {commentsInputMemo} - > - ), - [commentsInputMemo] - ); - - if (isIndexPatternLoading || !exception) { - return ; - } - - return ( - - {detailsSection} - - {criteriaSection} - {hasDuplicateFields && ( - <> - - - - - > - )} - {showAssignmentSection && ( - <> - {policiesSection} - > - )} - - {commentsSection} - - ); - } -); - -EventFiltersForm.displayName = 'EventFiltersForm'; diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/form/translations.ts b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/form/translations.ts deleted file mode 100644 index 20bdde0364e2c..0000000000000 --- a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/form/translations.ts +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { i18n } from '@kbn/i18n'; - -export const NAME_PLACEHOLDER = i18n.translate( - 'xpack.securitySolution.eventFilter.form.name.placeholder', - { - defaultMessage: 'Event filter name', - } -); - -export const NAME_LABEL = i18n.translate('xpack.securitySolution.eventFilter.form.name.label', { - defaultMessage: 'Name your event filter', -}); -export const DESCRIPTION_LABEL = i18n.translate( - 'xpack.securitySolution.eventFilter.form.description.placeholder', - { - defaultMessage: 'Description', - } -); - -export const DESCRIPTION_PLACEHOLDER = i18n.translate( - 'xpack.securitySolution.eventFilter.form.description.label', - { - defaultMessage: 'Describe your event filter', - } -); - -export const NAME_ERROR = i18n.translate('xpack.securitySolution.eventFilter.form.name.error', { - defaultMessage: "The name can't be empty", -}); - -export const OS_LABEL = i18n.translate('xpack.securitySolution.eventFilter.form.os.label', { - defaultMessage: 'Select operating system', -}); - -export const RULE_NAME = i18n.translate('xpack.securitySolution.eventFilter.form.rule.name', { - defaultMessage: 'Endpoint Event Filtering', -}); diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/event_filters_list.test.tsx b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/event_filters_list.test.tsx new file mode 100644 index 0000000000000..79afbce97caf6 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/event_filters_list.test.tsx @@ -0,0 +1,57 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { act, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import React from 'react'; +import { EVENT_FILTERS_PATH } from '../../../../../common/constants'; +import { AppContextTestRender, createAppRootMockRenderer } from '../../../../common/mock/endpoint'; +import { EventFiltersList } from './event_filters_list'; +import { exceptionsListAllHttpMocks } from '../../mocks/exceptions_list_http_mocks'; +import { SEARCHABLE_FIELDS } from '../constants'; +import { parseQueryFilterToKQL } from '../../../common/utils'; + +describe('When on the Event Filters list page', () => { + let render: () => ReturnType; + let renderResult: ReturnType; + let history: AppContextTestRender['history']; + let mockedContext: AppContextTestRender; + let apiMocks: ReturnType; + + beforeEach(() => { + mockedContext = createAppRootMockRenderer(); + ({ history } = mockedContext); + render = () => (renderResult = mockedContext.render()); + apiMocks = exceptionsListAllHttpMocks(mockedContext.coreStart.http); + act(() => { + history.push(EVENT_FILTERS_PATH); + }); + }); + + it('should search using expected exception item fields', async () => { + const expectedFilterString = parseQueryFilterToKQL('fooFooFoo', SEARCHABLE_FIELDS); + const { findAllByTestId } = render(); + await waitFor(async () => { + await expect(findAllByTestId('EventFiltersListPage-card')).resolves.toHaveLength(10); + }); + + apiMocks.responseProvider.exceptionsFind.mockClear(); + userEvent.type(renderResult.getByTestId('searchField'), 'fooFooFoo'); + userEvent.click(renderResult.getByTestId('searchButton')); + await waitFor(() => { + expect(apiMocks.responseProvider.exceptionsFind).toHaveBeenCalled(); + }); + + expect(apiMocks.responseProvider.exceptionsFind).toHaveBeenLastCalledWith( + expect.objectContaining({ + query: expect.objectContaining({ + filter: expectedFilterString, + }), + }) + ); + }); +}); diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/event_filters_list.tsx b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/event_filters_list.tsx new file mode 100644 index 0000000000000..f303987e1acab --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/event_filters_list.tsx @@ -0,0 +1,150 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { memo } from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { DocLinks } from '@kbn/doc-links'; +import { EuiLink } from '@elastic/eui'; + +import { useHttp } from '../../../../common/lib/kibana'; +import { ArtifactListPage, ArtifactListPageProps } from '../../../components/artifact_list_page'; +import { EventFiltersApiClient } from '../service/api_client'; +import { EventFiltersForm } from './components/form'; +import { SEARCHABLE_FIELDS } from '../constants'; + +export const ABOUT_EVENT_FILTERS = i18n.translate('xpack.securitySolution.eventFilters.aboutInfo', { + defaultMessage: + 'Event filters exclude high volume or unwanted events from being written to Elasticsearch.', +}); + +export const NAME_LABEL = i18n.translate('xpack.securitySolution.eventFilter.form.name.label', { + defaultMessage: 'Name', +}); +export const DESCRIPTION_LABEL = i18n.translate( + 'xpack.securitySolution.eventFilter.form.description.placeholder', + { + defaultMessage: 'Description', + } +); + +export const NAME_ERROR = i18n.translate('xpack.securitySolution.eventFilter.form.name.error', { + defaultMessage: "The name can't be empty", +}); + +export const OS_LABEL = i18n.translate('xpack.securitySolution.eventFilter.form.os.label', { + defaultMessage: 'Select operating system', +}); + +export const RULE_NAME = i18n.translate('xpack.securitySolution.eventFilter.form.rule.name', { + defaultMessage: 'Endpoint Event Filtering', +}); + +const EVENT_FILTERS_PAGE_LABELS: ArtifactListPageProps['labels'] = { + pageTitle: i18n.translate('xpack.securitySolution.eventFilters.pageTitle', { + defaultMessage: 'Event Filters', + }), + pageAboutInfo: i18n.translate('xpack.securitySolution.eventFilters.pageAboutInfo', { + defaultMessage: + 'Event filters exclude high volume or unwanted events from being written to Elasticsearch.', + }), + pageAddButtonTitle: i18n.translate('xpack.securitySolution.eventFilters.pageAddButtonTitle', { + defaultMessage: 'Add event filter', + }), + getShowingCountLabel: (total) => + i18n.translate('xpack.securitySolution.eventFilters.showingTotal', { + defaultMessage: 'Showing {total} {total, plural, one {event filter} other {event filters}}', + values: { total }, + }), + cardActionEditLabel: i18n.translate('xpack.securitySolution.eventFilters.cardActionEditLabel', { + defaultMessage: 'Edit event filter', + }), + cardActionDeleteLabel: i18n.translate( + 'xpack.securitySolution.eventFilters.cardActionDeleteLabel', + { + defaultMessage: 'Delete event filter', + } + ), + flyoutCreateTitle: i18n.translate('xpack.securitySolution.eventFilters.flyoutCreateTitle', { + defaultMessage: 'Add event filter', + }), + flyoutEditTitle: i18n.translate('xpack.securitySolution.eventFilters.flyoutEditTitle', { + defaultMessage: 'Edit event filter', + }), + flyoutCreateSubmitButtonLabel: i18n.translate( + 'xpack.securitySolution.eventFilters.flyoutCreateSubmitButtonLabel', + { defaultMessage: 'Add event filter' } + ), + flyoutCreateSubmitSuccess: ({ name }) => + i18n.translate('xpack.securitySolution.eventFilters.flyoutCreateSubmitSuccess', { + defaultMessage: '"{name}" has been added to the event filters list.', + values: { name }, + }), + flyoutEditSubmitSuccess: ({ name }) => + i18n.translate('xpack.securitySolution.eventFilters.flyoutEditSubmitSuccess', { + defaultMessage: '"{name}" has been updated.', + values: { name }, + }), + flyoutDowngradedLicenseDocsInfo: ( + securitySolutionDocsLinks: DocLinks['securitySolution'] + ): React.ReactNode => { + return ( + <> + + + + + > + ); + }, + deleteActionSuccess: (itemName) => + i18n.translate('xpack.securitySolution.eventFilters.deleteSuccess', { + defaultMessage: '"{itemName}" has been removed from event filters list.', + values: { itemName }, + }), + emptyStateTitle: i18n.translate('xpack.securitySolution.eventFilters.emptyStateTitle', { + defaultMessage: 'Add your first event filter', + }), + emptyStateInfo: i18n.translate('xpack.securitySolution.eventFilters.emptyStateInfo', { + defaultMessage: + 'Add an event filter to exclude high volume or unwanted events from being written to Elasticsearch.', + }), + emptyStatePrimaryButtonLabel: i18n.translate( + 'xpack.securitySolution.eventFilters.emptyStatePrimaryButtonLabel', + { defaultMessage: 'Add event filter' } + ), + searchPlaceholderInfo: i18n.translate( + 'xpack.securitySolution.eventFilters.searchPlaceholderInfo', + { + defaultMessage: 'Search on the fields below: name, description, comments, value', + } + ), +}; + +export const EventFiltersList = memo(() => { + const http = useHttp(); + const eventFiltersApiClient = EventFiltersApiClient.getInstance(http); + + return ( + + ); +}); + +EventFiltersList.displayName = 'EventFiltersList'; diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/event_filters_list_page.test.tsx b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/event_filters_list_page.test.tsx deleted file mode 100644 index ec0adf0c10a23..0000000000000 --- a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/event_filters_list_page.test.tsx +++ /dev/null @@ -1,247 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { AppContextTestRender, createAppRootMockRenderer } from '../../../../common/mock/endpoint'; -import React from 'react'; -import { fireEvent, act, waitFor } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; -import { EventFiltersListPage } from './event_filters_list_page'; -import { eventFiltersListQueryHttpMock } from '../test_utils'; -import { isFailedResourceState, isLoadedResourceState } from '../../../state'; -import { sendGetEndpointSpecificPackagePolicies } from '../../../services/policies/policies'; -import { sendGetEndpointSpecificPackagePoliciesMock } from '../../../services/policies/test_mock_utils'; - -// Needed to mock the data services used by the ExceptionItem component -jest.mock('../../../../common/lib/kibana'); -jest.mock('../../../../common/components/user_privileges'); -jest.mock('../../../services/policies/policies'); - -(sendGetEndpointSpecificPackagePolicies as jest.Mock).mockImplementation( - sendGetEndpointSpecificPackagePoliciesMock -); - -describe('When on the Event Filters List Page', () => { - let render: () => ReturnType; - let renderResult: ReturnType; - let history: AppContextTestRender['history']; - let coreStart: AppContextTestRender['coreStart']; - let waitForAction: AppContextTestRender['middlewareSpy']['waitForAction']; - let mockedApi: ReturnType; - - const dataReceived = () => - act(async () => { - await waitForAction('eventFiltersListPageDataChanged', { - validate(action) { - return isLoadedResourceState(action.payload); - }, - }); - }); - - beforeEach(() => { - const mockedContext = createAppRootMockRenderer(); - - ({ history, coreStart } = mockedContext); - render = () => (renderResult = mockedContext.render()); - mockedApi = eventFiltersListQueryHttpMock(coreStart.http); - waitForAction = mockedContext.middlewareSpy.waitForAction; - - act(() => { - history.push('/administration/event_filters'); - }); - }); - - describe('And no data exists', () => { - beforeEach(async () => { - mockedApi.responseProvider.eventFiltersList.mockReturnValue({ - data: [], - page: 1, - per_page: 10, - total: 0, - }); - - render(); - - await act(async () => { - await waitForAction('eventFiltersListPageDataExistsChanged', { - validate(action) { - return isLoadedResourceState(action.payload); - }, - }); - }); - }); - - it('should show the Empty message', () => { - expect(renderResult.getByTestId('eventFiltersEmpty')).toBeTruthy(); - expect(renderResult.getByTestId('eventFiltersListEmptyStateAddButton')).toBeTruthy(); - }); - - it('should open create flyout when add button in empty state is clicked', async () => { - act(() => { - fireEvent.click(renderResult.getByTestId('eventFiltersListEmptyStateAddButton')); - }); - - expect(renderResult.getByTestId('eventFiltersCreateEditFlyout')).toBeTruthy(); - expect(history.location.search).toEqual('?show=create'); - }); - }); - - describe('And data exists', () => { - it('should show loading indicator while retrieving data', async () => { - let releaseApiResponse: () => void; - - mockedApi.responseProvider.eventFiltersList.mockDelay.mockReturnValue( - new Promise((r) => (releaseApiResponse = r)) - ); - render(); - - expect(renderResult.getByTestId('eventFilterListLoader')).toBeTruthy(); - - const wasReceived = dataReceived(); - releaseApiResponse!(); - await wasReceived; - - expect(renderResult.container.querySelector('.euiProgress')).toBeNull(); - }); - - it('should show items on the list', async () => { - render(); - await dataReceived(); - - expect(renderResult.getByTestId('eventFilterCard')).toBeTruthy(); - }); - - it('should render expected fields on card', async () => { - render(); - await dataReceived(); - - [ - ['subHeader-touchedBy-createdBy-value', 'some user'], - ['subHeader-touchedBy-updatedBy-value', 'some user'], - ['header-created-value', '4/20/2020'], - ['header-updated-value', '4/20/2020'], - ].forEach(([suffix, value]) => - expect(renderResult.getByTestId(`eventFilterCard-${suffix}`).textContent).toEqual(value) - ); - }); - - it('should show API error if one is encountered', async () => { - mockedApi.responseProvider.eventFiltersList.mockImplementation(() => { - throw new Error('oh no'); - }); - render(); - await act(async () => { - await waitForAction('eventFiltersListPageDataChanged', { - validate(action) { - return isFailedResourceState(action.payload); - }, - }); - }); - - expect(renderResult.getByTestId('eventFiltersContent-error').textContent).toEqual(' oh no'); - }); - - it('should show modal when delete is clicked on a card', async () => { - render(); - await dataReceived(); - - await act(async () => { - (await renderResult.findAllByTestId('eventFilterCard-header-actions-button'))[0].click(); - }); - - await act(async () => { - (await renderResult.findByTestId('deleteEventFilterAction')).click(); - }); - - expect( - renderResult.baseElement.querySelector('[data-test-subj="eventFilterDeleteModalHeader"]') - ).not.toBeNull(); - }); - }); - - describe('And search is dispatched', () => { - beforeEach(async () => { - act(() => { - history.push('/administration/event_filters?filter=test'); - }); - renderResult = render(); - await act(async () => { - await waitForAction('eventFiltersListPageDataChanged'); - }); - }); - - it('search bar is filled with query params', () => { - expect(renderResult.getByDisplayValue('test')).not.toBeNull(); - }); - - it('search action is dispatched', async () => { - await act(async () => { - fireEvent.click(renderResult.getByTestId('searchButton')); - expect(await waitForAction('userChangedUrl')).not.toBeNull(); - }); - }); - }); - - describe('And policies select is dispatched', () => { - it('should apply policy filter', async () => { - const policies = await sendGetEndpointSpecificPackagePoliciesMock(); - (sendGetEndpointSpecificPackagePolicies as jest.Mock).mockResolvedValue(policies); - - renderResult = render(); - await waitFor(() => { - expect(sendGetEndpointSpecificPackagePolicies).toHaveBeenCalled(); - }); - - const firstPolicy = policies.items[0]; - - userEvent.click(renderResult.getByTestId('policiesSelectorButton')); - userEvent.click(renderResult.getByTestId(`policiesSelector-popover-items-${firstPolicy.id}`)); - await waitFor(() => expect(waitForAction('userChangedUrl')).not.toBeNull()); - }); - }); - - describe('and the back button is present', () => { - beforeEach(async () => { - renderResult = render(); - act(() => { - history.push('/administration/event_filters', { - onBackButtonNavigateTo: [{ appId: 'appId' }], - backButtonLabel: 'back to fleet', - backButtonUrl: '/fleet', - }); - }); - }); - - it('back button is present', () => { - const button = renderResult.queryByTestId('backToOrigin'); - expect(button).not.toBeNull(); - expect(button).toHaveAttribute('href', '/fleet'); - }); - - it('back button is still present after push history', () => { - act(() => { - history.push('/administration/event_filters'); - }); - const button = renderResult.queryByTestId('backToOrigin'); - expect(button).not.toBeNull(); - expect(button).toHaveAttribute('href', '/fleet'); - }); - }); - - describe('and the back button is not present', () => { - beforeEach(async () => { - renderResult = render(); - act(() => { - history.push('/administration/event_filters'); - }); - }); - - it('back button is not present when missing history params', () => { - const button = renderResult.queryByTestId('backToOrigin'); - expect(button).toBeNull(); - }); - }); -}); diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/event_filters_list_page.tsx b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/event_filters_list_page.tsx deleted file mode 100644 index b982c260f9ca8..0000000000000 --- a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/event_filters_list_page.tsx +++ /dev/null @@ -1,339 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { memo, useCallback, useMemo, useEffect } from 'react'; -import { useDispatch } from 'react-redux'; -import { Dispatch } from 'redux'; -import { useHistory, useLocation } from 'react-router-dom'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n-react'; -import { EuiButton, EuiSpacer, EuiText } from '@elastic/eui'; -import styled from 'styled-components'; - -import type { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; -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'; -import { - getListFetchError, - getListIsLoading, - getListItems, - getListPagination, - getCurrentLocation, - getListPageDoesDataExist, - getActionError, - getFormEntry, - showDeleteModal, - getTotalCountListItems, -} from '../store/selector'; -import { PaginatedContent, PaginatedContentProps } from '../../../components/paginated_content'; -import { Immutable, ListPageRouteState } from '../../../../../common/endpoint/types'; -import { ExceptionItem } from '../../../../common/components/exceptions/viewer/exception_item'; -import { - AnyArtifact, - ArtifactEntryCard, - ArtifactEntryCardProps, -} from '../../../components/artifact_entry_card'; -import { EventFilterDeleteModal } from './components/event_filter_delete_modal'; - -import { SearchExceptions } from '../../../components/search_exceptions'; -import { BackToExternalAppSecondaryButton } from '../../../components/back_to_external_app_secondary_button'; -import { BackToExternalAppButton } from '../../../components/back_to_external_app_button'; -import { ABOUT_EVENT_FILTERS } from './translations'; -import { useGetEndpointSpecificPolicies } from '../../../services/policies/hooks'; -import { useToasts } from '../../../../common/lib/kibana'; -import { getLoadPoliciesError } from '../../../common/translations'; -import { useEndpointPoliciesToArtifactPolicies } from '../../../components/artifact_entry_card/hooks/use_endpoint_policies_to_artifact_policies'; -import { ManagementPageLoader } from '../../../components/management_page_loader'; -import { useMemoizedRouteState } from '../../../common/hooks'; - -type ArtifactEntryCardType = typeof ArtifactEntryCard; - -type EventListPaginatedContent = PaginatedContentProps< - Immutable, - typeof ExceptionItem ->; - -const AdministrationListPage = styled(_AdministrationListPage)` - .event-filter-container > * { - margin-bottom: ${({ theme }) => theme.eui.euiSizeL}; - - &:last-child { - margin-bottom: 0; - } - } -`; - -const EDIT_EVENT_FILTER_ACTION_LABEL = i18n.translate( - 'xpack.securitySolution.eventFilters.list.cardAction.edit', - { - defaultMessage: 'Edit event filter', - } -); - -const DELETE_EVENT_FILTER_ACTION_LABEL = i18n.translate( - 'xpack.securitySolution.eventFilters.list.cardAction.delete', - { - defaultMessage: 'Delete event filter', - } -); - -export const EventFiltersListPage = memo(() => { - const { state: routeState } = useLocation(); - const history = useHistory(); - const dispatch = useDispatch>(); - const toasts = useToasts(); - const isActionError = useEventFiltersSelector(getActionError); - const formEntry = useEventFiltersSelector(getFormEntry); - const listItems = useEventFiltersSelector(getListItems); - const totalCountListItems = useEventFiltersSelector(getTotalCountListItems); - const pagination = useEventFiltersSelector(getListPagination); - const isLoading = useEventFiltersSelector(getListIsLoading); - const fetchError = useEventFiltersSelector(getListFetchError); - const location = useEventFiltersSelector(getCurrentLocation); - const doesDataExist = useEventFiltersSelector(getListPageDoesDataExist); - const showDelete = useEventFiltersSelector(showDeleteModal); - - const navigateCallback = useEventFiltersNavigateCallback(); - const showFlyout = !!location.show; - - const memoizedRouteState = useMemoizedRouteState(routeState); - - const backButtonEmptyComponent = useMemo(() => { - if (memoizedRouteState && memoizedRouteState.onBackButtonNavigateTo) { - return ; - } - }, [memoizedRouteState]); - - const backButtonHeaderComponent = useMemo(() => { - if (memoizedRouteState && memoizedRouteState.onBackButtonNavigateTo) { - return ; - } - }, [memoizedRouteState]); - - // load the list of policies - const policiesRequest = useGetEndpointSpecificPolicies({ - perPage: 1000, - onError: (err) => { - toasts.addDanger(getLoadPoliciesError(err)); - }, - }); - - const artifactCardPolicies = useEndpointPoliciesToArtifactPolicies(policiesRequest.data?.items); - - // 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({ - show: 'create', - id: undefined, - }), - [navigateCallback] - ); - - const handleCancelButtonClick = useCallback( - () => - navigateCallback({ - show: undefined, - id: undefined, - }), - [navigateCallback] - ); - - const handlePaginatedContentChange: EventListPaginatedContent['onChange'] = useCallback( - ({ pageIndex, pageSize }) => { - navigateCallback({ - page_index: pageIndex, - page_size: pageSize, - }); - }, - [navigateCallback] - ); - - const handleOnSearch = useCallback( - (query: string, includedPolicies?: string) => { - dispatch({ type: 'eventFiltersForceRefresh', payload: { forceRefresh: true } }); - navigateCallback({ filter: query, included_policies: includedPolicies }); - }, - [navigateCallback, dispatch] - ); - - const artifactCardPropsPerItem = useMemo(() => { - const cachedCardProps: Record = {}; - - // Casting `listItems` below to remove the `Immutable<>` from it in order to prevent errors - // with common component's props - for (const eventFilter of listItems as ExceptionListItemSchema[]) { - cachedCardProps[eventFilter.id] = { - item: eventFilter as AnyArtifact, - policies: artifactCardPolicies, - 'data-test-subj': 'eventFilterCard', - actions: [ - { - icon: 'controlsHorizontal', - onClick: () => { - history.push( - getEventFiltersListPath({ - ...location, - show: 'edit', - id: eventFilter.id, - }) - ); - }, - 'data-test-subj': 'editEventFilterAction', - children: EDIT_EVENT_FILTER_ACTION_LABEL, - }, - { - icon: 'trash', - onClick: () => { - dispatch({ - type: 'eventFilterForDeletion', - payload: eventFilter, - }); - }, - 'data-test-subj': 'deleteEventFilterAction', - children: DELETE_EVENT_FILTER_ACTION_LABEL, - }, - ], - hideDescription: !eventFilter.description, - hideComments: !eventFilter.comments.length, - }; - } - - return cachedCardProps; - }, [artifactCardPolicies, dispatch, history, listItems, location]); - - const handleArtifactCardProps = useCallback( - (eventFilter: ExceptionListItemSchema) => { - return artifactCardPropsPerItem[eventFilter.id]; - }, - [artifactCardPropsPerItem] - ); - - if (isLoading && !doesDataExist) { - return ; - } - - return ( - - } - subtitle={ABOUT_EVENT_FILTERS} - actions={ - doesDataExist && ( - - - - ) - } - hideHeader={!doesDataExist} - > - {showFlyout && ( - - )} - - {showDelete && } - - {doesDataExist && ( - <> - - - - - - - > - )} - - - items={listItems} - ItemComponent={ArtifactEntryCard} - itemComponentProps={handleArtifactCardProps} - onChange={handlePaginatedContentChange} - error={fetchError?.message} - loading={isLoading} - pagination={pagination} - contentClassName="event-filter-container" - data-test-subj="eventFiltersContent" - noItemsMessage={ - !doesDataExist && ( - - ) - } - /> - - ); -}); - -EventFiltersListPage.displayName = 'EventFiltersListPage'; diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/hooks.ts b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/hooks.ts deleted file mode 100644 index e48f11c7f8bae..0000000000000 --- a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/hooks.ts +++ /dev/null @@ -1,78 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { useState, useCallback } from 'react'; -import { useSelector } from 'react-redux'; -import { useHistory } from 'react-router-dom'; - -import { - isCreationSuccessful, - getFormEntryStateMutable, - getActionError, - getCurrentLocation, -} from '../store/selector'; - -import { useToasts } from '../../../../common/lib/kibana'; -import { - getCreationSuccessMessage, - getUpdateSuccessMessage, - getCreationErrorMessage, - getUpdateErrorMessage, - getGetErrorMessage, -} from './translations'; - -import { State } from '../../../../common/store'; -import { EventFiltersListPageState, EventFiltersPageLocation } from '../types'; -import { getEventFiltersListPath } from '../../../common/routing'; - -import { - MANAGEMENT_STORE_EVENT_FILTERS_NAMESPACE as EVENT_FILTER_NS, - MANAGEMENT_STORE_GLOBAL_NAMESPACE as GLOBAL_NS, -} from '../../../common/constants'; - -export function useEventFiltersSelector(selector: (state: EventFiltersListPageState) => R): R { - return useSelector((state: State) => - selector(state[GLOBAL_NS][EVENT_FILTER_NS] as EventFiltersListPageState) - ); -} - -export const useEventFiltersNotification = () => { - const creationSuccessful = useEventFiltersSelector(isCreationSuccessful); - 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); - 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)); - } - } -}; - -export function useEventFiltersNavigateCallback() { - const location = useEventFiltersSelector(getCurrentLocation); - const history = useHistory(); - - return useCallback( - (args: Partial) => - history.push(getEventFiltersListPath({ ...location, ...args })), - [history, location] - ); -} diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/translations.ts b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/translations.ts index 6177fb7822c92..db6908f2baa8d 100644 --- a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/translations.ts +++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/translations.ts @@ -5,47 +5,20 @@ * 2.0. */ +import { HttpFetchError } from '@kbn/core/public'; import { i18n } from '@kbn/i18n'; +import { ArtifactFormComponentProps } from '../../../components/artifact_list_page'; -import { ServerApiError } from '../../../../common/types'; -import { EventFiltersForm } from '../types'; - -export const getCreationSuccessMessage = (entry: EventFiltersForm['entry']) => { - return i18n.translate('xpack.securitySolution.eventFilter.form.creationSuccessToastTitle', { +export const getCreationSuccessMessage = (item: ArtifactFormComponentProps['item']) => { + return i18n.translate('xpack.securitySolution.eventFilter.flyoutForm.creationSuccessToastTitle', { defaultMessage: '"{name}" has been added to the event filters list.', - values: { name: entry?.name }, - }); -}; - -export const getUpdateSuccessMessage = (entry: EventFiltersForm['entry']) => { - 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.create', { - defaultMessage: 'There was an error creating the new event filter: "{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 event filter: "{error}"', - values: { error: updateError.message }, + values: { name: item?.name }, }); }; -export const getGetErrorMessage = (getError: ServerApiError) => { - return i18n.translate('xpack.securitySolution.eventFilter.form.failedToastTitle.get', { - defaultMessage: 'Unable to edit event filter: "{error}"', - values: { error: getError.message }, - }); +export const getCreationErrorMessage = (creationError: HttpFetchError) => { + return { + title: 'There was an error creating the new event filter: "{error}"', + message: { error: creationError.message }, + }; }; - -export const ABOUT_EVENT_FILTERS = i18n.translate('xpack.securitySolution.eventFilters.aboutInfo', { - defaultMessage: - 'Add an event filter to exclude high volume or unwanted events from being written to Elasticsearch.', -}); diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/use_event_filters_notification.test.tsx b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/use_event_filters_notification.test.tsx deleted file mode 100644 index 7643125c587e7..0000000000000 --- a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/use_event_filters_notification.test.tsx +++ /dev/null @@ -1,230 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { Provider } from 'react-redux'; -import { renderHook, act } from '@testing-library/react-hooks'; - -import { NotificationsStart } from '@kbn/core/public'; -import { coreMock } from '@kbn/core/public/mocks'; -import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public/context'; -import type { - CreateExceptionListItemSchema, - ExceptionListItemSchema, -} from '@kbn/securitysolution-io-ts-list-types'; - -import { - createdEventFilterEntryMock, - createGlobalNoMiddlewareStore, - ecsEventMock, -} from '../test_utils'; -import { useEventFiltersNotification } from './hooks'; -import { - getCreationErrorMessage, - getCreationSuccessMessage, - getGetErrorMessage, - getUpdateSuccessMessage, - getUpdateErrorMessage, -} from './translations'; -import { getInitialExceptionFromEvent } from '../store/utils'; -import { - getLastLoadedResourceState, - FailedResourceState, -} from '../../../state/async_resource_state'; - -const mockNotifications = () => coreMock.createStart({ basePath: '/mock' }).notifications; - -const renderNotifications = ( - store: ReturnType, - notifications: NotificationsStart -) => { - const Wrapper: React.FC = ({ children }) => ( - - {children} - - ); - return renderHook(useEventFiltersNotification, { wrapper: Wrapper }); -}; - -describe('EventFiltersNotification', () => { - it('renders correctly initially', () => { - const notifications = mockNotifications(); - - renderNotifications(createGlobalNoMiddlewareStore(), notifications); - - expect(notifications.toasts.addSuccess).not.toBeCalled(); - expect(notifications.toasts.addDanger).not.toBeCalled(); - }); - - it('shows success notification when creation successful', () => { - const store = createGlobalNoMiddlewareStore(); - const notifications = mockNotifications(); - - renderNotifications(store, notifications); - - act(() => { - const entry = getInitialExceptionFromEvent(ecsEventMock()); - store.dispatch({ - type: 'eventFiltersInitForm', - payload: { entry }, - }); - }); - - act(() => { - store.dispatch({ - type: 'eventFiltersFormStateChanged', - payload: { - type: 'LoadedResourceState', - data: store.getState().management.eventFilters.form.entry as ExceptionListItemSchema, - }, - }); - }); - - expect(notifications.toasts.addSuccess).toBeCalledWith( - getCreationSuccessMessage( - store.getState().management.eventFilters.form.entry as CreateExceptionListItemSchema - ) - ); - 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(); - - renderNotifications(store, notifications); - - act(() => { - const entry = getInitialExceptionFromEvent(ecsEventMock()); - store.dispatch({ - type: 'eventFiltersInitForm', - payload: { entry }, - }); - }); - - 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( - getCreationErrorMessage( - ( - store.getState().management.eventFilters.form - .submissionResourceState as FailedResourceState - ).error - ) - ); - }); - - 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 - ) - ); - }); -}); diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/store/utils.ts b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/utils.ts similarity index 100% rename from x-pack/plugins/security_solution/public/management/pages/event_filters/store/utils.ts rename to x-pack/plugins/security_solution/public/management/pages/event_filters/view/utils.ts diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/artifacts/delete_modal/policy_artifacts_delete_modal.test.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/artifacts/delete_modal/policy_artifacts_delete_modal.test.tsx index c30b5a8887338..11772324ff51c 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/artifacts/delete_modal/policy_artifacts_delete_modal.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/artifacts/delete_modal/policy_artifacts_delete_modal.test.tsx @@ -5,7 +5,10 @@ * 2.0. */ -import { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; +import { + CreateExceptionListSchema, + ExceptionListItemSchema, +} from '@kbn/securitysolution-io-ts-list-types'; import { act, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import React from 'react'; @@ -16,17 +19,26 @@ import { createAppRootMockRenderer, } from '../../../../../../common/mock/endpoint'; import { PolicyArtifactsDeleteModal } from './policy_artifacts_delete_modal'; -import { eventFiltersListQueryHttpMock } from '../../../../event_filters/test_utils'; -import { EventFiltersApiClient } from '../../../../event_filters/service/event_filters_api_client'; +import { exceptionsListAllHttpMocks } from '../../../../mocks/exceptions_list_http_mocks'; +import { ExceptionsListApiClient } from '../../../../../services/exceptions_list/exceptions_list_api_client'; import { POLICY_ARTIFACT_DELETE_MODAL_LABELS } from './translations'; -describe('Policy details artifacts delete modal', () => { +const listType: Array = [ + 'endpoint_events', + 'detection', + 'endpoint', + 'endpoint_trusted_apps', + 'endpoint_host_isolation_exceptions', + 'endpoint_blocklists', +]; + +describe.each(listType)('Policy details %s artifact delete modal', (type) => { let policyId: string; let render: () => Promise>; let renderResult: ReturnType; let mockedContext: AppContextTestRender; let exception: ExceptionListItemSchema; - let mockedApi: ReturnType; + let mockedApi: ReturnType; let onCloseMock: () => jest.Mock; beforeEach(() => { @@ -34,20 +46,30 @@ describe('Policy details artifacts delete modal', () => { mockedContext = createAppRootMockRenderer(); exception = getExceptionListItemSchemaMock(); onCloseMock = jest.fn(); - mockedApi = eventFiltersListQueryHttpMock(mockedContext.coreStart.http); + mockedApi = exceptionsListAllHttpMocks(mockedContext.coreStart.http); render = async () => { await act(async () => { renderResult = mockedContext.render( ); - await waitFor(mockedApi.responseProvider.eventFiltersList); + + mockedApi.responseProvider.exceptionsFind.mockReturnValue({ + data: [], + total: 0, + page: 1, + per_page: 10, + }); }); return renderResult; }; @@ -75,9 +97,9 @@ describe('Policy details artifacts delete modal', () => { const confirmButton = renderResult.getByTestId('confirmModalConfirmButton'); userEvent.click(confirmButton); await waitFor(() => { - expect(mockedApi.responseProvider.eventFiltersUpdateOne).toHaveBeenLastCalledWith({ + expect(mockedApi.responseProvider.exceptionUpdate).toHaveBeenLastCalledWith({ body: JSON.stringify( - EventFiltersApiClient.cleanExceptionsBeforeUpdate({ + ExceptionsListApiClient.cleanExceptionsBeforeUpdate({ ...exception, tags: ['policy:1234', 'policy:4321', 'not-a-policy-tag'], }) @@ -93,7 +115,7 @@ describe('Policy details artifacts delete modal', () => { userEvent.click(confirmButton); await waitFor(() => { - expect(mockedApi.responseProvider.eventFiltersUpdateOne).toHaveBeenCalled(); + expect(mockedApi.responseProvider.exceptionUpdate).toHaveBeenCalled(); }); expect(onCloseMock).toHaveBeenCalled(); @@ -102,7 +124,7 @@ describe('Policy details artifacts delete modal', () => { it('should show an error toast if the operation failed', async () => { const error = new Error('the server is too far away'); - mockedApi.responseProvider.eventFiltersUpdateOne.mockImplementation(() => { + mockedApi.responseProvider.exceptionUpdate.mockImplementation(() => { throw error; }); @@ -111,7 +133,7 @@ describe('Policy details artifacts delete modal', () => { userEvent.click(confirmButton); await waitFor(() => { - expect(mockedApi.responseProvider.eventFiltersUpdateOne).toHaveBeenCalled(); + expect(mockedApi.responseProvider.exceptionUpdate).toHaveBeenCalled(); }); expect(mockedContext.coreStart.notifications.toasts.addError).toHaveBeenCalledWith(error, { diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/artifacts/flyout/policy_artifacts_flyout.test.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/artifacts/flyout/policy_artifacts_flyout.test.tsx index edf9f5b21d8b4..056a8daa92d3a 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/artifacts/flyout/policy_artifacts_flyout.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/artifacts/flyout/policy_artifacts_flyout.test.tsx @@ -27,7 +27,7 @@ import { UpdateExceptionListItemSchema, } from '@kbn/securitysolution-io-ts-list-types'; import { cleanEventFilterToUpdate } from '../../../../event_filters/service/service_actions'; -import { EventFiltersApiClient } from '../../../../event_filters/service/event_filters_api_client'; +import { EventFiltersApiClient } from '../../../../event_filters/service/api_client'; import { POLICY_ARTIFACT_FLYOUT_LABELS } from './translations'; const getDefaultQueryParameters = (customFilter: string | undefined = '') => ({ diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/artifacts/layout/policy_artifacts_layout.test.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/artifacts/layout/policy_artifacts_layout.test.tsx index 453c84f63689e..67452fd11df53 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/artifacts/layout/policy_artifacts_layout.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/artifacts/layout/policy_artifacts_layout.test.tsx @@ -24,7 +24,7 @@ import { eventFiltersListQueryHttpMock } from '../../../../event_filters/test_ut import { getFoundExceptionListItemSchemaMock } from '@kbn/lists-plugin/common/schemas/response/found_exception_list_item_schema.mock'; import { getEndpointPrivilegesInitialStateMock } from '../../../../../../common/components/user_privileges/endpoint/mocks'; import { POLICY_ARTIFACT_EVENT_FILTERS_LABELS } from '../../tabs/event_filters_translations'; -import { EventFiltersApiClient } from '../../../../event_filters/service/event_filters_api_client'; +import { EventFiltersApiClient } from '../../../../event_filters/service/api_client'; import { SEARCHABLE_FIELDS as EVENT_FILTERS_SEARCHABLE_FIELDS } from '../../../../event_filters/constants'; import { FormattedMessage } from '@kbn/i18n-react'; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/artifacts/list/policy_artifacts_list.test.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/artifacts/list/policy_artifacts_list.test.tsx index de2f245a9c098..b3c104b27977f 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/artifacts/list/policy_artifacts_list.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/artifacts/list/policy_artifacts_list.test.tsx @@ -22,7 +22,7 @@ import { parseQueryFilterToKQL, parsePoliciesAndFilterToKql } from '../../../../ import { SEARCHABLE_FIELDS } from '../../../../event_filters/constants'; import { getEndpointPrivilegesInitialStateMock } from '../../../../../../common/components/user_privileges/endpoint/mocks'; import { POLICY_ARTIFACT_LIST_LABELS } from './translations'; -import { EventFiltersApiClient } from '../../../../event_filters/service/event_filters_api_client'; +import { EventFiltersApiClient } from '../../../../event_filters/service/api_client'; const endpointGenerator = new EndpointDocGenerator('seed'); const getDefaultQueryParameters = (customFilter: string | undefined = '') => ({ diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/components/fleet_artifacts_card.test.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/components/fleet_artifacts_card.test.tsx index 87860db1fe69d..16b5e9f975e22 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/components/fleet_artifacts_card.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/components/fleet_artifacts_card.test.tsx @@ -15,7 +15,7 @@ import { getEventFiltersListPath } from '../../../../../../common/routing'; import { eventFiltersListQueryHttpMock } from '../../../../../event_filters/test_utils'; import { getEndpointPrivilegesInitialStateMock } from '../../../../../../../common/components/user_privileges/endpoint/mocks'; import { useToasts } from '../../../../../../../common/lib/kibana'; -import { EventFiltersApiClient } from '../../../../../event_filters/service/event_filters_api_client'; +import { EventFiltersApiClient } from '../../../../../event_filters/service/api_client'; import { FleetArtifactsCard } from './fleet_artifacts_card'; import { EVENT_FILTERS_LABELS } from '..'; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/components/fleet_integration_artifacts_card.test.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/components/fleet_integration_artifacts_card.test.tsx index c88f54f01fd2b..b8724850e1188 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/components/fleet_integration_artifacts_card.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/components/fleet_integration_artifacts_card.test.tsx @@ -19,7 +19,7 @@ import { EndpointDocGenerator } from '../../../../../../../../common/endpoint/ge import { getPolicyEventFiltersPath } from '../../../../../../common/routing'; import { PolicyData } from '../../../../../../../../common/endpoint/types'; import { getSummaryExceptionListSchemaMock } from '@kbn/lists-plugin/common/schemas/response/exception_list_summary_schema.mock'; -import { EventFiltersApiClient } from '../../../../../event_filters/service/event_filters_api_client'; +import { EventFiltersApiClient } from '../../../../../event_filters/service/api_client'; import { SEARCHABLE_FIELDS } from '../../../../../event_filters/constants'; import { EVENT_FILTERS_LABELS } from '../../endpoint_policy_edit_extension'; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/endpoint_package_custom_extension.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/endpoint_package_custom_extension.tsx index 72cc9852b0e7d..f1af7c3505297 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/endpoint_package_custom_extension.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/endpoint_package_custom_extension.tsx @@ -11,7 +11,7 @@ import { PackageCustomExtensionComponentProps } from '@kbn/fleet-plugin/public'; import { useHttp } from '../../../../../../common/lib/kibana'; import { useCanSeeHostIsolationExceptionsMenu } from '../../../../host_isolation_exceptions/view/hooks'; import { TrustedAppsApiClient } from '../../../../trusted_apps/service/api_client'; -import { EventFiltersApiClient } from '../../../../event_filters/service/event_filters_api_client'; +import { EventFiltersApiClient } from '../../../../event_filters/service/api_client'; import { HostIsolationExceptionsApiClient } from '../../../../host_isolation_exceptions/host_isolation_exceptions_api_client'; import { BlocklistsApiClient } from '../../../../blocklist/services'; import { FleetArtifactsCard } from './components/fleet_artifacts_card'; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_policy_edit_extension.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_policy_edit_extension.tsx index dfb2677ecb594..9ac612aec05ed 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_policy_edit_extension.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_policy_edit_extension.tsx @@ -35,7 +35,7 @@ import { useUserPrivileges } from '../../../../../common/components/user_privile import { FleetIntegrationArtifactsCard } from './endpoint_package_custom_extension/components/fleet_integration_artifacts_card'; import { BlocklistsApiClient } from '../../../blocklist/services'; import { HostIsolationExceptionsApiClient } from '../../../host_isolation_exceptions/host_isolation_exceptions_api_client'; -import { EventFiltersApiClient } from '../../../event_filters/service/event_filters_api_client'; +import { EventFiltersApiClient } from '../../../event_filters/service/api_client'; import { TrustedAppsApiClient } from '../../../trusted_apps/service/api_client'; import { SEARCHABLE_FIELDS as BLOCKLIST_SEARCHABLE_FIELDS } from '../../../blocklist/constants'; import { SEARCHABLE_FIELDS as HOST_ISOLATION_EXCEPTIONS_SEARCHABLE_FIELDS } from '../../../host_isolation_exceptions/constants'; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/tabs/policy_tabs.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/tabs/policy_tabs.tsx index f3a20a1abfd66..f81b55b5e8a31 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/tabs/policy_tabs.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/tabs/policy_tabs.tsx @@ -42,7 +42,7 @@ import { POLICY_ARTIFACT_TRUSTED_APPS_LABELS } from './trusted_apps_translations import { POLICY_ARTIFACT_HOST_ISOLATION_EXCEPTIONS_LABELS } from './host_isolation_exceptions_translations'; import { POLICY_ARTIFACT_BLOCKLISTS_LABELS } from './blocklists_translations'; import { TrustedAppsApiClient } from '../../../trusted_apps/service/api_client'; -import { EventFiltersApiClient } from '../../../event_filters/service/event_filters_api_client'; +import { EventFiltersApiClient } from '../../../event_filters/service/api_client'; import { BlocklistsApiClient } from '../../../blocklist/services/blocklists_api_client'; import { HostIsolationExceptionsApiClient } from '../../../host_isolation_exceptions/host_isolation_exceptions_api_client'; import { SEARCHABLE_FIELDS as TRUSTED_APPS_SEARCHABLE_FIELDS } from '../../../trusted_apps/constants'; diff --git a/x-pack/plugins/security_solution/public/management/store/middleware.ts b/x-pack/plugins/security_solution/public/management/store/middleware.ts index 86a5ade340058..475fe0bc9bb7c 100644 --- a/x-pack/plugins/security_solution/public/management/store/middleware.ts +++ b/x-pack/plugins/security_solution/public/management/store/middleware.ts @@ -14,11 +14,9 @@ import { MANAGEMENT_STORE_ENDPOINTS_NAMESPACE, MANAGEMENT_STORE_GLOBAL_NAMESPACE, MANAGEMENT_STORE_POLICY_DETAILS_NAMESPACE, - MANAGEMENT_STORE_EVENT_FILTERS_NAMESPACE, } from '../common/constants'; import { policyDetailsMiddlewareFactory } from '../pages/policy/store/policy_details'; import { endpointMiddlewareFactory } from '../pages/endpoint_hosts/store/middleware'; -import { eventFiltersPageMiddlewareFactory } from '../pages/event_filters/store/middleware'; type ManagementSubStateKey = keyof State[typeof MANAGEMENT_STORE_GLOBAL_NAMESPACE]; @@ -40,10 +38,5 @@ export const managementMiddlewareFactory: SecuritySubPluginMiddlewareFactory = ( createSubStateSelector(MANAGEMENT_STORE_ENDPOINTS_NAMESPACE), endpointMiddlewareFactory(coreStart, depsStart) ), - - substateMiddlewareFactory( - createSubStateSelector(MANAGEMENT_STORE_EVENT_FILTERS_NAMESPACE), - eventFiltersPageMiddlewareFactory(coreStart, depsStart) - ), ]; }; diff --git a/x-pack/plugins/security_solution/public/management/store/reducer.ts b/x-pack/plugins/security_solution/public/management/store/reducer.ts index 2fd20129ddca8..678819a51d747 100644 --- a/x-pack/plugins/security_solution/public/management/store/reducer.ts +++ b/x-pack/plugins/security_solution/public/management/store/reducer.ts @@ -13,14 +13,11 @@ import { import { MANAGEMENT_STORE_ENDPOINTS_NAMESPACE, MANAGEMENT_STORE_POLICY_DETAILS_NAMESPACE, - MANAGEMENT_STORE_EVENT_FILTERS_NAMESPACE, } from '../common/constants'; import { ImmutableCombineReducers } from '../../common/store'; import { Immutable } from '../../../common/endpoint/types'; import { ManagementState } from '../types'; import { endpointListReducer } from '../pages/endpoint_hosts/store/reducer'; -import { initialEventFiltersPageState } from '../pages/event_filters/store/builders'; -import { eventFiltersPageReducer } from '../pages/event_filters/store/reducer'; import { initialEndpointPageState } from '../pages/endpoint_hosts/store/builders'; const immutableCombineReducers: ImmutableCombineReducers = combineReducers; @@ -31,7 +28,6 @@ const immutableCombineReducers: ImmutableCombineReducers = combineReducers; export const mockManagementState: Immutable = { [MANAGEMENT_STORE_POLICY_DETAILS_NAMESPACE]: initialPolicyDetailsState(), [MANAGEMENT_STORE_ENDPOINTS_NAMESPACE]: initialEndpointPageState(), - [MANAGEMENT_STORE_EVENT_FILTERS_NAMESPACE]: initialEventFiltersPageState(), }; /** @@ -40,5 +36,4 @@ export const mockManagementState: Immutable = { export const managementReducer = immutableCombineReducers({ [MANAGEMENT_STORE_POLICY_DETAILS_NAMESPACE]: policyDetailsReducer, [MANAGEMENT_STORE_ENDPOINTS_NAMESPACE]: endpointListReducer, - [MANAGEMENT_STORE_EVENT_FILTERS_NAMESPACE]: eventFiltersPageReducer, }); diff --git a/x-pack/plugins/security_solution/public/management/types.ts b/x-pack/plugins/security_solution/public/management/types.ts index 0ad0f2e757c00..f1cb7b2623b39 100644 --- a/x-pack/plugins/security_solution/public/management/types.ts +++ b/x-pack/plugins/security_solution/public/management/types.ts @@ -9,7 +9,6 @@ import { CombinedState } from 'redux'; import { SecurityPageName } from '../app/types'; import { PolicyDetailsState } from './pages/policy/types'; import { EndpointState } from './pages/endpoint_hosts/types'; -import { EventFiltersListPageState } from './pages/event_filters/types'; /** * The type for the management store global namespace. Used mostly internally to reference @@ -20,7 +19,6 @@ export type ManagementStoreGlobalNamespace = 'management'; export type ManagementState = CombinedState<{ policyDetails: PolicyDetailsState; endpoints: EndpointState; - eventFilters: EventFiltersListPageState; }>; /** diff --git a/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/footer.tsx b/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/footer.tsx index 0f3bb6e7177bd..86a8047b3ad76 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/footer.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/footer.tsx @@ -14,7 +14,7 @@ import type { TimelineEventsDetailsItem } from '../../../../../common/search_str import { TimelineId } from '../../../../../common/types'; import { useExceptionFlyout } from '../../../../detections/components/alerts_table/timeline_actions/use_add_exception_flyout'; import { AddExceptionFlyoutWrapper } from '../../../../detections/components/alerts_table/timeline_actions/alert_context_menu'; -import { EventFiltersFlyout } from '../../../../management/pages/event_filters/view/components/flyout'; +import { EventFiltersFlyout } from '../../../../management/pages/event_filters/view/components/event_filters_flyout'; import { useEventFilterModal } from '../../../../detections/components/alerts_table/timeline_actions/use_event_filter_modal'; import { getFieldValue } from '../../../../detections/components/host_isolation/helpers'; import { Status } from '../../../../../common/detection_engine/schemas/common/schemas'; diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index a9d350146c0d9..70d3a81a2f808 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -25469,52 +25469,22 @@ "xpack.securitySolution.eventDetails.viewAllFields": "Afficher tous les champs dans le tableau", "xpack.securitySolution.eventDetails.viewColumnCheckboxAriaLabel": "Afficher la colonne {field}", "xpack.securitySolution.eventDetails.viewRuleDetailPage": "Afficher la page Détails de la règle", - "xpack.securitySolution.eventFilter.form.creationSuccessToastTitle": "\"{name}\" a été ajouté à la liste de filtres d'événements.", - "xpack.securitySolution.eventFilter.form.description.label": "Décrivez votre filtre d'événement", "xpack.securitySolution.eventFilter.form.description.placeholder": "Description", - "xpack.securitySolution.eventFilter.form.failedToastTitle.create": "Une erreur est survenue lors de la création du nouveau filtre d'événement : \"{error}\"", - "xpack.securitySolution.eventFilter.form.failedToastTitle.get": "Impossible de modifier le filtre d'événement : \"{error}\"", - "xpack.securitySolution.eventFilter.form.failedToastTitle.update": "Une erreur est survenue lors de la mise à jour du filtre d'événement : \"{error}\"", "xpack.securitySolution.eventFilter.form.name.error": "Le nom doit être renseigné", "xpack.securitySolution.eventFilter.form.name.label": "Nommer votre filtre d'événement", - "xpack.securitySolution.eventFilter.form.name.placeholder": "Nom du filtre d'événement", "xpack.securitySolution.eventFilter.form.os.label": "Sélectionner un système d'exploitation", "xpack.securitySolution.eventFilter.form.rule.name": "Filtrage d'événement Endpoint", - "xpack.securitySolution.eventFilter.form.updateSuccessToastTitle": "{name} a été mis à jour avec succès.", - "xpack.securitySolution.eventFilter.search.placeholder": "Rechercher sur les champs ci-dessous : nom, description, commentaires, valeur", "xpack.securitySolution.eventFilters.aboutInfo": "Ajouter un filtre d'événement pour exclure les volumes importants ou les événements non souhaités de l'écriture dans Elasticsearch.", "xpack.securitySolution.eventFilters.commentsSectionDescription": "Ajouter un commentaire à votre filtre d'événement.", "xpack.securitySolution.eventFilters.commentsSectionTitle": "Commentaires", - "xpack.securitySolution.eventFilters.criteriaSectionDescription": "Sélectionnez un système d'exploitation et ajoutez des conditions.", "xpack.securitySolution.eventFilters.criteriaSectionTitle": "Conditions", - "xpack.securitySolution.eventFilters.deletionDialog.calloutMessage": "La suppression de cette entrée entraînera son retrait dans {count} {count, plural, one {politique associée} other {politiques associées}}.", - "xpack.securitySolution.eventFilters.deletionDialog.calloutTitle": "Avertissement", - "xpack.securitySolution.eventFilters.deletionDialog.cancelButton": "Annuler", - "xpack.securitySolution.eventFilters.deletionDialog.confirmButton": "Supprimer", - "xpack.securitySolution.eventFilters.deletionDialog.deleteFailure": "Impossible de retirer \"{name}\" de la liste de filtres d'événements. Raison : {message}", - "xpack.securitySolution.eventFilters.deletionDialog.deleteSuccess": "\"{name}\" a été retiré de la liste de filtres d'événements.", - "xpack.securitySolution.eventFilters.deletionDialog.subMessage": "Cette action ne peut pas être annulée. Voulez-vous vraiment continuer ?", - "xpack.securitySolution.eventFilters.deletionDialog.title": "Supprimer \"{name}\"", "xpack.securitySolution.eventFilters.detailsSectionTitle": "Détails", - "xpack.securitySolution.eventFilters.docsLink": "Documentation relative aux filtres d'événements.", "xpack.securitySolution.eventFilters.eventFiltersFlyout.actions.cancel": "Annuler", "xpack.securitySolution.eventFilters.eventFiltersFlyout.actions.confirm.create": "Ajouter un filtre d'événement", - "xpack.securitySolution.eventFilters.eventFiltersFlyout.actions.confirm.update": "Enregistrer", "xpack.securitySolution.eventFilters.eventFiltersFlyout.actions.confirm.update.withData": "Ajouter un filtre d'événement de point de terminaison", "xpack.securitySolution.eventFilters.eventFiltersFlyout.subtitle.create": "Ajouter un filtre d'événement", "xpack.securitySolution.eventFilters.eventFiltersFlyout.subtitle.create.withData": "Endpoint Security", - "xpack.securitySolution.eventFilters.eventFiltersFlyout.subtitle.update": "Mettre à jour le filtre d'événement", "xpack.securitySolution.eventFilters.eventFiltersFlyout.title.create.withData": "Ajouter un filtre d'événement de point de terminaison", - "xpack.securitySolution.eventFilters.expiredLicenseMessage": "Votre licence Kibana est passée à une version inférieure. Les futures configurations de politiques seront désormais globalement affectées à toutes les politiques. Pour en savoir plus, consultez notre ", - "xpack.securitySolution.eventFilters.expiredLicenseTitle": "Licence expirée", - "xpack.securitySolution.eventFilters.list.cardAction.delete": "Supprimer le filtre d'événement", - "xpack.securitySolution.eventFilters.list.cardAction.edit": "Modifier le filtre d'événement", - "xpack.securitySolution.eventFilters.list.pageAddButton": "Ajouter un filtre d'événement", - "xpack.securitySolution.eventFilters.list.pageTitle": "Filtres d'événements", - "xpack.securitySolution.eventFilters.list.totalCount": "Affichage de {total, plural, one {# filtre d'événement} other {# filtres d'événements}}", - "xpack.securitySolution.eventFilters.listEmpty.addButton": "Ajouter un filtre d'événement", - "xpack.securitySolution.eventFilters.listEmpty.message": "Ajouter un filtre d'événement pour exclure les volumes importants ou les événements non souhaités de l'écriture dans Elasticsearch.", - "xpack.securitySolution.eventFilters.listEmpty.title": "Ajouter votre premier filtre d'événement", "xpack.securitySolution.eventFiltersTab": "Filtres d'événements", "xpack.securitySolution.eventRenderers.alertsDescription": "Les alertes sont affichées lorsqu'un malware ou ransomware est bloqué ou détecté", "xpack.securitySolution.eventRenderers.alertsName": "Alertes", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 89813c1104606..a20feeeccdb1b 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -25619,52 +25619,22 @@ "xpack.securitySolution.eventDetails.viewAllFields": "テーブルのすべてのフィールドを表示", "xpack.securitySolution.eventDetails.viewColumnCheckboxAriaLabel": "{field} 列を表示", "xpack.securitySolution.eventDetails.viewRuleDetailPage": "ルール詳細ページを表示", - "xpack.securitySolution.eventFilter.form.creationSuccessToastTitle": "\"{name}\"がイベントフィルターリストに追加されました。", - "xpack.securitySolution.eventFilter.form.description.label": "イベントフィルターの説明", "xpack.securitySolution.eventFilter.form.description.placeholder": "説明", - "xpack.securitySolution.eventFilter.form.failedToastTitle.create": "新しいイベントフィルターの作成中にエラーが発生しました:\"{error}\"", - "xpack.securitySolution.eventFilter.form.failedToastTitle.get": "イベントフィルターを編集できません:\"{error}\"", - "xpack.securitySolution.eventFilter.form.failedToastTitle.update": "イベントフィルターの更新中にエラーが発生しました:\"{error}\"", "xpack.securitySolution.eventFilter.form.name.error": "名前を空にすることはできません", "xpack.securitySolution.eventFilter.form.name.label": "イベントフィルターの名前を付ける", - "xpack.securitySolution.eventFilter.form.name.placeholder": "イベントフィルター名", "xpack.securitySolution.eventFilter.form.os.label": "オペレーティングシステムを選択", "xpack.securitySolution.eventFilter.form.rule.name": "エンドポイントイベントフィルター", - "xpack.securitySolution.eventFilter.form.updateSuccessToastTitle": "\"{name}\"が正常に更新されました", - "xpack.securitySolution.eventFilter.search.placeholder": "次のフィールドで検索:名前、説明、コメント、値", "xpack.securitySolution.eventFilters.aboutInfo": "イベントフィルターを追加して、大量のイベントや不要なイベントがElasticsearchに書き込まれないように除外します。", "xpack.securitySolution.eventFilters.commentsSectionDescription": "イベントフィルターにコメントを追加します。", "xpack.securitySolution.eventFilters.commentsSectionTitle": "コメント", - "xpack.securitySolution.eventFilters.criteriaSectionDescription": "オペレーティングシステムを選択して、条件を追加します。", "xpack.securitySolution.eventFilters.criteriaSectionTitle": "条件", - "xpack.securitySolution.eventFilters.deletionDialog.calloutMessage": "このエントリを削除すると、{count}個の関連付けられた{count, plural, other {ポリシー}}から削除されます。", - "xpack.securitySolution.eventFilters.deletionDialog.calloutTitle": "警告", - "xpack.securitySolution.eventFilters.deletionDialog.cancelButton": "キャンセル", - "xpack.securitySolution.eventFilters.deletionDialog.confirmButton": "削除", - "xpack.securitySolution.eventFilters.deletionDialog.deleteFailure": "イベントフィルターリストから\"{name}\"を削除できません。理由:{message}", - "xpack.securitySolution.eventFilters.deletionDialog.deleteSuccess": "\"{name}\"がイベントフィルターリストから削除されました。", - "xpack.securitySolution.eventFilters.deletionDialog.subMessage": "この操作は元に戻すことができません。続行していいですか?", - "xpack.securitySolution.eventFilters.deletionDialog.title": "\"{name}\"を削除", "xpack.securitySolution.eventFilters.detailsSectionTitle": "詳細", - "xpack.securitySolution.eventFilters.docsLink": "イベントフィルタードキュメント。", "xpack.securitySolution.eventFilters.eventFiltersFlyout.actions.cancel": "キャンセル", "xpack.securitySolution.eventFilters.eventFiltersFlyout.actions.confirm.create": "イベントフィルターを追加", - "xpack.securitySolution.eventFilters.eventFiltersFlyout.actions.confirm.update": "保存", "xpack.securitySolution.eventFilters.eventFiltersFlyout.actions.confirm.update.withData": "エンドポイントイベントフィルターを追加", "xpack.securitySolution.eventFilters.eventFiltersFlyout.subtitle.create": "イベントフィルターを追加", "xpack.securitySolution.eventFilters.eventFiltersFlyout.subtitle.create.withData": "Endpoint Security", - "xpack.securitySolution.eventFilters.eventFiltersFlyout.subtitle.update": "イベントフィルターを更新", "xpack.securitySolution.eventFilters.eventFiltersFlyout.title.create.withData": "エンドポイントイベントフィルターを追加", - "xpack.securitySolution.eventFilters.expiredLicenseMessage": "Kibanaライセンスがダウングレードされました。今後のポリシー構成はグローバルにすべてのポリシーに割り当てられます。詳細はご覧ください。 ", - "xpack.securitySolution.eventFilters.expiredLicenseTitle": "失効したライセンス", - "xpack.securitySolution.eventFilters.list.cardAction.delete": "イベントフィルターを削除", - "xpack.securitySolution.eventFilters.list.cardAction.edit": "イベントフィルターを編集", - "xpack.securitySolution.eventFilters.list.pageAddButton": "イベントフィルターを追加", - "xpack.securitySolution.eventFilters.list.pageTitle": "イベントフィルター", - "xpack.securitySolution.eventFilters.list.totalCount": "{total, plural, other {# 個のイベントフィルター}}を表示中", - "xpack.securitySolution.eventFilters.listEmpty.addButton": "イベントフィルターを追加", - "xpack.securitySolution.eventFilters.listEmpty.message": "イベントフィルターを追加して、大量のイベントや不要なイベントがElasticsearchに書き込まれないように除外します。", - "xpack.securitySolution.eventFilters.listEmpty.title": "最初のイベントフィルターを追加", "xpack.securitySolution.eventFilters.warningMessage.duplicateFields": "同じフィールド値の乗数を使用すると、エンドポイントパフォーマンスが劣化したり、効果的ではないルールが作成されたりすることがあります", "xpack.securitySolution.eventFiltersTab": "イベントフィルター", "xpack.securitySolution.eventRenderers.alertsDescription": "マルウェアまたはランサムウェアが防御、検出されたときにアラートが表示されます。", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index a9278d13031f4..a2c33d9a1fae7 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -25652,52 +25652,22 @@ "xpack.securitySolution.eventDetails.viewAllFields": "查看表中的所有字段", "xpack.securitySolution.eventDetails.viewColumnCheckboxAriaLabel": "查看 {field} 列", "xpack.securitySolution.eventDetails.viewRuleDetailPage": "查看规则详情页面", - "xpack.securitySolution.eventFilter.form.creationSuccessToastTitle": "“{name}”已添加到事件筛选列表。", - "xpack.securitySolution.eventFilter.form.description.label": "描述您的事件筛选", "xpack.securitySolution.eventFilter.form.description.placeholder": "描述", - "xpack.securitySolution.eventFilter.form.failedToastTitle.create": "创建新事件筛选时出错:“{error}”", - "xpack.securitySolution.eventFilter.form.failedToastTitle.get": "无法编辑事件筛选:“{error}”", - "xpack.securitySolution.eventFilter.form.failedToastTitle.update": "更新事件筛选时出错:“{error}”", "xpack.securitySolution.eventFilter.form.name.error": "名称不能为空", "xpack.securitySolution.eventFilter.form.name.label": "命名您的事件筛选", - "xpack.securitySolution.eventFilter.form.name.placeholder": "事件筛选名称", "xpack.securitySolution.eventFilter.form.os.label": "选择操作系统", "xpack.securitySolution.eventFilter.form.rule.name": "终端事件筛选", - "xpack.securitySolution.eventFilter.form.updateSuccessToastTitle": "“{name}”已成功更新。", - "xpack.securitySolution.eventFilter.search.placeholder": "搜索下面的字段:name、description、comments、value", "xpack.securitySolution.eventFilters.aboutInfo": "添加事件筛选以阻止高数目或非预期事件写入到 Elasticsearch。", "xpack.securitySolution.eventFilters.commentsSectionDescription": "将注释添加到事件筛选。", "xpack.securitySolution.eventFilters.commentsSectionTitle": "注释", - "xpack.securitySolution.eventFilters.criteriaSectionDescription": "选择操作系统,然后添加条件。", "xpack.securitySolution.eventFilters.criteriaSectionTitle": "条件", - "xpack.securitySolution.eventFilters.deletionDialog.calloutMessage": "删除此条目会将其从 {count} 个关联{count, plural, other {策略}}中移除。", - "xpack.securitySolution.eventFilters.deletionDialog.calloutTitle": "警告", - "xpack.securitySolution.eventFilters.deletionDialog.cancelButton": "取消", - "xpack.securitySolution.eventFilters.deletionDialog.confirmButton": "删除", - "xpack.securitySolution.eventFilters.deletionDialog.deleteFailure": "无法从事件筛选列表中移除“{name}”。原因:{message}", - "xpack.securitySolution.eventFilters.deletionDialog.deleteSuccess": "“{name}”已从事件筛选列表中移除。", - "xpack.securitySolution.eventFilters.deletionDialog.subMessage": "此操作无法撤消。是否确定要继续?", - "xpack.securitySolution.eventFilters.deletionDialog.title": "删除“{name}”", "xpack.securitySolution.eventFilters.detailsSectionTitle": "详情", - "xpack.securitySolution.eventFilters.docsLink": "事件筛选文档。", "xpack.securitySolution.eventFilters.eventFiltersFlyout.actions.cancel": "取消", "xpack.securitySolution.eventFilters.eventFiltersFlyout.actions.confirm.create": "添加事件筛选", - "xpack.securitySolution.eventFilters.eventFiltersFlyout.actions.confirm.update": "保存", "xpack.securitySolution.eventFilters.eventFiltersFlyout.actions.confirm.update.withData": "添加终端事件筛选", "xpack.securitySolution.eventFilters.eventFiltersFlyout.subtitle.create": "添加事件筛选", "xpack.securitySolution.eventFilters.eventFiltersFlyout.subtitle.create.withData": "Endpoint Security", - "xpack.securitySolution.eventFilters.eventFiltersFlyout.subtitle.update": "更新事件筛选", "xpack.securitySolution.eventFilters.eventFiltersFlyout.title.create.withData": "添加终端事件筛选", - "xpack.securitySolution.eventFilters.expiredLicenseMessage": "您的 Kibana 许可证已降级。现在会将未来的策略配置全局分配给所有策略。有关更多信息,请参见 ", - "xpack.securitySolution.eventFilters.expiredLicenseTitle": "已过期许可证", - "xpack.securitySolution.eventFilters.list.cardAction.delete": "删除事件筛选", - "xpack.securitySolution.eventFilters.list.cardAction.edit": "编辑事件筛选", - "xpack.securitySolution.eventFilters.list.pageAddButton": "添加事件筛选", - "xpack.securitySolution.eventFilters.list.pageTitle": "事件筛选", - "xpack.securitySolution.eventFilters.list.totalCount": "正在显示 {total, plural, other {# 个事件筛选}}", - "xpack.securitySolution.eventFilters.listEmpty.addButton": "添加事件筛选", - "xpack.securitySolution.eventFilters.listEmpty.message": "添加事件筛选以阻止高数目或非预期事件写入到 Elasticsearch。", - "xpack.securitySolution.eventFilters.listEmpty.title": "添加您的首个事件筛选", "xpack.securitySolution.eventFilters.warningMessage.duplicateFields": "使用相同提交值的倍数可能会降低终端性能和/或创建低效规则", "xpack.securitySolution.eventFiltersTab": "事件筛选", "xpack.securitySolution.eventRenderers.alertsDescription": "阻止或检测到恶意软件或勒索软件时,显示告警",
- -
+ +
{ABOUT_EVENT_FILTERS}
+ {allowSelectOs ? ( + + ) : ( + + )} +