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 9c30205d10702..00b0ad5a95b0f 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
@@ -15,7 +15,7 @@ import type { TimelineTabs } from '../../../../../common/types';
import { InspectButton } from '../../../../common/components/inspect';
import { InputsModelId } from '../../../../common/store/inputs/constants';
import { NewTimelineAction } from './new_timeline';
-import { SaveTimelineButton } from './save_timeline_button';
+import { SaveTimelineButton } from '../../modal/actions/save_timeline_button';
import { OpenTimelineButton } from '../../modal/actions/open_timeline_button';
import { TIMELINE_TOUR_CONFIG_ANCHORS } from '../../timeline/tour/step_config';
diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/action_menu/save_timeline_button.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/action_menu/save_timeline_button.test.tsx
deleted file mode 100644
index 92bc0be3f54e1..0000000000000
--- a/x-pack/plugins/security_solution/public/timelines/components/flyout/action_menu/save_timeline_button.test.tsx
+++ /dev/null
@@ -1,146 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import React from 'react';
-import { render, fireEvent, waitFor, screen } from '@testing-library/react';
-import type { SaveTimelineButtonProps } from './save_timeline_button';
-import { SaveTimelineButton } from './save_timeline_button';
-import { TestProviders } from '../../../../common/mock';
-import { useUserPrivileges } from '../../../../common/components/user_privileges';
-import { getTimelineStatusByIdSelector } from '../header/selectors';
-import { TimelineStatus } from '../../../../../common/api/timeline';
-
-const TEST_ID = {
- SAVE_TIMELINE_MODAL: 'save-timeline-modal',
-};
-
-jest.mock('react-redux', () => {
- const actual = jest.requireActual('react-redux');
- return {
- ...actual,
- useDispatch: jest.fn().mockImplementation(() => () => {}),
- };
-});
-
-jest.mock('../../../../common/lib/kibana');
-jest.mock('../../../../common/components/user_privileges');
-jest.mock('../header/selectors', () => {
- return {
- getTimelineStatusByIdSelector: jest.fn().mockReturnValue(() => ({
- status: 'draft',
- isSaving: false,
- })),
- };
-});
-
-const props: SaveTimelineButtonProps = {
- timelineId: 'timeline-1',
-};
-
-const TestSaveTimelineButton = (_props: SaveTimelineButtonProps) => (
-
-
-
-);
-
-jest.mock('raf', () => {
- return jest.fn().mockImplementation((cb) => cb());
-});
-
-describe('SaveTimelineButton', () => {
- it('should disable the save timeline button when the user does not have write acceess', () => {
- (useUserPrivileges as jest.Mock).mockReturnValue({
- kibanaSecuritySolutionsPrivileges: { crud: false },
- });
- render(
-
-
-
- );
- expect(screen.getByRole('button')).toBeDisabled();
- });
-
- it('should disable the save timeline button when the timeline is immutable', () => {
- (useUserPrivileges as jest.Mock).mockReturnValue({
- kibanaSecuritySolutionsPrivileges: { crud: true },
- });
- (getTimelineStatusByIdSelector as jest.Mock).mockReturnValue(() => ({
- status: TimelineStatus.immutable,
- }));
- render(
-
-
-
- );
- expect(screen.getByRole('button')).toBeDisabled();
- });
-
- describe('with draft timeline', () => {
- beforeAll(() => {
- (getTimelineStatusByIdSelector as jest.Mock).mockReturnValue(() => ({
- status: TimelineStatus.draft,
- }));
- });
-
- it('should not show the save modal if user does not have write access', async () => {
- (useUserPrivileges as jest.Mock).mockReturnValue({
- kibanaSecuritySolutionsPrivileges: { crud: false },
- });
- render();
-
- expect(screen.queryByTestId(TEST_ID.SAVE_TIMELINE_MODAL)).not.toBeInTheDocument();
-
- const saveTimelineBtn = screen.getByRole('button');
-
- fireEvent.click(saveTimelineBtn);
-
- await waitFor(() => {
- expect(screen.queryAllByTestId(TEST_ID.SAVE_TIMELINE_MODAL)).toHaveLength(0);
- });
- });
-
- it('should show the save modal when user has crud privileges', async () => {
- (useUserPrivileges as jest.Mock).mockReturnValue({
- kibanaSecuritySolutionsPrivileges: { crud: true },
- });
- render();
- expect(screen.queryByTestId(TEST_ID.SAVE_TIMELINE_MODAL)).not.toBeInTheDocument();
-
- const saveTimelineBtn = screen.getByRole('button');
-
- fireEvent.click(saveTimelineBtn);
-
- await waitFor(() => {
- expect(screen.getByTestId(TEST_ID.SAVE_TIMELINE_MODAL)).toBeVisible();
- });
- });
- });
-
- describe('with active timeline', () => {
- beforeAll(() => {
- (getTimelineStatusByIdSelector as jest.Mock).mockReturnValue(() => ({
- status: TimelineStatus.active,
- isSaving: false,
- }));
- });
-
- it('should open the timeline save modal', async () => {
- (useUserPrivileges as jest.Mock).mockReturnValue({
- kibanaSecuritySolutionsPrivileges: { crud: true },
- });
- render();
-
- const saveTimelineBtn = screen.getByRole('button');
-
- fireEvent.click(saveTimelineBtn);
-
- await waitFor(() => {
- expect(screen.getByTestId(TEST_ID.SAVE_TIMELINE_MODAL)).toBeInTheDocument();
- });
- });
- });
-});
diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/action_menu/save_timeline_button.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/action_menu/save_timeline_button.tsx
deleted file mode 100644
index 24e806b43cd5b..0000000000000
--- a/x-pack/plugins/security_solution/public/timelines/components/flyout/action_menu/save_timeline_button.tsx
+++ /dev/null
@@ -1,87 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import React, { useCallback, useMemo, useState } from 'react';
-import { EuiButton, EuiToolTip } from '@elastic/eui';
-import { useDeepEqualSelector } from '../../../../common/hooks/use_selector';
-import { TimelineStatus } from '../../../../../common/api/timeline';
-import { useUserPrivileges } from '../../../../common/components/user_privileges';
-
-import { SaveTimelineModal } from './save_timeline_modal';
-import * as timelineTranslations from './translations';
-import { getTimelineStatusByIdSelector } from '../header/selectors';
-import { TIMELINE_TOUR_CONFIG_ANCHORS } from '../../timeline/tour/step_config';
-
-export interface SaveTimelineButtonProps {
- timelineId: string;
-}
-
-export const SaveTimelineButton = React.memo(({ timelineId }) => {
- const [showEditTimelineOverlay, setShowEditTimelineOverlay] = useState(false);
-
- const closeSaveTimeline = useCallback(() => {
- setShowEditTimelineOverlay(false);
- }, []);
-
- const openEditTimeline = useCallback(() => {
- setShowEditTimelineOverlay(true);
- }, []);
-
- // Case: 1
- // check if user has crud privileges so that user can be allowed to edit the timeline
- // Case: 2
- // TODO: User may have Crud privileges but they may not have access to timeline index.
- // Do we need to check that?
- const {
- kibanaSecuritySolutionsPrivileges: { crud: canEditTimelinePrivilege },
- } = useUserPrivileges();
-
- const getTimelineStatus = useMemo(() => getTimelineStatusByIdSelector(), []);
-
- const { status: timelineStatus, isSaving } = useDeepEqualSelector((state) =>
- getTimelineStatus(state, timelineId)
- );
-
- const canEditTimeline = canEditTimelinePrivilege && timelineStatus !== TimelineStatus.immutable;
-
- const isUnsaved = timelineStatus === TimelineStatus.draft;
- const tooltipContent = canEditTimeline ? null : timelineTranslations.CALL_OUT_UNAUTHORIZED_MSG;
-
- return (
-
- <>
-
- {timelineTranslations.SAVE}
-
- {showEditTimelineOverlay && canEditTimeline ? (
-
- ) : null}
- >
-
- );
-});
-
-SaveTimelineButton.displayName = 'SaveTimelineButton';
diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/action_menu/save_timeline_modal.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/action_menu/save_timeline_modal.test.tsx
deleted file mode 100644
index 43cdb85da8c30..0000000000000
--- a/x-pack/plugins/security_solution/public/timelines/components/flyout/action_menu/save_timeline_modal.test.tsx
+++ /dev/null
@@ -1,274 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import React from 'react';
-import { mount } from 'enzyme';
-
-import { TestProviders } from '../../../../common/mock';
-import { useDeepEqualSelector } from '../../../../common/hooks/use_selector';
-import { TimelineStatus, TimelineType } from '../../../../../common/api/timeline';
-import { SaveTimelineModal } from './save_timeline_modal';
-import * as i18n from './translations';
-
-jest.mock('../../../../common/hooks/use_selector', () => ({
- useDeepEqualSelector: jest.fn(),
-}));
-
-jest.mock('../../timeline/properties/use_create_timeline', () => ({
- useCreateTimeline: jest.fn(),
-}));
-
-jest.mock('react-redux', () => {
- const actual = jest.requireActual('react-redux');
- return {
- ...actual,
- useDispatch: jest.fn(),
- };
-});
-
-describe('EditTimelineModal', () => {
- describe('save timeline', () => {
- const props = {
- initialFocusOn: 'title' as const,
- closeSaveTimeline: jest.fn(),
- timelineId: 'timeline-1',
- };
-
- const mockGetButton = jest.fn().mockReturnValue();
-
- beforeEach(() => {
- (useDeepEqualSelector as jest.Mock).mockReturnValue({
- description: '',
- isSaving: true,
- status: TimelineStatus.draft,
- title: 'my timeline',
- timelineType: TimelineType.default,
- });
- });
-
- afterEach(() => {
- (useDeepEqualSelector as jest.Mock).mockReset();
- mockGetButton.mockClear();
- });
-
- test('show process bar while saving', () => {
- const component = mount(, {
- wrappingComponent: TestProviders,
- });
- expect(component.find('[data-test-subj="progress-bar"]').exists()).toEqual(true);
- });
-
- test('Show correct header for edit timeline modal', () => {
- const component = mount(, {
- wrappingComponent: TestProviders,
- });
- expect(component.find('[data-test-subj="modal-header"]').at(1).prop('children')).toEqual(
- i18n.SAVE_TIMELINE
- );
- });
-
- test('Show correct header for edit timeline template modal', () => {
- (useDeepEqualSelector as jest.Mock).mockReturnValue({
- description: '',
- isSaving: true,
- status: TimelineStatus.draft,
- title: 'my timeline',
- timelineType: TimelineType.template,
- });
- const component = mount(, {
- wrappingComponent: TestProviders,
- });
- expect(component.find('[data-test-subj="modal-header"]').at(1).prop('children')).toEqual(
- i18n.SAVE_TIMELINE_TEMPLATE
- );
- });
-
- test('Show name field', () => {
- const component = mount(, {
- wrappingComponent: TestProviders,
- });
- expect(component.find('[data-test-subj="save-timeline-title"]').exists()).toEqual(true);
- });
-
- test('Show description field', () => {
- const component = mount(, {
- wrappingComponent: TestProviders,
- });
- expect(component.find('[data-test-subj="save-timeline-description"]').exists()).toEqual(true);
- });
-
- test('Show close button', () => {
- const component = mount(, {
- wrappingComponent: TestProviders,
- });
- expect(component.find('[data-test-subj="close-button"]').exists()).toEqual(true);
- });
-
- test('Show saveButton', () => {
- const component = mount(, {
- wrappingComponent: TestProviders,
- });
- expect(component.find('[data-test-subj="save-button"]').exists()).toEqual(true);
- });
-
- test('Does not show save as new switch', () => {
- const component = mount(, {
- wrappingComponent: TestProviders,
- });
- expect(component.find('[data-test-subj="save-as-new-switch"]').exists()).toEqual(false);
- });
- });
-
- describe('update timeline', () => {
- const props = {
- initialFocusOn: 'title' as const,
- closeSaveTimeline: jest.fn(),
- timelineId: 'timeline-1',
- };
-
- const mockGetButton = jest.fn().mockReturnValue();
-
- beforeEach(() => {
- (useDeepEqualSelector as jest.Mock).mockReturnValue({
- description: 'xxxx',
- isSaving: true,
- status: TimelineStatus.active,
- title: 'my timeline',
- timelineType: TimelineType.default,
- });
- });
-
- afterEach(() => {
- (useDeepEqualSelector as jest.Mock).mockReset();
- mockGetButton.mockClear();
- });
-
- test('show process bar while saving', () => {
- const component = mount(, {
- wrappingComponent: TestProviders,
- });
- expect(component.find('[data-test-subj="progress-bar"]').exists()).toEqual(true);
- });
-
- test('Show correct header for save timeline modal', () => {
- const component = mount(, {
- wrappingComponent: TestProviders,
- });
- expect(component.find('[data-test-subj="modal-header"]').at(1).prop('children')).toEqual(
- i18n.SAVE_TIMELINE
- );
- });
-
- test('Show correct header for edit timeline template modal', () => {
- (useDeepEqualSelector as jest.Mock).mockReturnValue({
- description: 'xxxx',
- isSaving: true,
- status: TimelineStatus.active,
- title: 'my timeline',
- timelineType: TimelineType.template,
- });
- const component = mount(, {
- wrappingComponent: TestProviders,
- });
- expect(component.find('[data-test-subj="modal-header"]').at(1).prop('children')).toEqual(
- i18n.NAME_TIMELINE_TEMPLATE
- );
- });
-
- test('Show name field', () => {
- const component = mount(, {
- wrappingComponent: TestProviders,
- });
- expect(component.find('[data-test-subj="save-timeline-title"]').exists()).toEqual(true);
- });
-
- test('Show description field', () => {
- const component = mount(, {
- wrappingComponent: TestProviders,
- });
- expect(component.find('[data-test-subj="save-timeline-description"]').exists()).toEqual(true);
- });
-
- test('Show saveButton', () => {
- const component = mount(, {
- wrappingComponent: TestProviders,
- });
- expect(component.find('[data-test-subj="save-button"]').exists()).toEqual(true);
- });
-
- test('Show save as new switch', () => {
- const component = mount(, {
- wrappingComponent: TestProviders,
- });
- expect(component.find('[data-test-subj="save-as-new-switch"]').exists()).toEqual(true);
- });
- });
-
- describe('showWarning', () => {
- const props = {
- initialFocusOn: 'title' as const,
- closeSaveTimeline: jest.fn(),
- timelineId: 'timeline-1',
- showWarning: true,
- };
-
- const mockGetButton = jest.fn().mockReturnValue();
-
- beforeEach(() => {
- (useDeepEqualSelector as jest.Mock).mockReturnValue({
- description: '',
- isSaving: true,
- status: TimelineStatus.draft,
- title: 'my timeline',
- timelineType: TimelineType.default,
- showWarnging: true,
- });
- });
-
- afterEach(() => {
- (useDeepEqualSelector as jest.Mock).mockReset();
- mockGetButton.mockClear();
- });
-
- test('Show EuiCallOut', () => {
- const component = mount(, {
- wrappingComponent: TestProviders,
- });
- expect(component.find('[data-test-subj="edit-timeline-callout"]').exists()).toEqual(true);
- });
-
- test('Show discardTimelineButton', () => {
- const component = mount(, {
- wrappingComponent: TestProviders,
- });
- expect(component.find('[data-test-subj="close-button"]').at(2).text()).toEqual(
- 'Discard Timeline'
- );
- });
-
- test('get discardTimelineTemplateButton with correct props', () => {
- (useDeepEqualSelector as jest.Mock).mockReturnValue({
- description: 'xxxx',
- isSaving: true,
- status: TimelineStatus.draft,
- title: 'my timeline',
- timelineType: TimelineType.template,
- });
- const component = mount(, {
- wrappingComponent: TestProviders,
- });
- expect(component.find('[data-test-subj="close-button"]').at(2).text()).toEqual(
- 'Discard Timeline Template'
- );
- });
-
- test('Show saveButton', () => {
- const component = mount();
- expect(component.find('[data-test-subj="save-button"]').at(1).exists()).toEqual(true);
- });
- });
-});
diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/action_menu/translations.ts b/x-pack/plugins/security_solution/public/timelines/components/flyout/action_menu/translations.ts
index c7384e0368992..9bc1abd662bb7 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/flyout/action_menu/translations.ts
+++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/action_menu/translations.ts
@@ -6,8 +6,6 @@
*/
import { i18n } from '@kbn/i18n';
-import type { TimelineTypeLiteral } from '../../../../../common/api/timeline';
-import { TimelineType } from '../../../../../common/api/timeline';
export const NEW_TIMELINE_BTN = i18n.translate(
'xpack.securitySolution.flyout.timeline.actionMenu.newTimelineBtn',
@@ -29,97 +27,3 @@ export const NEW_TEMPLATE_TIMELINE = i18n.translate(
defaultMessage: 'New Timeline template',
}
);
-
-export const CALL_OUT_UNAUTHORIZED_MSG = i18n.translate(
- 'xpack.securitySolution.timeline.callOut.unauthorized.message.description',
- {
- defaultMessage:
- 'You can use Timeline to investigate events, but you do not have the required permissions to save timelines for future use. If you need to save timelines, contact your Kibana administrator.',
- }
-);
-
-export const SAVE_TIMELINE = i18n.translate(
- 'xpack.securitySolution.timeline.saveTimeline.modal.header',
- {
- defaultMessage: 'Save Timeline',
- }
-);
-
-export const SAVE_TIMELINE_TEMPLATE = i18n.translate(
- 'xpack.securitySolution.timeline.saveTimelineTemplate.modal.header',
- {
- defaultMessage: 'Save Timeline Template',
- }
-);
-
-export const SAVE = i18n.translate('xpack.securitySolution.timeline.nameTimeline.save.title', {
- defaultMessage: 'Save',
-});
-
-export const NAME_TIMELINE_TEMPLATE = i18n.translate(
- 'xpack.securitySolution.timeline.nameTimelineTemplate.modal.header',
- {
- defaultMessage: 'Name Timeline Template',
- }
-);
-
-export const DISCARD_TIMELINE = i18n.translate(
- 'xpack.securitySolution.timeline.saveTimeline.modal.discard.title',
- {
- defaultMessage: 'Discard Timeline',
- }
-);
-
-export const DISCARD_TIMELINE_TEMPLATE = i18n.translate(
- 'xpack.securitySolution.timeline.saveTimelineTemplate.modal.discard.title',
- {
- defaultMessage: 'Discard Timeline Template',
- }
-);
-
-export const CLOSE_MODAL = i18n.translate(
- 'xpack.securitySolution.timeline.saveTimeline.modal.close.title',
- {
- defaultMessage: 'Close',
- }
-);
-
-export const UNSAVED_TIMELINE_WARNING = (timelineType: TimelineTypeLiteral) =>
- i18n.translate('xpack.securitySolution.timeline.saveTimeline.modal.warning.title', {
- values: {
- timeline: timelineType === TimelineType.template ? 'timeline template' : 'timeline',
- },
- defaultMessage: 'You have an unsaved {timeline}. Do you wish to save it?',
- });
-
-export const TIMELINE_TITLE = i18n.translate(
- 'xpack.securitySolution.timeline.saveTimeline.modal.titleAriaLabel',
- {
- defaultMessage: 'Title',
- }
-);
-
-export const TIMELINE_DESCRIPTION = i18n.translate(
- 'xpack.securitySolution.timeline.saveTimeline.modal.descriptionLabel',
- {
- defaultMessage: 'Description',
- }
-);
-
-export const OPTIONAL = i18n.translate(
- 'xpack.securitySolution.timeline.saveTimeline.modal.optionalLabel',
- {
- defaultMessage: 'Optional',
- }
-);
-
-export const TITLE = i18n.translate('xpack.securitySolution.timeline.saveTimeline.modal.title', {
- defaultMessage: 'Title',
-});
-
-export const SAVE_AS_NEW = i18n.translate(
- 'xpack.securitySolution.timeline.saveTimeline.modal.saveAsNew',
- {
- defaultMessage: 'Save as new timeline',
- }
-);
diff --git a/x-pack/plugins/security_solution/public/timelines/components/modal/actions/save_timeline_button.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/modal/actions/save_timeline_button.test.tsx
new file mode 100644
index 0000000000000..b417d81726324
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/timelines/components/modal/actions/save_timeline_button.test.tsx
@@ -0,0 +1,107 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React from 'react';
+import { render, waitFor } from '@testing-library/react';
+import { SaveTimelineButton } from './save_timeline_button';
+import { mockTimelineModel, TestProviders } from '../../../../common/mock';
+import { useUserPrivileges } from '../../../../common/components/user_privileges';
+import { TimelineStatus } from '../../../../../common/api/timeline';
+import { useCreateTimeline } from '../../timeline/properties/use_create_timeline';
+
+jest.mock('../../../../common/components/user_privileges');
+jest.mock('../../timeline/properties/use_create_timeline');
+
+const mockGetState = jest.fn();
+jest.mock('react-redux', () => {
+ const original = jest.requireActual('react-redux');
+ return {
+ ...original,
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ useSelector: (selector: any) =>
+ selector({
+ timeline: {
+ timelineById: {
+ 'timeline-1': {
+ ...mockGetState(),
+ },
+ },
+ },
+ }),
+ };
+});
+
+const renderSaveTimelineButton = () =>
+ render(
+
+
+
+ );
+
+describe('SaveTimelineButton', () => {
+ it('should render components', async () => {
+ (useUserPrivileges as jest.Mock).mockReturnValue({
+ kibanaSecuritySolutionsPrivileges: { crud: true },
+ });
+ mockGetState.mockReturnValue({
+ ...mockTimelineModel,
+ status: TimelineStatus.active,
+ isSaving: false,
+ });
+ (useCreateTimeline as jest.Mock).mockReturnValue({});
+
+ const { getByTestId, getByText, queryByTestId } = renderSaveTimelineButton();
+
+ expect(getByTestId('timeline-modal-save-timeline')).toBeInTheDocument();
+ expect(getByText('Save')).toBeInTheDocument();
+
+ expect(queryByTestId('save-timeline-modal')).not.toBeInTheDocument();
+ });
+
+ it('should open the timeline save modal', async () => {
+ (useUserPrivileges as jest.Mock).mockReturnValue({
+ kibanaSecuritySolutionsPrivileges: { crud: true },
+ });
+ mockGetState.mockReturnValue({
+ ...mockTimelineModel,
+ status: TimelineStatus.active,
+ isSaving: false,
+ });
+ (useCreateTimeline as jest.Mock).mockReturnValue({});
+
+ const { getByTestId } = renderSaveTimelineButton();
+
+ getByTestId('timeline-modal-save-timeline').click();
+
+ await waitFor(() => {
+ expect(getByTestId('save-timeline-modal')).toBeInTheDocument();
+ expect(getByTestId('save-timeline-modal')).toBeVisible();
+ });
+ });
+
+ it('should disable the save timeline button when the user does not have write access', () => {
+ (useUserPrivileges as jest.Mock).mockReturnValue({
+ kibanaSecuritySolutionsPrivileges: { crud: false },
+ });
+ mockGetState.mockReturnValue(mockTimelineModel);
+
+ const { getByTestId } = renderSaveTimelineButton();
+
+ expect(getByTestId('timeline-modal-save-timeline')).toBeDisabled();
+ });
+
+ it('should disable the save timeline button when the timeline is immutable', () => {
+ (useUserPrivileges as jest.Mock).mockReturnValue({
+ kibanaSecuritySolutionsPrivileges: { crud: true },
+ });
+ mockGetState.mockReturnValue({ ...mockTimelineModel, status: TimelineStatus.immutable });
+
+ const { getByTestId } = renderSaveTimelineButton();
+
+ expect(getByTestId('timeline-modal-save-timeline')).toBeDisabled();
+ });
+});
diff --git a/x-pack/plugins/security_solution/public/timelines/components/modal/actions/save_timeline_button.tsx b/x-pack/plugins/security_solution/public/timelines/components/modal/actions/save_timeline_button.tsx
new file mode 100644
index 0000000000000..9d332d66fb2f5
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/timelines/components/modal/actions/save_timeline_button.tsx
@@ -0,0 +1,80 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React, { useCallback, useState } from 'react';
+import { EuiButton, EuiToolTip } from '@elastic/eui';
+import { useSelector } from 'react-redux';
+import { TimelineStatus } from '../../../../../common/api/timeline';
+import { useUserPrivileges } from '../../../../common/components/user_privileges';
+import { SaveTimelineModal } from './save_timeline_modal';
+import * as i18n from './translations';
+import { selectTimelineById } from '../../../store/selectors';
+import type { State } from '../../../../common/store';
+import { TIMELINE_TOUR_CONFIG_ANCHORS } from '../../timeline/tour/step_config';
+
+export interface SaveTimelineButtonProps {
+ /**
+ * Id of the timeline to be displayed in the bottom bar and within the modal
+ */
+ timelineId: string;
+}
+
+/**
+ * Button that allows user to save the timeline. Clicking it opens the `SaveTimelineModal`
+ */
+export const SaveTimelineButton = React.memo(({ timelineId }) => {
+ const [showEditTimelineOverlay, setShowEditTimelineOverlay] = useState(false);
+ const toggleSaveTimeline = useCallback(() => setShowEditTimelineOverlay((prev) => !prev), []);
+
+ // Case: 1
+ // check if user has crud privileges so that user can be allowed to edit the timeline
+ // Case: 2
+ // TODO: User may have Crud privileges but they may not have access to timeline index.
+ // Do we need to check that?
+ const {
+ kibanaSecuritySolutionsPrivileges: { crud: canEditTimelinePrivilege },
+ } = useUserPrivileges();
+
+ const { status, isSaving } = useSelector((state: State) => selectTimelineById(state, timelineId));
+
+ const canSaveTimeline = canEditTimelinePrivilege && status !== TimelineStatus.immutable;
+ const isUnsaved = status === TimelineStatus.draft;
+ const unauthorizedMessage = canSaveTimeline ? null : i18n.CALL_OUT_UNAUTHORIZED_MSG;
+
+ return (
+ <>
+
+
+ {i18n.SAVE}
+
+
+ {showEditTimelineOverlay && canSaveTimeline ? (
+
+ ) : null}
+ >
+ );
+});
+
+SaveTimelineButton.displayName = 'SaveTimelineButton';
diff --git a/x-pack/plugins/security_solution/public/timelines/components/modal/actions/save_timeline_modal.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/modal/actions/save_timeline_modal.test.tsx
new file mode 100644
index 0000000000000..36cef9de70473
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/timelines/components/modal/actions/save_timeline_modal.test.tsx
@@ -0,0 +1,208 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React from 'react';
+import { render } from '@testing-library/react';
+import { mockTimelineModel, TestProviders } from '../../../../common/mock';
+import { TimelineStatus, TimelineType } from '../../../../../common/api/timeline';
+import { SaveTimelineModal } from './save_timeline_modal';
+import * as i18n from './translations';
+
+jest.mock('../../timeline/properties/use_create_timeline', () => ({
+ useCreateTimeline: jest.fn(),
+}));
+
+const mockGetState = jest.fn();
+jest.mock('react-redux', () => {
+ const actual = jest.requireActual('react-redux');
+ return {
+ ...actual,
+ useDispatch: jest.fn(),
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ useSelector: (selector: any) =>
+ selector({
+ timeline: {
+ timelineById: {
+ 'timeline-1': {
+ ...mockGetState(),
+ },
+ },
+ },
+ }),
+ };
+});
+
+const renderSaveTimelineModal = (showWarning?: boolean) =>
+ render(
+
+
+
+ );
+
+describe('SaveTimelineModal', () => {
+ describe('save timeline', () => {
+ it('should show process bar while saving', () => {
+ mockGetState.mockReturnValue({
+ ...mockTimelineModel,
+ isSaving: true,
+ });
+
+ const { getByTestId } = renderSaveTimelineModal();
+
+ expect(getByTestId('progress-bar')).toBeInTheDocument();
+ });
+
+ it('should show correct header for save timeline modal', () => {
+ mockGetState.mockReturnValue(mockTimelineModel);
+
+ const { getByTestId } = renderSaveTimelineModal();
+
+ expect(getByTestId('save-timeline-modal-header')).toBeInTheDocument();
+ expect(getByTestId('save-timeline-modal-header')).toHaveTextContent(i18n.SAVE_TIMELINE);
+ });
+
+ it('should show correct header for save timeline template modal', () => {
+ mockGetState.mockReturnValue({
+ ...mockTimelineModel,
+ status: TimelineStatus.draft,
+ timelineType: TimelineType.template,
+ });
+
+ const { getByTestId } = renderSaveTimelineModal();
+
+ expect(getByTestId('save-timeline-modal-header')).toBeInTheDocument();
+ expect(getByTestId('save-timeline-modal-header')).toHaveTextContent(
+ i18n.SAVE_TIMELINE_TEMPLATE
+ );
+ });
+
+ it('should render all the dom elements of the modal', () => {
+ mockGetState.mockReturnValue({
+ ...mockTimelineModel,
+ status: TimelineStatus.draft,
+ });
+
+ const { getByTestId, queryByTestId } = renderSaveTimelineModal();
+
+ expect(getByTestId('save-timeline-modal-title-input')).toBeInTheDocument();
+ expect(getByTestId('save-timeline-modal-description-input')).toBeInTheDocument();
+ expect(getByTestId('save-timeline-modal-description-input')).toHaveTextContent(
+ 'This is a sample rule description'
+ );
+ expect(getByTestId('save-timeline-modal-close-button')).toBeInTheDocument();
+ expect(getByTestId('save-timeline-modal-save-button')).toBeInTheDocument();
+ expect(queryByTestId('save-timeline-modal-save-as-new-switch')).not.toBeInTheDocument();
+ });
+ });
+
+ describe('edit timeline', () => {
+ it('should show process bar while saving', () => {
+ mockGetState.mockReturnValue({
+ ...mockTimelineModel,
+ isSaving: true,
+ title: 'my timeline',
+ });
+
+ const { getByTestId } = renderSaveTimelineModal();
+
+ expect(getByTestId('progress-bar')).toBeInTheDocument();
+ });
+
+ it('should show correct header for edit timeline template modal', () => {
+ mockGetState.mockReturnValue({
+ ...mockTimelineModel,
+ status: TimelineStatus.active,
+ });
+
+ const { getByTestId } = renderSaveTimelineModal();
+
+ expect(getByTestId('save-timeline-modal-header')).toBeInTheDocument();
+ expect(getByTestId('save-timeline-modal-header')).toHaveTextContent(i18n.SAVE_TIMELINE);
+ });
+
+ it('should show correct header for save timeline template modal', () => {
+ mockGetState.mockReturnValue({
+ status: TimelineStatus.active,
+ timelineType: TimelineType.template,
+ });
+
+ const { getByTestId } = renderSaveTimelineModal();
+
+ expect(getByTestId('save-timeline-modal-header')).toBeInTheDocument();
+ expect(getByTestId('save-timeline-modal-header')).toHaveTextContent(
+ i18n.NAME_TIMELINE_TEMPLATE
+ );
+ });
+
+ it('should render all the dom elements of the modal', () => {
+ mockGetState.mockReturnValue({
+ ...mockTimelineModel,
+ description: 'my description',
+ status: TimelineStatus.active,
+ title: 'my timeline',
+ timelineType: TimelineType.default,
+ });
+
+ const { getByTestId } = renderSaveTimelineModal();
+
+ expect(getByTestId('save-timeline-modal-title-input')).toBeInTheDocument();
+ expect(getByTestId('save-timeline-modal-title-input')).toHaveProperty('value', 'my timeline');
+ expect(getByTestId('save-timeline-modal-description-input')).toBeInTheDocument();
+ expect(getByTestId('save-timeline-modal-description-input')).toHaveTextContent(
+ 'my description'
+ );
+ expect(getByTestId('save-timeline-modal-close-button')).toBeInTheDocument();
+ expect(getByTestId('save-timeline-modal-save-button')).toBeInTheDocument();
+ expect(getByTestId('save-timeline-modal-save-as-new-switch')).toBeInTheDocument();
+ });
+ });
+
+ describe('showWarning', () => {
+ it('should show EuiCallOut', () => {
+ const { getByTestId } = renderSaveTimelineModal(true);
+
+ expect(getByTestId('save-timeline-modal-callout')).toBeInTheDocument();
+ expect(getByTestId('save-timeline-modal-callout')).toHaveTextContent(
+ 'You have an unsaved timeline. Do you wish to save it?'
+ );
+ });
+
+ it('should show discard timeline in the close button', () => {
+ mockGetState.mockReturnValue({
+ ...mockTimelineModel,
+ status: TimelineStatus.draft,
+ });
+
+ const { getByTestId } = renderSaveTimelineModal(true);
+
+ expect(getByTestId('save-timeline-modal-save-button')).toBeInTheDocument();
+ expect(getByTestId('save-timeline-modal-close-button')).toBeInTheDocument();
+ expect(getByTestId('save-timeline-modal-close-button')).toHaveTextContent('Discard Timeline');
+ });
+
+ it('should show discard timeline template in the close button', () => {
+ mockGetState.mockReturnValue({
+ ...mockTimelineModel,
+ timelineType: TimelineType.template,
+ status: TimelineStatus.draft,
+ });
+
+ const { getByTestId } = renderSaveTimelineModal(true);
+
+ expect(getByTestId('save-timeline-modal-save-button')).toBeInTheDocument();
+ expect(getByTestId('save-timeline-modal-close-button')).toBeInTheDocument();
+ expect(getByTestId('save-timeline-modal-close-button')).toHaveTextContent(
+ 'Discard Timeline Template'
+ );
+ });
+ });
+});
diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/action_menu/save_timeline_modal.tsx b/x-pack/plugins/security_solution/public/timelines/components/modal/actions/save_timeline_modal.tsx
similarity index 86%
rename from x-pack/plugins/security_solution/public/timelines/components/flyout/action_menu/save_timeline_modal.tsx
rename to x-pack/plugins/security_solution/public/timelines/components/modal/actions/save_timeline_modal.tsx
index 8028eb92eac7b..56dae333fc7e5 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/flyout/action_menu/save_timeline_modal.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/modal/actions/save_timeline_modal.tsx
@@ -5,7 +5,6 @@
* 2.0.
*/
-import { pick } from 'lodash/fp';
import {
EuiButton,
EuiFlexGroup,
@@ -20,14 +19,15 @@ import {
} from '@elastic/eui';
import type { EuiSwitchEvent } from '@elastic/eui';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
-import { useDispatch } from 'react-redux';
+import { useDispatch, useSelector } from 'react-redux';
import usePrevious from 'react-use/lib/usePrevious';
+import type { State } from '../../../../common/store';
+import { selectTimelineById } from '../../../store/selectors';
import { getUseField, Field, Form, useForm } from '../../../../shared_imports';
import { TimelineId } from '../../../../../common/types/timeline';
import { TimelineStatus, TimelineType } from '../../../../../common/api/timeline';
-import { useDeepEqualSelector } from '../../../../common/hooks/use_selector';
-import { timelineActions, timelineSelectors } from '../../../store';
+import { timelineActions } from '../../../store';
import * as commonI18n from '../../timeline/properties/translations';
import * as i18n from './translations';
import { useStartTransaction } from '../../../../common/lib/apm/use_start_transaction';
@@ -37,9 +37,21 @@ import { NOTES_PANEL_WIDTH } from '../../timeline/properties/notes_size';
import { formSchema } from './schema';
const CommonUseField = getUseField({ component: Field });
+
+const descriptionLabel = `${i18n.TIMELINE_DESCRIPTION} (${i18n.OPTIONAL})`;
+
interface SaveTimelineModalProps {
+ /**
+ * Callback called when the modal closes
+ */
closeSaveTimeline: () => void;
+ /**
+ * Sets the initial focus to either the title of the modal or the save button
+ */
initialFocusOn?: 'title' | 'save';
+ /**
+ * Id of the timeline to be displayed in the bottom bar and within the modal
+ */
timelineId: string;
/**
* When showWarning is true, the modal is used as a reminder
@@ -48,36 +60,32 @@ interface SaveTimelineModalProps {
showWarning?: boolean;
}
+/**
+ * This component renders the modal to save a timeline with title, description, save as new timeline switch and save / cancel buttons
+ */
export const SaveTimelineModal = React.memo(
({ closeSaveTimeline, initialFocusOn, timelineId, showWarning }) => {
const { startTransaction } = useStartTransaction();
- const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []);
- const {
- isSaving,
- description = '',
- status,
- title = '',
- timelineType,
- } = useDeepEqualSelector((state) =>
- pick(
- ['isSaving', 'description', 'status', 'title', 'timelineType'],
- getTimeline(state, timelineId)
- )
+
+ const dispatch = useDispatch();
+ const { isSaving, description, status, title, timelineType } = useSelector((state: State) =>
+ selectTimelineById(state, timelineId)
);
+
+ const [saveAsNewTimeline, setSaveAsNewTimeline] = useState(false);
+ const onSaveAsNewChanged = useCallback(
+ (e: EuiSwitchEvent) => setSaveAsNewTimeline(e.target.checked),
+ []
+ );
+
const isUnsaved = status === TimelineStatus.draft;
const prevIsSaving = usePrevious(isSaving);
- const dispatch = useDispatch();
+
// Resetting the timeline by replacing the active one with a new empty one
const resetTimeline = useCreateTimeline({
timelineId: TimelineId.active,
timelineType: TimelineType.default,
});
- const [saveAsNewTimeline, setSaveAsNewTimeline] = useState(false);
-
- const onSaveAsNewChanged = useCallback(
- (e: EuiSwitchEvent) => setSaveAsNewTimeline(e.target.checked),
- []
- );
const handleSubmit = useCallback(
(titleAndDescription, isValid) => {
@@ -166,13 +174,11 @@ export const SaveTimelineModal = React.memo(
[timelineType]
);
- const descriptionLabel = useMemo(() => `${i18n.TIMELINE_DESCRIPTION} (${i18n.OPTIONAL})`, []);
-
const titleFieldProps = useMemo(
() => ({
'aria-label': i18n.TIMELINE_TITLE,
autoFocus: initialFocusOn === 'title',
- 'data-test-subj': 'save-timeline-title',
+ 'data-test-subj': 'save-timeline-modal-title-input',
disabled: isSaving,
spellCheck: true,
placeholder:
@@ -186,7 +192,7 @@ export const SaveTimelineModal = React.memo(
const descriptionFieldProps = useMemo(
() => ({
'aria-label': i18n.TIMELINE_DESCRIPTION,
- 'data-test-subj': 'save-timeline-description',
+ 'data-test-subj': 'save-timeline-modal-description-input',
disabled: isSaving,
placeholder: commonI18n.DESCRIPTION,
}),
@@ -208,7 +214,7 @@ export const SaveTimelineModal = React.memo(
{isSaving && (
)}
- {modalHeader}
+ {modalHeader}
{showWarning && (
@@ -217,7 +223,7 @@ export const SaveTimelineModal = React.memo(
title={calloutMessage}
color="danger"
iconType="warning"
- data-test-subj="edit-timeline-callout"
+ data-test-subj="save-timeline-modal-callout"
/>
@@ -248,7 +254,7 @@ export const SaveTimelineModal = React.memo(
label={i18n.SAVE_AS_NEW}
checked={saveAsNewTimeline}
onChange={onSaveAsNewChanged}
- data-test-subj="save-as-new-switch"
+ data-test-subj="save-timeline-modal-save-as-new-switch"
/>
) : null}
@@ -258,7 +264,7 @@ export const SaveTimelineModal = React.memo(
fill={false}
onClick={handleCancel}
isDisabled={isSaving}
- data-test-subj="close-button"
+ data-test-subj="save-timeline-modal-close-button"
>
{closeModalText}
@@ -270,7 +276,7 @@ export const SaveTimelineModal = React.memo(
isDisabled={isSaving || isSubmitting}
fill={true}
onClick={onSubmit}
- data-test-subj="save-button"
+ data-test-subj="save-timeline-modal-save-button"
>
{saveButtonTitle}
diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/action_menu/schema.ts b/x-pack/plugins/security_solution/public/timelines/components/modal/actions/schema.ts
similarity index 100%
rename from x-pack/plugins/security_solution/public/timelines/components/flyout/action_menu/schema.ts
rename to x-pack/plugins/security_solution/public/timelines/components/modal/actions/schema.ts
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 a30c77bd3632b..f0f853ee03201 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
@@ -6,6 +6,8 @@
*/
import { i18n } from '@kbn/i18n';
+import type { TimelineTypeLiteral } from '../../../../../common/api/timeline';
+import { TimelineType } from '../../../../../common/api/timeline';
export const OPEN_TIMELINE_BTN = i18n.translate(
'xpack.securitySolution.timeline.modal.openTimelineBtn',
@@ -40,3 +42,97 @@ export const ATTACH_TO_EXISTING_CASE = i18n.translate(
defaultMessage: 'Attach to existing case',
}
);
+
+export const CALL_OUT_UNAUTHORIZED_MSG = i18n.translate(
+ 'xpack.securitySolution.timeline.callOut.unauthorized.message.description',
+ {
+ defaultMessage:
+ 'You can use Timeline to investigate events, but you do not have the required permissions to save timelines for future use. If you need to save timelines, contact your Kibana administrator.',
+ }
+);
+
+export const SAVE_TIMELINE = i18n.translate(
+ 'xpack.securitySolution.timeline.saveTimeline.modal.header',
+ {
+ defaultMessage: 'Save Timeline',
+ }
+);
+
+export const SAVE_TIMELINE_TEMPLATE = i18n.translate(
+ 'xpack.securitySolution.timeline.saveTimelineTemplate.modal.header',
+ {
+ defaultMessage: 'Save Timeline Template',
+ }
+);
+
+export const SAVE = i18n.translate('xpack.securitySolution.timeline.nameTimeline.save.title', {
+ defaultMessage: 'Save',
+});
+
+export const NAME_TIMELINE_TEMPLATE = i18n.translate(
+ 'xpack.securitySolution.timeline.nameTimelineTemplate.modal.header',
+ {
+ defaultMessage: 'Name Timeline Template',
+ }
+);
+
+export const DISCARD_TIMELINE = i18n.translate(
+ 'xpack.securitySolution.timeline.saveTimeline.modal.discard.title',
+ {
+ defaultMessage: 'Discard Timeline',
+ }
+);
+
+export const DISCARD_TIMELINE_TEMPLATE = i18n.translate(
+ 'xpack.securitySolution.timeline.saveTimelineTemplate.modal.discard.title',
+ {
+ defaultMessage: 'Discard Timeline Template',
+ }
+);
+
+export const CLOSE_MODAL = i18n.translate(
+ 'xpack.securitySolution.timeline.saveTimeline.modal.close.title',
+ {
+ defaultMessage: 'Close',
+ }
+);
+
+export const UNSAVED_TIMELINE_WARNING = (timelineType: TimelineTypeLiteral) =>
+ i18n.translate('xpack.securitySolution.timeline.saveTimeline.modal.warning.title', {
+ values: {
+ timeline: timelineType === TimelineType.template ? 'timeline template' : 'timeline',
+ },
+ defaultMessage: 'You have an unsaved {timeline}. Do you wish to save it?',
+ });
+
+export const TIMELINE_TITLE = i18n.translate(
+ 'xpack.securitySolution.timeline.saveTimeline.modal.titleAriaLabel',
+ {
+ defaultMessage: 'Title',
+ }
+);
+
+export const TIMELINE_DESCRIPTION = i18n.translate(
+ 'xpack.securitySolution.timeline.saveTimeline.modal.descriptionLabel',
+ {
+ defaultMessage: 'Description',
+ }
+);
+
+export const OPTIONAL = i18n.translate(
+ 'xpack.securitySolution.timeline.saveTimeline.modal.optionalLabel',
+ {
+ defaultMessage: 'Optional',
+ }
+);
+
+export const TITLE = i18n.translate('xpack.securitySolution.timeline.saveTimeline.modal.title', {
+ defaultMessage: 'Title',
+});
+
+export const SAVE_AS_NEW = i18n.translate(
+ 'xpack.securitySolution.timeline.saveTimeline.modal.saveAsNew',
+ {
+ defaultMessage: 'Save as new timeline',
+ }
+);
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 8e37caf463325..e5ae1c8ee9c8d 100644
--- a/x-pack/test/security_solution_cypress/cypress/screens/timeline.ts
+++ b/x-pack/test/security_solution_cypress/cypress/screens/timeline.ts
@@ -136,7 +136,8 @@ export const TIMELINE_DATA_PROVIDER_VALUE = `[data-test-subj="value"]`;
export const SAVE_DATA_PROVIDER_BTN = `[data-test-subj="save"]`;
-export const TIMELINE_DESCRIPTION_INPUT = '[data-test-subj="save-timeline-description"]';
+export const TIMELINE_DESCRIPTION_INPUT =
+ '[data-test-subj="save-timeline-modal-description-input"]';
export const TIMELINE_DROPPED_DATA_PROVIDERS = '[data-test-subj="providerContainer"]';
@@ -205,7 +206,7 @@ export const TIMELINE_KQLLANGUAGE_BUTTON = '[data-test-subj="kqlLanguageMenuItem
export const TIMELINE_TITLE = '[data-test-subj="timeline-title"]';
-export const TIMELINE_TITLE_INPUT = '[data-test-subj="save-timeline-title"]';
+export const TIMELINE_TITLE_INPUT = '[data-test-subj="save-timeline-modal-title-input"]';
export const TIMESTAMP_HEADER_FIELD = '[data-test-subj="header-text-@timestamp"]';
@@ -216,9 +217,10 @@ export const TOGGLE_TIMELINE_EXPAND_EVENT = '[data-test-subj="expand-event"]';
export const TIMELINE_SAVE_MODAL = '[data-test-subj="save-timeline-modal"]';
-export const TIMELINE_EDIT_MODAL_SAVE_BUTTON = '[data-test-subj="save-button"]';
+export const TIMELINE_SAVE_MODAL_SAVE_BUTTON = '[data-test-subj="save-timeline-modal-save-button"]';
-export const TIMELINE_EDIT_MODAL_SAVE_AS_NEW_SWITCH = '[data-test-subj="save-as-new-switch"]';
+export const TIMELINE_SAVE_MODAL_SAVE_AS_NEW_SWITCH =
+ '[data-test-subj="save-timeline-modal-save-as-new-switch"]';
export const TIMELINE_EXIT_FULL_SCREEN_BUTTON = '[data-test-subj="exit-full-screen"]';
@@ -312,8 +314,10 @@ export const TIMELINE_FILTER_BADGE = `[data-test-subj^='timeline-filters-contain
export const NEW_TIMELINE_ACTION = getDataTestSubjectSelector('new-timeline-action');
export const SAVE_TIMELINE_ACTION = getDataTestSubjectSelector('save-timeline-action');
-export const SAVE_TIMELINE_ACTION_BTN = getDataTestSubjectSelector('save-timeline-action-btn');
+export const SAVE_TIMELINE_ACTION_BTN = getDataTestSubjectSelector('timeline-modal-save-timeline');
-export const SAVE_TIMELINE_TOOLTIP = getDataTestSubjectSelector('save-timeline-btn-tooltip');
+export const SAVE_TIMELINE_TOOLTIP = getDataTestSubjectSelector(
+ 'timeline-modal-save-timeline-tooltip'
+);
export const TOGGLE_DATA_PROVIDER_BTN = getDataTestSubjectSelector('toggle-data-provider');
diff --git a/x-pack/test/security_solution_cypress/cypress/tasks/timeline.ts b/x-pack/test/security_solution_cypress/cypress/tasks/timeline.ts
index f24fc5d735b85..8fecbdedcbc06 100644
--- a/x-pack/test/security_solution_cypress/cypress/tasks/timeline.ts
+++ b/x-pack/test/security_solution_cypress/cypress/tasks/timeline.ts
@@ -47,8 +47,8 @@ import {
TOGGLE_TIMELINE_EXPAND_EVENT,
CREATE_NEW_TIMELINE_TEMPLATE,
TIMELINE_SAVE_MODAL,
- TIMELINE_EDIT_MODAL_SAVE_BUTTON,
- TIMELINE_EDIT_MODAL_SAVE_AS_NEW_SWITCH,
+ TIMELINE_SAVE_MODAL_SAVE_BUTTON,
+ TIMELINE_SAVE_MODAL_SAVE_AS_NEW_SWITCH,
TIMELINE_PROGRESS_BAR,
QUERY_TAB_BUTTON,
TIMELINE_ADD_FIELD_BUTTON,
@@ -105,7 +105,7 @@ export const addDescriptionToTimeline = (
}
cy.get(TIMELINE_DESCRIPTION_INPUT).should('not.be.disabled').type(description);
cy.get(TIMELINE_DESCRIPTION_INPUT).invoke('val').should('equal', description);
- cy.get(TIMELINE_EDIT_MODAL_SAVE_BUTTON).click();
+ cy.get(TIMELINE_SAVE_MODAL_SAVE_BUTTON).click();
cy.get(TIMELINE_TITLE_INPUT).should('not.exist');
};
@@ -114,7 +114,7 @@ export const addNameToTimelineAndSave = (name: string) => {
cy.get(TIMELINE_TITLE_INPUT).should('not.be.disabled').clear();
cy.get(TIMELINE_TITLE_INPUT).type(`${name}{enter}`);
cy.get(TIMELINE_TITLE_INPUT).should('have.attr', 'value', name);
- cy.get(TIMELINE_EDIT_MODAL_SAVE_BUTTON).click();
+ cy.get(TIMELINE_SAVE_MODAL_SAVE_BUTTON).click();
cy.get(TIMELINE_TITLE_INPUT).should('not.exist');
};
@@ -123,9 +123,9 @@ export const addNameToTimelineAndSaveAsNew = (name: string) => {
cy.get(TIMELINE_TITLE_INPUT).should('not.be.disabled').clear();
cy.get(TIMELINE_TITLE_INPUT).type(`${name}{enter}`);
cy.get(TIMELINE_TITLE_INPUT).should('have.attr', 'value', name);
- cy.get(TIMELINE_EDIT_MODAL_SAVE_AS_NEW_SWITCH).should('exist');
- cy.get(TIMELINE_EDIT_MODAL_SAVE_AS_NEW_SWITCH).click();
- cy.get(TIMELINE_EDIT_MODAL_SAVE_BUTTON).click();
+ cy.get(TIMELINE_SAVE_MODAL_SAVE_AS_NEW_SWITCH).should('exist');
+ cy.get(TIMELINE_SAVE_MODAL_SAVE_AS_NEW_SWITCH).click();
+ cy.get(TIMELINE_SAVE_MODAL_SAVE_BUTTON).click();
cy.get(TIMELINE_TITLE_INPUT).should('not.exist');
};
@@ -140,7 +140,7 @@ export const addNameAndDescriptionToTimeline = (
cy.get(TIMELINE_TITLE_INPUT).should('have.attr', 'value', timeline.title);
cy.get(TIMELINE_DESCRIPTION_INPUT).type(timeline.description);
cy.get(TIMELINE_DESCRIPTION_INPUT).invoke('val').should('equal', timeline.description);
- cy.get(TIMELINE_EDIT_MODAL_SAVE_BUTTON).click();
+ cy.get(TIMELINE_SAVE_MODAL_SAVE_BUTTON).click();
cy.get(TIMELINE_TITLE_INPUT).should('not.exist');
};
@@ -377,8 +377,8 @@ export const saveTimeline = () => {
cy.get(TIMELINE_PROGRESS_BAR).should('not.exist');
cy.get(TIMELINE_TITLE_INPUT).should('not.be.disabled');
- cy.get(TIMELINE_EDIT_MODAL_SAVE_BUTTON).should('not.be.disabled');
- cy.get(TIMELINE_EDIT_MODAL_SAVE_BUTTON).click();
+ cy.get(TIMELINE_SAVE_MODAL_SAVE_BUTTON).should('not.be.disabled');
+ cy.get(TIMELINE_SAVE_MODAL_SAVE_BUTTON).click();
cy.get(TIMELINE_PROGRESS_BAR).should('exist');
cy.get(TIMELINE_PROGRESS_BAR).should('not.exist');