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] + ); +}