- {addToCaseActionProps && timelinesUi.getAddToCaseAction(addToCaseActionProps)}
{items.length > 0 && (
diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_add_to_case_actions.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_add_to_case_actions.tsx
index cc0ef8d4e8b79..2ca4525c7e1ab 100644
--- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_add_to_case_actions.tsx
+++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_add_to_case_actions.tsx
@@ -5,16 +5,19 @@
* 2.0.
*/
-import { useMemo } from 'react';
+import React, { useCallback, useMemo } from 'react';
+import { EuiContextMenuItem } from '@elastic/eui';
+import { CommentType } from '../../../../../../cases/common';
+import { CaseAttachments } from '../../../../../../cases/public';
import { useGetUserCasesPermissions, useKibana } from '../../../../common/lib/kibana';
import type { TimelineNonEcsData } from '../../../../../common/search_strategy';
import { TimelineId } from '../../../../../common/types';
-import { APP_ID, APP_UI_ID } from '../../../../../common/constants';
-import { useInsertTimeline } from '../../../../cases/components/use_insert_timeline';
+import { APP_ID } from '../../../../../common/constants';
import { Ecs } from '../../../../../common/ecs';
+import { ADD_TO_EXISTING_CASE, ADD_TO_NEW_CASE } from '../translations';
export interface UseAddToCaseActions {
- afterCaseSelection: () => void;
+ onMenuItemClick: () => void;
ariaLabel?: string;
ecsData?: Ecs;
nonEcsData?: TimelineNonEcsData[];
@@ -22,51 +25,92 @@ export interface UseAddToCaseActions {
}
export const useAddToCaseActions = ({
- afterCaseSelection,
+ onMenuItemClick,
ariaLabel,
ecsData,
nonEcsData,
timelineId,
}: UseAddToCaseActions) => {
- const { timelines: timelinesUi } = useKibana().services;
+ const { cases: casesUi } = useKibana().services;
const casePermissions = useGetUserCasesPermissions();
- const insertTimelineHook = useInsertTimeline;
+ const hasWritePermissions = casePermissions?.crud ?? false;
- const addToCaseActionProps = useMemo(
- () =>
- ecsData?._id
- ? {
- ariaLabel,
- event: { data: nonEcsData ?? [], ecs: ecsData, _id: ecsData?._id },
- useInsertTimeline: insertTimelineHook,
- casePermissions,
- appId: APP_UI_ID,
+ const caseAttachments: CaseAttachments = useMemo(() => {
+ return ecsData?._id
+ ? [
+ {
+ alertId: ecsData?._id ?? '',
+ index: ecsData?._index ?? '',
owner: APP_ID,
- onClose: afterCaseSelection,
- }
- : null,
- [ecsData, ariaLabel, nonEcsData, insertTimelineHook, casePermissions, afterCaseSelection]
- );
- const hasWritePermissions = casePermissions?.crud ?? false;
- const addToCaseActionItems = useMemo(
- () =>
+ type: CommentType.alert,
+ rule: casesUi.helpers.getRuleIdFromEvent({ ecs: ecsData, data: nonEcsData ?? [] }),
+ },
+ ]
+ : [];
+ }, [casesUi.helpers, ecsData, nonEcsData]);
+
+ const createCaseFlyout = casesUi.hooks.getUseCasesAddToNewCaseFlyout({
+ attachments: caseAttachments,
+ onClose: onMenuItemClick,
+ });
+
+ const selectCaseModal = casesUi.hooks.getUseCasesAddToExistingCaseModal({
+ attachments: caseAttachments,
+ onClose: onMenuItemClick,
+ });
+
+ const handleAddToNewCaseClick = useCallback(() => {
+ // TODO rename this, this is really `closePopover()`
+ onMenuItemClick();
+ createCaseFlyout.open();
+ }, [onMenuItemClick, createCaseFlyout]);
+
+ const handleAddToExistingCaseClick = useCallback(() => {
+ // TODO rename this, this is really `closePopover()`
+ onMenuItemClick();
+ selectCaseModal.open();
+ }, [onMenuItemClick, selectCaseModal]);
+
+ const addToCaseActionItems = useMemo(() => {
+ if (
[
TimelineId.detectionsPage,
TimelineId.detectionsRulesDetailsPage,
TimelineId.active,
].includes(timelineId as TimelineId) &&
- hasWritePermissions &&
- addToCaseActionProps
- ? [
- timelinesUi.getAddToExistingCaseButton(addToCaseActionProps),
- timelinesUi.getAddToNewCaseButton(addToCaseActionProps),
- ]
- : [],
- [addToCaseActionProps, hasWritePermissions, timelineId, timelinesUi]
- );
+ hasWritePermissions
+ ) {
+ return [
+ // add to existing case menu item
+
+ {ADD_TO_EXISTING_CASE}
+ ,
+ // add to new case menu item
+
+ {ADD_TO_NEW_CASE}
+ ,
+ ];
+ }
+ return [];
+ }, [
+ ariaLabel,
+ handleAddToExistingCaseClick,
+ handleAddToNewCaseClick,
+ hasWritePermissions,
+ timelineId,
+ ]);
return {
addToCaseActionItems,
- addToCaseActionProps,
};
};
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 590b5759ecae4..bdddd8ab46207 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
@@ -285,3 +285,17 @@ export const TRIGGERED = i18n.translate(
defaultMessage: 'Triggered',
}
);
+
+export const ADD_TO_EXISTING_CASE = i18n.translate(
+ 'xpack.securitySolution.detectionEngine.alerts.actions.addToCase',
+ {
+ defaultMessage: 'Add to existing case',
+ }
+);
+
+export const ADD_TO_NEW_CASE = i18n.translate(
+ 'xpack.securitySolution.detectionEngine.alerts.actions.addToNewCase',
+ {
+ defaultMessage: 'Add to new case',
+ }
+);
diff --git a/x-pack/plugins/security_solution/public/detections/components/take_action_dropdown/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/take_action_dropdown/index.test.tsx
index 0c525a2d77706..a15a717f6f42a 100644
--- a/x-pack/plugins/security_solution/public/detections/components/take_action_dropdown/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/detections/components/take_action_dropdown/index.test.tsx
@@ -17,6 +17,7 @@ import { TestProviders } from '../../../common/mock';
import { mockTimelines } from '../../../common/mock/mock_timelines_plugin';
import { createStartServicesMock } from '../../../common/lib/kibana/kibana_react.mock';
import { useKibana } from '../../../common/lib/kibana';
+import { mockCasesContract } from '../../../../../cases/public/mocks';
jest.mock('../user_info', () => ({
useUserData: jest.fn().mockReturnValue([{ canUserCRUD: true, hasIndexWrite: true }]),
@@ -82,6 +83,7 @@ describe('take action dropdown', () => {
services: {
...mockStartServicesMock,
timelines: { ...mockTimelines },
+ cases: mockCasesContract(),
application: {
capabilities: { siem: { crud_alerts: true, read_alerts: true } },
},
diff --git a/x-pack/plugins/security_solution/public/detections/components/take_action_dropdown/index.tsx b/x-pack/plugins/security_solution/public/detections/components/take_action_dropdown/index.tsx
index 8ad76c70247bf..d9dfcd0fee7da 100644
--- a/x-pack/plugins/security_solution/public/detections/components/take_action_dropdown/index.tsx
+++ b/x-pack/plugins/security_solution/public/detections/components/take_action_dropdown/index.tsx
@@ -137,7 +137,7 @@ export const TakeActionDropdown = React.memo(
disabled: !isEndpointEvent,
});
- const afterCaseSelection = useCallback(() => {
+ const onMenuItemClick = useCallback(() => {
closePopoverHandler();
}, [closePopoverHandler]);
@@ -175,7 +175,7 @@ export const TakeActionDropdown = React.memo(
const { addToCaseActionItems } = useAddToCaseActions({
ecsData,
nonEcsData: detailsData?.map((d) => ({ field: d.field, value: d.values })) ?? [],
- afterCaseSelection,
+ onMenuItemClick,
timelineId,
});
diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.test.tsx
index 4b6cbb6f7e16d..4891c75744e38 100644
--- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.test.tsx
+++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.test.tsx
@@ -24,7 +24,7 @@ import { createStore, State } from '../../../common/store';
import { mockHistory, Router } from '../../../common/mock/router';
import { mockTimelines } from '../../../common/mock/mock_timelines_plugin';
import { mockBrowserFields } from '../../../common/containers/source/mock';
-import { mockCasesContext } from '../../../common/mock/mock_cases_context';
+import { mockCasesContext } from '../../../../../cases/public/mocks/mock_cases_context';
// Test will fail because we will to need to mock some core services to make the test work
// For now let's forget about SiemSearchBar and QueryBar
diff --git a/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/footer.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/footer.test.tsx
index 71d6f6253010d..4a0b7d0fe0501 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/footer.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/footer.test.tsx
@@ -15,6 +15,7 @@ import { mockAlertDetailsData } from '../../../../common/components/event_detail
import type { TimelineEventsDetailsItem } from '../../../../../common/search_strategy';
import { KibanaServices, useKibana } from '../../../../common/lib/kibana';
import { coreMock } from '../../../../../../../../src/core/public/mocks';
+import { mockCasesContract } from '../../../../../../cases/public/mocks';
const ecsData: Ecs = {
_id: '1',
@@ -114,6 +115,7 @@ describe('event details footer component', () => {
},
query: jest.fn(),
},
+ cases: mockCasesContract(),
},
});
});
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.test.tsx
index b6e6aa40876cc..7a94dcef31cf7 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.test.tsx
@@ -12,6 +12,7 @@ import { TestProviders, mockTimelineModel, mockTimelineData } from '../../../../
import { Actions } from '.';
import { mockTimelines } from '../../../../../common/mock/mock_timelines_plugin';
import { useIsExperimentalFeatureEnabled } from '../../../../../common/hooks/use_experimental_features';
+import { mockCasesContract } from '../../../../../../../cases/public/mocks';
jest.mock('../../../../../detections/components/user_info', () => ({
useUserData: jest.fn().mockReturnValue([{ canUserCRUD: true, hasIndexWrite: true }]),
@@ -43,6 +44,7 @@ jest.mock('../../../../../common/lib/kibana', () => ({
siem: { crud_alerts: true, read_alerts: true },
},
},
+ cases: mockCasesContract(),
uiSettings: {
get: jest.fn(),
},
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.test.tsx
index b9e04060881d4..890175ac8daf9 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.test.tsx
@@ -20,6 +20,7 @@ import { getDefaultControlColumn } from '../control_columns';
import { testLeadingControlColumn } from '../../../../../common/mock/mock_timeline_control_columns';
import { mockTimelines } from '../../../../../common/mock/mock_timelines_plugin';
import { getActionsColumnWidth } from '../../../../../../../timelines/public';
+import { mockCasesContract } from '../../../../../../../cases/public/mocks';
jest.mock('../../../../../common/hooks/use_experimental_features');
const useIsExperimentalFeatureEnabledMock = useIsExperimentalFeatureEnabled as jest.Mock;
@@ -40,6 +41,7 @@ jest.mock('../../../../../common/lib/kibana', () => ({
siem: { crud_alerts: true, read_alerts: true },
},
},
+ cases: mockCasesContract(),
},
}),
useToasts: jest.fn().mockReturnValue({
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.test.tsx
index 66a140987475c..f616b4afc2af5 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.test.tsx
@@ -24,12 +24,12 @@ import { useMountAppended } from '../../../../common/utils/use_mount_appended';
import { timelineActions } from '../../../store/timeline';
import { ColumnHeaderOptions, TimelineTabs } from '../../../../../common/types/timeline';
import { defaultRowRenderers } from './renderers';
-import { mockCasesContext } from '../../../../common/mock/mock_cases_context';
jest.mock('../../../../common/lib/kibana/hooks');
jest.mock('../../../../common/hooks/use_app_toasts');
jest.mock('../../../../common/lib/kibana', () => {
const originalModule = jest.requireActual('../../../../common/lib/kibana');
+ const mockCasesContract = jest.requireActual('../../../../../../cases/public/mocks');
return {
...originalModule,
useKibana: jest.fn().mockReturnValue({
@@ -41,9 +41,7 @@ jest.mock('../../../../common/lib/kibana', () => {
siem: { crud_alerts: true, read_alerts: true },
},
},
- cases: {
- getCasesContext: () => mockCasesContext,
- },
+ cases: mockCasesContract.mockCasesContract(),
data: {
search: jest.fn(),
query: jest.fn(),
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/eql_tab_content/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/eql_tab_content/index.test.tsx
index 43622b7e45365..943abc88cf2b0 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/eql_tab_content/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/eql_tab_content/index.test.tsx
@@ -23,7 +23,7 @@ import { useTimelineEventsDetails } from '../../../containers/details/index';
import { useSourcererDataView } from '../../../../common/containers/sourcerer';
import { mockSourcererScope } from '../../../../common/containers/sourcerer/mocks';
import { useDraggableKeyboardWrapper as mockUseDraggableKeyboardWrapper } from '../../../../../../timelines/public/components';
-import { mockCasesContext } from '../../../../common/mock/mock_cases_context';
+import { mockCasesContext } from '../../../../../../cases/public/mocks/mock_cases_context';
jest.mock('../../../containers/index', () => ({
useTimelineEvents: jest.fn(),
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/pinned_tab_content/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/pinned_tab_content/index.test.tsx
index ffe50f935b9fe..954f54fdba777 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/pinned_tab_content/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/pinned_tab_content/index.test.tsx
@@ -24,7 +24,7 @@ import { mockSourcererScope } from '../../../../common/containers/sourcerer/mock
import { PinnedTabContentComponent, Props as PinnedTabContentComponentProps } from '.';
import { Direction } from '../../../../../common/search_strategy';
import { useDraggableKeyboardWrapper as mockUseDraggableKeyboardWrapper } from '../../../../../../timelines/public/components';
-import { mockCasesContext } from '../../../../common/mock/mock_cases_context';
+import { mockCasesContext } from '../../../../../../cases/public/mocks/mock_cases_context';
jest.mock('../../../containers/index', () => ({
useTimelineEvents: jest.fn(),
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.test.tsx
index 019bedacbffe8..c16afa945cc08 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.test.tsx
@@ -26,7 +26,7 @@ import { useSourcererDataView } from '../../../../common/containers/sourcerer';
import { mockSourcererScope } from '../../../../common/containers/sourcerer/mocks';
import { Direction } from '../../../../../common/search_strategy';
import * as helpers from '../helpers';
-import { mockCasesContext } from '../../../../common/mock/mock_cases_context';
+import { mockCasesContext } from '../../../../../../cases/public/mocks/mock_cases_context';
jest.mock('../../../containers/index', () => ({
useTimelineEvents: jest.fn(),