From 83105bdc96e89c6408af5cb0813923e1a99c9de3 Mon Sep 17 00:00:00 2001 From: Esteban Beltran Date: Thu, 3 Mar 2022 11:38:00 +0100 Subject: [PATCH] [Cases] Refactor: Move cases action buttons out of timelines (#126265) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * WIP * WIP2 * Use new cases context hooks to open and close the flyout * Update timelines to use new hooks * CLose flyout on create success * Add back sucess toast * Move code to a dedicated component * Add CasesContext to observability * Remove dependency * Small refactor * Use observabilityAppId instead of observabilityFeatureId for buttons * Add CasesContext to timetable * Fix detection engine test cases * Fix broken tests * Fix broken tests * Rename hook * Add test cases for cases context ui * Add test for new hook * Remove state from the provider context * Remove basevalue * apply suggested renaming * Add usecallback * Add reducer types, fix test type, remove redundant check * Accept attachments as a prop for the cases select modal * Expose useCasesAddToExistingCase hook, reducer code and global component * use the new hook to open the select cases modasl * Fix tests and types * Add tests for cases global components * [Fleet] showing agent policy creation error message on UI (#125931) * showing agent policy creation error message on UI * mapping the error instead of showing from the backend Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> * [ResponseOps] Adds tooltip to time window selector in ES query rule flyout (#125764) * [Lens] Allow detaching from global time range (#125563) * allow detaching from global time range * add test * fix time field recognition * fix tests Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> * [Fleet] refactor auto upgrade package policies logic (#125909) * refactor upgrade package policies * fixed tests * code cleanup * review improvements * added api test Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> * skip flaky suite (#126027) * Remove deprecated api (#125524) * [Fleet] Remove deprecated kibana APIs - License * Remove basePath from FleetApp * Replace AsyncPlugin with Plugin * Get fieldFormats from fieldFormats plugin rather than data plugin * Fix ts errors * Attempt fixing wrong type * Move licenseService to FleetStartDeps * Fix types and mocks Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> * Upgrade `markdown-it` dependency (`10.0.0` → `12.3.2`). (#125526) * skipping failing tests (#126039) * remove unused deprecated code and use field format plugin directly for data view field editor (#126029) * [data views] Improve preview pane (#126013) * fix preview pane * fix preview pane * one less span tag Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> * [Alerting] Provide services to set context for recovered alerts (#124972) * Rename alert instance to alert and add create fn to alert factory * Rename alert instance to alert and add create fn to alert factory * Fixing types * Fixing types * Adding flag for rule types to opt into setting recovery context * Only showing context in action variable menu if flag set to true * Adding recovery functions to createAlertFactory * Setting recovery in index threshold and fixing types * Fixing lint issues and some refactoring * Cleanup * Functional tests for index threshold rule recovery context * Return array of recovered alerts instead of record * Fixing types * Fixing types * Cleanup * Handling nulls and more tests * Updating developer docs * Making getRecoveryAlerts non-optional * Setting unknown in index threshold recovery value * PR feedback * Adding a test Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> * [Discover] Re-introduce saved_searches test (#126059) * [Archive Migration] index pattern without timefield (#125870) * kbn_archive date_nanos * kbn_archive date_nanos in context and discover * kbn_archiver more date_nanos tests * split out kbnArchiver for index_pattern_without_timefield * remove date_nanos files from a different PR * update another test for usage of the same archives * set default index pattern for test * remove duplicate const kibanaServer Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> * delete unused es_archive visualize_embedding (#126001) * delete unused es_archive * remove other unused es_archive * more unused es_archives Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> * Bump packages (#126119) * url-parse 1.5.3 -> 1.5.9 * follow-redirects 1.y.z -> 1.14.9 * Add tests for all cases selector and attachments * Add tests for use add to existing case hook * First version of the cases timeline actions * export add alert to new case button from cases plugin * Make Cases ECS compatible with timelines and security_solution * Delete new case button * Add helpers * Use the cases hook directly for add to new case * Remov unused dependencies * Rename callbacks, remove timelines calls * Fixing tests for the dropdown * Fix broken test * mocking cases for tests * Fix detectiosn tests * Observability now uses the new cases hooks * Wrap events viewer into cases context * Open the create case flyout if create case was selected in the modal * Fix cases mocks for security_solution * Update tests * Add tests for use cases toast * Improve cases mock * delete security mock * replace tests mocks for cases * fix import mock * Do not require onRowClick * Show the toast inside the modal * show the toast inside the flyout * remove toast logic from the consumer plugin * fix typescript types * Rename type * Fix broken test * Fix file name and broken test * Use internal navigation hook * Update hook dependencies * Move useCaseToast * Fix mock paths * fix eslint * Add test cases for the toast content * Add cases context to the overview page Co-authored-by: Julia Bardi <90178898+juliaElastic@users.noreply.github.com> Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: István Zoltán Szabó Co-authored-by: Joe Reuter Co-authored-by: Tiago Costa Co-authored-by: Cristina Amico Co-authored-by: Aleh Zasypkin Co-authored-by: Gloria Hornero Co-authored-by: Matthew Kime Co-authored-by: Ying Mao Co-authored-by: Maja Grubic Co-authored-by: Lee Drengenberg Co-authored-by: Joe Portner <5295965+jportner@users.noreply.github.com> --- x-pack/plugins/cases/common/ui/types.ts | 2 +- .../cases/public/common/translations.ts | 14 ++ .../public/common/use_cases_toast.test.tsx | 75 +++++++++++ .../cases/public/common/use_cases_toast.tsx | 82 ++++++++++++ .../all_cases_selector_modal.tsx | 6 +- ...cases_add_to_existing_case_modal.test.tsx} | 8 +- .../use_cases_add_to_existing_case_modal.tsx | 36 +++++- .../use_cases_add_to_new_case_flyout.test.tsx | 1 + .../use_cases_add_to_new_case_flyout.tsx | 13 +- x-pack/plugins/cases/public/index.tsx | 2 +- .../public/methods/get_rule_id_from_event.ts | 46 +++++++ x-pack/plugins/cases/public/mocks.ts | 10 +- .../public/mocks}/mock_cases_context.tsx | 0 x-pack/plugins/cases/public/plugin.ts | 4 + x-pack/plugins/cases/public/types.ts | 4 + .../alerts_table_t_grid.tsx | 81 +++++++----- .../alerts_table_t_grid/translations.ts | 22 ++++ .../pages/overview/old_overview_page.tsx | 26 +++- .../common/components/events_viewer/index.tsx | 121 +++++++++--------- .../common/lib/kibana/__mocks__/index.ts | 2 + .../alert_context_menu.test.tsx | 2 + .../timeline_actions/alert_context_menu.tsx | 9 +- .../use_add_to_case_actions.tsx | 112 +++++++++++----- .../components/alerts_table/translations.ts | 14 ++ .../take_action_dropdown/index.test.tsx | 2 + .../components/take_action_dropdown/index.tsx | 4 +- .../detection_engine.test.tsx | 2 +- .../side_panel/event_details/footer.test.tsx | 2 + .../timeline/body/actions/index.test.tsx | 2 + .../body/events/event_column_view.test.tsx | 2 + .../components/timeline/body/index.test.tsx | 6 +- .../timeline/eql_tab_content/index.test.tsx | 2 +- .../pinned_tab_content/index.test.tsx | 2 +- .../timeline/query_tab_content/index.test.tsx | 2 +- 34 files changed, 565 insertions(+), 153 deletions(-) create mode 100644 x-pack/plugins/cases/public/common/use_cases_toast.test.tsx create mode 100644 x-pack/plugins/cases/public/common/use_cases_toast.tsx rename x-pack/plugins/cases/public/components/all_cases/selector_modal/{uses_cases_add_to_existing_case_modal.test.tsx => use_cases_add_to_existing_case_modal.test.tsx} (89%) create mode 100644 x-pack/plugins/cases/public/methods/get_rule_id_from_event.ts rename x-pack/plugins/{security_solution/public/common/mock => cases/public/mocks}/mock_cases_context.tsx (100%) create mode 100644 x-pack/plugins/observability/public/pages/alerts/containers/alerts_table_t_grid/translations.ts diff --git a/x-pack/plugins/cases/common/ui/types.ts b/x-pack/plugins/cases/common/ui/types.ts index 95135f4a0e9a09..8abc0805b8a3fc 100644 --- a/x-pack/plugins/cases/common/ui/types.ts +++ b/x-pack/plugins/cases/common/ui/types.ts @@ -185,7 +185,7 @@ export interface RuleEcs { id?: string[]; rule_id?: string[]; name?: string[]; - false_positives: string[]; + false_positives?: string[]; saved_id?: string[]; timeline_id?: string[]; timeline_title?: string[]; diff --git a/x-pack/plugins/cases/public/common/translations.ts b/x-pack/plugins/cases/public/common/translations.ts index c4535c8f8da2f6..046eb67d38b248 100644 --- a/x-pack/plugins/cases/public/common/translations.ts +++ b/x-pack/plugins/cases/public/common/translations.ts @@ -254,3 +254,17 @@ export const MAX_LENGTH_ERROR = (field: string, length: number) => export const LINK_APPROPRIATE_LICENSE = i18n.translate('xpack.cases.common.appropriateLicense', { defaultMessage: 'appropriate license', }); + +export const CASE_SUCCESS_TOAST = (title: string) => + i18n.translate('xpack.cases.actions.caseSuccessToast', { + values: { title }, + defaultMessage: 'An alert has been added to "{title}"', + }); + +export const CASE_SUCCESS_SYNC_TEXT = i18n.translate('xpack.cases.actions.caseSuccessSyncText', { + defaultMessage: 'Alerts in this case have their status synched with the case status', +}); + +export const VIEW_CASE = i18n.translate('xpack.cases.actions.viewCase', { + defaultMessage: 'View Case', +}); diff --git a/x-pack/plugins/cases/public/common/use_cases_toast.test.tsx b/x-pack/plugins/cases/public/common/use_cases_toast.test.tsx new file mode 100644 index 00000000000000..9bd6a6675a5c16 --- /dev/null +++ b/x-pack/plugins/cases/public/common/use_cases_toast.test.tsx @@ -0,0 +1,75 @@ +/* + * 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 { renderHook } from '@testing-library/react-hooks'; +import { useToasts } from '../common/lib/kibana'; +import { AppMockRenderer, createAppMockRenderer, TestProviders } from '../common/mock'; +import { CaseToastSuccessContent, useCasesToast } from './use_cases_toast'; +import { mockCase } from '../containers/mock'; +import React from 'react'; +import userEvent from '@testing-library/user-event'; + +jest.mock('../common/lib/kibana'); + +const useToastsMock = useToasts as jest.Mock; + +describe('Use cases toast hook', () => { + describe('Toast hook', () => { + const successMock = jest.fn(); + useToastsMock.mockImplementation(() => { + return { + addSuccess: successMock, + }; + }); + it('should create a success tost when invoked with a case', () => { + const { result } = renderHook( + () => { + return useCasesToast(); + }, + { wrapper: TestProviders } + ); + result.current.showSuccessAttach(mockCase); + expect(successMock).toHaveBeenCalled(); + }); + }); + describe('Toast content', () => { + let appMockRender: AppMockRenderer; + const onViewCaseClick = jest.fn(); + beforeEach(() => { + appMockRender = createAppMockRenderer(); + onViewCaseClick.mockReset(); + }); + + it('renders a correct successfull message with synced alerts', () => { + const result = appMockRender.render( + + ); + expect(result.getByTestId('toaster-content-sync-text')).toHaveTextContent( + 'Alerts in this case have their status synched with the case status' + ); + expect(result.getByTestId('toaster-content-case-view-link')).toHaveTextContent('View Case'); + expect(onViewCaseClick).not.toHaveBeenCalled(); + }); + + it('renders a correct successfull message with not synced alerts', () => { + const result = appMockRender.render( + + ); + expect(result.queryByTestId('toaster-content-sync-text')).toBeFalsy(); + expect(result.getByTestId('toaster-content-case-view-link')).toHaveTextContent('View Case'); + expect(onViewCaseClick).not.toHaveBeenCalled(); + }); + + it('Calls the onViewCaseClick when clicked', () => { + const result = appMockRender.render( + + ); + userEvent.click(result.getByTestId('toaster-content-case-view-link')); + expect(onViewCaseClick).toHaveBeenCalled(); + }); + }); +}); diff --git a/x-pack/plugins/cases/public/common/use_cases_toast.tsx b/x-pack/plugins/cases/public/common/use_cases_toast.tsx new file mode 100644 index 00000000000000..98cc7fa1d8faa0 --- /dev/null +++ b/x-pack/plugins/cases/public/common/use_cases_toast.tsx @@ -0,0 +1,82 @@ +/* + * 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 { EuiButtonEmpty, EuiText } from '@elastic/eui'; +import React from 'react'; +import styled from 'styled-components'; +import { toMountPoint } from '../../../../../src/plugins/kibana_react/public'; +import { Case } from '../../common'; +import { useToasts } from '../common/lib/kibana'; +import { useCaseViewNavigation } from '../common/navigation'; +import { CASE_SUCCESS_SYNC_TEXT, CASE_SUCCESS_TOAST, VIEW_CASE } from './translations'; + +const LINE_CLAMP = 3; +const Title = styled.span` + text-overflow: ellipsis; + display: -webkit-box; + -webkit-line-clamp: ${LINE_CLAMP}; + -webkit-box-orient: vertical; + overflow: hidden; +`; +const EuiTextStyled = styled(EuiText)` + ${({ theme }) => ` + margin-bottom: ${theme.eui?.paddingSizes?.s ?? 8}px; + `} +`; + +export const useCasesToast = () => { + const { navigateToCaseView } = useCaseViewNavigation(); + + const toasts = useToasts(); + + return { + showSuccessAttach: (theCase: Case) => { + const onViewCaseClick = () => { + navigateToCaseView({ + detailName: theCase.id, + }); + }; + return toasts.addSuccess({ + color: 'success', + iconType: 'check', + title: toMountPoint({CASE_SUCCESS_TOAST(theCase.title)}), + text: toMountPoint( + + ), + }); + }, + }; +}; +export const CaseToastSuccessContent = ({ + syncAlerts, + onViewCaseClick, +}: { + syncAlerts: boolean; + onViewCaseClick: () => void; +}) => { + return ( + <> + {syncAlerts && ( + + {CASE_SUCCESS_SYNC_TEXT} + + )} + + {VIEW_CASE} + + + ); +}; +CaseToastSuccessContent.displayName = 'CaseToastSuccessContent'; diff --git a/x-pack/plugins/cases/public/components/all_cases/selector_modal/all_cases_selector_modal.tsx b/x-pack/plugins/cases/public/components/all_cases/selector_modal/all_cases_selector_modal.tsx index 08c99c51593997..ba553b28a34e09 100644 --- a/x-pack/plugins/cases/public/components/all_cases/selector_modal/all_cases_selector_modal.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/selector_modal/all_cases_selector_modal.tsx @@ -26,7 +26,7 @@ export interface AllCasesSelectorModalProps { */ alertData?: Omit; hiddenStatuses?: CaseStatusWithAllStatus[]; - onRowClick: (theCase?: Case) => void; + onRowClick?: (theCase?: Case) => void; updateCase?: (newCase: Case) => void; onClose?: () => void; attachments?: CaseAttachments; @@ -52,7 +52,9 @@ export const AllCasesSelectorModal = React.memo( const onClick = useCallback( (theCase?: Case) => { closeModal(); - onRowClick(theCase); + if (onRowClick) { + onRowClick(theCase); + } }, [closeModal, onRowClick] ); diff --git a/x-pack/plugins/cases/public/components/all_cases/selector_modal/uses_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 similarity index 89% rename from x-pack/plugins/cases/public/components/all_cases/selector_modal/uses_cases_add_to_existing_case_modal.test.tsx rename to x-pack/plugins/cases/public/components/all_cases/selector_modal/use_cases_add_to_existing_case_modal.test.tsx index 6eeff6102ae6a3..6a224949db8be2 100644 --- a/x-pack/plugins/cases/public/components/all_cases/selector_modal/uses_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 @@ -12,6 +12,7 @@ import React from 'react'; import { CasesContext } from '../../cases_context'; import { CasesContextStoreActionsList } from '../../cases_context/cases_context_reducer'; import { useCasesAddToExistingCaseModal } from './use_cases_add_to_existing_case_modal'; +jest.mock('../../../common/use_cases_toast'); describe('use cases add to existing case modal hook', () => { const dispatch = jest.fn(); @@ -65,7 +66,7 @@ describe('use cases add to existing case modal hook', () => { ); }); - it('should dispatch the close action when invoked', () => { + it('should dispatch the close action for modal and flyout when invoked', () => { const { result } = renderHook( () => { return useCasesAddToExistingCaseModal(defaultParams()); @@ -78,5 +79,10 @@ describe('use cases add to existing case modal hook', () => { type: CasesContextStoreActionsList.CLOSE_ADD_TO_CASE_MODAL, }) ); + expect(dispatch).toHaveBeenCalledWith( + expect.objectContaining({ + type: CasesContextStoreActionsList.CLOSE_CREATE_CASE_FLYOUT, + }) + ); }); }); 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 b2ad07c2375dfe..5341f5be4183d5 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 @@ -7,15 +7,36 @@ import { useCallback } from 'react'; import { AllCasesSelectorModalProps } from '.'; +import { useCasesToast } from '../../../common/use_cases_toast'; +import { Case } from '../../../containers/types'; import { CasesContextStoreActionsList } from '../../cases_context/cases_context_reducer'; import { useCasesContext } from '../../cases_context/use_cases_context'; +import { useCasesAddToNewCaseFlyout } from '../../create/flyout/use_cases_add_to_new_case_flyout'; export const useCasesAddToExistingCaseModal = (props: AllCasesSelectorModalProps) => { + const createNewCaseFlyout = useCasesAddToNewCaseFlyout({ + attachments: props.attachments, + onClose: props.onClose, + // TODO there's no need for onSuccess to be async. This will be fixed + // in a follow up clean up + onSuccess: async (theCase?: Case) => { + if (props.onRowClick) { + return props.onRowClick(theCase); + } + }, + }); const { dispatch } = useCasesContext(); + const casesToasts = useCasesToast(); + const closeModal = useCallback(() => { dispatch({ type: CasesContextStoreActionsList.CLOSE_ADD_TO_CASE_MODAL, }); + // in case the flyout was also open when selecting + // create a new case + dispatch({ + type: CasesContextStoreActionsList.CLOSE_CREATE_CASE_FLYOUT, + }); }, [dispatch]); const openModal = useCallback(() => { @@ -23,6 +44,19 @@ export const useCasesAddToExistingCaseModal = (props: AllCasesSelectorModalProps type: CasesContextStoreActionsList.OPEN_ADD_TO_CASE_MODAL, payload: { ...props, + onRowClick: (theCase?: Case) => { + // when the case is undefined in the modal + // the user clicked "create new case" + if (theCase === undefined) { + closeModal(); + createNewCaseFlyout.open(); + } else { + casesToasts.showSuccessAttach(theCase); + if (props.onRowClick) { + props.onRowClick(theCase); + } + } + }, onClose: () => { closeModal(); if (props.onClose) { @@ -37,7 +71,7 @@ export const useCasesAddToExistingCaseModal = (props: AllCasesSelectorModalProps }, }, }); - }, [closeModal, dispatch, props]); + }, [casesToasts, closeModal, createNewCaseFlyout, dispatch, props]); return { open: openModal, close: closeModal, diff --git a/x-pack/plugins/cases/public/components/create/flyout/use_cases_add_to_new_case_flyout.test.tsx b/x-pack/plugins/cases/public/components/create/flyout/use_cases_add_to_new_case_flyout.test.tsx index 103e24c4b7a656..e569b1ee799526 100644 --- a/x-pack/plugins/cases/public/components/create/flyout/use_cases_add_to_new_case_flyout.test.tsx +++ b/x-pack/plugins/cases/public/components/create/flyout/use_cases_add_to_new_case_flyout.test.tsx @@ -12,6 +12,7 @@ import React from 'react'; import { CasesContext } from '../../cases_context'; import { CasesContextStoreActionsList } from '../../cases_context/cases_context_reducer'; import { useCasesAddToNewCaseFlyout } from './use_cases_add_to_new_case_flyout'; +jest.mock('../../../common/use_cases_toast'); describe('use cases add to new case flyout hook', () => { const dispatch = jest.fn(); 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 e4ae4d72f48dab..5422ab9be995d9 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,12 +6,15 @@ */ import { useCallback } from 'react'; +import { useCasesToast } from '../../../common/use_cases_toast'; +import { Case } from '../../../containers/types'; import { CasesContextStoreActionsList } from '../../cases_context/cases_context_reducer'; import { useCasesContext } from '../../cases_context/use_cases_context'; import { CreateCaseFlyoutProps } from './create_case_flyout'; export const useCasesAddToNewCaseFlyout = (props: CreateCaseFlyoutProps) => { const { dispatch } = useCasesContext(); + const casesToasts = useCasesToast(); const closeFlyout = useCallback(() => { dispatch({ @@ -30,6 +33,14 @@ export const useCasesAddToNewCaseFlyout = (props: CreateCaseFlyoutProps) => { return props.onClose(); } }, + onSuccess: async (theCase: Case) => { + if (theCase) { + casesToasts.showSuccessAttach(theCase); + } + if (props.onSuccess) { + return props.onSuccess(theCase); + } + }, afterCaseCreated: async (...args) => { closeFlyout(); if (props.afterCaseCreated) { @@ -38,7 +49,7 @@ export const useCasesAddToNewCaseFlyout = (props: CreateCaseFlyoutProps) => { }, }, }); - }, [closeFlyout, dispatch, props]); + }, [casesToasts, closeFlyout, dispatch, props]); return { open: openFlyout, close: closeFlyout, diff --git a/x-pack/plugins/cases/public/index.tsx b/x-pack/plugins/cases/public/index.tsx index 0190df8204fc1a..42dd1a94199917 100644 --- a/x-pack/plugins/cases/public/index.tsx +++ b/x-pack/plugins/cases/public/index.tsx @@ -21,7 +21,7 @@ export type { GetCreateCaseFlyoutProps } from './methods/get_create_case_flyout' export type { GetAllCasesSelectorModalProps } from './methods/get_all_cases_selector_modal'; export type { GetRecentCasesProps } from './methods/get_recent_cases'; -export type { CaseAttachments } from './types'; +export type { CaseAttachments, SupportedCaseAttachment } from './types'; export type { ICasesDeepLinkId } from './common/navigation'; export { diff --git a/x-pack/plugins/cases/public/methods/get_rule_id_from_event.ts b/x-pack/plugins/cases/public/methods/get_rule_id_from_event.ts new file mode 100644 index 00000000000000..b07ba032e7b191 --- /dev/null +++ b/x-pack/plugins/cases/public/methods/get_rule_id_from_event.ts @@ -0,0 +1,46 @@ +/* + * 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 { ALERT_RULE_NAME, ALERT_RULE_UUID } from '@kbn/rule-data-utils'; +import { get } from 'lodash/fp'; +import { Ecs } from '../../common'; + +type Maybe = T | null; +interface Event { + data: EventNonEcsData[]; + ecs: Ecs; +} +interface EventNonEcsData { + field: string; + value?: Maybe; +} + +export function getRuleIdFromEvent(event: Event): { + id: string; + name: string; +} { + const ruleUuidData = event && event.data.find(({ field }) => field === ALERT_RULE_UUID); + const ruleNameData = event && event.data.find(({ field }) => field === ALERT_RULE_NAME); + const ruleUuidValueData = ruleUuidData && ruleUuidData.value && ruleUuidData.value[0]; + const ruleNameValueData = ruleNameData && ruleNameData.value && ruleNameData.value[0]; + + const ruleUuid = + ruleUuidValueData ?? + get(`ecs.${ALERT_RULE_UUID}[0]`, event) ?? + get(`ecs.signal.rule.id[0]`, event) ?? + null; + const ruleName = + ruleNameValueData ?? + get(`ecs.${ALERT_RULE_NAME}[0]`, event) ?? + get(`ecs.signal.rule.name[0]`, event) ?? + null; + + return { + id: ruleUuid, + name: ruleName, + }; +} diff --git a/x-pack/plugins/cases/public/mocks.ts b/x-pack/plugins/cases/public/mocks.ts index f7f80170a87750..a3876e9e19322e 100644 --- a/x-pack/plugins/cases/public/mocks.ts +++ b/x-pack/plugins/cases/public/mocks.ts @@ -5,12 +5,13 @@ * 2.0. */ +import { mockCasesContext } from './mocks/mock_cases_context'; import { CasesUiStart } from './types'; -const createStartContract = (): jest.Mocked => ({ +export const mockCasesContract = (): jest.Mocked => ({ canUseCases: jest.fn(), getCases: jest.fn(), - getCasesContext: jest.fn(), + getCasesContext: jest.fn().mockImplementation(() => mockCasesContext), getAllCasesSelectorModal: jest.fn(), getAllCasesSelectorModalNoProvider: jest.fn(), getCreateCaseFlyout: jest.fn(), @@ -20,8 +21,11 @@ const createStartContract = (): jest.Mocked => ({ getUseCasesAddToNewCaseFlyout: jest.fn(), getUseCasesAddToExistingCaseModal: jest.fn(), }, + helpers: { + getRuleIdFromEvent: jest.fn(), + }, }); export const casesPluginMock = { - createStartContract, + createStartContract: mockCasesContract, }; diff --git a/x-pack/plugins/security_solution/public/common/mock/mock_cases_context.tsx b/x-pack/plugins/cases/public/mocks/mock_cases_context.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/common/mock/mock_cases_context.tsx rename to x-pack/plugins/cases/public/mocks/mock_cases_context.tsx diff --git a/x-pack/plugins/cases/public/plugin.ts b/x-pack/plugins/cases/public/plugin.ts index 9dbc6ea35125ad..31185ae78f076c 100644 --- a/x-pack/plugins/cases/public/plugin.ts +++ b/x-pack/plugins/cases/public/plugin.ts @@ -21,6 +21,7 @@ import { CasesUiConfigType } from '../common/ui/types'; import { getCasesContextLazy } from './methods/get_cases_context'; import { useCasesAddToNewCaseFlyout } from './components/create/flyout/use_cases_add_to_new_case_flyout'; import { useCasesAddToExistingCaseModal } from './components/all_cases/selector_modal/use_cases_add_to_existing_case_modal'; +import { getRuleIdFromEvent } from './methods/get_rule_id_from_event'; /** * @public @@ -52,6 +53,9 @@ export class CasesUiPlugin implements Plugin ({ ...acc, [d.field]: d.value }), {}); const [openActionsPopoverId, setActionsPopover] = useState(null); const { - timelines, + cases, application: {}, - } = useKibana().services; + } = useKibana().services; const parseObservabilityAlert = useMemo( () => parseAlert(observabilityRuleTypeRegistry), @@ -158,10 +161,6 @@ function ObservabilityActions({ const alert = parseObservabilityAlert(dataFieldEs); const { prepend } = core.http.basePath; - const afterCaseSelection = useCallback(() => { - setActionsPopover(null); - }, []); - const closeActionsPopover = useCallback(() => { setActionsPopover(null); }, []); @@ -171,35 +170,59 @@ function ObservabilityActions({ }, []); const casePermissions = useGetUserCasesPermissions(); - const event = useMemo(() => { - return { - data, - _id: eventId, - ecs: ecsData, - }; - }, [data, eventId, ecsData]); - const ruleId = alert.fields['kibana.alert.rule.uuid'] ?? null; const linkToRule = ruleId ? prepend(paths.management.ruleDetails(ruleId)) : null; + const caseAttachments: CaseAttachments = useMemo(() => { + return ecsData?._id + ? [ + { + alertId: ecsData?._id ?? '', + index: ecsData?._index ?? '', + owner: observabilityFeatureId, + type: CommentType.alert, + rule: cases.helpers.getRuleIdFromEvent({ ecs: ecsData, data: data ?? [] }), + }, + ] + : []; + }, [ecsData, cases.helpers, data]); + + const createCaseFlyout = cases.hooks.getUseCasesAddToNewCaseFlyout({ + attachments: caseAttachments, + }); + + const selectCaseModal = cases.hooks.getUseCasesAddToExistingCaseModal({ + attachments: caseAttachments, + }); + + const handleAddToNewCaseClick = useCallback(() => { + createCaseFlyout.open(); + closeActionsPopover(); + }, [createCaseFlyout, closeActionsPopover]); + + const handleAddToExistingCaseClick = useCallback(() => { + selectCaseModal.open(); + closeActionsPopover(); + }, [closeActionsPopover, selectCaseModal]); + const actionsMenuItems = useMemo(() => { return [ ...(casePermissions?.crud ? [ - timelines.getAddToExistingCaseButton({ - event, - casePermissions, - appId: observabilityAppId, - owner: observabilityFeatureId, - onClose: afterCaseSelection, - }), - timelines.getAddToNewCaseButton({ - event, - casePermissions, - appId: observabilityAppId, - owner: observabilityFeatureId, - onClose: afterCaseSelection, - }), + + {ADD_TO_EXISTING_CASE} + , + + {ADD_TO_NEW_CASE} + , ] : []), @@ -215,7 +238,7 @@ function ObservabilityActions({ ] : []), ]; - }, [afterCaseSelection, casePermissions, timelines, event, linkToRule]); + }, [casePermissions?.crud, handleAddToExistingCaseClick, handleAddToNewCaseClick, linkToRule]); const actionsToolTip = actionsMenuItems.length <= 0 diff --git a/x-pack/plugins/observability/public/pages/alerts/containers/alerts_table_t_grid/translations.ts b/x-pack/plugins/observability/public/pages/alerts/containers/alerts_table_t_grid/translations.ts new file mode 100644 index 00000000000000..c72dc4c9437592 --- /dev/null +++ b/x-pack/plugins/observability/public/pages/alerts/containers/alerts_table_t_grid/translations.ts @@ -0,0 +1,22 @@ +/* + * 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 { i18n } from '@kbn/i18n'; + +export const ADD_TO_EXISTING_CASE = i18n.translate( + 'xpack.observability.detectionEngine.alerts.actions.addToCase', + { + defaultMessage: 'Add to existing case', + } +); + +export const ADD_TO_NEW_CASE = i18n.translate( + 'xpack.observability.detectionEngine.alerts.actions.addToNewCase', + { + defaultMessage: 'Add to new case', + } +); diff --git a/x-pack/plugins/observability/public/pages/overview/old_overview_page.tsx b/x-pack/plugins/observability/public/pages/overview/old_overview_page.tsx index 4d4ef5b8148438..ac5900ca3dc6ae 100644 --- a/x-pack/plugins/observability/public/pages/overview/old_overview_page.tsx +++ b/x-pack/plugins/observability/public/pages/overview/old_overview_page.tsx @@ -8,6 +8,8 @@ import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiHorizontalRule } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React, { useMemo, useRef, useCallback } from 'react'; +import { observabilityFeatureId } from '../../../common'; +import { useKibana } from '../../../../../../src/plugins/kibana_react/public'; import { useTrackPageview } from '../..'; import { EmptySections } from '../../components/app/empty_sections'; import { ObservabilityHeaderMenu } from '../../components/app/header'; @@ -28,6 +30,8 @@ import { DataSections } from './data_sections'; import { LoadingObservability } from './loading_observability'; import { AlertsTableTGrid } from '../alerts/containers/alerts_table_t_grid/alerts_table_t_grid'; import { SectionContainer } from '../../components/app/section'; +import { ObservabilityAppServices } from '../../application/types'; +import { useGetUserCasesPermissions } from '../../hooks/use_get_user_cases_permissions'; interface Props { routeParams: RouteParams<'/overview'>; } @@ -85,6 +89,10 @@ export function OverviewPage({ routeParams }: Props) { return refetch.current && refetch.current(); }, []); + const kibana = useKibana(); + const CasesContext = kibana.services.cases.getCasesContext(); + const userPermissions = useGetUserCasesPermissions(); + if (hasAnyData === undefined) { return ; } @@ -130,12 +138,18 @@ export function OverviewPage({ routeParams }: Props) { })} hasError={false} > - + + + 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 a9fd9a5d9d44f8..5e3fc4e81f9dc3 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 @@ -13,7 +13,7 @@ import type { Filter } from '@kbn/es-query'; import { inputsModel, inputsSelectors, State } from '../../store'; import { inputsActions } from '../../store/actions'; import { ControlColumnProps, RowRenderer, TimelineId } from '../../../../common/types/timeline'; -import { APP_UI_ID } from '../../../../common/constants'; +import { APP_ID, APP_UI_ID } from '../../../../common/constants'; import { timelineSelectors, timelineActions } from '../../../timelines/store/timeline'; import type { SubsetTimelineModel, TimelineModel } from '../../../timelines/store/timeline/model'; import { Status } from '../../../../common/detection_engine/schemas/common/schemas'; @@ -27,7 +27,7 @@ import { TGridCellAction } from '../../../../../timelines/common/types'; import { DetailsPanel } from '../../../timelines/components/side_panel'; import { CellValueElementProps } from '../../../timelines/components/timeline/cell_rendering'; import { FIELDS_WITHOUT_CELL_ACTIONS } from '../../lib/cell_actions/constants'; -import { useKibana } from '../../lib/kibana'; +import { useGetUserCasesPermissions, useKibana } from '../../lib/kibana'; import { GraphOverlay } from '../../../timelines/components/graph_overlay'; import { CreateFieldEditorActions, @@ -109,7 +109,7 @@ const StatefulEventsViewerComponent: React.FC = ({ unit, }) => { const dispatch = useDispatch(); - const { timelines: timelinesUi } = useKibana().services; + const { timelines: timelinesUi, cases: casesUi } = useKibana().services; const { browserFields, dataViewId, @@ -179,63 +179,68 @@ const StatefulEventsViewerComponent: React.FC = ({ const createFieldComponent = useCreateFieldButton(scopeId, id, editorActionsRef); + const casesPermissions = useGetUserCasesPermissions(); + const CasesContext = casesUi.getCasesContext(); + return ( <> - - - {timelinesUi.getTGrid<'embedded'>({ - additionalFilters, - appId: APP_UI_ID, - browserFields, - bulkActions, - columns, - dataProviders, - dataViewId, - defaultCellActions, - deletedEventIds, - disabledCellActions: FIELDS_WITHOUT_CELL_ACTIONS, - docValueFields, - end, - entityType, - filters: globalFilters, - filterStatus: currentFilter, - globalFullScreen, - graphEventId, - graphOverlay, - hasAlertsCrud, - id, - indexNames: selectedPatterns, - indexPattern, - isLive, - isLoadingIndexPattern, - itemsPerPage, - itemsPerPageOptions, - kqlMode, - leadingControlColumns, - onRuleChange, - query, - renderCellValue, - rowRenderers, - runtimeMappings, - setQuery, - sort, - start, - tGridEventRenderedViewEnabled, - trailingControlColumns, - type: 'embedded', - unit, - createFieldComponent, - })} - - - + + + + {timelinesUi.getTGrid<'embedded'>({ + additionalFilters, + appId: APP_UI_ID, + browserFields, + bulkActions, + columns, + dataProviders, + dataViewId, + defaultCellActions, + deletedEventIds, + disabledCellActions: FIELDS_WITHOUT_CELL_ACTIONS, + docValueFields, + end, + entityType, + filters: globalFilters, + filterStatus: currentFilter, + globalFullScreen, + graphEventId, + graphOverlay, + hasAlertsCrud, + id, + indexNames: selectedPatterns, + indexPattern, + isLive, + isLoadingIndexPattern, + itemsPerPage, + itemsPerPageOptions, + kqlMode, + leadingControlColumns, + onRuleChange, + query, + renderCellValue, + rowRenderers, + runtimeMappings, + setQuery, + sort, + start, + tGridEventRenderedViewEnabled, + trailingControlColumns, + type: 'embedded', + unit, + createFieldComponent, + })} + + + + ); }; diff --git a/x-pack/plugins/security_solution/public/common/lib/kibana/__mocks__/index.ts b/x-pack/plugins/security_solution/public/common/lib/kibana/__mocks__/index.ts index aacc1dc9516934..b76b5ee99843e8 100644 --- a/x-pack/plugins/security_solution/public/common/lib/kibana/__mocks__/index.ts +++ b/x-pack/plugins/security_solution/public/common/lib/kibana/__mocks__/index.ts @@ -18,6 +18,7 @@ import { createWithKibanaMock, } from '../kibana_react.mock'; import { APP_UI_ID } from '../../../../../common/constants'; +import { mockCasesContract } from '../../../../../../cases/public/mocks'; const mockStartServicesMock = createStartServicesMock(); export const KibanaServices = { get: jest.fn(), getKibanaVersion: jest.fn(() => '8.0.0') }; @@ -28,6 +29,7 @@ export const useKibana = jest.fn().mockReturnValue({ get: jest.fn(), set: jest.fn(), }, + cases: mockCasesContract(), data: { ...mockStartServicesMock.data, search: { diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.test.tsx index 3c9d2115f7ef28..d5fc54c5cbac73 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.test.tsx @@ -12,6 +12,7 @@ import { TestProviders } from '../../../../common/mock'; import React from 'react'; import { Ecs } from '../../../../../common/ecs'; import { mockTimelines } from '../../../../common/mock/mock_timelines_plugin'; +import { mockCasesContract } from '../../../../../../cases/public/mocks'; const ecsRowData: Ecs = { _id: '1', @@ -51,6 +52,7 @@ jest.mock('../../../../common/lib/kibana', () => ({ application: { capabilities: { siem: { crud_alerts: true, read_alerts: true } }, }, + cases: mockCasesContract(), }, }), useGetUserCasesPermissions: jest.fn().mockReturnValue({ diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx index b5e630de50f79d..d472b9bf3f1915 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx @@ -33,7 +33,6 @@ import { useExceptionFlyout } from './use_add_exception_flyout'; import { useExceptionActions } from './use_add_exception_actions'; import { useEventFilterModal } from './use_event_filter_modal'; import { Status } from '../../../../../common/detection_engine/schemas/common/schemas'; -import { useKibana } from '../../../../common/lib/kibana'; import { ATTACH_ALERT_TO_CASE_FOR_ROW } from '../../../../timelines/components/timeline/body/translations'; import { useEventFilterAction } from './use_event_filter_action'; import { useAddToCaseActions } from './use_add_to_case_actions'; @@ -65,16 +64,15 @@ const AlertContextMenuComponent: React.FC { + const onMenuItemClick = useCallback(() => { setPopover(false); }, []); const ruleId = get(0, ecsRowData?.kibana?.alert?.rule?.uuid); const ruleName = get(0, ecsRowData?.kibana?.alert?.rule?.name); - const { timelines: timelinesUi } = useKibana().services; - const { addToCaseActionProps, addToCaseActionItems } = useAddToCaseActions({ + const { addToCaseActionItems } = useAddToCaseActions({ ecsData: ecsRowData, - afterCaseSelection: afterItemSelection, + onMenuItemClick, timelineId, ariaLabel: ATTACH_ALERT_TO_CASE_FOR_ROW({ ariaRowindex, columnValues }), }); @@ -186,7 +184,6 @@ const AlertContextMenuComponent: React.FC - {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 cc0ef8d4e8b797..2ca4525c7e1ab5 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 590b5759ecae45..bdddd8ab462076 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 0c525a2d77706a..a15a717f6f42a2 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 8ad76c70247bf8..d9dfcd0fee7dae 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 4b6cbb6f7e16dd..4891c75744e385 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 71d6f6253010d7..4a0b7d0fe0501e 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 b6e6aa40876ccf..7a94dcef31cf7b 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 b9e04060881d4f..890175ac8daf9a 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 66a140987475c5..f616b4afc2af5f 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 43622b7e453652..943abc88cf2b05 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 ffe50f935b9fec..954f54fdba7770 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 019bedacbffe88..c16afa945cc08d 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(),