From a48ec3b8f56e4c58bee7316c46e5d0206e12911a Mon Sep 17 00:00:00 2001 From: David Sanchez Soler Date: Fri, 23 Apr 2021 12:23:11 +0200 Subject: [PATCH 01/10] New flyout with event filters form --- .../view/components/flyout/index.test.tsx | 169 ++++++++++++++++++ .../view/components/flyout/index.tsx | 129 +++++++++++++ 2 files changed, 298 insertions(+) create mode 100644 x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/flyout/index.test.tsx create mode 100644 x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/flyout/index.tsx 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 new file mode 100644 index 00000000000000..045497e33e294c --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/flyout/index.test.tsx @@ -0,0 +1,169 @@ +/* + * 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 } 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 { + CreateExceptionListItemSchema, + ExceptionListItemSchema, +} from '../../../../../../shared_imports'; + +jest.mock('../form'); +jest.mock('../../hooks', () => { + const originalModule = jest.requireActual('../../hooks'); + const useEventFiltersNotification = jest.fn().mockImplementation(() => {}); + + return { + ...originalModule, + useEventFiltersNotification, + }; +}); + +let mockedContext: AppContextTestRender; +let waitForAction: MiddlewareActionSpyHelper['waitForAction']; +let render: () => ReturnType; +const act = reactTestingLibrary.act; +let onCancelMock: jest.Mock; + +describe('Event filter flyout', () => { + beforeEach(() => { + mockedContext = createAppRootMockRenderer(); + waitForAction = mockedContext.middlewareSpy.waitForAction; + onCancelMock = jest.fn(); + render = () => mockedContext.render(); + }); + + afterEach(() => reactTestingLibrary.cleanup()); + + it('should renders correctly', () => { + const component = render(); + expect(component.getAllByText('Add Endpoint Event Filter')).not.toBeNull(); + expect(component.getByText('cancel')).not.toBeNull(); + expect(component.getByText('Endpoint Security')).not.toBeNull(); + }); + + it('should dispatch action to init form store on mount', async () => { + await act(async () => { + render(); + await waitForAction('eventFiltersInitForm'); + }); + + expect(mockedContext.store.getState().management.eventFilters.form.entry).not.toBeUndefined(); + expect( + mockedContext.store.getState().management.eventFilters.form.entry!.entries[0].field + ).toBe(''); + }); + + it('should confirm form when button is disabled', () => { + const component = render(); + const confirmButton = component.getByTestId('add-exception-confirm-button'); + act(() => { + fireEvent.click(confirmButton); + }); + expect( + mockedContext.store.getState().management.eventFilters.form.submissionResourceState.type + ).toBe('UninitialisedResourceState'); + }); + + it('should confirm form when button is enabled', async () => { + const component = render(); + mockedContext.store.dispatch({ + type: 'eventFiltersChangeForm', + payload: { + entry: { + ...(mockedContext.store.getState().management.eventFilters.form! + .entry as CreateExceptionListItemSchema), + name: 'test', + os_types: ['windows'], + }, + hasNameError: false, + hasOSError: false, + }, + }); + const confirmButton = component.getByTestId('add-exception-confirm-button'); + + await act(async () => { + fireEvent.click(confirmButton); + await waitForAction('eventFiltersCreateSuccess'); + }); + expect( + mockedContext.store.getState().management.eventFilters.form.submissionResourceState.type + ).toBe('UninitialisedResourceState'); + 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: mockedContext.store.getState().management.eventFilters.form! + .entry as ExceptionListItemSchema, + }, + }); + }); + + expect(onCancelMock).toHaveBeenCalledTimes(1); + }); + + it('should close when click on cancel button', () => { + const 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', () => { + const 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', () => { + const 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); + }); +}); 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 new file mode 100644 index 00000000000000..b7f760cba4b0c6 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/flyout/index.tsx @@ -0,0 +1,129 @@ +/* + * 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 } from 'react'; +import { useDispatch } from 'react-redux'; +import { Dispatch } from 'redux'; + +import { FormattedMessage } from '@kbn/i18n/react'; +import { + EuiFlyout, + EuiFlyoutHeader, + EuiTitle, + EuiFlyoutBody, + EuiFlyoutFooter, + EuiButton, + EuiButtonEmpty, + EuiFlexGroup, + EuiFlexItem, +} from '@elastic/eui'; +import { AppAction } from '../../../../../../common/store/actions'; +import { Ecs } from '../../../../../../../common/ecs'; +import { EventFiltersForm } from '../form'; +import { useEventFiltersSelector, useEventFiltersNotification } from '../../hooks'; +import { + getFormHasError, + isCreationInProgress, + isCreationSuccessful, +} from '../../../store/selector'; +import { getInitialExceptionFromEvent } from '../../../store/utils'; + +export interface EventFiltersFlyoutProps { + data?: Ecs; + onCancel(): void; +} + +export const EventFiltersFlyout: React.FC = memo(({ data, onCancel }) => { + useEventFiltersNotification(); + const dispatch = useDispatch>(); + const formHasError = useEventFiltersSelector(getFormHasError); + const creationInProgress = useEventFiltersSelector(isCreationInProgress); + const creationSuccessful = useEventFiltersSelector(isCreationSuccessful); + + useEffect(() => { + if (creationSuccessful) { + onCancel(); + dispatch({ + type: 'eventFiltersFormStateChanged', + payload: { + type: 'UninitialisedResourceState', + }, + }); + } + }, [creationSuccessful, onCancel, dispatch]); + + // Initialize the store with the event passed as prop to allow render the form. It acts as componentDidMount + useEffect(() => { + dispatch({ + type: 'eventFiltersInitForm', + payload: { entry: getInitialExceptionFromEvent(data) }, + }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + const handleOnCancel = useCallback(() => { + if (creationInProgress) return; + onCancel(); + }, [creationInProgress, onCancel]); + + const confirmButtonMemo = useMemo( + () => ( + dispatch({ type: 'eventFiltersCreateStart' })} + isLoading={creationInProgress} + > + + + ), + [dispatch, formHasError, creationInProgress] + ); + + return ( + + + +

+ +

+
+ +
+ + + + + + + + + + + + + {confirmButtonMemo} + + +
+ ); +}); + +EventFiltersFlyout.displayName = 'EventFiltersFlyout'; From 2fe9ee387e8d30b5167c09c84a5a950730a7d721 Mon Sep 17 00:00:00 2001 From: David Sanchez Soler Date: Fri, 23 Apr 2021 12:24:15 +0200 Subject: [PATCH 02/10] Changes on event filters form to allow OS selector. Add new error on state for OS. Add created entry to the entries list --- .../pages/event_filters/state/index.ts | 1 + .../pages/event_filters/store/action.ts | 1 + .../pages/event_filters/store/builders.ts | 1 + .../event_filters/store/middleware.test.ts | 1 + .../pages/event_filters/store/middleware.ts | 6 ++++ .../pages/event_filters/store/reducer.test.ts | 30 +++++++++++++++++++ .../pages/event_filters/store/reducer.ts | 13 ++++++++ .../pages/event_filters/store/selector.ts | 2 +- .../event_filters/store/selectors.test.ts | 14 ++++++++- .../pages/event_filters/store/utils.ts | 20 ++++++++++--- .../view/components/form/index.tsx | 26 +++++++++++----- .../view/components/form/translations.ts | 2 +- 12 files changed, 103 insertions(+), 14 deletions(-) diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/state/index.ts b/x-pack/plugins/security_solution/public/management/pages/event_filters/state/index.ts index b5e867e64888de..805898aec566e8 100644 --- a/x-pack/plugins/security_solution/public/management/pages/event_filters/state/index.ts +++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/state/index.ts @@ -13,6 +13,7 @@ export interface EventFiltersListPageState { entry: CreateExceptionListItemSchema | ExceptionListItemSchema | undefined; hasNameError: boolean; hasItemsError: boolean; + hasOSError: boolean; submissionResourceState: AsyncResourceState; }; } 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 index de2a74ed6f72d0..79ef5cbc4e42cd 100644 --- 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 @@ -20,6 +20,7 @@ export type EventFiltersChangeForm = Action<'eventFiltersChangeForm'> & { entry: ExceptionListItemSchema | CreateExceptionListItemSchema; hasNameError?: boolean; hasItemsError?: boolean; + hasOSError?: boolean; }; }; 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 index 86ba9b1e49c38c..62888ea0d3a7d9 100644 --- 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 @@ -13,6 +13,7 @@ export const initialEventFiltersPageState = (): EventFiltersListPageState => ({ entry: undefined, hasNameError: false, hasItemsError: false, + hasOSError: false, submissionResourceState: { type: 'UninitialisedResourceState' }, }, }); 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 index afb82ebe011ff1..b0b47bc1d6fa04 100644 --- 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 @@ -86,6 +86,7 @@ describe('middleware', () => { await spyMiddleware.waitForAction('eventFiltersFormStateChanged'); expect(store.getState()).toStrictEqual({ ...initialState, + entries: [createdEventFilterEntryMock()], form: { ...store.getState().form, submissionResourceState: { 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 index 2f2c7a9e22661f..b379d0893c1334 100644 --- 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 @@ -37,6 +37,12 @@ const eventFiltersCreate = async ( const sanitizedEntry = transformNewItemOutput(formEntry as CreateExceptionListItemSchema); const exception = await eventFiltersService.addEventFilters(sanitizedEntry); + store.dispatch({ + type: 'eventFiltersCreateSuccess', + payload: { + exception, + }, + }); store.dispatch({ type: 'eventFiltersFormStateChanged', payload: { 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 index 5ee956ddac3e9f..c6300ef847dcfe 100644 --- 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 @@ -78,5 +78,35 @@ describe('reducer', () => { }, }); }); + + it('create is success when there is no entry on entries list', () => { + const result = eventFiltersPageReducer(initialState, { + type: 'eventFiltersCreateSuccess', + payload: { + exception: createdEventFilterEntryMock(), + }, + }); + + expect(result).toStrictEqual({ + ...initialState, + entries: [createdEventFilterEntryMock()], + }); + }); + + it('create is success when there there are entries on entries list', () => { + const customizedInitialState = { + ...initialState, + entries: [createdEventFilterEntryMock(), createdEventFilterEntryMock()], + }; + const result = eventFiltersPageReducer(customizedInitialState, { + type: 'eventFiltersCreateSuccess', + payload: { + exception: { ...createdEventFilterEntryMock(), meta: {} }, + }, + }); + + expect(result.entries).toHaveLength(3); + expect(result.entries[0]!.meta).not.toBeUndefined(); + }); }); }); 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 index c8f80eaee18524..20e93376cdf848 100644 --- 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 @@ -13,6 +13,7 @@ import { EventFiltersInitForm, EventFiltersChangeForm, EventFiltersFormStateChanged, + EventFiltersCreateSuccess, } from './action'; import { EventFiltersListPageState } from '../state'; @@ -31,6 +32,7 @@ const eventFiltersInitForm: CaseReducer = (state, action) ...state.form, entry: action.payload.entry, hasNameError: !action.payload.entry.name, + hasOSError: !action.payload.entry.os_types?.length, submissionResourceState: { type: 'UninitialisedResourceState', }, @@ -52,6 +54,8 @@ const eventFiltersChangeForm: CaseReducer = (state, acti action.payload.hasNameError !== undefined ? action.payload.hasNameError : state.form.hasNameError, + hasOSError: + action.payload.hasOSError !== undefined ? action.payload.hasOSError : state.form.hasOSError, }, }; }; @@ -66,6 +70,13 @@ const eventFiltersFormStateChanged: CaseReducer = }; }; +const eventFiltersCreateSuccess: CaseReducer = (state, action) => { + return { + ...state, + entries: [action.payload.exception, ...state.entries], + }; +}; + export const eventFiltersPageReducer: StateReducer = ( state = initialEventFiltersPageState(), action @@ -77,6 +88,8 @@ export const eventFiltersPageReducer: StateReducer = ( return eventFiltersChangeForm(state, action); case 'eventFiltersFormStateChanged': return eventFiltersFormStateChanged(state, action); + case 'eventFiltersCreateSuccess': + return eventFiltersCreateSuccess(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 index ece754f71b3183..37bbf288089465 100644 --- 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 @@ -21,7 +21,7 @@ export const getFormEntry = ( }; export const getFormHasError = (state: EventFiltersListPageState): boolean => { - return state.form.hasItemsError || state.form.hasNameError; + return state.form.hasItemsError || state.form.hasNameError || state.form.hasOSError; }; export const isCreationInProgress = (state: EventFiltersListPageState): boolean => { 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 index 94f15907fb58ed..25db2cf1c31445 100644 --- 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 @@ -53,13 +53,24 @@ describe('selectors', () => { }; expect(getFormHasError(state)).toBeTruthy(); }); - it('returns true when entry with item error and name error', () => { + 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(); @@ -72,6 +83,7 @@ describe('selectors', () => { ...initialState.form, hasItemsError: false, hasNameError: false, + hasOSError: false, }, }; expect(getFormHasError(state)).toBeFalsy(); 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/store/utils.ts index 251aaef0897e4e..90673a9a24728c 100644 --- 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/store/utils.ts @@ -10,11 +10,11 @@ import { CreateExceptionListItemSchema } from '../../../../shared_imports'; import { Ecs } from '../../../../../common/ecs'; import { ENDPOINT_EVENT_FILTERS_LIST_ID } from '../constants'; -export const getInitialExceptionFromEvent = (data: Ecs): CreateExceptionListItemSchema => ({ +export const getInitialExceptionFromEvent = (data?: Ecs): CreateExceptionListItemSchema => ({ comments: [], description: '', entries: - data.event && data.process + data && data.event && data.process ? [ { field: 'event.category', @@ -29,7 +29,14 @@ export const getInitialExceptionFromEvent = (data: Ecs): CreateExceptionListItem value: (data.process.executable ?? [])[0], }, ] - : [], + : [ + { + field: '', + operator: 'included', + type: 'match', + value: '', + }, + ], item_id: undefined, list_id: ENDPOINT_EVENT_FILTERS_LIST_ID, meta: { @@ -40,5 +47,10 @@ export const getInitialExceptionFromEvent = (data: Ecs): CreateExceptionListItem tags: [], type: 'simple', // TODO: Try to fix this type casting - os_types: [(data.host ? data.host.os?.family ?? [] : [])[0] as 'windows' | 'linux' | 'macos'], + os_types: [ + (data && data.host ? data.host.os?.family ?? ['windows'] : ['windows'])[0] as + | 'windows' + | 'linux' + | 'macos', + ], }); 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 index 2aeb53ed5bcb2b..ca5ac398780cc6 100644 --- 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 @@ -76,12 +76,13 @@ export const EventFiltersForm: React.FC = memo( ...arg.exceptionItems[0], name: exception?.name ?? '', comments: exception?.comments ?? [], + os_types: exception?.os_types ?? [OperatingSystem.WINDOWS], }, - hasItemsError: arg.errorExists, + hasItemsError: arg.errorExists || !arg.exceptionItems[0].entries.length, }, }); }, - [dispatch, exception?.name, exception?.comments] + [dispatch, exception?.name, exception?.comments, exception?.os_types] ); const handleOnChangeName = useCallback( @@ -161,15 +162,26 @@ export const EventFiltersForm: React.FC = memo( { + if (!exception) return; + dispatch({ + type: 'eventFiltersChangeForm', + payload: { + entry: { + ...exception, + os_types: [value as 'windows' | 'linux' | 'macos'], + }, + }, + }); + }} /> ), - [exception?.os_types, osOptions] + [dispatch, exception, osOptions] ); const commentsInputMemo = useMemo( @@ -187,7 +199,7 @@ export const EventFiltersForm: React.FC = memo( {FORM_DESCRIPTION} {nameInputMemo} - + {allowSelectOs ? ( <> {osInputMemo} @@ -195,7 +207,7 @@ export const EventFiltersForm: React.FC = memo( ) : null} {exceptionBuilderComponentMemo} - + {commentsInputMemo} ) : ( 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 index 79cf296928e842..36796e250fe1fb 100644 --- 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 @@ -30,7 +30,7 @@ export const NAME_ERROR = i18n.translate('xpack.securitySolution.eventFilter.for }); export const OS_LABEL = i18n.translate('xpack.securitySolution.eventFilter.form.os.label', { - defaultMessage: 'Seelct OS', + defaultMessage: 'Seelct operating system', }); export const RULE_NAME = i18n.translate('xpack.securitySolution.eventFilter.form.rule.name', { From 7aef7c9d713fbbeaa8f726f4fcfc73a230c7ef6c Mon Sep 17 00:00:00 2001 From: David Sanchez Soler Date: Fri, 23 Apr 2021 13:06:18 +0200 Subject: [PATCH 03/10] Fixes typo --- .../pages/event_filters/view/components/form/translations.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index 36796e250fe1fb..086f2298d2c1af 100644 --- 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 @@ -30,7 +30,7 @@ export const NAME_ERROR = i18n.translate('xpack.securitySolution.eventFilter.for }); export const OS_LABEL = i18n.translate('xpack.securitySolution.eventFilter.form.os.label', { - defaultMessage: 'Seelct operating system', + defaultMessage: 'Select operating system', }); export const RULE_NAME = i18n.translate('xpack.securitySolution.eventFilter.form.rule.name', { From dd7e2461e6889bf2e85b85f5d1a03c1134da86de Mon Sep 17 00:00:00 2001 From: David Sanchez Soler Date: Mon, 26 Apr 2021 11:30:19 +0200 Subject: [PATCH 04/10] Adds empty page with an add button that opens flyout. Alos added route and path management --- .../public/management/common/routing.ts | 38 +++++++++++++- .../pages/event_filters/state/index.ts | 10 ++++ .../pages/event_filters/store/builders.ts | 5 ++ .../pages/event_filters/store/reducer.test.ts | 17 ++++++ .../pages/event_filters/store/reducer.ts | 28 +++++++++- .../pages/event_filters/store/selector.ts | 5 +- .../event_filters/store/selectors.test.ts | 18 ++++++- .../view/components/empty/index.tsx | 52 +++++++++++++++++++ .../view/event_filters_list_page.tsx | 18 +++++++ .../pages/event_filters/view/hooks.ts | 29 +++++++++-- 10 files changed, 213 insertions(+), 7 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/empty/index.tsx diff --git a/x-pack/plugins/security_solution/public/management/common/routing.ts b/x-pack/plugins/security_solution/public/management/common/routing.ts index d9da1d95dce964..c97f3e8bf8d42d 100644 --- a/x-pack/plugins/security_solution/public/management/common/routing.ts +++ b/x-pack/plugins/security_solution/public/management/common/routing.ts @@ -24,6 +24,7 @@ import { AdministrationSubTab } from '../types'; import { appendSearch } from '../../common/components/link_to/helpers'; import { EndpointIndexUIQueryParams } from '../pages/endpoint_hosts/types'; import { TrustedAppsListPageLocation } from '../pages/trusted_apps/state'; +import { EventFiltersPageLocation } from '../pages/event_filters/state'; import { EventFiltersListPageUrlSearchParams } from '../pages/event_filters/types'; // Taken from: https://github.com/microsoft/TypeScript/issues/12936#issuecomment-559034150 @@ -118,6 +119,26 @@ const normalizeTrustedAppsPageLocation = ( } }; +const normalizeEventFiltersPageLocation = ( + location?: Partial +): Partial => { + if (location) { + return { + ...(!isDefaultOrMissing(location.page_index, MANAGEMENT_DEFAULT_PAGE) + ? { page_index: location.page_index } + : {}), + ...(!isDefaultOrMissing(location.page_size, MANAGEMENT_DEFAULT_PAGE_SIZE) + ? { page_size: location.page_size } + : {}), + ...(!isDefaultOrMissing(location.show, undefined) ? { show: location.show } : {}), + ...(!isDefaultOrMissing(location.id, undefined) ? { id: location.id } : {}), + ...(!isDefaultOrMissing(location.filter, '') ? { filter: location.filter } : ''), + }; + } else { + return {}; + } +}; + /** * Given an object with url params, and a given key, return back only the first param value (case multiples were defined) * @param query @@ -181,6 +202,19 @@ export const getTrustedAppsListPath = (location?: Partial { + const showParamValue = extractFirstParamValue(query, 'show') as EventFiltersPageLocation['show']; + + return { + ...extractListPaginationParams(query), + show: + showParamValue && ['edit', 'create'].includes(showParamValue) ? showParamValue : undefined, + id: extractFirstParamValue(query, 'id'), + }; +}; + export const getEventFiltersListPath = ( location?: Partial ): string => { @@ -188,5 +222,7 @@ export const getEventFiltersListPath = ( tabName: AdministrationSubTab.eventFilters, }); - return `${path}${appendSearch(querystring.stringify(location))}`; + return `${path}${appendSearch( + querystring.stringify(normalizeEventFiltersPageLocation(location)) + )}`; }; diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/state/index.ts b/x-pack/plugins/security_solution/public/management/pages/event_filters/state/index.ts index 805898aec566e8..caaa48ddf1987e 100644 --- a/x-pack/plugins/security_solution/public/management/pages/event_filters/state/index.ts +++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/state/index.ts @@ -7,6 +7,15 @@ import { ExceptionListItemSchema, CreateExceptionListItemSchema } from '../../../../shared_imports'; import { AsyncResourceState } from '../../../state/async_resource_state'; + +export interface EventFiltersPageLocation { + page_index: number; + page_size: number; + show?: 'create' | 'edit'; + /** Used for editing. The ID of the selected event filter */ + id?: string; + filter: string; +} export interface EventFiltersListPageState { entries: ExceptionListItemSchema[]; form: { @@ -16,4 +25,5 @@ export interface EventFiltersListPageState { hasOSError: boolean; submissionResourceState: AsyncResourceState; }; + location: EventFiltersPageLocation; } 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 index 62888ea0d3a7d9..f45ff0df906368 100644 --- 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 @@ -16,4 +16,9 @@ export const initialEventFiltersPageState = (): EventFiltersListPageState => ({ hasOSError: false, submissionResourceState: { type: 'UninitialisedResourceState' }, }, + location: { + page_index: 0, + page_size: 10, + filter: '', + }, }); 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 index c6300ef847dcfe..f14f8ecc5f8ad8 100644 --- 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 @@ -109,4 +109,21 @@ describe('reducer', () => { expect(result.entries[0]!.meta).not.toBeUndefined(); }); }); + describe('UserChangedUrl', () => { + it('receives a url change with show=create', () => { + const result = eventFiltersPageReducer(initialState, { + type: 'userChangedUrl', + payload: { search: '?show=create', pathname: '/event_filters', hash: '' }, + }); + + expect(result).toStrictEqual({ + ...initialState, + location: { + ...initialState.location, + id: undefined, + show: 'create', + }, + }); + }); + }); }); 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 index 20e93376cdf848..50d825b83b8daf 100644 --- 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 @@ -5,9 +5,15 @@ * 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 { Immutable } from '../../../../../common/endpoint/types'; 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 { extractEventFiltetrsPageLocation } from '../../../common/routing'; import { EventFiltersInitForm, @@ -25,6 +31,15 @@ type CaseReducer = ( action: Immutable ) => Immutable; +const isEventFIltersPageLocation = (location: Immutable) => { + return ( + matchPath(location.pathname ?? '', { + path: MANAGEMENT_ROUTING_EVENT_FILTERS_PATH, + exact: true, + }) !== null + ); +}; + const eventFiltersInitForm: CaseReducer = (state, action) => { return { ...state, @@ -77,6 +92,15 @@ const eventFiltersCreateSuccess: CaseReducer = (state }; }; +const userChangedUrl: CaseReducer = (state, action) => { + if (isEventFIltersPageLocation(action.payload)) { + const location = extractEventFiltetrsPageLocation(parse(action.payload.search.slice(1))); + return { ...state, location }; + } else { + return initialEventFiltersPageState(); + } +}; + export const eventFiltersPageReducer: StateReducer = ( state = initialEventFiltersPageState(), action @@ -90,6 +114,8 @@ export const eventFiltersPageReducer: StateReducer = ( return eventFiltersFormStateChanged(state, action); case 'eventFiltersCreateSuccess': return eventFiltersCreateSuccess(state, action); + case 'userChangedUrl': + return userChangedUrl(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 index 37bbf288089465..dca16be0b1668a 100644 --- 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 @@ -5,7 +5,7 @@ * 2.0. */ -import { EventFiltersListPageState } from '../state'; +import { EventFiltersListPageState, EventFiltersPageLocation } from '../state'; import { ExceptionListItemSchema, CreateExceptionListItemSchema } from '../../../../shared_imports'; import { ServerApiError } from '../../../../common/types'; import { @@ -37,3 +37,6 @@ export const getCreationError = (state: EventFiltersListPageState): ServerApiErr return isFailedResourceState(submissionResourceState) ? submissionResourceState.error : undefined; }; + +export const getCurrentLocation = (state: EventFiltersListPageState): EventFiltersPageLocation => + state.location; 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 index 25db2cf1c31445..9ddbef726768a7 100644 --- 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 @@ -6,9 +6,10 @@ */ import { initialEventFiltersPageState } from './builders'; -import { getFormEntry, getFormHasError } from './selector'; +import { getFormEntry, getFormHasError, getCurrentLocation } from './selector'; import { ecsEventMock } from '../test_utils'; import { getInitialExceptionFromEvent } from './utils'; +import { EventFiltersPageLocation } from '../state'; const initialState = initialEventFiltersPageState(); @@ -89,4 +90,19 @@ describe('selectors', () => { expect(getFormHasError(state)).toBeFalsy(); }); }); + describe('getCurrentLocation()', () => { + it('returns current locations', () => { + const expectedLocation: EventFiltersPageLocation = { + show: 'create', + page_index: 1, + page_size: 20, + filter: 'filter', + }; + const state = { + ...initialState, + location: expectedLocation, + }; + expect(getCurrentLocation(state)).toBe(expectedLocation); + }); + }); }); 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 new file mode 100644 index 00000000000000..b32bebc39e6a6c --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/empty/index.tsx @@ -0,0 +1,52 @@ +/* + * 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 { EuiButton, EuiEmptyPrompt } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; + +export const Empty = memo<{ + onAdd: () => void; + /** Should the Add button be disabled */ + isAddDisabled?: boolean; +}>(({ onAdd, isAddDisabled = false }) => { + return ( + + + + } + body={ + + } + actions={ + + + + } + /> + ); +}); + +Empty.displayName = 'Empty'; 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 index a1a31f69274905..d56bbf768cb723 100644 --- 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 @@ -9,8 +9,24 @@ import React, { memo } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { AdministrationListPage } from '../../../components/administration_list_page'; +import { Empty } from './components/empty'; +import { useEventFiltersNavigateCallback, useEventFiltersSelector } from './hooks'; +import { getCurrentLocation } from '../store/selector'; +import { EventFiltersFlyout } from './components/flyout'; export const EventFiltersListPage = memo(() => { + const handleAddButtonClick = useEventFiltersNavigateCallback(() => ({ + show: 'create', + id: undefined, + })); + + const handleFlyoutClose = useEventFiltersNavigateCallback(() => ({ + show: undefined, + id: undefined, + })); + + const location = useEventFiltersSelector(getCurrentLocation); + const showFlyout = !!location.show; return ( { })} > {/* */} + + {showFlyout ? : null} ); }); 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 index 407dee896f5ac4..7f67ffb1c62ce4 100644 --- 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 @@ -5,16 +5,23 @@ * 2.0. */ -import { useState } from 'react'; +import { useState, useCallback } from 'react'; import { useSelector } from 'react-redux'; +import { useHistory } from 'react-router-dom'; -import { isCreationSuccessful, getFormEntry, getCreationError } from '../store/selector'; +import { + isCreationSuccessful, + getFormEntry, + getCreationError, + getCurrentLocation, +} from '../store/selector'; import { useToasts } from '../../../../common/lib/kibana'; import { getCreationSuccessMessage, getCreationErrorMessage } from './translations'; import { State } from '../../../../common/store'; -import { EventFiltersListPageState } from '../state'; +import { EventFiltersListPageState, EventFiltersPageLocation } from '../state'; +import { getEventFiltersListPath } from '../../../common/routing'; import { MANAGEMENT_STORE_EVENT_FILTERS_NAMESPACE as EVENT_FILTER_NS, @@ -42,3 +49,19 @@ export const useEventFiltersNotification = () => { toasts.addDanger(getCreationErrorMessage(creationError)); } }; + +export type NavigationCallback = ( + ...args: Parameters[0]> +) => Partial; + +export function useEventFiltersNavigateCallback(callback: NavigationCallback) { + const location = useEventFiltersSelector(getCurrentLocation); + const history = useHistory(); + + return useCallback( + (...args) => history.push(getEventFiltersListPath({ ...location, ...callback(...args) })), + // TODO: needs more investigation, but if callback is in dependencies list memoization will never happen + // eslint-disable-next-line react-hooks/exhaustive-deps + [history, location] + ); +} From b7577315e1da320496036a3edb610c227e9ac049 Mon Sep 17 00:00:00 2001 From: David Sanchez Soler Date: Mon, 26 Apr 2021 11:51:58 +0200 Subject: [PATCH 05/10] Fixes type and adds a TODO comment. Also removes ESlit rule for useCallback deps --- .../public/management/pages/event_filters/store/reducer.ts | 4 ++-- .../pages/event_filters/view/event_filters_list_page.tsx | 1 + .../public/management/pages/event_filters/view/hooks.ts | 4 +--- 3 files changed, 4 insertions(+), 5 deletions(-) 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 index 50d825b83b8daf..9c90a255b82c15 100644 --- 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 @@ -31,7 +31,7 @@ type CaseReducer = ( action: Immutable ) => Immutable; -const isEventFIltersPageLocation = (location: Immutable) => { +const isEventFiltersPageLocation = (location: Immutable) => { return ( matchPath(location.pathname ?? '', { path: MANAGEMENT_ROUTING_EVENT_FILTERS_PATH, @@ -93,7 +93,7 @@ const eventFiltersCreateSuccess: CaseReducer = (state }; const userChangedUrl: CaseReducer = (state, action) => { - if (isEventFIltersPageLocation(action.payload)) { + if (isEventFiltersPageLocation(action.payload)) { const location = extractEventFiltetrsPageLocation(parse(action.payload.search.slice(1))); return { ...state, location }; } else { 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 index d56bbf768cb723..f6acc5288cfef3 100644 --- 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 @@ -41,6 +41,7 @@ export const EventFiltersListPage = memo(() => { })} > {/* */} + {/* TODO: Display this only when list is empty (there are no endpoint events) */} {showFlyout ? : null} 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 index 7f67ffb1c62ce4..8eb72e9bd85ae3 100644 --- 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 @@ -60,8 +60,6 @@ export function useEventFiltersNavigateCallback(callback: NavigationCallback) { return useCallback( (...args) => history.push(getEventFiltersListPath({ ...location, ...callback(...args) })), - // TODO: needs more investigation, but if callback is in dependencies list memoization will never happen - // eslint-disable-next-line react-hooks/exhaustive-deps - [history, location] + [callback, history, location] ); } From 95d0031db0d8537af493058ec2abe4b1e8812472 Mon Sep 17 00:00:00 2001 From: David Sanchez Soler Date: Mon, 26 Apr 2021 12:34:16 +0200 Subject: [PATCH 06/10] Fixes unit test. Adds consts for default page size and page index --- .../management/pages/event_filters/store/builders.ts | 5 +++-- .../management/pages/event_filters/store/reducer.ts | 2 +- .../pages/event_filters/store/selectors.test.ts | 5 +++-- .../pages/event_filters/view/components/empty/index.tsx | 9 ++++++++- 4 files changed, 15 insertions(+), 6 deletions(-) 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 index f45ff0df906368..92290de4a24ed3 100644 --- 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 @@ -6,6 +6,7 @@ */ import { EventFiltersListPageState } from '../state'; +import { MANAGEMENT_DEFAULT_PAGE, MANAGEMENT_DEFAULT_PAGE_SIZE } from '../../../common/constants'; export const initialEventFiltersPageState = (): EventFiltersListPageState => ({ entries: [], @@ -17,8 +18,8 @@ export const initialEventFiltersPageState = (): EventFiltersListPageState => ({ submissionResourceState: { type: 'UninitialisedResourceState' }, }, location: { - page_index: 0, - page_size: 10, + page_index: MANAGEMENT_DEFAULT_PAGE, + page_size: MANAGEMENT_DEFAULT_PAGE_SIZE, filter: '', }, }); 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 index 9c90a255b82c15..a52de492f9a66c 100644 --- 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 @@ -97,7 +97,7 @@ const userChangedUrl: CaseReducer = (state, action) => { const location = extractEventFiltetrsPageLocation(parse(action.payload.search.slice(1))); return { ...state, location }; } else { - return initialEventFiltersPageState(); + return state; } }; 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 index 9ddbef726768a7..dbc962f1beaf10 100644 --- 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 @@ -10,6 +10,7 @@ import { getFormEntry, getFormHasError, getCurrentLocation } from './selector'; import { ecsEventMock } from '../test_utils'; import { getInitialExceptionFromEvent } from './utils'; import { EventFiltersPageLocation } from '../state'; +import { MANAGEMENT_DEFAULT_PAGE, MANAGEMENT_DEFAULT_PAGE_SIZE } from '../../../common/constants'; const initialState = initialEventFiltersPageState(); @@ -94,8 +95,8 @@ describe('selectors', () => { it('returns current locations', () => { const expectedLocation: EventFiltersPageLocation = { show: 'create', - page_index: 1, - page_size: 20, + page_index: MANAGEMENT_DEFAULT_PAGE, + page_size: MANAGEMENT_DEFAULT_PAGE_SIZE, filter: 'filter', }; const state = { 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 index b32bebc39e6a6c..e5c33d8eb9e851 100644 --- 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 @@ -6,16 +6,23 @@ */ import React, { memo } from 'react'; +import styled, { css } from 'styled-components'; import { EuiButton, EuiEmptyPrompt } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; +const EmptyPrompt = styled(EuiEmptyPrompt)` + ${({ theme }) => css` + max-width: ${theme.eui.euiBreakpoints.m}; + `} +`; + export const Empty = memo<{ onAdd: () => void; /** Should the Add button be disabled */ isAddDisabled?: boolean; }>(({ onAdd, isAddDisabled = false }) => { return ( - Date: Mon, 26 Apr 2021 17:08:55 +0200 Subject: [PATCH 07/10] Fixes warning state update on an unmounted component --- .../public/common/lib/kibana/hooks.ts | 87 ++++++++++--------- 1 file changed, 48 insertions(+), 39 deletions(-) diff --git a/x-pack/plugins/security_solution/public/common/lib/kibana/hooks.ts b/x-pack/plugins/security_solution/public/common/lib/kibana/hooks.ts index c2b8ed24202e18..df7fad5443062d 100644 --- a/x-pack/plugins/security_solution/public/common/lib/kibana/hooks.ts +++ b/x-pack/plugins/security_solution/public/common/lib/kibana/hooks.ts @@ -7,7 +7,7 @@ import moment from 'moment-timezone'; -import { useCallback, useEffect, useState } from 'react'; +import { useCallback, useEffect, useState, useRef } from 'react'; import { i18n } from '@kbn/i18n'; import { DEFAULT_DATE_FORMAT, DEFAULT_DATE_FORMAT_TZ } from '../../../../common/constants'; @@ -51,57 +51,66 @@ export interface AuthenticatedElasticUser { } export const useCurrentUser = (): AuthenticatedElasticUser | null => { + const isMounted = useRef(false); const [user, setUser] = useState(null); const [, dispatchToaster] = useStateToaster(); const { security } = useKibana().services; - const fetchUser = useCallback(() => { - let didCancel = false; - const fetchData = async () => { - try { - if (security != null) { - const response = await security.authc.getCurrentUser(); + const fetchUser = useCallback( + () => { + let didCancel = false; + const fetchData = async () => { + try { + if (security != null) { + const response = await security.authc.getCurrentUser(); + if (!isMounted.current) return; + if (!didCancel) { + setUser(convertToCamelCase(response)); + } + } else { + setUser({ + username: i18n.translate('xpack.securitySolution.getCurrentUser.unknownUser', { + defaultMessage: 'Unknown', + }), + email: '', + fullName: '', + roles: [], + enabled: false, + authenticationRealm: { name: '', type: '' }, + lookupRealm: { name: '', type: '' }, + authenticationProvider: '', + }); + } + } catch (error) { if (!didCancel) { - setUser(convertToCamelCase(response)); + errorToToaster({ + title: i18n.translate('xpack.securitySolution.getCurrentUser.Error', { + defaultMessage: 'Error getting user', + }), + error: error.body && error.body.message ? new Error(error.body.message) : error, + dispatchToaster, + }); + setUser(null); } - } else { - setUser({ - username: i18n.translate('xpack.securitySolution.getCurrentUser.unknownUser', { - defaultMessage: 'Unknown', - }), - email: '', - fullName: '', - roles: [], - enabled: false, - authenticationRealm: { name: '', type: '' }, - lookupRealm: { name: '', type: '' }, - authenticationProvider: '', - }); - } - } catch (error) { - if (!didCancel) { - errorToToaster({ - title: i18n.translate('xpack.securitySolution.getCurrentUser.Error', { - defaultMessage: 'Error getting user', - }), - error: error.body && error.body.message ? new Error(error.body.message) : error, - dispatchToaster, - }); - setUser(null); } - } - }; - fetchData(); - return () => { - didCancel = true; - }; + }; + fetchData(); + return () => { + didCancel = true; + }; + }, // eslint-disable-next-line react-hooks/exhaustive-deps - }, [security]); + [security] + ); useEffect(() => { + isMounted.current = true; fetchUser(); + return () => { + isMounted.current = false; + }; // eslint-disable-next-line react-hooks/exhaustive-deps }, []); return user; From 38363333fba6f906a6847c7d44c2eb582e4cf385 Mon Sep 17 00:00:00 2001 From: David Sanchez Soler Date: Mon, 26 Apr 2021 18:58:39 +0200 Subject: [PATCH 08/10] Fixes infinite useEffect loop useFetchIndex hook because non memoized value --- .../pages/event_filters/view/components/form/index.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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 index ca5ac398780cc6..a9d05cc865f72b 100644 --- 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 @@ -56,7 +56,9 @@ export const EventFiltersForm: React.FC = memo( const dispatch = useDispatch>(); const exception = useEventFiltersSelector(getFormEntry); - const [isIndexPatternLoading, { indexPatterns }] = useFetchIndex(['logs-endpoint.events.*']); + // 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 osOptions: Array> = useMemo( () => OPERATING_SYSTEMS.map((os) => ({ value: os, inputDisplay: OS_TITLES[os] })), From 5b3e7e5dc62ac745d1cfe329ffb84cfe8ebc0e08 Mon Sep 17 00:00:00 2001 From: David Sanchez Soler Date: Tue, 27 Apr 2021 18:26:31 +0200 Subject: [PATCH 09/10] Adds policy:all to eventFilter.tag and disables or button on exception builder --- .../public/management/pages/event_filters/store/utils.ts | 2 +- .../pages/event_filters/view/components/form/index.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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/store/utils.ts index 90673a9a24728c..35ba7ce5853a63 100644 --- 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/store/utils.ts @@ -44,7 +44,7 @@ export const getInitialExceptionFromEvent = (data?: Ecs): CreateExceptionListIte }, name: '', namespace_type: 'agnostic', - tags: [], + tags: ['policy:all'], type: 'simple', // TODO: Try to fix this type casting os_types: [ 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 index a9d05cc865f72b..89832840a09d89 100644 --- 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 @@ -128,7 +128,7 @@ export const EventFiltersForm: React.FC = memo( listNamespaceType={'agnostic'} ruleName={RULE_NAME} indexPatterns={indexPatterns} - isOrDisabled={false} + isOrDisabled={true} // TODO: pending to be validated isAndDisabled={false} isNestedDisabled={false} data-test-subj="alert-exception-builder" From 9eec29ca9fa3003b175a11b7a844e697c4d1b8ca Mon Sep 17 00:00:00 2001 From: David Sanchez Soler Date: Wed, 28 Apr 2021 11:08:03 +0200 Subject: [PATCH 10/10] Changes component name and simplify hook using useCallback without a custom callback --- .../view/components/empty/index.tsx | 4 +- .../view/event_filters_list_page.tsx | 37 ++++++++++++------- .../pages/event_filters/view/hooks.ts | 16 +++----- 3 files changed, 31 insertions(+), 26 deletions(-) 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 index e5c33d8eb9e851..5298578c38e17e 100644 --- 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 @@ -16,7 +16,7 @@ const EmptyPrompt = styled(EuiEmptyPrompt)` `} `; -export const Empty = memo<{ +export const EventFiltersListEmptyState = memo<{ onAdd: () => void; /** Should the Add button be disabled */ isAddDisabled?: boolean; @@ -56,4 +56,4 @@ export const Empty = memo<{ ); }); -Empty.displayName = 'Empty'; +EventFiltersListEmptyState.displayName = 'EventFiltersListEmptyState'; 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 index f6acc5288cfef3..ac38b57fdb6352 100644 --- 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 @@ -5,28 +5,37 @@ * 2.0. */ -import React, { memo } from 'react'; +import React, { memo, useCallback } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { AdministrationListPage } from '../../../components/administration_list_page'; -import { Empty } from './components/empty'; +import { EventFiltersListEmptyState } from './components/empty'; import { useEventFiltersNavigateCallback, useEventFiltersSelector } from './hooks'; import { getCurrentLocation } from '../store/selector'; import { EventFiltersFlyout } from './components/flyout'; export const EventFiltersListPage = memo(() => { - const handleAddButtonClick = useEventFiltersNavigateCallback(() => ({ - show: 'create', - id: undefined, - })); - - const handleFlyoutClose = useEventFiltersNavigateCallback(() => ({ - show: undefined, - id: undefined, - })); - const location = useEventFiltersSelector(getCurrentLocation); + const navigateCallback = useEventFiltersNavigateCallback(); const showFlyout = !!location.show; + + const handleAddButtonClick = useCallback( + () => + navigateCallback({ + show: 'create', + id: undefined, + }), + [navigateCallback] + ); + + const handleCancelButtonClick = useCallback( + () => + navigateCallback({ + show: undefined, + id: undefined, + }), + [navigateCallback] + ); return ( { > {/* */} {/* TODO: Display this only when list is empty (there are no endpoint events) */} - - {showFlyout ? : null} + + {showFlyout ? : null} ); }); 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 index 8eb72e9bd85ae3..df87d150891c19 100644 --- 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 @@ -20,7 +20,7 @@ import { useToasts } from '../../../../common/lib/kibana'; import { getCreationSuccessMessage, getCreationErrorMessage } from './translations'; import { State } from '../../../../common/store'; -import { EventFiltersListPageState, EventFiltersPageLocation } from '../state'; +import { EventFiltersListPageState } from '../state'; import { getEventFiltersListPath } from '../../../common/routing'; import { @@ -50,16 +50,12 @@ export const useEventFiltersNotification = () => { } }; -export type NavigationCallback = ( - ...args: Parameters[0]> -) => Partial; - -export function useEventFiltersNavigateCallback(callback: NavigationCallback) { +export function useEventFiltersNavigateCallback() { const location = useEventFiltersSelector(getCurrentLocation); const history = useHistory(); - return useCallback( - (...args) => history.push(getEventFiltersListPath({ ...location, ...callback(...args) })), - [callback, history, location] - ); + return useCallback((args) => history.push(getEventFiltersListPath({ ...location, ...args })), [ + history, + location, + ]); }