Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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