Skip to content

Commit

Permalink
[SIEM] [Cases] Create case from timeline (elastic#60711)
Browse files Browse the repository at this point in the history
  • Loading branch information
stephmilovic authored Mar 20, 2020
1 parent fc24feb commit cf9b64e
Show file tree
Hide file tree
Showing 9 changed files with 162 additions and 7 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

import React from 'react';
import { mount } from 'enzyme';
/* eslint-disable @kbn/eslint/module_migration */
import routeData from 'react-router';
/* eslint-enable @kbn/eslint/module_migration */
import { InsertTimelinePopoverComponent } from './';

const mockDispatch = jest.fn();
jest.mock('react-redux', () => ({
useDispatch: () => mockDispatch,
}));
const mockLocation = {
pathname: '/apath',
hash: '',
search: '',
state: '',
};
const mockLocationWithState = {
...mockLocation,
state: {
insertTimeline: {
timelineId: 'timeline-id',
timelineTitle: 'Timeline title',
},
},
};

const onTimelineChange = jest.fn();
const defaultProps = {
isDisabled: false,
onTimelineChange,
};

describe('Insert timeline popover ', () => {
beforeEach(() => {
jest.resetAllMocks();
});

it('should insert a timeline when passed in the router state', () => {
jest.spyOn(routeData, 'useLocation').mockReturnValue(mockLocationWithState);
mount(<InsertTimelinePopoverComponent {...defaultProps} />);
expect(mockDispatch).toBeCalledWith({
payload: { id: 'timeline-id', show: false },
type: 'x-pack/siem/local/timeline/SHOW_TIMELINE',
});
expect(onTimelineChange).toBeCalledWith('Timeline title', 'timeline-id');
});
it('should do nothing when router state', () => {
jest.spyOn(routeData, 'useLocation').mockReturnValue(mockLocation);
mount(<InsertTimelinePopoverComponent {...defaultProps} />);
expect(mockDispatch).toHaveBeenCalledTimes(0);
expect(onTimelineChange).toHaveBeenCalledTimes(0);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -5,24 +5,52 @@
*/

import { EuiButtonIcon, EuiPopover, EuiSelectableOption } from '@elastic/eui';
import React, { memo, useCallback, useMemo, useState } from 'react';
import React, { memo, useCallback, useEffect, useMemo, useState } from 'react';
import { useLocation } from 'react-router-dom';
import { useDispatch } from 'react-redux';

import { OpenTimelineResult } from '../../open_timeline/types';
import { SelectableTimeline } from '../selectable_timeline';
import * as i18n from '../translations';
import { timelineActions } from '../../../store/timeline';

interface InsertTimelinePopoverProps {
isDisabled: boolean;
hideUntitled?: boolean;
onTimelineChange: (timelineTitle: string, timelineId: string | null) => void;
}

const InsertTimelinePopoverComponent: React.FC<InsertTimelinePopoverProps> = ({
interface RouterState {
insertTimeline: {
timelineId: string;
timelineTitle: string;
};
}

type Props = InsertTimelinePopoverProps;

export const InsertTimelinePopoverComponent: React.FC<Props> = ({
isDisabled,
hideUntitled = false,
onTimelineChange,
}) => {
const dispatch = useDispatch();
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
const { state } = useLocation();
const [routerState, setRouterState] = useState<RouterState | null>(state ?? null);

useEffect(() => {
if (routerState && routerState.insertTimeline) {
dispatch(
timelineActions.showTimeline({ id: routerState.insertTimeline.timelineId, show: false })
);
onTimelineChange(
routerState.insertTimeline.timelineTitle,
routerState.insertTimeline.timelineId
);
setRouterState(null);
}
}, [routerState]);

const handleClosePopover = useCallback(() => {
setIsPopoverOpen(false);
Expand Down Expand Up @@ -65,6 +93,7 @@ const InsertTimelinePopoverComponent: React.FC<InsertTimelinePopoverProps> = ({

return (
<EuiPopover
data-test-subj="insert-timeline-popover"
id="searchTimelinePopover"
button={insertTimelineButton}
isOpen={isPopoverOpen}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,15 @@ import {
import React, { useCallback } from 'react';
import uuid from 'uuid';
import styled from 'styled-components';
import { useHistory } from 'react-router-dom';

import { Note } from '../../../lib/note';
import { Notes } from '../../notes';
import { AssociateNote, UpdateNote } from '../../notes/helpers';
import { NOTES_PANEL_WIDTH } from './notes_size';
import { ButtonContainer, DescriptionContainer, LabelText, NameField, StyledStar } from './styles';
import * as i18n from './translations';
import { SiemPageName } from '../../../pages/home/types';

export const historyToolTip = 'The chronological history of actions related to this timeline';
export const streamLiveToolTip = 'Update the Timeline as new data arrives';
Expand Down Expand Up @@ -111,6 +113,41 @@ export const Name = React.memo<NameProps>(({ timelineId, title, updateTitle }) =
));
Name.displayName = 'Name';

interface NewCaseProps {
onClosePopover: () => void;
timelineId: string;
timelineTitle: string;
}

export const NewCase = React.memo<NewCaseProps>(({ onClosePopover, timelineId, timelineTitle }) => {
const history = useHistory();
const handleClick = useCallback(() => {
onClosePopover();
history.push({
pathname: `/${SiemPageName.case}/create`,
state: {
insertTimeline: {
timelineId,
timelineTitle: timelineTitle.length > 0 ? timelineTitle : i18n.UNTITLED_TIMELINE,
},
},
});
}, [onClosePopover, history, timelineId, timelineTitle]);

return (
<EuiButtonEmpty
data-test-subj="attach-timeline-case"
color="text"
iconSide="left"
iconType="paperClip"
onClick={handleClick}
>
{i18n.ATTACH_TIMELINE_TO_NEW_CASE}
</EuiButtonEmpty>
);
});
NewCase.displayName = 'NewCase';

interface NewTimelineProps {
createTimeline: CreateTimeline;
onClosePopover: () => void;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ export const Properties = React.memo<Props>(
showTimelineModal={showTimelineModal}
showUsersView={title.length > 0}
timelineId={timelineId}
title={title}
updateDescription={updateDescription}
updateNote={updateNote}
usersViewing={usersViewing}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
EuiToolTip,
EuiAvatar,
} from '@elastic/eui';
import { NewTimeline, Description, NotesButton } from './helpers';
import { NewTimeline, Description, NotesButton, NewCase } from './helpers';
import { OpenTimelineModalButton } from '../../open_timeline/open_timeline_modal/open_timeline_modal_button';
import { OpenTimelineModal } from '../../open_timeline/open_timeline_modal';
import { InspectButton, InspectButtonContainer } from '../../inspect';
Expand Down Expand Up @@ -79,6 +79,7 @@ interface Props {
onCloseTimelineModal: () => void;
onOpenTimelineModal: () => void;
showTimelineModal: boolean;
title: string;
updateNote: UpdateNote;
}

Expand All @@ -104,6 +105,7 @@ const PropertiesRightComponent: React.FC<Props> = ({
showTimelineModal,
onCloseTimelineModal,
onOpenTimelineModal,
title,
}) => (
<PropertiesRightStyle alignItems="flexStart" data-test-subj="properties-right" gutterSize="s">
<EuiFlexItem grow={false}>
Expand Down Expand Up @@ -135,6 +137,14 @@ const PropertiesRightComponent: React.FC<Props> = ({
<OpenTimelineModalButton onClick={onOpenTimelineModal} />
</EuiFlexItem>

<EuiFlexItem grow={false}>
<NewCase
onClosePopover={onClosePopover}
timelineId={timelineId}
timelineTitle={title}
/>
</EuiFlexItem>

<EuiFlexItem grow={false}>
<InspectButton
queryId={timelineId}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ export const INSPECT_TIMELINE_TITLE = i18n.translate(
export const UNTITLED_TIMELINE = i18n.translate(
'xpack.siem.timeline.properties.untitledTimelinePlaceholder',
{
defaultMessage: 'Untitled Timeline',
defaultMessage: 'Untitled timeline',
}
);

Expand Down Expand Up @@ -87,7 +87,14 @@ export const STREAM_LIVE_TOOL_TIP = i18n.translate(
export const NEW_TIMELINE = i18n.translate(
'xpack.siem.timeline.properties.newTimelineButtonLabel',
{
defaultMessage: 'Create New Timeline',
defaultMessage: 'Create new timeline',
}
);

export const ATTACH_TIMELINE_TO_NEW_CASE = i18n.translate(
'xpack.siem.timeline.properties.newCaseButtonLabel',
{
defaultMessage: 'Attach timeline to new case',
}
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ describe('Timeline', () => {
.find('[data-test-subj="timeline-title"]')
.first()
.props().placeholder
).toContain('Untitled Timeline');
).toContain('Untitled timeline');
});

test('it renders the timeline table', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
import React from 'react';
import { Router } from 'react-router-dom';
import { mount } from 'enzyme';
/* eslint-disable @kbn/eslint/module_migration */
import routeData from 'react-router';
/* eslint-enable @kbn/eslint/module_migration */
import { CaseComponent } from './';
import { caseProps, caseClosedProps, data, dataClosed } from './__mock__';
import { TestProviders } from '../../../../mock';
Expand Down Expand Up @@ -35,6 +38,13 @@ const mockHistory = {
listen: jest.fn(),
};

const mockLocation = {
pathname: '/welcome',
hash: '',
search: '',
state: '',
};

describe('CaseView ', () => {
const updateCaseProperty = jest.fn();
/* eslint-disable no-console */
Expand All @@ -59,6 +69,7 @@ describe('CaseView ', () => {
beforeEach(() => {
jest.resetAllMocks();
useUpdateCaseMock.mockImplementation(() => defaultUpdateCaseState);
jest.spyOn(routeData, 'useLocation').mockReturnValue(mockLocation);
});

it('should render CaseComponent', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ const renderUsers = (
<EuiFlexItem grow={false}>
<EuiButtonIcon
data-test-subj="user-list-email-button"
onClick={handleSendEmail.bind(null, email)} // TO DO
onClick={handleSendEmail.bind(null, email)}
iconType="email"
aria-label="email"
/>
Expand Down

0 comments on commit cf9b64e

Please sign in to comment.