diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/action_menu/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/action_menu/index.test.tsx
index 7203e74fe02c1e..54eb8cf2769a94 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/flyout/action_menu/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/action_menu/index.test.tsx
@@ -88,7 +88,9 @@ describe('Action menu', () => {
);
- expect(screen.getByTestId('attach-timeline-case-button')).toBeInTheDocument();
+ expect(
+ screen.getByTestId('timeline-modal-attach-to-case-dropdown-button')
+ ).toBeInTheDocument();
});
it('does not render the button when the user does not have create permissions', () => {
@@ -104,7 +106,9 @@ describe('Action menu', () => {
);
- expect(screen.queryByTestId('attach-timeline-case-button')).not.toBeInTheDocument();
+ expect(
+ screen.queryByTestId('timeline-modal-attach-to-case-dropdown-button')
+ ).not.toBeInTheDocument();
});
});
});
diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/action_menu/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/action_menu/index.tsx
index b1be7562fcf97e..9c30205d107020 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/flyout/action_menu/index.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/action_menu/index.tsx
@@ -8,12 +8,12 @@
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import React from 'react';
import styled from 'styled-components';
+import { AttachToCaseButton } from '../../modal/actions/attach_to_case_button';
import { useKibana } from '../../../../common/lib/kibana/kibana_react';
import { APP_ID } from '../../../../../common';
import type { TimelineTabs } from '../../../../../common/types';
import { InspectButton } from '../../../../common/components/inspect';
import { InputsModelId } from '../../../../common/store/inputs/constants';
-import { AddToCaseButton } from '../add_to_case_button';
import { NewTimelineAction } from './new_timeline';
import { SaveTimelineButton } from './save_timeline_button';
import { OpenTimelineButton } from '../../modal/actions/open_timeline_button';
@@ -71,7 +71,7 @@ const TimelineActionMenuComponent = ({
-
+
>
) : null}
diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/add_to_case_button/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/modal/actions/attach_to_case_button.test.tsx
similarity index 52%
rename from x-pack/plugins/security_solution/public/timelines/components/flyout/add_to_case_button/index.test.tsx
rename to x-pack/plugins/security_solution/public/timelines/components/modal/actions/attach_to_case_button.test.tsx
index 063d6da3f2d072..22a9be583329d6 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/flyout/add_to_case_button/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/modal/actions/attach_to_case_button.test.tsx
@@ -6,14 +6,11 @@
*/
import React from 'react';
-import { render, screen } from '@testing-library/react';
-import userEvent from '@testing-library/user-event';
+import { render } from '@testing-library/react';
import { waitForEuiPopoverOpen } from '@elastic/eui/lib/test/rtl';
-
import { useKibana } from '../../../../common/lib/kibana';
-import { useDeepEqualSelector } from '../../../../common/hooks/use_selector';
import { mockTimelineModel, TestProviders } from '../../../../common/mock';
-import { AddToCaseButton } from '.';
+import { AttachToCaseButton } from './attach_to_case_button';
import { SecurityPageName } from '../../../../../common/constants';
jest.mock('../../../../common/components/link_to', () => {
@@ -26,28 +23,54 @@ jest.mock('../../../../common/components/link_to', () => {
}),
};
});
-const mockDispatch = jest.fn();
jest.mock('react-redux', () => {
const original = jest.requireActual('react-redux');
return {
...original,
- useDispatch: () => mockDispatch,
+ useDispatch: () => jest.fn(),
+ useSelector: () => mockTimelineModel,
};
});
-
jest.mock('../../../../common/lib/kibana');
-jest.mock('../../../../common/hooks/use_selector');
const useKibanaMock = useKibana as jest.Mocked;
-describe('AddToCaseButton', () => {
+const renderAttachToCaseButton = () =>
+ render(
+
+
+
+ );
+
+describe('AttachToCaseButton', () => {
const navigateToApp = jest.fn();
beforeEach(() => {
useKibanaMock().services.application.navigateToApp = navigateToApp;
});
- it('navigates to the correct path without id', async () => {
+ it('should render the 2 options in the popover when clicking on the button', () => {
+ const { getByTestId } = renderAttachToCaseButton();
+
+ const button = getByTestId('timeline-modal-attach-to-case-dropdown-button');
+
+ expect(button).toBeInTheDocument();
+ expect(button).toHaveTextContent('Attach to case');
+
+ button.click();
+
+ expect(getByTestId('timeline-modal-attach-timeline-to-new-case')).toBeInTheDocument();
+ expect(getByTestId('timeline-modal-attach-timeline-to-new-case')).toHaveTextContent(
+ 'Attach to new case'
+ );
+
+ expect(getByTestId('timeline-modal-attach-timeline-to-existing-case')).toBeInTheDocument();
+ expect(getByTestId('timeline-modal-attach-timeline-to-existing-case')).toHaveTextContent(
+ 'Attach to existing case'
+ );
+ });
+
+ it('should navigate to the create case page when clicking on attach to new case', async () => {
const here = jest.fn();
useKibanaMock().services.cases.ui.getAllCasesSelectorModal = here.mockImplementation(
({ onRowClick }) => {
@@ -55,16 +78,14 @@ describe('AddToCaseButton', () => {
return <>>;
}
);
- (useDeepEqualSelector as jest.Mock).mockReturnValue(mockTimelineModel);
- render(
-
-
-
- );
- userEvent.click(screen.getByTestId('attach-timeline-case-button'));
+
+ const { getByTestId } = renderAttachToCaseButton();
+
+ getByTestId('timeline-modal-attach-to-case-dropdown-button').click();
+
await waitForEuiPopoverOpen();
- userEvent.click(screen.getByTestId('attach-timeline-existing-case'));
+ getByTestId('timeline-modal-attach-timeline-to-existing-case').click();
expect(navigateToApp).toHaveBeenCalledWith('securitySolutionUI', {
path: '/create',
@@ -72,23 +93,21 @@ describe('AddToCaseButton', () => {
});
});
- it('navigates to the correct path with id', async () => {
+ it('should open modal and navigate to the case page when clicking on attach to existing case', async () => {
useKibanaMock().services.cases.ui.getAllCasesSelectorModal = jest
.fn()
.mockImplementation(({ onRowClick }) => {
onRowClick({ id: 'case-id' });
return <>>;
});
- (useDeepEqualSelector as jest.Mock).mockReturnValue(mockTimelineModel);
- render(
-
-
-
- );
- userEvent.click(screen.getByTestId('attach-timeline-case-button'));
+
+ const { getByTestId } = renderAttachToCaseButton();
+
+ getByTestId('timeline-modal-attach-to-case-dropdown-button').click();
+
await waitForEuiPopoverOpen();
- userEvent.click(screen.getByTestId('attach-timeline-existing-case'));
+ getByTestId('timeline-modal-attach-timeline-to-existing-case').click();
expect(navigateToApp).toHaveBeenCalledWith('securitySolutionUI', {
path: '/case-id',
diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/add_to_case_button/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/modal/actions/attach_to_case_button.tsx
similarity index 62%
rename from x-pack/plugins/security_solution/public/timelines/components/flyout/add_to_case_button/index.tsx
rename to x-pack/plugins/security_solution/public/timelines/components/modal/actions/attach_to_case_button.tsx
index 4eb701c5a8d98b..fb9f6d9edcbc7f 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/flyout/add_to_case_button/index.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/modal/actions/attach_to_case_button.tsx
@@ -5,34 +5,33 @@
* 2.0.
*/
-import { pick } from 'lodash/fp';
import { EuiContextMenuPanel, EuiContextMenuItem, EuiPopover, EuiButtonEmpty } from '@elastic/eui';
import React, { useCallback, useMemo, useState } from 'react';
-import { useDispatch } from 'react-redux';
-
+import { useDispatch, useSelector } from 'react-redux';
import type { CaseUI } from '@kbn/cases-plugin/common';
+import { UNTITLED_TIMELINE } from '../../timeline/properties/translations';
+import { selectTimelineById } from '../../../store/selectors';
+import type { State } from '../../../../common/store';
import { APP_ID, APP_UI_ID } from '../../../../../common/constants';
-import { timelineSelectors } from '../../../store';
import { setInsertTimeline, showTimeline } from '../../../store/actions';
-import { useDeepEqualSelector } from '../../../../common/hooks/use_selector';
import { useKibana } from '../../../../common/lib/kibana';
import { TimelineId } from '../../../../../common/types/timeline';
import { TimelineStatus, TimelineType } from '../../../../../common/api/timeline';
import { getCreateCaseUrl, getCaseDetailsUrl } from '../../../../common/components/link_to';
import { SecurityPageName } from '../../../../app/types';
-import { timelineDefaults } from '../../../store/defaults';
-import * as i18n from '../../timeline/properties/translations';
+import * as i18n from './translations';
-interface Props {
+interface AttachToCaseButtonProps {
+ /**
+ * Id of the timeline to be displayed in the bottom bar and within the modal
+ */
timelineId: string;
}
-const AddToCaseButtonComponent: React.FC = ({ timelineId }) => {
- const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []);
- const {
- cases,
- application: { navigateToApp },
- } = useKibana().services;
+/**
+ * Button that opens a popover with options to attach the timeline to new or existing case
+ */
+export const AttachToCaseButton = React.memo(({ timelineId }) => {
const dispatch = useDispatch();
const {
graphEventId,
@@ -40,18 +39,23 @@ const AddToCaseButtonComponent: React.FC = ({ timelineId }) => {
status: timelineStatus,
title: timelineTitle,
timelineType,
- } = useDeepEqualSelector((state) =>
- pick(
- ['graphEventId', 'savedObjectId', 'status', 'title', 'timelineType'],
- getTimeline(state, timelineId) ?? timelineDefaults
- )
- );
+ } = useSelector((state: State) => selectTimelineById(state, timelineId));
+
+ const {
+ cases,
+ application: { navigateToApp },
+ } = useKibana().services;
+ const userCasesPermissions = cases.helpers.canUseCases([APP_ID]);
+
const [isPopoverOpen, setPopover] = useState(false);
const [isCaseModalOpen, openCaseModal] = useState(false);
+ const togglePopover = useCallback(() => setPopover((currentIsOpen) => !currentIsOpen), []);
+ const closeCaseModal = useCallback(() => openCaseModal(false), [openCaseModal]);
+
const onRowClick = useCallback(
async (theCase?: CaseUI) => {
- openCaseModal(false);
+ closeCaseModal();
await navigateToApp(APP_UI_ID, {
deepLinkId: SecurityPageName.case,
path: theCase != null ? getCaseDetailsUrl({ id: theCase.id }) : getCreateCaseUrl(),
@@ -65,19 +69,19 @@ const AddToCaseButtonComponent: React.FC = ({ timelineId }) => {
})
);
},
- [dispatch, graphEventId, navigateToApp, savedObjectId, timelineId, timelineTitle]
+ [
+ closeCaseModal,
+ dispatch,
+ graphEventId,
+ navigateToApp,
+ savedObjectId,
+ timelineId,
+ timelineTitle,
+ ]
);
- const userCasesPermissions = cases.helpers.canUseCases([APP_ID]);
-
- const handleButtonClick = useCallback(() => {
- setPopover((currentIsOpen) => !currentIsOpen);
- }, []);
-
- const handlePopoverClose = useCallback(() => setPopover(false), []);
-
- const handleNewCaseClick = useCallback(() => {
- handlePopoverClose();
+ const attachToNewCase = useCallback(() => {
+ togglePopover();
navigateToApp(APP_UI_ID, {
deepLinkId: SecurityPageName.case,
@@ -88,7 +92,7 @@ const AddToCaseButtonComponent: React.FC = ({ timelineId }) => {
graphEventId,
timelineId,
timelineSavedObjectId: savedObjectId,
- timelineTitle: timelineTitle.length > 0 ? timelineTitle : i18n.UNTITLED_TIMELINE,
+ timelineTitle: timelineTitle.length > 0 ? timelineTitle : UNTITLED_TIMELINE,
})
);
dispatch(showTimeline({ id: TimelineId.active, show: false }));
@@ -97,84 +101,71 @@ const AddToCaseButtonComponent: React.FC = ({ timelineId }) => {
dispatch,
graphEventId,
navigateToApp,
- handlePopoverClose,
savedObjectId,
timelineId,
timelineTitle,
+ togglePopover,
]);
- const handleExistingCaseClick = useCallback(() => {
- handlePopoverClose();
+ const attachToExistingCase = useCallback(() => {
+ togglePopover();
openCaseModal(true);
- }, [openCaseModal, handlePopoverClose]);
-
- const onCaseModalClose = useCallback(() => {
- openCaseModal(false);
- }, [openCaseModal]);
-
- const closePopover = useCallback(() => {
- setPopover(false);
- }, []);
+ }, [togglePopover, openCaseModal]);
const button = useMemo(
() => (
{i18n.ATTACH_TO_CASE}
),
- [handleButtonClick, timelineStatus, timelineType]
+ [togglePopover, timelineStatus, timelineType]
);
const items = useMemo(
() => [
{i18n.ATTACH_TO_NEW_CASE}
,
{i18n.ATTACH_TO_EXISTING_CASE}
,
],
- [handleExistingCaseClick, handleNewCaseClick]
+ [attachToExistingCase, attachToNewCase]
);
return (
<>
{isCaseModalOpen &&
cases.ui.getAllCasesSelectorModal({
onRowClick,
- onClose: onCaseModalClose,
+ onClose: closeCaseModal,
owner: [APP_ID],
permissions: userCasesPermissions,
})}
>
);
-};
-
-AddToCaseButtonComponent.displayName = 'AddToCaseButtonComponent';
+});
-export const AddToCaseButton = React.memo(AddToCaseButtonComponent);
+AttachToCaseButton.displayName = 'AttachToCaseButton';
diff --git a/x-pack/plugins/security_solution/public/timelines/components/modal/actions/translations.ts b/x-pack/plugins/security_solution/public/timelines/components/modal/actions/translations.ts
index 364e7eeb8c9a90..a30c77bd3632b2 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/modal/actions/translations.ts
+++ b/x-pack/plugins/security_solution/public/timelines/components/modal/actions/translations.ts
@@ -20,3 +20,23 @@ export const OPEN_TIMELINE_BTN_LABEL = i18n.translate(
defaultMessage: 'Open Existing Timeline',
}
);
+
+export const ATTACH_TO_CASE = i18n.translate(
+ 'xpack.securitySolution.timeline.modal.attachToCaseButtonLabel',
+ {
+ defaultMessage: 'Attach to case',
+ }
+);
+
+export const ATTACH_TO_NEW_CASE = i18n.translate(
+ 'xpack.securitySolution.timeline.modal.attachToNewCaseButtonLabel',
+ {
+ defaultMessage: 'Attach to new case',
+ }
+);
+export const ATTACH_TO_EXISTING_CASE = i18n.translate(
+ 'xpack.securitySolution.timeline.modal.attachToExistingCaseButtonLabel',
+ {
+ defaultMessage: 'Attach to existing case',
+ }
+);
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/translations.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/translations.ts
index aa1bf0ad308572..c037ae5d94cfaa 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/translations.ts
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/translations.ts
@@ -52,23 +52,3 @@ export const ADD_TIMELINE = i18n.translate(
defaultMessage: 'Add new timeline or template',
}
);
-
-export const ATTACH_TO_CASE = i18n.translate(
- 'xpack.securitySolution.timeline.properties.attachToCaseButtonLabel',
- {
- defaultMessage: 'Attach to case',
- }
-);
-
-export const ATTACH_TO_NEW_CASE = i18n.translate(
- 'xpack.securitySolution.timeline.properties.attachToNewCaseButtonLabel',
- {
- defaultMessage: 'Attach to new case',
- }
-);
-export const ATTACH_TO_EXISTING_CASE = i18n.translate(
- 'xpack.securitySolution.timeline.properties.attachToExistingCaseButtonLabel',
- {
- defaultMessage: 'Attach to existing case',
- }
-);
diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json
index 13277d812a4524..9633397e242dac 100644
--- a/x-pack/plugins/translations/translations/fr-FR.json
+++ b/x-pack/plugins/translations/translations/fr-FR.json
@@ -36107,9 +36107,6 @@
"xpack.securitySolution.timeline.participantsTitle": "Participants",
"xpack.securitySolution.timeline.promptDeleteNoteLabel": "Supprimer la note sur la chronologie ?",
"xpack.securitySolution.timeline.properties.addTimelineButtonLabel": "Ajouter une nouvelle chronologie ou un nouveau modèle",
- "xpack.securitySolution.timeline.properties.attachToCaseButtonLabel": "Attacher à un cas",
- "xpack.securitySolution.timeline.properties.attachToExistingCaseButtonLabel": "Attacher à un cas existant",
- "xpack.securitySolution.timeline.properties.attachToNewCaseButtonLabel": "Attacher au nouveau cas",
"xpack.securitySolution.timeline.properties.descriptionPlaceholder": "Ajouter une description",
"xpack.securitySolution.timeline.properties.lockDatePickerDescription": "Verrouiller le sélecteur de date global sur le sélecteur de date de chronologie",
"xpack.securitySolution.timeline.properties.lockDatePickerTooltip": "Désactiver la synchronisation de la plage de date/heure entre la page actuellement consultée et votre chronologie",
diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json
index 1742ae147ca255..4698cc060b7d9e 100644
--- a/x-pack/plugins/translations/translations/ja-JP.json
+++ b/x-pack/plugins/translations/translations/ja-JP.json
@@ -36107,9 +36107,6 @@
"xpack.securitySolution.timeline.participantsTitle": "参加者",
"xpack.securitySolution.timeline.promptDeleteNoteLabel": "タイムラインメモを削除しますか?",
"xpack.securitySolution.timeline.properties.addTimelineButtonLabel": "新しいタイムラインまたはテンプレートの追加",
- "xpack.securitySolution.timeline.properties.attachToCaseButtonLabel": "ケースに関連付ける",
- "xpack.securitySolution.timeline.properties.attachToExistingCaseButtonLabel": "既存のケースに添付",
- "xpack.securitySolution.timeline.properties.attachToNewCaseButtonLabel": "新しいケースに添付",
"xpack.securitySolution.timeline.properties.descriptionPlaceholder": "説明を追加",
"xpack.securitySolution.timeline.properties.lockDatePickerDescription": "グローバル日付ピッカーをタイムライン日付ピッカーにロック",
"xpack.securitySolution.timeline.properties.lockDatePickerTooltip": "現在表示中のページとタイムラインの間の日付/時刻範囲の同期を無効にします",
diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json
index f88e19663d0574..9a99a93093d678 100644
--- a/x-pack/plugins/translations/translations/zh-CN.json
+++ b/x-pack/plugins/translations/translations/zh-CN.json
@@ -36089,9 +36089,6 @@
"xpack.securitySolution.timeline.participantsTitle": "参与者",
"xpack.securitySolution.timeline.promptDeleteNoteLabel": "删除时间线备注?",
"xpack.securitySolution.timeline.properties.addTimelineButtonLabel": "添加新时间线或模板",
- "xpack.securitySolution.timeline.properties.attachToCaseButtonLabel": "附加到案例",
- "xpack.securitySolution.timeline.properties.attachToExistingCaseButtonLabel": "附加到现有案例",
- "xpack.securitySolution.timeline.properties.attachToNewCaseButtonLabel": "附加到新案例",
"xpack.securitySolution.timeline.properties.descriptionPlaceholder": "添加描述",
"xpack.securitySolution.timeline.properties.lockDatePickerDescription": "将全局日期选取器锁定到时间线日期选取器",
"xpack.securitySolution.timeline.properties.lockDatePickerTooltip": "禁用当前查看的页面与您的时间线之间的日期/时间范围同步",
diff --git a/x-pack/test/security_solution_cypress/cypress/screens/timeline.ts b/x-pack/test/security_solution_cypress/cypress/screens/timeline.ts
index fb2859842842b9..8e37caf4633258 100644
--- a/x-pack/test/security_solution_cypress/cypress/screens/timeline.ts
+++ b/x-pack/test/security_solution_cypress/cypress/screens/timeline.ts
@@ -13,12 +13,14 @@ export const ADD_NOTE_BUTTON = '[data-test-subj="add-note"]';
export const ADD_FILTER =
'[data-test-subj="timeline-search-or-filter"] [data-test-subj="addFilter"]';
-export const ATTACH_TIMELINE_TO_CASE_BUTTON = '[data-test-subj="attach-timeline-case-button"]';
+export const ATTACH_TIMELINE_TO_CASE_BUTTON =
+ '[data-test-subj="timeline-modal-attach-to-case-dropdown-button"]';
-export const ATTACH_TIMELINE_TO_NEW_CASE_ICON = '[data-test-subj="attach-timeline-new-case"]';
+export const ATTACH_TIMELINE_TO_NEW_CASE_ICON =
+ '[data-test-subj="timeline-modal-attach-timeline-to-new-case"]';
export const ATTACH_TIMELINE_TO_EXISTING_CASE_ICON =
- '[data-test-subj="attach-timeline-existing-case"]';
+ '[data-test-subj="timeline-modal-attach-timeline-to-existing-case"]';
export const SELECT_CASE = (id: string) => {
return `[data-test-subj="cases-table-row-select-${id}"]`;