From 5c66f0b0dfaa1f0829f464bf9fe9f4540fe1a81a Mon Sep 17 00:00:00 2001 From: Esteban Beltran Date: Tue, 26 Apr 2022 11:35:58 +0200 Subject: [PATCH 01/69] Wip show cases bulk actions --- .../common/types/timeline/actions/index.ts | 5 ++++ .../alert_status_bulk_actions.tsx | 8 ++++++ .../public/components/t_grid/translations.ts | 14 ++++++++++ .../hooks/use_status_bulk_action_items.tsx | 27 ++++++++++++++++++- 4 files changed, 53 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/timelines/common/types/timeline/actions/index.ts b/x-pack/plugins/timelines/common/types/timeline/actions/index.ts index 6a9c6bf8e74a0..33233f107dd3a 100644 --- a/x-pack/plugins/timelines/common/types/timeline/actions/index.ts +++ b/x-pack/plugins/timelines/common/types/timeline/actions/index.ts @@ -53,6 +53,9 @@ export type OnUpdateAlertStatusSuccess = ( ) => void; export type OnUpdateAlertStatusError = (status: AlertStatus, error: Error) => void; +export type OnCasesAttachToNewCase = (eventIds: string[]) => void; +export type OnCasesAttachToExistingCase = (eventIds: string[]) => void; + export interface StatusBulkActionsProps { eventIds: string[]; currentStatus?: AlertStatus; @@ -62,6 +65,8 @@ export interface StatusBulkActionsProps { setEventsDeleted: SetEventsDeleted; onUpdateSuccess?: OnUpdateAlertStatusSuccess; onUpdateFailure?: OnUpdateAlertStatusError; + onCasesAttachToNewCase?: OnCasesAttachToNewCase; + onCasesAttachToExistingCase?: OnCasesAttachToExistingCase; timelineId?: string; } export interface HeaderActionProps { diff --git a/x-pack/plugins/timelines/public/components/t_grid/toolbar/bulk_actions/alert_status_bulk_actions.tsx b/x-pack/plugins/timelines/public/components/t_grid/toolbar/bulk_actions/alert_status_bulk_actions.tsx index 2f580fdaf08e6..b0322084c2cd0 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/toolbar/bulk_actions/alert_status_bulk_actions.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/toolbar/bulk_actions/alert_status_bulk_actions.tsx @@ -13,6 +13,8 @@ import type { SetEventsDeleted, OnUpdateAlertStatusSuccess, OnUpdateAlertStatusError, + OnCasesAttachToNewCase, + OnCasesAttachToExistingCase, } from '../../../../../common/types'; import type { Refetch } from '../../../../store/t_grid/inputs'; import { tGridActions, TGridModel, tGridSelectors, TimelineState } from '../../../../store/t_grid'; @@ -27,6 +29,8 @@ interface OwnProps { indexName: string; onActionSuccess?: OnUpdateAlertStatusSuccess; onActionFailure?: OnUpdateAlertStatusError; + onCasesAttachToNewCase?: OnCasesAttachToNewCase; + onCasesAttachToExistingCase?: OnCasesAttachToExistingCase; refetch: Refetch; } @@ -47,6 +51,8 @@ export const AlertStatusBulkActionsComponent = React.memo { const dispatch = useDispatch(); @@ -120,6 +126,8 @@ export const AlertStatusBulkActionsComponent = React.memo i18n.translate('xpack.timelines.timeline.closedAlertSuccessToastMessage', { values: { totalAlerts }, diff --git a/x-pack/plugins/timelines/public/hooks/use_status_bulk_action_items.tsx b/x-pack/plugins/timelines/public/hooks/use_status_bulk_action_items.tsx index 8fc81a57e2b86..8e133aee06b40 100644 --- a/x-pack/plugins/timelines/public/hooks/use_status_bulk_action_items.tsx +++ b/x-pack/plugins/timelines/public/hooks/use_status_bulk_action_items.tsx @@ -28,6 +28,8 @@ export const useStatusBulkActionItems = ({ onUpdateSuccess, onUpdateFailure, timelineId, + onCasesAttachToNewCase, + onCasesAttachToExistingCase, }: StatusBulkActionsProps) => { const { updateAlertStatus } = useUpdateAlertsStatus(timelineId !== STANDALONE_ID); const { addSuccess, addError, addWarning } = useAppToasts(); @@ -156,8 +158,31 @@ export const useStatusBulkActionItems = ({ ); } + + if (onCasesAttachToNewCase) { + actionItems.push( + onCasesAttachToNewCase(eventIds)} + > + {i18n.BULK_ACTION_ATTACH_NEW_CASE} + + ); + } + if (onCasesAttachToExistingCase) { + actionItems.push( + onCasesAttachToExistingCase(eventIds)} + > + {i18n.BULK_ACTION_ATTACH_EXISTING_CASE} + + ); + } return actionItems; - }, [currentStatus, onClickUpdate]); + }, [currentStatus, eventIds, onCasesAttachToExistingCase, onCasesAttachToNewCase, onClickUpdate]); return items; }; From 4c39e396c49efa3974089781eb16e0c96d2134b0 Mon Sep 17 00:00:00 2001 From: Esteban Beltran Date: Tue, 26 Apr 2022 12:10:18 +0200 Subject: [PATCH 02/69] WIP --- .../common/components/events_viewer/index.tsx | 28 ++++++++++----- .../common/types/timeline/actions/index.ts | 4 ++- .../public/components/t_grid/body/index.tsx | 36 +++++++++++++++++++ .../hooks/use_status_bulk_action_items.tsx | 1 + 4 files changed, 59 insertions(+), 10 deletions(-) diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx index fa03522f54ef6..e40161c20f1fc 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx @@ -9,7 +9,7 @@ import React, { useRef, useCallback, useMemo, useEffect } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import styled from 'styled-components'; import type { Filter } from '@kbn/es-query'; -import type { EntityType } from '@kbn/timelines-plugin/common'; +import type { EntityType, TimelineItem } from '@kbn/timelines-plugin/common'; import { TGridCellAction } from '@kbn/timelines-plugin/common/types'; import { inputsModel, State } from '../../store'; import { inputsActions } from '../../store/actions'; @@ -186,14 +186,24 @@ const StatefulEventsViewerComponent: React.FC = ({ const refetchQuery = (newQueries: inputsModel.GlobalQuery[]) => { newQueries.forEach((q) => q.refetch && (q.refetch as inputsModel.Refetch)()); }; - const onAlertStatusActionSuccess = useCallback(() => { - if (id === TimelineId.active) { - refetchQuery([timelineQuery]); - } else { - refetchQuery(globalQueries); - } - }, [id, timelineQuery, globalQueries]); - const bulkActions = useMemo(() => ({ onAlertStatusActionSuccess }), [onAlertStatusActionSuccess]); + const bulkActions = useMemo( + () => ({ + onAlertStatusActionSuccess: () => { + if (id === TimelineId.active) { + refetchQuery([timelineQuery]); + } else { + refetchQuery(globalQueries); + } + }, + onCasesAttachToNewCase: (items: TimelineItem[]) => { + console.log('new one', items); + }, + onCasesAttachToExistingCase: (items: TimelineItem[]) => { + console.log('existing one', items); + }, + }), + [globalQueries, id, timelineQuery] + ); const fieldBrowserOptions = useFieldBrowserOptions({ sourcererScope: scopeId, diff --git a/x-pack/plugins/timelines/common/types/timeline/actions/index.ts b/x-pack/plugins/timelines/common/types/timeline/actions/index.ts index 33233f107dd3a..2ed89b72e0e9d 100644 --- a/x-pack/plugins/timelines/common/types/timeline/actions/index.ts +++ b/x-pack/plugins/timelines/common/types/timeline/actions/index.ts @@ -10,7 +10,7 @@ import { EuiDataGridControlColumn, EuiDataGridCellValueElementProps } from '@ela import { OnRowSelected, SortColumnTimeline, TimelineTabs } from '..'; import { BrowserFields } from '../../../search_strategy/index_fields'; import { ColumnHeaderOptions } from '../columns'; -import { TimelineNonEcsData } from '../../../search_strategy'; +import { TimelineItem, TimelineNonEcsData } from '../../../search_strategy'; import { Ecs } from '../../../ecs'; import { FieldBrowserOptions } from '../../fields_browser'; @@ -122,6 +122,8 @@ export interface BulkActionsObjectProp { alertStatusActions?: boolean; onAlertStatusActionSuccess?: OnUpdateAlertStatusSuccess; onAlertStatusActionFailure?: OnUpdateAlertStatusError; + onCasesAttachToNewCase?: (items: TimelineItem[]) => void; + onCasesAttachToExistingCase?: (items: TimelineItem[]) => void; } export type BulkActionsProp = boolean | BulkActionsObjectProp; diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/index.tsx b/x-pack/plugins/timelines/public/components/t_grid/body/index.tsx index ee40f80047682..fbf494e1970ad 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/body/index.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/body/index.tsx @@ -425,6 +425,36 @@ export const BodyComponent = React.memo( } }, [bulkActions]); + const onCasesAttachToExistingCase = useMemo(() => { + if ( + bulkActions && + bulkActions !== true && + bulkActions.onCasesAttachToExistingCase !== undefined + ) { + return (eventIds: string[]) => { + if (eventIds.length > 0 && bulkActions.onCasesAttachToExistingCase !== undefined) { + const items = data.filter((item) => { + return eventIds.find((event) => item._id === event); + }); + bulkActions.onCasesAttachToExistingCase(items); + } + }; + } + }, [bulkActions, data]); + + const onCasesAttachToNewCase = useMemo(() => { + if (bulkActions && bulkActions !== true && bulkActions.onCasesAttachToNewCase) { + return (eventIds: string[]) => { + if (eventIds.length > 0 && bulkActions.onCasesAttachToNewCase !== undefined) { + const items = data.filter((item) => { + return eventIds.find((event) => item._id === event); + }); + bulkActions.onCasesAttachToNewCase(items); + } + }; + } + }, [bulkActions, data]); + const showBulkActions = useMemo(() => { if (!hasAlertsCrud) { return false; @@ -456,6 +486,8 @@ export const BodyComponent = React.memo( indexName={indexNames.join()} onActionSuccess={onAlertStatusActionSuccess} onActionFailure={onAlertStatusActionFailure} + onCasesAttachToExistingCase={onCasesAttachToExistingCase} + onCasesAttachToNewCase={onCasesAttachToNewCase} refetch={refetch} /> @@ -470,6 +502,8 @@ export const BodyComponent = React.memo( indexNames, onAlertStatusActionFailure, onAlertStatusActionSuccess, + onCasesAttachToExistingCase, + onCasesAttachToNewCase, refetch, showBulkActions, totalItems, @@ -495,6 +529,8 @@ export const BodyComponent = React.memo( indexName={indexNames.join()} onActionSuccess={onAlertStatusActionSuccess} onActionFailure={onAlertStatusActionFailure} + onCasesAttachToExistingCase={onCasesAttachToExistingCase} + onCasesAttachToNewCase={onCasesAttachToNewCase} refetch={refetch} /> diff --git a/x-pack/plugins/timelines/public/hooks/use_status_bulk_action_items.tsx b/x-pack/plugins/timelines/public/hooks/use_status_bulk_action_items.tsx index 8e133aee06b40..49d3b84d9c904 100644 --- a/x-pack/plugins/timelines/public/hooks/use_status_bulk_action_items.tsx +++ b/x-pack/plugins/timelines/public/hooks/use_status_bulk_action_items.tsx @@ -170,6 +170,7 @@ export const useStatusBulkActionItems = ({ ); } + if (onCasesAttachToExistingCase) { actionItems.push( Date: Thu, 28 Apr 2022 09:37:45 +0200 Subject: [PATCH 03/69] Use custom bulk actions --- .../common/components/events_viewer/index.tsx | 24 ++++++--- .../common/types/timeline/actions/index.ts | 18 ++++--- .../public/components/t_grid/body/index.tsx | 54 +++++++------------ .../alert_status_bulk_actions.tsx | 12 ++--- .../hooks/use_status_bulk_action_items.tsx | 42 ++++++--------- 5 files changed, 69 insertions(+), 81 deletions(-) diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx index e40161c20f1fc..6abe0383c01ee 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx @@ -195,12 +195,24 @@ const StatefulEventsViewerComponent: React.FC = ({ refetchQuery(globalQueries); } }, - onCasesAttachToNewCase: (items: TimelineItem[]) => { - console.log('new one', items); - }, - onCasesAttachToExistingCase: (items: TimelineItem[]) => { - console.log('existing one', items); - }, + customBulkActions: [ + { + label: 'Attach to new case', + key: 'attach-new-case', + 'data-test-subj': 'attach-new-case', + onClick: (items?: TimelineItem[]) => { + console.log('new one', items); + }, + }, + { + label: 'Attach to existing case', + key: 'attach-existing-case', + 'data-test-subj': 'attach-existing-case', + onClick: (items?: TimelineItem[]) => { + console.log('existing one', items); + }, + }, + ], }), [globalQueries, id, timelineQuery] ); diff --git a/x-pack/plugins/timelines/common/types/timeline/actions/index.ts b/x-pack/plugins/timelines/common/types/timeline/actions/index.ts index 2ed89b72e0e9d..a567f8b0b8246 100644 --- a/x-pack/plugins/timelines/common/types/timeline/actions/index.ts +++ b/x-pack/plugins/timelines/common/types/timeline/actions/index.ts @@ -53,8 +53,16 @@ export type OnUpdateAlertStatusSuccess = ( ) => void; export type OnUpdateAlertStatusError = (status: AlertStatus, error: Error) => void; -export type OnCasesAttachToNewCase = (eventIds: string[]) => void; -export type OnCasesAttachToExistingCase = (eventIds: string[]) => void; +export interface CustomBulkAction { + key: string; + label: string; + onClick: (items?: TimelineItem[]) => void; + ['data-test-subj']?: string; +} + +export type StatusCustomBulkAction = Omit & { + onClick: (eventIds: string[]) => void; +}; export interface StatusBulkActionsProps { eventIds: string[]; @@ -65,8 +73,7 @@ export interface StatusBulkActionsProps { setEventsDeleted: SetEventsDeleted; onUpdateSuccess?: OnUpdateAlertStatusSuccess; onUpdateFailure?: OnUpdateAlertStatusError; - onCasesAttachToNewCase?: OnCasesAttachToNewCase; - onCasesAttachToExistingCase?: OnCasesAttachToExistingCase; + customBulkActions?: StatusCustomBulkAction[]; timelineId?: string; } export interface HeaderActionProps { @@ -122,8 +129,7 @@ export interface BulkActionsObjectProp { alertStatusActions?: boolean; onAlertStatusActionSuccess?: OnUpdateAlertStatusSuccess; onAlertStatusActionFailure?: OnUpdateAlertStatusError; - onCasesAttachToNewCase?: (items: TimelineItem[]) => void; - onCasesAttachToExistingCase?: (items: TimelineItem[]) => void; + customBulkActions?: CustomBulkAction[]; } export type BulkActionsProp = boolean | BulkActionsObjectProp; diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/index.tsx b/x-pack/plugins/timelines/public/components/t_grid/body/index.tsx index fbf494e1970ad..7af5e3fe06eb4 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/body/index.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/body/index.tsx @@ -425,33 +425,19 @@ export const BodyComponent = React.memo( } }, [bulkActions]); - const onCasesAttachToExistingCase = useMemo(() => { - if ( - bulkActions && - bulkActions !== true && - bulkActions.onCasesAttachToExistingCase !== undefined - ) { - return (eventIds: string[]) => { - if (eventIds.length > 0 && bulkActions.onCasesAttachToExistingCase !== undefined) { - const items = data.filter((item) => { - return eventIds.find((event) => item._id === event); - }); - bulkActions.onCasesAttachToExistingCase(items); - } - }; - } - }, [bulkActions, data]); - - const onCasesAttachToNewCase = useMemo(() => { - if (bulkActions && bulkActions !== true && bulkActions.onCasesAttachToNewCase) { - return (eventIds: string[]) => { - if (eventIds.length > 0 && bulkActions.onCasesAttachToNewCase !== undefined) { - const items = data.filter((item) => { - return eventIds.find((event) => item._id === event); - }); - bulkActions.onCasesAttachToNewCase(items); - } - }; + const additionalBulkActions = useMemo(() => { + if (bulkActions && bulkActions !== true && bulkActions.customBulkActions !== undefined) { + return bulkActions.customBulkActions.map((action) => { + return { + ...action, + onClick: (eventIds: string[]) => { + const items = data.filter((item) => { + return eventIds.find((event) => item._id === event); + }); + action.onClick(items); + }, + }; + }); } }, [bulkActions, data]); @@ -486,8 +472,7 @@ export const BodyComponent = React.memo( indexName={indexNames.join()} onActionSuccess={onAlertStatusActionSuccess} onActionFailure={onAlertStatusActionFailure} - onCasesAttachToExistingCase={onCasesAttachToExistingCase} - onCasesAttachToNewCase={onCasesAttachToNewCase} + customBulkActions={additionalBulkActions} refetch={refetch} /> @@ -495,6 +480,7 @@ export const BodyComponent = React.memo( ), [ + additionalBulkActions, alertCountText, filterQuery, filterStatus, @@ -502,8 +488,6 @@ export const BodyComponent = React.memo( indexNames, onAlertStatusActionFailure, onAlertStatusActionSuccess, - onCasesAttachToExistingCase, - onCasesAttachToNewCase, refetch, showBulkActions, totalItems, @@ -529,8 +513,7 @@ export const BodyComponent = React.memo( indexName={indexNames.join()} onActionSuccess={onAlertStatusActionSuccess} onActionFailure={onAlertStatusActionFailure} - onCasesAttachToExistingCase={onCasesAttachToExistingCase} - onCasesAttachToNewCase={onCasesAttachToNewCase} + customBulkActions={additionalBulkActions} refetch={refetch} /> @@ -564,21 +547,22 @@ export const BodyComponent = React.memo( showDisplaySelector: false, }), [ + isLoading, alertCountText, showBulkActions, id, totalSelectAllAlerts, totalItems, - fieldBrowserOptions, filterStatus, filterQuery, indexNames, onAlertStatusActionSuccess, onAlertStatusActionFailure, + additionalBulkActions, refetch, - isLoading, additionalControls, browserFields, + fieldBrowserOptions, columnHeaders, ] ); diff --git a/x-pack/plugins/timelines/public/components/t_grid/toolbar/bulk_actions/alert_status_bulk_actions.tsx b/x-pack/plugins/timelines/public/components/t_grid/toolbar/bulk_actions/alert_status_bulk_actions.tsx index b0322084c2cd0..c0b8da013318e 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/toolbar/bulk_actions/alert_status_bulk_actions.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/toolbar/bulk_actions/alert_status_bulk_actions.tsx @@ -13,8 +13,7 @@ import type { SetEventsDeleted, OnUpdateAlertStatusSuccess, OnUpdateAlertStatusError, - OnCasesAttachToNewCase, - OnCasesAttachToExistingCase, + AlertStatusCustomBulkAction, } from '../../../../../common/types'; import type { Refetch } from '../../../../store/t_grid/inputs'; import { tGridActions, TGridModel, tGridSelectors, TimelineState } from '../../../../store/t_grid'; @@ -29,8 +28,7 @@ interface OwnProps { indexName: string; onActionSuccess?: OnUpdateAlertStatusSuccess; onActionFailure?: OnUpdateAlertStatusError; - onCasesAttachToNewCase?: OnCasesAttachToNewCase; - onCasesAttachToExistingCase?: OnCasesAttachToExistingCase; + customBulkActions?: AlertStatusCustomBulkAction[]; refetch: Refetch; } @@ -51,8 +49,7 @@ export const AlertStatusBulkActionsComponent = React.memo { const dispatch = useDispatch(); @@ -126,8 +123,7 @@ export const AlertStatusBulkActionsComponent = React.memo { const { updateAlertStatus } = useUpdateAlertsStatus(timelineId !== STANDALONE_ID); const { addSuccess, addError, addWarning } = useAppToasts(); @@ -159,31 +158,22 @@ export const useStatusBulkActionItems = ({ ); } - if (onCasesAttachToNewCase) { - actionItems.push( - onCasesAttachToNewCase(eventIds)} - > - {i18n.BULK_ACTION_ATTACH_NEW_CASE} - - ); - } + const additionalItems = customBulkActions + ? customBulkActions.map((action) => { + return ( + action.onClick(eventIds)} + > + {action.label} + + ); + }) + : []; - if (onCasesAttachToExistingCase) { - actionItems.push( - onCasesAttachToExistingCase(eventIds)} - > - {i18n.BULK_ACTION_ATTACH_EXISTING_CASE} - - ); - } - return actionItems; - }, [currentStatus, eventIds, onCasesAttachToExistingCase, onCasesAttachToNewCase, onClickUpdate]); + return [...actionItems, ...additionalItems]; + }, [currentStatus, customBulkActions, eventIds, onClickUpdate]); return items; }; From 54725353465bff3e43217cbbd3cc008347fe3fba Mon Sep 17 00:00:00 2001 From: Esteban Beltran Date: Thu, 28 Apr 2022 10:27:17 +0200 Subject: [PATCH 04/69] new use add bulk hook --- .../use_cases_add_to_existing_case_modal.tsx | 43 +++++++----- .../use_cases_add_to_new_case_flyout.tsx | 69 ++++++++++--------- .../common/components/events_viewer/index.tsx | 24 ++----- .../use_bulk_add_to_case_actions.tsx | 68 ++++++++++++++++++ 4 files changed, 134 insertions(+), 70 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_bulk_add_to_case_actions.tsx diff --git a/x-pack/plugins/cases/public/components/all_cases/selector_modal/use_cases_add_to_existing_case_modal.tsx b/x-pack/plugins/cases/public/components/all_cases/selector_modal/use_cases_add_to_existing_case_modal.tsx index 269e411e8eb89..107b2ad63f6fa 100644 --- a/x-pack/plugins/cases/public/components/all_cases/selector_modal/use_cases_add_to_existing_case_modal.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/selector_modal/use_cases_add_to_existing_case_modal.tsx @@ -53,18 +53,17 @@ export const useCasesAddToExistingCaseModal = (props: AddToExistingFlyoutProps) }, [dispatch]); const handleOnRowClick = useCallback( - async (theCase?: Case) => { + async (theCase: Case | undefined, attachments: CaseAttachments) => { // when the case is undefined in the modal // the user clicked "create new case" if (theCase === undefined) { closeModal(); - createNewCaseFlyout.open(); + createNewCaseFlyout.open(attachments); return; } try { // add attachments to the case - const attachments = props.attachments; if (attachments !== undefined && attachments.length > 0) { await createAttachments({ caseId: theCase.id, @@ -74,7 +73,7 @@ export const useCasesAddToExistingCaseModal = (props: AddToExistingFlyoutProps) casesToasts.showSuccessAttach({ theCase, - attachments: props.attachments, + attachments, title: props.toastTitle, content: props.toastContent, }); @@ -91,22 +90,28 @@ export const useCasesAddToExistingCaseModal = (props: AddToExistingFlyoutProps) [casesToasts, closeModal, createNewCaseFlyout, createAttachments, props] ); - const openModal = useCallback(() => { - dispatch({ - type: CasesContextStoreActionsList.OPEN_ADD_TO_CASE_MODAL, - payload: { - ...props, - hiddenStatuses: [CaseStatuses.closed, StatusAll], - onRowClick: handleOnRowClick, - onClose: () => { - closeModal(); - if (props.onClose) { - return props.onClose(); - } + const openModal = useCallback( + (attachments?: CaseAttachments) => { + dispatch({ + type: CasesContextStoreActionsList.OPEN_ADD_TO_CASE_MODAL, + payload: { + ...props, + hiddenStatuses: [CaseStatuses.closed, StatusAll], + onRowClick: (theCase?: Case) => { + const caseAttachments = attachments ?? props.attachments ?? []; + handleOnRowClick(theCase, caseAttachments); + }, + onClose: () => { + closeModal(); + if (props.onClose) { + return props.onClose(); + } + }, }, - }, - }); - }, [closeModal, dispatch, handleOnRowClick, props]); + }); + }, + [closeModal, dispatch, handleOnRowClick, props] + ); return { open: openModal, close: closeModal, diff --git a/x-pack/plugins/cases/public/components/create/flyout/use_cases_add_to_new_case_flyout.tsx b/x-pack/plugins/cases/public/components/create/flyout/use_cases_add_to_new_case_flyout.tsx index c1c0793fe2340..3bc3ab8b7558b 100644 --- a/x-pack/plugins/cases/public/components/create/flyout/use_cases_add_to_new_case_flyout.tsx +++ b/x-pack/plugins/cases/public/components/create/flyout/use_cases_add_to_new_case_flyout.tsx @@ -6,6 +6,7 @@ */ import { useCallback } from 'react'; +import { CaseAttachments } from '../../../types'; import { useCasesToast } from '../../../common/use_cases_toast'; import { Case } from '../../../containers/types'; import { CasesContextStoreActionsList } from '../../cases_context/cases_context_reducer'; @@ -27,39 +28,43 @@ export const useCasesAddToNewCaseFlyout = (props: AddToNewCaseFlyoutProps) => { }); }, [dispatch]); - const openFlyout = useCallback(() => { - dispatch({ - type: CasesContextStoreActionsList.OPEN_CREATE_CASE_FLYOUT, - payload: { - ...props, - onClose: () => { - closeFlyout(); - if (props.onClose) { - return props.onClose(); - } - }, - onSuccess: async (theCase: Case) => { - if (theCase) { - casesToasts.showSuccessAttach({ - theCase, - attachments: props.attachments, - title: props.toastTitle, - content: props.toastContent, - }); - } - if (props.onSuccess) { - return props.onSuccess(theCase); - } + const openFlyout = useCallback( + (attachments?: CaseAttachments) => { + dispatch({ + type: CasesContextStoreActionsList.OPEN_CREATE_CASE_FLYOUT, + payload: { + ...props, + attachments: attachments ?? props.attachments, + onClose: () => { + closeFlyout(); + if (props.onClose) { + return props.onClose(); + } + }, + onSuccess: async (theCase: Case) => { + if (theCase) { + casesToasts.showSuccessAttach({ + theCase, + attachments: attachments ?? props.attachments, + title: props.toastTitle, + content: props.toastContent, + }); + } + if (props.onSuccess) { + return props.onSuccess(theCase); + } + }, + afterCaseCreated: async (...args) => { + closeFlyout(); + if (props.afterCaseCreated) { + return props.afterCaseCreated(...args); + } + }, }, - afterCaseCreated: async (...args) => { - closeFlyout(); - if (props.afterCaseCreated) { - return props.afterCaseCreated(...args); - } - }, - }, - }); - }, [casesToasts, closeFlyout, dispatch, props]); + }); + }, + [casesToasts, closeFlyout, dispatch, props] + ); return { open: openFlyout, close: closeFlyout, diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx index 6abe0383c01ee..64b68d206e76f 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx @@ -11,6 +11,7 @@ import styled from 'styled-components'; import type { Filter } from '@kbn/es-query'; import type { EntityType, TimelineItem } from '@kbn/timelines-plugin/common'; import { TGridCellAction } from '@kbn/timelines-plugin/common/types'; +import { useBulkAddToCaseActions } from '../../../detections/components/alerts_table/timeline_actions/use_bulk_add_to_case_actions'; import { inputsModel, State } from '../../store'; import { inputsActions } from '../../store/actions'; import { ControlColumnProps, RowRenderer, TimelineId } from '../../../../common/types/timeline'; @@ -186,6 +187,8 @@ const StatefulEventsViewerComponent: React.FC = ({ const refetchQuery = (newQueries: inputsModel.GlobalQuery[]) => { newQueries.forEach((q) => q.refetch && (q.refetch as inputsModel.Refetch)()); }; + + const addToCaseBulkActions = useBulkAddToCaseActions(); const bulkActions = useMemo( () => ({ onAlertStatusActionSuccess: () => { @@ -195,26 +198,9 @@ const StatefulEventsViewerComponent: React.FC = ({ refetchQuery(globalQueries); } }, - customBulkActions: [ - { - label: 'Attach to new case', - key: 'attach-new-case', - 'data-test-subj': 'attach-new-case', - onClick: (items?: TimelineItem[]) => { - console.log('new one', items); - }, - }, - { - label: 'Attach to existing case', - key: 'attach-existing-case', - 'data-test-subj': 'attach-existing-case', - onClick: (items?: TimelineItem[]) => { - console.log('existing one', items); - }, - }, - ], + customBulkActions: addToCaseBulkActions, }), - [globalQueries, id, timelineQuery] + [addToCaseBulkActions, globalQueries, id, timelineQuery] ); const fieldBrowserOptions = useFieldBrowserOptions({ diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_bulk_add_to_case_actions.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_bulk_add_to_case_actions.tsx new file mode 100644 index 0000000000000..9e96da041d490 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_bulk_add_to_case_actions.tsx @@ -0,0 +1,68 @@ +/* + * 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 { CommentType } from '@kbn/cases-plugin/common'; +import { CasesUiStart } from '@kbn/cases-plugin/public'; +import { useMemo } from 'react'; +import { APP_ID } from '../../../../../common/constants'; +import type { TimelineItem } from '../../../../../common/search_strategy'; +import { useKibana } from '../../../../common/lib/kibana'; + +export interface UseAddToCaseActions { + onClose?: () => void; + onSuccess?: () => Promise; +} + +function timelineItemsToCaseAttachments(items: TimelineItem[] = [], casesUi: CasesUiStart) { + return items.map((item) => { + return { + alertId: item.ecs._id ?? '', + index: item.ecs._index ?? '', + owner: APP_ID, + type: CommentType.alert as const, + rule: casesUi.helpers.getRuleIdFromEvent({ ecs: item.ecs, data: item.data }), + }; + }); +} + +export const useBulkAddToCaseActions = ({ onClose, onSuccess }: UseAddToCaseActions = {}) => { + const { cases: casesUi } = useKibana().services; + + const createCaseFlyout = casesUi.hooks.getUseCasesAddToNewCaseFlyout({ + attachments: [], + onClose, + onSuccess, + }); + const selectCaseModal = casesUi.hooks.getUseCasesAddToExistingCaseModal({ + attachments: [], + onClose, + onRowClick: onSuccess, + }); + + return useMemo(() => { + return [ + { + label: 'Attach to new case', + key: 'attach-new-case', + 'data-test-subj': 'attach-new-case', + onClick: (items?: TimelineItem[]) => { + const caseAttachments = timelineItemsToCaseAttachments(items, casesUi); + createCaseFlyout.open(caseAttachments); + }, + }, + { + label: 'Attach to existing case', + key: 'attach-existing-case', + 'data-test-subj': 'attach-new-case', + onClick: (items?: TimelineItem[]) => { + const caseAttachments = timelineItemsToCaseAttachments(items, casesUi); + selectCaseModal.open(caseAttachments); + }, + }, + ]; + }, [casesUi, createCaseFlyout, selectCaseModal]); +}; From 9a7f866edf91101836773ba0ceb33bdf5a4d0561 Mon Sep 17 00:00:00 2001 From: Esteban Beltran Date: Thu, 28 Apr 2022 10:34:31 +0200 Subject: [PATCH 05/69] Close the popover if isOpen --- .../components/t_grid/toolbar/bulk_actions/index.tsx | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/timelines/public/components/t_grid/toolbar/bulk_actions/index.tsx b/x-pack/plugins/timelines/public/components/t_grid/toolbar/bulk_actions/index.tsx index a1d8d467a3a60..94b197bbe2d05 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/toolbar/bulk_actions/index.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/toolbar/bulk_actions/index.tsx @@ -60,6 +60,12 @@ const BulkActionsComponent: React.FC = ({ setIsActionsPopoverOpen(false); }, [setIsActionsPopoverOpen]); + const closeIfPopoverIsOpen = useCallback(() => { + if (isActionsPopoverOpen) { + setIsActionsPopoverOpen(false); + } + }, [isActionsPopoverOpen]); + const toggleSelectAll = useCallback(() => { if (!showClearSelection) { onSelectAll(); @@ -91,7 +97,10 @@ const BulkActionsComponent: React.FC = ({ ); return ( - + Date: Thu, 28 Apr 2022 10:39:26 +0200 Subject: [PATCH 06/69] Use translations --- .../timeline_actions/use_bulk_add_to_case_actions.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_bulk_add_to_case_actions.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_bulk_add_to_case_actions.tsx index 9e96da041d490..fecb9dd8ef9e6 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_bulk_add_to_case_actions.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_bulk_add_to_case_actions.tsx @@ -11,6 +11,7 @@ import { useMemo } from 'react'; import { APP_ID } from '../../../../../common/constants'; import type { TimelineItem } from '../../../../../common/search_strategy'; import { useKibana } from '../../../../common/lib/kibana'; +import { ADD_TO_EXISTING_CASE, ADD_TO_NEW_CASE } from '../translations'; export interface UseAddToCaseActions { onClose?: () => void; @@ -46,7 +47,7 @@ export const useBulkAddToCaseActions = ({ onClose, onSuccess }: UseAddToCaseActi return useMemo(() => { return [ { - label: 'Attach to new case', + label: ADD_TO_NEW_CASE, key: 'attach-new-case', 'data-test-subj': 'attach-new-case', onClick: (items?: TimelineItem[]) => { @@ -55,7 +56,7 @@ export const useBulkAddToCaseActions = ({ onClose, onSuccess }: UseAddToCaseActi }, }, { - label: 'Attach to existing case', + label: ADD_TO_EXISTING_CASE, key: 'attach-existing-case', 'data-test-subj': 'attach-new-case', onClick: (items?: TimelineItem[]) => { From a1b263cc69ef8be4754b4f727e929a8ec77f7fca Mon Sep 17 00:00:00 2001 From: Esteban Beltran Date: Thu, 28 Apr 2022 14:04:46 +0200 Subject: [PATCH 07/69] Disable cases bulk actions when a query is present --- .../common/components/events_viewer/index.tsx | 2 +- .../use_bulk_add_to_case_actions.tsx | 6 +++++- .../components/alerts_table/translations.ts | 7 +++++++ .../common/types/timeline/actions/index.ts | 2 ++ .../public/hooks/use_status_bulk_action_items.tsx | 14 +++++++++----- 5 files changed, 24 insertions(+), 7 deletions(-) diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx index 64b68d206e76f..e4542a6ef7e6c 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx @@ -9,7 +9,7 @@ import React, { useRef, useCallback, useMemo, useEffect } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import styled from 'styled-components'; import type { Filter } from '@kbn/es-query'; -import type { EntityType, TimelineItem } from '@kbn/timelines-plugin/common'; +import type { EntityType } from '@kbn/timelines-plugin/common'; import { TGridCellAction } from '@kbn/timelines-plugin/common/types'; import { useBulkAddToCaseActions } from '../../../detections/components/alerts_table/timeline_actions/use_bulk_add_to_case_actions'; import { inputsModel, State } from '../../store'; diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_bulk_add_to_case_actions.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_bulk_add_to_case_actions.tsx index fecb9dd8ef9e6..35ce1aa5dec28 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_bulk_add_to_case_actions.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_bulk_add_to_case_actions.tsx @@ -11,7 +11,7 @@ import { useMemo } from 'react'; import { APP_ID } from '../../../../../common/constants'; import type { TimelineItem } from '../../../../../common/search_strategy'; import { useKibana } from '../../../../common/lib/kibana'; -import { ADD_TO_EXISTING_CASE, ADD_TO_NEW_CASE } from '../translations'; +import { ADD_TO_CASE_DISABLED, ADD_TO_EXISTING_CASE, ADD_TO_NEW_CASE } from '../translations'; export interface UseAddToCaseActions { onClose?: () => void; @@ -50,6 +50,8 @@ export const useBulkAddToCaseActions = ({ onClose, onSuccess }: UseAddToCaseActi label: ADD_TO_NEW_CASE, key: 'attach-new-case', 'data-test-subj': 'attach-new-case', + disableOnQuery: true, + disabledLabel: ADD_TO_CASE_DISABLED, onClick: (items?: TimelineItem[]) => { const caseAttachments = timelineItemsToCaseAttachments(items, casesUi); createCaseFlyout.open(caseAttachments); @@ -58,6 +60,8 @@ export const useBulkAddToCaseActions = ({ onClose, onSuccess }: UseAddToCaseActi { label: ADD_TO_EXISTING_CASE, key: 'attach-existing-case', + disableOnQuery: true, + disabledLabel: ADD_TO_CASE_DISABLED, 'data-test-subj': 'attach-new-case', onClick: (items?: TimelineItem[]) => { const caseAttachments = timelineItemsToCaseAttachments(items, casesUi); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/translations.ts b/x-pack/plugins/security_solution/public/detections/components/alerts_table/translations.ts index bdddd8ab46207..f9387b5aef171 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/translations.ts +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/translations.ts @@ -299,3 +299,10 @@ export const ADD_TO_NEW_CASE = i18n.translate( defaultMessage: 'Add to new case', } ); + +export const ADD_TO_CASE_DISABLED = i18n.translate( + 'xpack.securitySolution.detectionEngine.alerts.actions.addToCaseDisabled', + { + defaultMessage: 'Add to case is not supported for this selection', + } +); diff --git a/x-pack/plugins/timelines/common/types/timeline/actions/index.ts b/x-pack/plugins/timelines/common/types/timeline/actions/index.ts index a567f8b0b8246..ae4d8db93b4a6 100644 --- a/x-pack/plugins/timelines/common/types/timeline/actions/index.ts +++ b/x-pack/plugins/timelines/common/types/timeline/actions/index.ts @@ -56,6 +56,8 @@ export type OnUpdateAlertStatusError = (status: AlertStatus, error: Error) => vo export interface CustomBulkAction { key: string; label: string; + disableOnQuery?: boolean; + disabledLabel?: string; onClick: (items?: TimelineItem[]) => void; ['data-test-subj']?: string; } diff --git a/x-pack/plugins/timelines/public/hooks/use_status_bulk_action_items.tsx b/x-pack/plugins/timelines/public/hooks/use_status_bulk_action_items.tsx index 02a53e8c98e5a..794854279ad22 100644 --- a/x-pack/plugins/timelines/public/hooks/use_status_bulk_action_items.tsx +++ b/x-pack/plugins/timelines/public/hooks/use_status_bulk_action_items.tsx @@ -123,7 +123,7 @@ export const useStatusBulkActionItems = ({ ); const items = useMemo(() => { - const actionItems = []; + const actionItems: JSX.Element[] = []; if (currentStatus !== FILTER_OPEN) { actionItems.push( { - return ( + ? customBulkActions.reduce((acc, action) => { + const isDisabled = !!(query && action.disableOnQuery); + acc.push( action.onClick(eventIds)} > {action.label} ); - }) + return acc; + }, []) : []; return [...actionItems, ...additionalItems]; - }, [currentStatus, customBulkActions, eventIds, onClickUpdate]); + }, [currentStatus, customBulkActions, eventIds, onClickUpdate, query]); return items; }; From 65f1988013abd4ea53b7db6514fa774634bc0859 Mon Sep 17 00:00:00 2001 From: Esteban Beltran Date: Thu, 28 Apr 2022 15:40:10 +0200 Subject: [PATCH 08/69] Allow to hide custom bulk actions when in query mode --- .../common/types/timeline/actions/index.ts | 1 + .../public/components/t_grid/body/index.tsx | 13 +++- .../alert_status_bulk_actions.tsx | 7 +- .../hooks/use_status_bulk_action_items.tsx | 69 ++++++++++--------- 4 files changed, 54 insertions(+), 36 deletions(-) diff --git a/x-pack/plugins/timelines/common/types/timeline/actions/index.ts b/x-pack/plugins/timelines/common/types/timeline/actions/index.ts index ae4d8db93b4a6..d92197cdc432d 100644 --- a/x-pack/plugins/timelines/common/types/timeline/actions/index.ts +++ b/x-pack/plugins/timelines/common/types/timeline/actions/index.ts @@ -73,6 +73,7 @@ export interface StatusBulkActionsProps { indexName: string; setEventsLoading: SetEventsLoading; setEventsDeleted: SetEventsDeleted; + showAlertStatusActions?: boolean; onUpdateSuccess?: OnUpdateAlertStatusSuccess; onUpdateFailure?: OnUpdateAlertStatusError; customBulkActions?: StatusCustomBulkAction[]; diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/index.tsx b/x-pack/plugins/timelines/public/components/t_grid/body/index.tsx index 7af5e3fe06eb4..641f41b7c43ff 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/body/index.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/body/index.tsx @@ -441,6 +441,13 @@ export const BodyComponent = React.memo( } }, [bulkActions, data]); + const showAlertStatusActions = useMemo(() => { + if (typeof bulkActions === 'boolean') { + return bulkActions; + } + return bulkActions.alertStatusActions ?? true; + }, [bulkActions]); + const showBulkActions = useMemo(() => { if (!hasAlertsCrud) { return false; @@ -452,7 +459,7 @@ export const BodyComponent = React.memo( if (typeof bulkActions === 'boolean') { return bulkActions; } - return bulkActions.alertStatusActions ?? true; + return (bulkActions?.customBulkActions?.length || bulkActions?.alertStatusActions) ?? true; }, [hasAlertsCrud, selectedCount, showCheckboxes, bulkActions]); const alertToolbar = useMemo( @@ -464,6 +471,7 @@ export const BodyComponent = React.memo( {showBulkActions && ( }> ( onAlertStatusActionFailure, onAlertStatusActionSuccess, refetch, + showAlertStatusActions, showBulkActions, totalItems, totalSelectAllAlerts, @@ -505,6 +514,7 @@ export const BodyComponent = React.memo( <> }> ( isLoading, alertCountText, showBulkActions, + showAlertStatusActions, id, totalSelectAllAlerts, totalItems, diff --git a/x-pack/plugins/timelines/public/components/t_grid/toolbar/bulk_actions/alert_status_bulk_actions.tsx b/x-pack/plugins/timelines/public/components/t_grid/toolbar/bulk_actions/alert_status_bulk_actions.tsx index c0b8da013318e..d2d42d0556716 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/toolbar/bulk_actions/alert_status_bulk_actions.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/toolbar/bulk_actions/alert_status_bulk_actions.tsx @@ -13,7 +13,7 @@ import type { SetEventsDeleted, OnUpdateAlertStatusSuccess, OnUpdateAlertStatusError, - AlertStatusCustomBulkAction, + StatusCustomBulkAction, } from '../../../../../common/types'; import type { Refetch } from '../../../../store/t_grid/inputs'; import { tGridActions, TGridModel, tGridSelectors, TimelineState } from '../../../../store/t_grid'; @@ -26,9 +26,10 @@ interface OwnProps { filterStatus?: AlertStatus; query?: string; indexName: string; + showAlertStatusActions?: boolean; onActionSuccess?: OnUpdateAlertStatusSuccess; onActionFailure?: OnUpdateAlertStatusError; - customBulkActions?: AlertStatusCustomBulkAction[]; + customBulkActions?: StatusCustomBulkAction[]; refetch: Refetch; } @@ -47,6 +48,7 @@ export const AlertStatusBulkActionsComponent = React.memo { const actionItems: JSX.Element[] = []; - if (currentStatus !== FILTER_OPEN) { - actionItems.push( - onClickUpdate(FILTER_OPEN)} - > - {i18n.BULK_ACTION_OPEN_SELECTED} - - ); - } - if (currentStatus !== FILTER_ACKNOWLEDGED) { - actionItems.push( - onClickUpdate(FILTER_ACKNOWLEDGED)} - > - {i18n.BULK_ACTION_ACKNOWLEDGED_SELECTED} - - ); - } - if (currentStatus !== FILTER_CLOSED) { - actionItems.push( - onClickUpdate(FILTER_CLOSED)} - > - {i18n.BULK_ACTION_CLOSE_SELECTED} - - ); + if (showAlertStatusActions) { + if (currentStatus !== FILTER_OPEN) { + actionItems.push( + onClickUpdate(FILTER_OPEN)} + > + {i18n.BULK_ACTION_OPEN_SELECTED} + + ); + } + if (currentStatus !== FILTER_ACKNOWLEDGED) { + actionItems.push( + onClickUpdate(FILTER_ACKNOWLEDGED)} + > + {i18n.BULK_ACTION_ACKNOWLEDGED_SELECTED} + + ); + } + if (currentStatus !== FILTER_CLOSED) { + actionItems.push( + onClickUpdate(FILTER_CLOSED)} + > + {i18n.BULK_ACTION_CLOSE_SELECTED} + + ); + } } const additionalItems = customBulkActions @@ -177,7 +180,7 @@ export const useStatusBulkActionItems = ({ : []; return [...actionItems, ...additionalItems]; - }, [currentStatus, customBulkActions, eventIds, onClickUpdate, query]); + }, [currentStatus, customBulkActions, eventIds, onClickUpdate, query, showAlertStatusActions]); return items; }; From 6f1af3eb6ad78f5f4c5f02f3c44e668ebd51b9ab Mon Sep 17 00:00:00 2001 From: Esteban Beltran Date: Thu, 28 Apr 2022 16:28:15 +0200 Subject: [PATCH 09/69] Send all cases attachments as single attachments --- .../client/helpers/group_alerts_by_rule.ts | 50 +++++++++++++++++++ x-pack/plugins/cases/public/mocks.ts | 1 + x-pack/plugins/cases/public/plugin.ts | 2 + x-pack/plugins/cases/public/types.ts | 2 + .../use_bulk_add_to_case_actions.tsx | 18 +------ .../hooks/use_status_bulk_action_items.tsx | 2 +- 6 files changed, 58 insertions(+), 17 deletions(-) create mode 100644 x-pack/plugins/cases/public/client/helpers/group_alerts_by_rule.ts diff --git a/x-pack/plugins/cases/public/client/helpers/group_alerts_by_rule.ts b/x-pack/plugins/cases/public/client/helpers/group_alerts_by_rule.ts new file mode 100644 index 0000000000000..030359253ef38 --- /dev/null +++ b/x-pack/plugins/cases/public/client/helpers/group_alerts_by_rule.ts @@ -0,0 +1,50 @@ +/* + * 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 { CommentRequestAlertType } from '../../../common/api'; +import { CommentType, Ecs } from '../../../common'; +import { getRuleIdFromEvent } from './get_rule_id_from_event'; +import { CaseAttachments } from '../../types'; + +type Maybe = T | null; +interface Event { + data: EventNonEcsData[]; + ecs: Ecs; +} +interface EventNonEcsData { + field: string; + value?: Maybe; +} + +export const groupAlertsByRule = (items: Event[], owner: string): CaseAttachments => { + const attachmentsByRule = items.reduce>((acc, item) => { + const rule = getRuleIdFromEvent(item); + if (!acc[rule.id]) { + acc[rule.id] = { + alertId: [], + index: [], + owner, + type: CommentType.alert as const, + rule, + }; + } + const alerts = acc[rule.id].alertId; + const indexes = acc[rule.id].index; + if (Array.isArray(alerts) && Array.isArray(indexes)) { + alerts.push(item.ecs._id ?? ''); + indexes.push(item.ecs._index ?? ''); + } + return acc; + }, {}); + const attachments: CaseAttachments = []; + const keys = Object.keys(attachmentsByRule); + for (const key of keys) { + const x = attachmentsByRule[key]; + attachments.push(x); + } + return attachments; +}; diff --git a/x-pack/plugins/cases/public/mocks.ts b/x-pack/plugins/cases/public/mocks.ts index f8c0eaaaef7de..e161d8b01b0dd 100644 --- a/x-pack/plugins/cases/public/mocks.ts +++ b/x-pack/plugins/cases/public/mocks.ts @@ -29,6 +29,7 @@ const hooksMock: jest.Mocked = { const helpersMock: jest.Mocked = { canUseCases: jest.fn(), getRuleIdFromEvent: jest.fn(), + groupAlertsByRule: jest.fn(), }; export interface CaseUiClientMock { diff --git a/x-pack/plugins/cases/public/plugin.ts b/x-pack/plugins/cases/public/plugin.ts index 3b124f920e889..8decf2e5251b6 100644 --- a/x-pack/plugins/cases/public/plugin.ts +++ b/x-pack/plugins/cases/public/plugin.ts @@ -23,6 +23,7 @@ import { getCasesLazy } from './client/ui/get_cases'; import { getCasesContextLazy } from './client/ui/get_cases_context'; import { getCreateCaseFlyoutLazy } from './client/ui/get_create_case_flyout'; import { getRecentCasesLazy } from './client/ui/get_recent_cases'; +import { groupAlertsByRule } from './client/helpers/group_alerts_by_rule'; /** * @public @@ -102,6 +103,7 @@ export class CasesUiPlugin helpers: { canUseCases: canUseCases(core.application.capabilities), getRuleIdFromEvent, + groupAlertsByRule, }, }; } diff --git a/x-pack/plugins/cases/public/types.ts b/x-pack/plugins/cases/public/types.ts index 65cc1de25d345..3c9d81efcd0f6 100644 --- a/x-pack/plugins/cases/public/types.ts +++ b/x-pack/plugins/cases/public/types.ts @@ -36,6 +36,7 @@ import type { GetCasesProps } from './client/ui/get_cases'; import { GetAllCasesSelectorModalProps } from './client/ui/get_all_cases_selector_modal'; import { GetCreateCaseFlyoutProps } from './client/ui/get_create_case_flyout'; import { GetRecentCasesProps } from './client/ui/get_recent_cases'; +import { groupAlertsByRule } from './client/helpers/group_alerts_by_rule'; export interface CasesPluginSetup { security: SecurityPluginSetup; @@ -128,6 +129,7 @@ export interface CasesUiStart { */ canUseCases: (owners?: CasesOwners[]) => { crud: boolean; read: boolean }; getRuleIdFromEvent: typeof getRuleIdFromEvent; + groupAlertsByRule: typeof groupAlertsByRule; }; } diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_bulk_add_to_case_actions.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_bulk_add_to_case_actions.tsx index 35ce1aa5dec28..453ea04984f70 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_bulk_add_to_case_actions.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_bulk_add_to_case_actions.tsx @@ -5,8 +5,6 @@ * 2.0. */ -import { CommentType } from '@kbn/cases-plugin/common'; -import { CasesUiStart } from '@kbn/cases-plugin/public'; import { useMemo } from 'react'; import { APP_ID } from '../../../../../common/constants'; import type { TimelineItem } from '../../../../../common/search_strategy'; @@ -18,18 +16,6 @@ export interface UseAddToCaseActions { onSuccess?: () => Promise; } -function timelineItemsToCaseAttachments(items: TimelineItem[] = [], casesUi: CasesUiStart) { - return items.map((item) => { - return { - alertId: item.ecs._id ?? '', - index: item.ecs._index ?? '', - owner: APP_ID, - type: CommentType.alert as const, - rule: casesUi.helpers.getRuleIdFromEvent({ ecs: item.ecs, data: item.data }), - }; - }); -} - export const useBulkAddToCaseActions = ({ onClose, onSuccess }: UseAddToCaseActions = {}) => { const { cases: casesUi } = useKibana().services; @@ -53,7 +39,7 @@ export const useBulkAddToCaseActions = ({ onClose, onSuccess }: UseAddToCaseActi disableOnQuery: true, disabledLabel: ADD_TO_CASE_DISABLED, onClick: (items?: TimelineItem[]) => { - const caseAttachments = timelineItemsToCaseAttachments(items, casesUi); + const caseAttachments = items ? casesUi.helpers.groupAlertsByRule(items, APP_ID) : []; createCaseFlyout.open(caseAttachments); }, }, @@ -64,7 +50,7 @@ export const useBulkAddToCaseActions = ({ onClose, onSuccess }: UseAddToCaseActi disabledLabel: ADD_TO_CASE_DISABLED, 'data-test-subj': 'attach-new-case', onClick: (items?: TimelineItem[]) => { - const caseAttachments = timelineItemsToCaseAttachments(items, casesUi); + const caseAttachments = items ? casesUi.helpers.groupAlertsByRule(items, APP_ID) : []; selectCaseModal.open(caseAttachments); }, }, diff --git a/x-pack/plugins/timelines/public/hooks/use_status_bulk_action_items.tsx b/x-pack/plugins/timelines/public/hooks/use_status_bulk_action_items.tsx index a5fb986c8a0ab..25e3633d2eca8 100644 --- a/x-pack/plugins/timelines/public/hooks/use_status_bulk_action_items.tsx +++ b/x-pack/plugins/timelines/public/hooks/use_status_bulk_action_items.tsx @@ -24,7 +24,7 @@ export const useStatusBulkActionItems = ({ query, indexName, setEventsLoading, - showAlertStatusActions, + showAlertStatusActions = true, setEventsDeleted, onUpdateSuccess, onUpdateFailure, From 7ec01d8944a8ec402a099bc556dd92100625282e Mon Sep 17 00:00:00 2001 From: Esteban Beltran Date: Thu, 28 Apr 2022 16:42:34 +0200 Subject: [PATCH 10/69] Enhance hooks tests to include attachments --- ..._cases_add_to_existing_case_modal.test.tsx | 41 +++++++++++++++++ .../use_cases_add_to_new_case_flyout.test.tsx | 44 ++++++++++++++++++- 2 files changed, 84 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/cases/public/components/all_cases/selector_modal/use_cases_add_to_existing_case_modal.test.tsx b/x-pack/plugins/cases/public/components/all_cases/selector_modal/use_cases_add_to_existing_case_modal.test.tsx index f0eea39d551a7..f47176a0a51bd 100644 --- a/x-pack/plugins/cases/public/components/all_cases/selector_modal/use_cases_add_to_existing_case_modal.test.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/selector_modal/use_cases_add_to_existing_case_modal.test.tsx @@ -45,6 +45,17 @@ const TestComponent: React.FC = () => { return