From 67e63f7642f5c64d7f5a88e9608bbeab00a20f9a Mon Sep 17 00:00:00 2001 From: David Sanchez Soler Date: Wed, 22 Dec 2021 17:10:19 +0100 Subject: [PATCH 1/7] Displays assignable artifact list and allow selection. Also displays empty state message --- .../policy/view/event_filters/flyout/index.ts | 8 + .../flyout/policy_event_filters_flyout.tsx | 223 ++++++++++++++++++ .../pages/policy/view/event_filters/hooks.ts | 42 +++- .../layout/policy_event_filters_layout.tsx | 30 ++- 4 files changed, 297 insertions(+), 6 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/management/pages/policy/view/event_filters/flyout/index.ts create mode 100644 x-pack/plugins/security_solution/public/management/pages/policy/view/event_filters/flyout/policy_event_filters_flyout.tsx diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/event_filters/flyout/index.ts b/x-pack/plugins/security_solution/public/management/pages/policy/view/event_filters/flyout/index.ts new file mode 100644 index 0000000000000..ae6861787044d --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/event_filters/flyout/index.ts @@ -0,0 +1,8 @@ +/* + * 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. + */ + +export { PolicyEventFiltersFlyout } from './policy_event_filters_flyout'; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/event_filters/flyout/policy_event_filters_flyout.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/event_filters/flyout/policy_event_filters_flyout.tsx new file mode 100644 index 0000000000000..f37cddbf1085f --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/event_filters/flyout/policy_event_filters_flyout.tsx @@ -0,0 +1,223 @@ +/* + * 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, { useCallback, useMemo, useState } from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { isEmpty, without } from 'lodash/fp'; +import { + EuiTitle, + EuiFlyout, + EuiSpacer, + EuiFlyoutHeader, + EuiFlyoutBody, + EuiFlyoutFooter, + EuiFlexGroup, + EuiFlexItem, + EuiButtonEmpty, + EuiButton, + EuiCallOut, + EuiEmptyPrompt, +} from '@elastic/eui'; +import { SearchExceptions } from '../../../../../components/search_exceptions'; +import { ImmutableObject, PolicyData } from '../../../../../../../common/endpoint/types'; +import { EventFiltersHttpService } from '../../../../event_filters/service'; +import { useHttp } from '../../../../../../common/lib/kibana'; +import { useSearchNotAssignedEventFilters, useGetAllAssignedEventFilters } from '../hooks'; +import { PolicyArtifactsAssignableList } from '../../artifacts/assignable'; + +interface PolicyEventFiltersFlyoutProps { + policyItem: ImmutableObject; + onClose: () => void; +} + +const MAX_ALLOWED_RESULTS = 100; + +export const PolicyEventFiltersFlyout = React.memo( + ({ policyItem, onClose }) => { + const [selectedArtifactIds, setSelectedArtifactIds] = useState([]); + const [currentFilter, setCurrentFilter] = useState(''); + const http = useHttp(); + const eventFiltersService = useMemo(() => new EventFiltersHttpService(http), [http]); + + const handleOnSearch = useCallback((query) => { + setSelectedArtifactIds([]); + setCurrentFilter(query); + }, []); + const handleOnConfirmAction = useCallback(() => {}, []); + + const handleSelectArtifacts = (artifactId: string, selected: boolean) => { + setSelectedArtifactIds((currentSelectedArtifactIds) => + selected + ? [...currentSelectedArtifactIds, artifactId] + : without([artifactId], currentSelectedArtifactIds) + ); + }; + + const { data: eventFilters, isLoading: isLoadingEventFilters } = + useSearchNotAssignedEventFilters(eventFiltersService, policyItem.id, { + filter: currentFilter, + }); + + const { data: allAssigned, isLoading: isLoadingAllAssigned } = useGetAllAssignedEventFilters( + policyItem.id, + currentFilter !== '' && eventFilters?.total === 0, + 'assignmentFlyout' + ); + + const searchWarningMessage = useMemo( + () => ( + <> + + {i18n.translate( + 'xpack.securitySolution.endpoint.policy.eventFilters.layout.flyout.searchWarning.text', + { + defaultMessage: + 'Only the first 100 event filters are displayed. Please use the search bar to refine the results.', + } + )} + + + + ), + [] + ); + + const noItemsMessage = useMemo(() => { + if (isLoadingEventFilters || isLoadingAllAssigned) { + return null; + } + + // there are no event filters assignable to this policy + if (allAssigned?.total === 0 || (eventFilters?.total === 0 && currentFilter === '')) { + return ( + + } + /> + ); + } + + // there are no results for the current search + if (eventFilters?.total === 0) { + return ( + + } + /> + ); + } + }, [ + allAssigned?.total, + currentFilter, + eventFilters?.total, + isLoadingAllAssigned, + isLoadingEventFilters, + ]); + + return ( + + + +

+ +

+
+ + +
+ + {(eventFilters?.total || 0) > MAX_ALLOWED_RESULTS ? searchWarningMessage : null} + + + + + + {noItemsMessage} + + + + + + + + + + + + + + + +
+ ); + } +); + +PolicyEventFiltersFlyout.displayName = 'PolicyEventFiltersFlyout'; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/event_filters/hooks.ts b/x-pack/plugins/security_solution/public/management/pages/policy/view/event_filters/hooks.ts index 91297a1119e5b..3eb33ba047105 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/event_filters/hooks.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/event_filters/hooks.ts @@ -13,13 +13,15 @@ import { parseQueryFilterToKQL } from '../../../../common/utils'; import { SEARCHABLE_FIELDS } from '../../../event_filters/constants'; export function useGetAllAssignedEventFilters( - policyId?: string + policyId: string, + enabled: boolean = true, + customQueryId: string = '' ): QueryObserverResult { const http = useHttp(); const eventFiltersService = new EventFiltersHttpService(http); return useQuery( - ['eventFilters', 'assigned', policyId], + ['eventFilters', 'assigned', policyId, customQueryId], () => { return eventFiltersService.getList({ filter: `(exception-list-agnostic.attributes.tags:"policy:${policyId}" OR exception-list-agnostic.attributes.tags:"policy:all")`, @@ -28,8 +30,7 @@ export function useGetAllAssignedEventFilters( { refetchIntervalInBackground: false, refetchOnWindowFocus: false, - enabled: !!policyId, - refetchOnMount: true, + enabled, } ); } @@ -68,6 +69,39 @@ export function useSearchAssignedEventFilters( ); } +export function useSearchNotAssignedEventFilters( + eventFiltersService: EventFiltersHttpService, + policyId: string, + options: { filter?: string } +): QueryObserverResult { + return useQuery( + ['eventFilters', 'notAssigned', policyId, options], + () => { + const { filter } = options; + const kuery = [ + `((exception-list-agnostic.attributes.tags:"policy:${policyId}") OR (exception-list-agnostic.attributes.tags:"policy:all"))`, + ]; + + if (filter) { + const filterKuery = parseQueryFilterToKQL(filter, SEARCHABLE_FIELDS) || undefined; + if (filterKuery) { + kuery.push(filterKuery); + } + } + + return eventFiltersService.getList({ + filter: kuery.join(' AND '), + }); + }, + { + refetchIntervalInBackground: false, + refetchOnWindowFocus: false, + enabled: !!policyId, + refetchOnMount: true, + } + ); +} + export function useGetAllEventFilters(): QueryObserverResult< FoundExceptionListItemSchema, ServerApiError diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/event_filters/layout/policy_event_filters_layout.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/event_filters/layout/policy_event_filters_layout.tsx index d03832e233b30..61970c74a4969 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/event_filters/layout/policy_event_filters_layout.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/event_filters/layout/policy_event_filters_layout.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { useMemo } from 'react'; +import React, { useMemo, useCallback } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; import { @@ -15,6 +15,7 @@ import { EuiText, EuiSpacer, EuiLink, + EuiButton, } from '@elastic/eui'; import { useAppUrl } from '../../../../../../common/lib/kibana'; import { APP_UI_ID } from '../../../../../../../common/constants'; @@ -23,6 +24,9 @@ import { getEventFiltersListPath } from '../../../../../common/routing'; import { useGetAllAssignedEventFilters, useGetAllEventFilters } from '../hooks'; import { ManagementPageLoader } from '../../../../../components/management_page_loader'; import { PolicyEventFiltersEmptyUnassigned, PolicyEventFiltersEmptyUnexisting } from '../empty'; +import { usePolicyDetailsSelector } from '../../policy_hooks'; +import { getCurrentArtifactsLocation } from '../../../store/policy_details/selectors'; +import { PolicyEventFiltersFlyout } from '../flyout'; interface PolicyEventFiltersLayoutProps { policyItem?: ImmutableObject | undefined; @@ -30,12 +34,13 @@ interface PolicyEventFiltersLayoutProps { export const PolicyEventFiltersLayout = React.memo( ({ policyItem }) => { const { getAppUrl } = useAppUrl(); + const urlParams = usePolicyDetailsSelector(getCurrentArtifactsLocation); const { data: allAssigned, isLoading: isLoadingAllAssigned, isRefetching: isRefetchingAllAssigned, - } = useGetAllAssignedEventFilters(policyItem?.id); + } = useGetAllAssignedEventFilters(policyItem?.id || '', !!policyItem?.id); const { data: allEventFilters, @@ -43,6 +48,9 @@ export const PolicyEventFiltersLayout = React.memo {}, []); + const handleOnCloseFlyout = useCallback(() => {}, []); + const aboutInfo = useMemo(() => { const link = ( {aboutInfo}

+ + + {i18n.translate( + 'xpack.securitySolution.endpoint.policy.eventFilters.layout.assignToPolicy', + { + defaultMessage: 'Assign event filters to policy', + } + )} + + + {urlParams.show === 'list' && ( + + )} ); } From 5e6479a78c75dc18d40a3e64c921e2baede6d35f Mon Sep 17 00:00:00 2001 From: David Sanchez Soler Date: Thu, 23 Dec 2021 12:40:08 +0100 Subject: [PATCH 2/7] Adds mutation to update event filters with new tags --- .../pages/event_filters/service/index.ts | 13 ++++ .../pages/event_filters/store/middleware.ts | 14 ---- .../flyout/policy_event_filters_flyout.tsx | 49 +++++++++----- .../pages/policy/view/event_filters/hooks.ts | 67 +++++++++++++++---- 4 files changed, 100 insertions(+), 43 deletions(-) diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/service/index.ts b/x-pack/plugins/security_solution/public/management/pages/event_filters/service/index.ts index 05729c6d1c14d..70ac198dd3478 100644 --- a/x-pack/plugins/security_solution/public/management/pages/event_filters/service/index.ts +++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/service/index.ts @@ -90,6 +90,19 @@ export class EventFiltersHttpService implements EventFiltersService { async updateOne( exception: Immutable ): Promise { + // Clean unnecessary fields for update action + [ + 'created_at', + 'created_by', + 'created_at', + 'created_by', + 'list_id', + 'tie_breaker_id', + 'updated_at', + 'updated_by', + ].forEach((field) => { + delete exception[field as keyof UpdateExceptionListItemSchema]; + }); return (await this.httpWrapper()).put(EXCEPTION_LIST_ITEM_URL, { body: JSON.stringify(exception), }); 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 d1693d24363f2..e1bc2b18b15f0 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 @@ -136,20 +136,6 @@ const eventFiltersUpdate = async ( getNewComment(store.getState()) ) as UpdateExceptionListItemSchema; - // Clean unnecessary fields for update action - [ - 'created_at', - 'created_by', - 'created_at', - 'created_by', - 'list_id', - 'tie_breaker_id', - 'updated_at', - 'updated_by', - ].forEach((field) => { - delete updatedCommentsEntry[field as keyof UpdateExceptionListItemSchema]; - }); - updatedCommentsEntry.comments = updatedCommentsEntry.comments?.map((comment) => ({ comment: comment.comment, id: comment.id, diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/event_filters/flyout/policy_event_filters_flyout.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/event_filters/flyout/policy_event_filters_flyout.tsx index f37cddbf1085f..52fb228907107 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/event_filters/flyout/policy_event_filters_flyout.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/event_filters/flyout/policy_event_filters_flyout.tsx @@ -9,6 +9,7 @@ import React, { useCallback, useMemo, useState } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; import { isEmpty, without } from 'lodash/fp'; +import { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; import { EuiTitle, EuiFlyout, @@ -27,7 +28,11 @@ import { SearchExceptions } from '../../../../../components/search_exceptions'; import { ImmutableObject, PolicyData } from '../../../../../../../common/endpoint/types'; import { EventFiltersHttpService } from '../../../../event_filters/service'; import { useHttp } from '../../../../../../common/lib/kibana'; -import { useSearchNotAssignedEventFilters, useGetAllAssignedEventFilters } from '../hooks'; +import { + useSearchNotAssignedEventFilters, + useGetAllAssignedEventFilters, + useBulkUpdateEventFilters, +} from '../hooks'; import { PolicyArtifactsAssignableList } from '../../artifacts/assignable'; interface PolicyEventFiltersFlyoutProps { @@ -44,11 +49,37 @@ export const PolicyEventFiltersFlyout = React.memo new EventFiltersHttpService(http), [http]); + const bulkUpdateMutation = useBulkUpdateEventFilters(); + const { data: eventFilters, isLoading: isLoadingEventFilters } = + useSearchNotAssignedEventFilters(eventFiltersService, policyItem.id, { + filter: currentFilter, + }); + + const { data: allAssigned, isLoading: isLoadingAllAssigned } = useGetAllAssignedEventFilters( + policyItem.id, + currentFilter !== '' && eventFilters?.total === 0, + 'assignmentFlyout' + ); + const handleOnSearch = useCallback((query) => { setSelectedArtifactIds([]); setCurrentFilter(query); }, []); - const handleOnConfirmAction = useCallback(() => {}, []); + + const handleOnConfirmAction = useCallback(() => { + if (!eventFilters) { + return; + } + const eventFiltersToUpdate: ExceptionListItemSchema[] = []; + selectedArtifactIds.forEach((selectedId) => { + const eventFilter = eventFilters.data.find((current) => current.id === selectedId); + if (eventFilter) { + eventFilter.tags = [...eventFilter.tags, `policy:${policyItem.id}`]; + eventFiltersToUpdate.push(eventFilter); + } + }); + bulkUpdateMutation.mutate(eventFiltersToUpdate); + }, [bulkUpdateMutation, eventFilters, policyItem.id, selectedArtifactIds]); const handleSelectArtifacts = (artifactId: string, selected: boolean) => { setSelectedArtifactIds((currentSelectedArtifactIds) => @@ -58,17 +89,6 @@ export const PolicyEventFiltersFlyout = React.memo ( <> @@ -199,8 +219,7 @@ export const PolicyEventFiltersFlyout = React.memo diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/event_filters/hooks.ts b/x-pack/plugins/security_solution/public/management/pages/policy/view/event_filters/hooks.ts index 3eb33ba047105..0a755e27c10dc 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/event_filters/hooks.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/event_filters/hooks.ts @@ -4,12 +4,16 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { FoundExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; -import { QueryObserverResult, useQuery } from 'react-query'; +import pMap from 'p-map'; +import { + ExceptionListItemSchema, + FoundExceptionListItemSchema, +} from '@kbn/securitysolution-io-ts-list-types'; +import { QueryObserverResult, useMutation, useQuery, useQueryClient } from 'react-query'; import { ServerApiError } from '../../../../../common/types'; import { useHttp } from '../../../../../common/lib/kibana/hooks'; import { EventFiltersHttpService } from '../../../event_filters/service'; -import { parseQueryFilterToKQL } from '../../../../common/utils'; +import { parsePoliciesAndFilterToKql, parseQueryFilterToKQL } from '../../../../common/utils'; import { SEARCHABLE_FIELDS } from '../../../event_filters/constants'; export function useGetAllAssignedEventFilters( @@ -78,19 +82,12 @@ export function useSearchNotAssignedEventFilters( ['eventFilters', 'notAssigned', policyId, options], () => { const { filter } = options; - const kuery = [ - `((exception-list-agnostic.attributes.tags:"policy:${policyId}") OR (exception-list-agnostic.attributes.tags:"policy:all"))`, - ]; - - if (filter) { - const filterKuery = parseQueryFilterToKQL(filter, SEARCHABLE_FIELDS) || undefined; - if (filterKuery) { - kuery.push(filterKuery); - } - } return eventFiltersService.getList({ - filter: kuery.join(' AND '), + filter: parsePoliciesAndFilterToKql({ + excludedPolicies: [policyId, 'all'], + kuery: parseQueryFilterToKQL(filter || '', SEARCHABLE_FIELDS), + }), }); }, { @@ -102,6 +99,48 @@ export function useSearchNotAssignedEventFilters( ); } +export function useBulkUpdateEventFilters( + callbacks: { + onUpdateSuccess?: () => void; + onUpdateError?: () => void; + onSettledCallback?: () => void; + } = {} +) { + const http = useHttp(); + const eventFiltersService = new EventFiltersHttpService(http); + const queryClient = useQueryClient(); + + const { + onUpdateSuccess = () => {}, + onUpdateError = () => {}, + onSettledCallback = () => {}, + } = callbacks; + + return useMutation( + (eventFilters: ExceptionListItemSchema[]) => { + return pMap( + eventFilters, + (eventFilter) => { + return eventFiltersService.updateOne(eventFilter); + }, + { + concurrency: 5, + } + ); + }, + { + onSuccess: onUpdateSuccess, + onError: onUpdateError, + onSettled: () => { + queryClient.invalidateQueries(['notAssigned']); + queryClient.invalidateQueries(['assigned']); + queryClient.invalidateQueries(['eventFilters', 'all']); + onSettledCallback(); + }, + } + ); +} + export function useGetAllEventFilters(): QueryObserverResult< FoundExceptionListItemSchema, ServerApiError From d512fe786c2b516fc0ab2f4fcb7c48ebbace4a3b Mon Sep 17 00:00:00 2001 From: David Sanchez Soler Date: Thu, 23 Dec 2021 13:58:13 +0100 Subject: [PATCH 3/7] Shows confirm toast when action has ben done correctly --- .../flyout/policy_event_filters_flyout.tsx | 35 +++++++++++++++++-- .../pages/policy/view/event_filters/hooks.ts | 2 +- 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/event_filters/flyout/policy_event_filters_flyout.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/event_filters/flyout/policy_event_filters_flyout.tsx index 52fb228907107..0cfab821ba8aa 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/event_filters/flyout/policy_event_filters_flyout.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/event_filters/flyout/policy_event_filters_flyout.tsx @@ -27,7 +27,7 @@ import { import { SearchExceptions } from '../../../../../components/search_exceptions'; import { ImmutableObject, PolicyData } from '../../../../../../../common/endpoint/types'; import { EventFiltersHttpService } from '../../../../event_filters/service'; -import { useHttp } from '../../../../../../common/lib/kibana'; +import { useHttp, useToasts } from '../../../../../../common/lib/kibana'; import { useSearchNotAssignedEventFilters, useGetAllAssignedEventFilters, @@ -44,12 +44,43 @@ const MAX_ALLOWED_RESULTS = 100; export const PolicyEventFiltersFlyout = React.memo( ({ policyItem, onClose }) => { + const toasts = useToasts(); const [selectedArtifactIds, setSelectedArtifactIds] = useState([]); const [currentFilter, setCurrentFilter] = useState(''); const http = useHttp(); const eventFiltersService = useMemo(() => new EventFiltersHttpService(http), [http]); - const bulkUpdateMutation = useBulkUpdateEventFilters(); + const bulkUpdateMutation = useBulkUpdateEventFilters({ + onUpdateSuccess: (updatedExceptions: ExceptionListItemSchema[]) => { + toasts.addSuccess({ + title: i18n.translate( + 'xpack.securitySolution.endpoint.policy.eventFilters.layout.flyout.toastSuccess.title', + { + defaultMessage: 'Success', + } + ), + text: + updatedExceptions.length > 1 + ? i18n.translate( + 'xpack.securitySolution.endpoint.policy.eventFilters.layout.flyout.toastSuccess.textMultiples', + { + defaultMessage: '{count} event filters have been added to your list.', + values: { count: updatedExceptions.length }, + } + ) + : i18n.translate( + 'xpack.securitySolution.endpoint.policy.eventFilters.layout.flyout.toastSuccess.textSingle', + { + defaultMessage: '"{name}" has been added to your event filters list.', + values: { name: updatedExceptions[0].name }, + } + ), + }); + }, + onUpdateError: () => {}, + onSettledCallback: () => {}, + }); + const { data: eventFilters, isLoading: isLoadingEventFilters } = useSearchNotAssignedEventFilters(eventFiltersService, policyItem.id, { filter: currentFilter, diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/event_filters/hooks.ts b/x-pack/plugins/security_solution/public/management/pages/policy/view/event_filters/hooks.ts index 0a755e27c10dc..9f65b4bc87d0f 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/event_filters/hooks.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/event_filters/hooks.ts @@ -101,7 +101,7 @@ export function useSearchNotAssignedEventFilters( export function useBulkUpdateEventFilters( callbacks: { - onUpdateSuccess?: () => void; + onUpdateSuccess?: (updatedExceptions: ExceptionListItemSchema[]) => void; onUpdateError?: () => void; onSettledCallback?: () => void; } = {} From 3fa7fdf53496a6133440ffef5cb649b6260e60b6 Mon Sep 17 00:00:00 2001 From: David Sanchez Soler Date: Thu, 23 Dec 2021 16:04:25 +0100 Subject: [PATCH 4/7] Fix ui/ux loading states and added unit test for flyout --- .../policy_artifacts_assignable_list.test.tsx | 2 +- .../policy_artifacts_assignable_list.tsx | 11 +- .../policy_event_filters_flyout.test.tsx | 271 ++++++++++++++++++ .../flyout/policy_event_filters_flyout.tsx | 60 ++-- .../pages/policy/view/event_filters/hooks.ts | 25 +- .../layout/policy_event_filters_layout.tsx | 42 ++- 6 files changed, 347 insertions(+), 64 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/management/pages/policy/view/event_filters/flyout/policy_event_filters_flyout.test.tsx diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/artifacts/assignable/policy_artifacts_assignable_list.test.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/artifacts/assignable/policy_artifacts_assignable_list.test.tsx index a93ae878eb6dd..d10b44e5b2640 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/artifacts/assignable/policy_artifacts_assignable_list.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/artifacts/assignable/policy_artifacts_assignable_list.test.tsx @@ -41,7 +41,7 @@ describe('Policy artifacts list', () => { selectedArtifactsUpdated: selectedArtifactsUpdatedMock, }); - expect(component.getByTestId('loading-spinner')).not.toBeNull(); + expect(component.getByTestId('artifactsAssignableListLoader')).not.toBeNull(); }); it('should artifacts list without data', async () => { diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/artifacts/assignable/policy_artifacts_assignable_list.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/artifacts/assignable/policy_artifacts_assignable_list.tsx index 6b21f92f9ed87..ec2ec70b81dab 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/artifacts/assignable/policy_artifacts_assignable_list.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/artifacts/assignable/policy_artifacts_assignable_list.tsx @@ -5,10 +5,10 @@ * 2.0. */ +import { EuiProgress } from '@elastic/eui'; import { FoundExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; import React, { useMemo } from 'react'; import { GetTrustedAppsListResponse, Immutable } from '../../../../../../../common/endpoint/types'; -import { Loader } from '../../../../../../common/components/loader'; import { AnyArtifact, ArtifactEntryCardMinified, @@ -56,7 +56,14 @@ export const PolicyArtifactsAssignableList = React.memo :
{assignableList}
; + return ( + <> + {isListLoading && ( + + )} +
{assignableList}
+ + ); } ); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/event_filters/flyout/policy_event_filters_flyout.test.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/event_filters/flyout/policy_event_filters_flyout.test.tsx new file mode 100644 index 0000000000000..dbc0d7eb638b3 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/event_filters/flyout/policy_event_filters_flyout.test.tsx @@ -0,0 +1,271 @@ +/* + * 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 uuid from 'uuid'; +import { getFoundExceptionListItemSchemaMock } from '../../../../../../../../lists/common/schemas/response/found_exception_list_item_schema.mock'; +import { getExceptionListItemSchemaMock } from '../../../../../../../../lists/common/schemas/response/exception_list_item_schema.mock'; +import { + AppContextTestRender, + createAppRootMockRenderer, +} from '../../../../../../common/mock/endpoint'; +import { EndpointDocGenerator } from '../../../../../../../common/endpoint/generate_data'; +import { PolicyData } from '../../../../../../../common/endpoint/types'; +import { getPolicyEventFiltersPath } from '../../../../../common/routing'; +import { eventFiltersListQueryHttpMock } from '../../../../event_filters/test_utils'; +import { PolicyEventFiltersFlyout } from './policy_event_filters_flyout'; +import { parseQueryFilterToKQL, parsePoliciesAndFilterToKql } from '../../../../../common/utils'; +import { SEARCHABLE_FIELDS } from '../../../../event_filters/constants'; +import { FoundExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; + +const endpointGenerator = new EndpointDocGenerator('seed'); +const getDefaultQueryParameters = (customFilter: string | undefined = '') => ({ + path: '/api/exception_lists/items/_find', + query: { + filter: customFilter, + list_id: ['endpoint_event_filters'], + namespace_type: ['agnostic'], + page: undefined, + per_page: 100, + sort_field: undefined, + sort_order: undefined, + }, +}); +const emptyList = { + data: [], + page: 1, + per_page: 10, + total: 0, +}; + +describe('Policy details event filters flyout', () => { + let render: () => Promise>; + let renderResult: ReturnType; + let history: AppContextTestRender['history']; + let mockedContext: AppContextTestRender; + let mockedApi: ReturnType; + let policy: PolicyData; + let onCloseMock: jest.Mock; + + beforeEach(() => { + policy = endpointGenerator.generatePolicyPackagePolicy(); + mockedContext = createAppRootMockRenderer(); + mockedApi = eventFiltersListQueryHttpMock(mockedContext.coreStart.http); + onCloseMock = jest.fn(); + ({ history } = mockedContext); + render = async () => { + await act(async () => { + renderResult = mockedContext.render( + + ); + await waitFor(mockedApi.responseProvider.eventFiltersList); + }); + return renderResult; + }; + + history.push(getPolicyEventFiltersPath(policy.id)); + }); + + it('should render a list of assignable policies and searchbar', async () => { + mockedApi.responseProvider.eventFiltersList.mockImplementation(() => { + return getFoundExceptionListItemSchemaMock(1); + }); + await render(); + expect(mockedApi.responseProvider.eventFiltersList).toHaveBeenLastCalledWith( + getDefaultQueryParameters( + parsePoliciesAndFilterToKql({ + excludedPolicies: [policy.id, 'all'], + kuery: parseQueryFilterToKQL('', SEARCHABLE_FIELDS), + }) + ) + ); + expect(await renderResult.findByTestId('artifactsList')).toBeTruthy(); + expect(renderResult.getByTestId('searchField')).toBeTruthy(); + }); + + it('should render "no items found" when searched for a term without data', async () => { + // first render + mockedApi.responseProvider.eventFiltersList.mockImplementationOnce(() => { + return getFoundExceptionListItemSchemaMock(1); + }); + await render(); + expect(await renderResult.findByTestId('artifactsList')).toBeTruthy(); + + // results for search + mockedApi.responseProvider.eventFiltersList.mockImplementationOnce(() => emptyList); + + // do a search + userEvent.type(renderResult.getByTestId('searchField'), 'no results with this{enter}'); + + await waitFor(() => { + expect(mockedApi.responseProvider.eventFiltersList).toHaveBeenCalledWith( + getDefaultQueryParameters( + parsePoliciesAndFilterToKql({ + excludedPolicies: [policy.id, 'all'], + kuery: parseQueryFilterToKQL('no results with this', SEARCHABLE_FIELDS), + }) + ) + ); + expect(renderResult.getByTestId('eventFilters-no-items-found')).toBeTruthy(); + }); + }); + + it('should render "not assignable items" when no possible exceptions can be assigned', async () => { + // both exceptions list requests will return no results + mockedApi.responseProvider.eventFiltersList.mockImplementation(() => emptyList); + await render(); + expect(await renderResult.findByTestId('eventFilters-no-assignable-items')).toBeTruthy(); + }); + + it('should disable the submit button if no exceptions are selected', async () => { + mockedApi.responseProvider.eventFiltersList.mockImplementationOnce(() => { + return getFoundExceptionListItemSchemaMock(1); + }); + await render(); + expect(await renderResult.findByTestId('artifactsList')).toBeTruthy(); + expect(renderResult.getByTestId('eventFilters-assign-confirm-button')).toBeDisabled(); + }); + + it('should enable the submit button if an exception is selected', async () => { + const exceptions = getFoundExceptionListItemSchemaMock(1); + const firstOneName = exceptions.data[0].name; + mockedApi.responseProvider.eventFiltersList.mockImplementation(() => exceptions); + + await render(); + expect(await renderResult.findByTestId('artifactsList')).toBeTruthy(); + + // click the first item + userEvent.click(renderResult.getByTestId(`${firstOneName}_checkbox`)); + + expect(renderResult.getByTestId('eventFilters-assign-confirm-button')).toBeEnabled(); + }); + + it('should warn the user when there are over 100 results in the flyout', async () => { + mockedApi.responseProvider.eventFiltersList.mockImplementation(() => { + return { + ...getFoundExceptionListItemSchemaMock(1), + total: 120, + }; + }); + await render(); + expect(await renderResult.findByTestId('artifactsList')).toBeTruthy(); + expect(renderResult.getByTestId('eventFilters-too-many-results')).toBeTruthy(); + }); + + describe('when submitting the form', () => { + const FIRST_ONE_NAME = uuid.v4(); + const SECOND_ONE_NAME = uuid.v4(); + const testTags = ['policy:1234', 'non-policy-tag', 'policy:4321']; + let exceptions: FoundExceptionListItemSchema; + + beforeEach(async () => { + exceptions = { + ...emptyList, + total: 2, + data: [ + getExceptionListItemSchemaMock({ + name: FIRST_ONE_NAME, + id: uuid.v4(), + tags: testTags, + }), + getExceptionListItemSchemaMock({ + name: SECOND_ONE_NAME, + id: uuid.v4(), + tags: testTags, + }), + ], + }; + mockedApi.responseProvider.eventFiltersList.mockImplementation(() => exceptions); + + await render(); + // wait fo the list to render + expect(await renderResult.findByTestId('artifactsList')).toBeTruthy(); + }); + + it('should submit the exception when submit is pressed (1 exception), display a toast and close the flyout', async () => { + mockedApi.responseProvider.eventFiltersUpdateOne.mockImplementation(() => exceptions.data[0]); + // click the first item + userEvent.click(renderResult.getByTestId(`${FIRST_ONE_NAME}_checkbox`)); + // submit the form + userEvent.click(renderResult.getByTestId('eventFilters-assign-confirm-button')); + + // verify the request with the new tag + await waitFor(() => { + expect(mockedApi.responseProvider.eventFiltersUpdateOne).toHaveBeenCalledWith({ + body: JSON.stringify({ + ...exceptions.data[0], + tags: [...testTags, `policy:${policy.id}`], + }), + path: '/api/exception_lists/items', + }); + }); + + await waitFor(() => { + expect(mockedContext.coreStart.notifications.toasts.addSuccess).toHaveBeenCalledWith({ + text: `"${FIRST_ONE_NAME}" has been added to your event filters list.`, + title: 'Success', + }); + }); + expect(onCloseMock).toHaveBeenCalled(); + }); + + it('should submit the exception when submit is pressed (2 exceptions), display a toast and close the flyout', async () => { + // click the first two items + userEvent.click(renderResult.getByTestId(`${FIRST_ONE_NAME}_checkbox`)); + userEvent.click(renderResult.getByTestId(`${SECOND_ONE_NAME}_checkbox`)); + // submit the form + userEvent.click(renderResult.getByTestId('eventFilters-assign-confirm-button')); + + // verify the request with the new tag + await waitFor(() => { + // first exception + expect(mockedApi.responseProvider.eventFiltersUpdateOne).toHaveBeenCalledWith({ + body: JSON.stringify({ + ...exceptions.data[0], + tags: [...testTags, `policy:${policy.id}`], + }), + path: '/api/exception_lists/items', + }); + // second exception + expect(mockedApi.responseProvider.eventFiltersUpdateOne).toHaveBeenCalledWith({ + body: JSON.stringify({ + ...exceptions.data[0], + tags: [...testTags, `policy:${policy.id}`], + }), + path: '/api/exception_lists/items', + }); + }); + + await waitFor(() => { + expect(mockedContext.coreStart.notifications.toasts.addSuccess).toHaveBeenCalledWith({ + text: '2 event filters have been added to your list.', + title: 'Success', + }); + }); + expect(onCloseMock).toHaveBeenCalled(); + }); + + it('should show a toast error when the request fails and close the flyout', async () => { + mockedApi.responseProvider.eventFiltersUpdateOne.mockImplementation(() => { + throw new Error('the server is too far away'); + }); + // click first item + userEvent.click(renderResult.getByTestId(`${FIRST_ONE_NAME}_checkbox`)); + // submit the form + userEvent.click(renderResult.getByTestId('eventFilters-assign-confirm-button')); + + await waitFor(() => { + expect(mockedContext.coreStart.notifications.toasts.addDanger).toHaveBeenCalledWith( + 'An error occurred updating artifacts' + ); + expect(onCloseMock).toHaveBeenCalled(); + }); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/event_filters/flyout/policy_event_filters_flyout.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/event_filters/flyout/policy_event_filters_flyout.tsx index 0cfab821ba8aa..5c97b914bb1ca 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/event_filters/flyout/policy_event_filters_flyout.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/event_filters/flyout/policy_event_filters_flyout.tsx @@ -26,13 +26,8 @@ import { } from '@elastic/eui'; import { SearchExceptions } from '../../../../../components/search_exceptions'; import { ImmutableObject, PolicyData } from '../../../../../../../common/endpoint/types'; -import { EventFiltersHttpService } from '../../../../event_filters/service'; -import { useHttp, useToasts } from '../../../../../../common/lib/kibana'; -import { - useSearchNotAssignedEventFilters, - useGetAllAssignedEventFilters, - useBulkUpdateEventFilters, -} from '../hooks'; +import { useToasts } from '../../../../../../common/lib/kibana'; +import { useSearchNotAssignedEventFilters, useBulkUpdateEventFilters } from '../hooks'; import { PolicyArtifactsAssignableList } from '../../artifacts/assignable'; interface PolicyEventFiltersFlyoutProps { @@ -47,8 +42,6 @@ export const PolicyEventFiltersFlyout = React.memo([]); const [currentFilter, setCurrentFilter] = useState(''); - const http = useHttp(); - const eventFiltersService = useMemo(() => new EventFiltersHttpService(http), [http]); const bulkUpdateMutation = useBulkUpdateEventFilters({ onUpdateSuccess: (updatedExceptions: ExceptionListItemSchema[]) => { @@ -77,20 +70,32 @@ export const PolicyEventFiltersFlyout = React.memo {}, - onSettledCallback: () => {}, + onUpdateError: () => { + toasts.addDanger( + i18n.translate( + 'xpack.securitySolution.endpoint.policy.eventFilters.layout.flyout.toastError.text', + { + defaultMessage: `An error occurred updating artifacts`, + } + ) + ); + }, + onSettledCallback: onClose, }); - const { data: eventFilters, isLoading: isLoadingEventFilters } = - useSearchNotAssignedEventFilters(eventFiltersService, policyItem.id, { - filter: currentFilter, - }); + const { + data: eventFilters, + isLoading: isLoadingEventFilters, + isRefetching: isRefetchingEventFilters, + } = useSearchNotAssignedEventFilters(policyItem.id, { + perPage: MAX_ALLOWED_RESULTS, + filter: currentFilter, + }); - const { data: allAssigned, isLoading: isLoadingAllAssigned } = useGetAllAssignedEventFilters( - policyItem.id, - currentFilter !== '' && eventFilters?.total === 0, - 'assignmentFlyout' - ); + const { data: allNotAssigned, isLoading: isLoadingAllNotAssigned } = + useSearchNotAssignedEventFilters(policyItem.id, { + enabled: currentFilter !== '' && eventFilters?.total === 0, + }); const handleOnSearch = useCallback((query) => { setSelectedArtifactIds([]); @@ -150,12 +155,12 @@ export const PolicyEventFiltersFlyout = React.memo { - if (isLoadingEventFilters || isLoadingAllAssigned) { + if (isLoadingEventFilters || isRefetchingEventFilters || isLoadingAllNotAssigned) { return null; } // there are no event filters assignable to this policy - if (allAssigned?.total === 0 || (eventFilters?.total === 0 && currentFilter === '')) { + if (allNotAssigned?.total === 0 || (eventFilters?.total === 0 && currentFilter === '')) { return ( @@ -250,7 +256,9 @@ export const PolicyEventFiltersFlyout = React.memo diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/event_filters/hooks.ts b/x-pack/plugins/security_solution/public/management/pages/policy/view/event_filters/hooks.ts index 6b7c862aa892c..ce19e0d4cfcc0 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/event_filters/hooks.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/event_filters/hooks.ts @@ -38,7 +38,9 @@ export function useGetAllAssignedEventFilters( { refetchIntervalInBackground: false, refetchOnWindowFocus: false, + refetchOnMount: true, enabled, + keepPreviousData: true, } ); } @@ -72,27 +74,29 @@ export function useSearchAssignedEventFilters( } export function useSearchNotAssignedEventFilters( - eventFiltersService: EventFiltersHttpService, policyId: string, - options: { filter?: string } + options: { filter?: string; perPage?: number; enabled?: boolean } ): QueryObserverResult { + const service = useGetEventFiltersService(); return useQuery( ['eventFilters', 'notAssigned', policyId, options], () => { - const { filter } = options; + const { filter, perPage } = options; - return eventFiltersService.getList({ + return service.getList({ filter: parsePoliciesAndFilterToKql({ excludedPolicies: [policyId, 'all'], kuery: parseQueryFilterToKQL(filter || '', SEARCHABLE_FIELDS), }), + perPage, }); }, { refetchIntervalInBackground: false, refetchOnWindowFocus: false, - enabled: !!policyId, refetchOnMount: true, + keepPreviousData: true, + enabled: options.enabled ?? true, } ); } @@ -104,8 +108,7 @@ export function useBulkUpdateEventFilters( onSettledCallback?: () => void; } = {} ) { - const http = useHttp(); - const eventFiltersService = new EventFiltersHttpService(http); + const service = useGetEventFiltersService(); const queryClient = useQueryClient(); const { @@ -119,7 +122,7 @@ export function useBulkUpdateEventFilters( return pMap( eventFilters, (eventFilter) => { - return eventFiltersService.updateOne(eventFilter); + return service.updateOne(eventFilter); }, { concurrency: 5, @@ -130,9 +133,8 @@ export function useBulkUpdateEventFilters( onSuccess: onUpdateSuccess, onError: onUpdateError, onSettled: () => { - queryClient.invalidateQueries(['notAssigned']); - queryClient.invalidateQueries(['assigned']); - queryClient.invalidateQueries(['eventFilters', 'all']); + queryClient.invalidateQueries(['eventFilters', 'notAssigned']); + queryClient.invalidateQueries(['eventFilters', 'assigned']); onSettledCallback(); }, } @@ -153,6 +155,7 @@ export function useGetAllEventFilters(): QueryObserverResult< refetchIntervalInBackground: false, refetchOnWindowFocus: false, refetchOnMount: true, + keepPreviousData: true, } ); } diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/event_filters/layout/policy_event_filters_layout.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/event_filters/layout/policy_event_filters_layout.tsx index b14d04227d882..a7091f6d92229 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/event_filters/layout/policy_event_filters_layout.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/event_filters/layout/policy_event_filters_layout.tsx @@ -25,7 +25,10 @@ import { getEventFiltersListPath } from '../../../../../common/routing'; import { useGetAllAssignedEventFilters, useGetAllEventFilters } from '../hooks'; import { ManagementPageLoader } from '../../../../../components/management_page_loader'; import { PolicyEventFiltersEmptyUnassigned, PolicyEventFiltersEmptyUnexisting } from '../empty'; -import { usePolicyDetailsSelector } from '../../policy_hooks'; +import { + usePolicyDetailsSelector, + usePolicyDetailsEventFiltersNavigateCallback, +} from '../../policy_hooks'; import { getCurrentArtifactsLocation } from '../../../store/policy_details/selectors'; import { PolicyEventFiltersFlyout } from '../flyout'; import { PolicyEventFiltersList } from '../list'; @@ -36,22 +39,22 @@ interface PolicyEventFiltersLayoutProps { export const PolicyEventFiltersLayout = React.memo( ({ policyItem }) => { const { getAppUrl } = useAppUrl(); + const navigateCallback = usePolicyDetailsEventFiltersNavigateCallback(); const urlParams = usePolicyDetailsSelector(getCurrentArtifactsLocation); - const { - data: allAssigned, - isLoading: isLoadingAllAssigned, - isRefetching: isRefetchingAllAssigned, - } = useGetAllAssignedEventFilters(policyItem?.id || '', !!policyItem?.id); + const { data: allAssigned, isLoading: isLoadingAllAssigned } = useGetAllAssignedEventFilters( + policyItem?.id || '', + !!policyItem?.id + ); - const { - data: allEventFilters, - isLoading: isLoadingAllEventFilters, - isRefetching: isRefetchingAllEventFilters, - } = useGetAllEventFilters(); + const { data: allEventFilters, isLoading: isLoadingAllEventFilters } = useGetAllEventFilters(); - const handleOnClickAssignButton = useCallback(() => {}, []); - const handleOnCloseFlyout = useCallback(() => {}, []); + const handleOnClickAssignButton = useCallback(() => { + navigateCallback({ show: 'list' }); + }, [navigateCallback]); + const handleOnCloseFlyout = useCallback(() => { + navigateCallback({ show: undefined }); + }, [navigateCallback]); const aboutInfo = useMemo(() => { const link = ( @@ -79,17 +82,8 @@ export const PolicyEventFiltersLayout = React.memo - isLoadingAllAssigned || - isRefetchingAllAssigned || - isLoadingAllEventFilters || - isRefetchingAllEventFilters, - [ - isLoadingAllAssigned, - isLoadingAllEventFilters, - isRefetchingAllAssigned, - isRefetchingAllEventFilters, - ] + () => isLoadingAllAssigned || isLoadingAllEventFilters, + [isLoadingAllAssigned, isLoadingAllEventFilters] ); const isEmptyState = useMemo(() => allAssigned && allAssigned.total === 0, [allAssigned]); From 776ed8e554cb7ace4ac0cb146bcebf8619041951 Mon Sep 17 00:00:00 2001 From: David Sanchez Soler Date: Thu, 23 Dec 2021 16:09:51 +0100 Subject: [PATCH 5/7] Remove customqueryId --- .../management/pages/policy/view/event_filters/hooks.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/event_filters/hooks.ts b/x-pack/plugins/security_solution/public/management/pages/policy/view/event_filters/hooks.ts index ce19e0d4cfcc0..571dd8779d093 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/event_filters/hooks.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/event_filters/hooks.ts @@ -24,12 +24,11 @@ export function useGetEventFiltersService() { export function useGetAllAssignedEventFilters( policyId: string, - enabled: boolean = true, - customQueryId: string = '' + enabled: boolean = true ): QueryObserverResult { const service = useGetEventFiltersService(); return useQuery( - ['eventFilters', 'assigned', policyId, customQueryId], + ['eventFilters', 'assigned', policyId], () => { return service.getList({ filter: parsePoliciesAndFilterToKql({ policies: [...(policyId ? [policyId] : []), 'all'] }), From ddf4d36db2c2eaf88b4c0f0172710fba5e60576f Mon Sep 17 00:00:00 2001 From: David Sanchez Soler Date: Tue, 4 Jan 2022 09:57:15 +0100 Subject: [PATCH 6/7] Use new object reference befor update it --- .../public/management/pages/event_filters/service/index.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/service/index.ts b/x-pack/plugins/security_solution/public/management/pages/event_filters/service/index.ts index 70ac198dd3478..a4ac378610e49 100644 --- a/x-pack/plugins/security_solution/public/management/pages/event_filters/service/index.ts +++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/service/index.ts @@ -90,6 +90,7 @@ export class EventFiltersHttpService implements EventFiltersService { async updateOne( exception: Immutable ): Promise { + const exceptionToUpdateCleaned = { ...exception }; // Clean unnecessary fields for update action [ 'created_at', @@ -101,10 +102,10 @@ export class EventFiltersHttpService implements EventFiltersService { 'updated_at', 'updated_by', ].forEach((field) => { - delete exception[field as keyof UpdateExceptionListItemSchema]; + delete exceptionToUpdateCleaned[field as keyof UpdateExceptionListItemSchema]; }); return (await this.httpWrapper()).put(EXCEPTION_LIST_ITEM_URL, { - body: JSON.stringify(exception), + body: JSON.stringify(exceptionToUpdateCleaned), }); } From 6a23f6db92f25938c66b6b793464c7ab85c5b582 Mon Sep 17 00:00:00 2001 From: David Sanchez Soler Date: Tue, 4 Jan 2022 11:05:56 +0100 Subject: [PATCH 7/7] Fixes unit test --- .../policy_event_filters_flyout.test.tsx | 51 ++++++++++++++----- 1 file changed, 38 insertions(+), 13 deletions(-) diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/event_filters/flyout/policy_event_filters_flyout.test.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/event_filters/flyout/policy_event_filters_flyout.test.tsx index dbc0d7eb638b3..092a787d1f7e0 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/event_filters/flyout/policy_event_filters_flyout.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/event_filters/flyout/policy_event_filters_flyout.test.tsx @@ -22,7 +22,10 @@ import { eventFiltersListQueryHttpMock } from '../../../../event_filters/test_ut import { PolicyEventFiltersFlyout } from './policy_event_filters_flyout'; import { parseQueryFilterToKQL, parsePoliciesAndFilterToKql } from '../../../../../common/utils'; import { SEARCHABLE_FIELDS } from '../../../../event_filters/constants'; -import { FoundExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; +import { + FoundExceptionListItemSchema, + UpdateExceptionListItemSchema, +} from '@kbn/securitysolution-io-ts-list-types'; const endpointGenerator = new EndpointDocGenerator('seed'); const getDefaultQueryParameters = (customFilter: string | undefined = '') => ({ @@ -44,6 +47,31 @@ const emptyList = { total: 0, }; +const getCleanedExceptionWithNewTags = ( + exception: UpdateExceptionListItemSchema, + testTags: string[], + policy: PolicyData +) => { + const exceptionToUpdateCleaned = { + ...exception, + tags: [...testTags, `policy:${policy.id}`], + }; + // Clean unnecessary fields for update action + [ + 'created_at', + 'created_by', + 'created_at', + 'created_by', + 'list_id', + 'tie_breaker_id', + 'updated_at', + 'updated_by', + ].forEach((field) => { + delete exceptionToUpdateCleaned[field as keyof UpdateExceptionListItemSchema]; + }); + return exceptionToUpdateCleaned; +}; + describe('Policy details event filters flyout', () => { let render: () => Promise>; let renderResult: ReturnType; @@ -198,10 +226,9 @@ describe('Policy details event filters flyout', () => { // verify the request with the new tag await waitFor(() => { expect(mockedApi.responseProvider.eventFiltersUpdateOne).toHaveBeenCalledWith({ - body: JSON.stringify({ - ...exceptions.data[0], - tags: [...testTags, `policy:${policy.id}`], - }), + body: JSON.stringify( + getCleanedExceptionWithNewTags(exceptions.data[0], testTags, policy) + ), path: '/api/exception_lists/items', }); }); @@ -226,18 +253,16 @@ describe('Policy details event filters flyout', () => { await waitFor(() => { // first exception expect(mockedApi.responseProvider.eventFiltersUpdateOne).toHaveBeenCalledWith({ - body: JSON.stringify({ - ...exceptions.data[0], - tags: [...testTags, `policy:${policy.id}`], - }), + body: JSON.stringify( + getCleanedExceptionWithNewTags(exceptions.data[0], testTags, policy) + ), path: '/api/exception_lists/items', }); // second exception expect(mockedApi.responseProvider.eventFiltersUpdateOne).toHaveBeenCalledWith({ - body: JSON.stringify({ - ...exceptions.data[0], - tags: [...testTags, `policy:${policy.id}`], - }), + body: JSON.stringify( + getCleanedExceptionWithNewTags(exceptions.data[0], testTags, policy) + ), path: '/api/exception_lists/items', }); });