Skip to content

Commit

Permalink
[SIEM] Cases] Capture timeline click and open timeline in case view (e…
Browse files Browse the repository at this point in the history
  • Loading branch information
stephmilovic committed May 18, 2020
1 parent 8aa49d0 commit ed187eb
Show file tree
Hide file tree
Showing 11 changed files with 200 additions and 29 deletions.
13 changes: 5 additions & 8 deletions x-pack/plugins/siem/cypress/integration/cases.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ import {
CASE_DETAILS_PUSH_TO_EXTERNAL_SERVICE_BTN,
CASE_DETAILS_STATUS,
CASE_DETAILS_TAGS,
CASE_DETAILS_TIMELINE_MARKDOWN,
CASE_DETAILS_USER_ACTION,
CASE_DETAILS_USERNAMES,
PARTICIPANTS,
Expand Down Expand Up @@ -103,13 +102,11 @@ describe('Cases', () => {
.should('have.text', case1.reporter);
cy.get(CASE_DETAILS_TAGS).should('have.text', expectedTags);
cy.get(CASE_DETAILS_PUSH_TO_EXTERNAL_SERVICE_BTN).should('have.attr', 'disabled');
cy.get(CASE_DETAILS_TIMELINE_MARKDOWN).then($element => {
const timelineLink = $element.prop('href').match(/http(s?):\/\/\w*:\w*(\S*)/)[0];
openCaseTimeline(timelineLink);

cy.get(TIMELINE_TITLE).should('have.attr', 'value', case1.timeline.title);
cy.get(TIMELINE_DESCRIPTION).should('have.attr', 'value', case1.timeline.description);
cy.get(TIMELINE_QUERY).should('have.attr', 'value', case1.timeline.query);
});
openCaseTimeline();

cy.get(TIMELINE_TITLE).should('have.attr', 'value', case1.timeline.title);
cy.get(TIMELINE_DESCRIPTION).should('have.attr', 'value', case1.timeline.description);
cy.get(TIMELINE_QUERY).should('have.attr', 'value', case1.timeline.query);
});
});
2 changes: 1 addition & 1 deletion x-pack/plugins/siem/cypress/screens/case_details.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export const CASE_DETAILS_STATUS = '[data-test-subj="case-view-status"]';

export const CASE_DETAILS_TAGS = '[data-test-subj="case-tags"]';

export const CASE_DETAILS_TIMELINE_MARKDOWN = '[data-test-subj="markdown-link"]';
export const CASE_DETAILS_TIMELINE_LINK_MARKDOWN = '[data-test-subj="markdown-timeline-link"]';

export const CASE_DETAILS_USER_ACTION = '[data-test-subj="user-action-title"] .euiFlexItem';

Expand Down
7 changes: 3 additions & 4 deletions x-pack/plugins/siem/cypress/tasks/case_details.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,9 @@
*/

import { TIMELINE_TITLE } from '../screens/timeline';
import { CASE_DETAILS_TIMELINE_LINK_MARKDOWN } from '../screens/case_details';

export const openCaseTimeline = (link: string) => {
cy.visit('/app/kibana');
cy.visit(link);
cy.contains('a', 'SIEM');
export const openCaseTimeline = () => {
cy.get(CASE_DETAILS_TIMELINE_LINK_MARKDOWN).click();
cy.get(TIMELINE_TITLE).should('exist');
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/*
* 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';
import { Router, mockHistory } from '../__mock__/router';
import { UserActionMarkdown } from './user_action_markdown';
import { TestProviders } from '../../../common/mock';
import * as timelineHelpers from '../../../timelines/components/open_timeline/helpers';
import { useApolloClient } from '../../../common/utils/apollo_context';
const mockUseApolloClient = useApolloClient as jest.Mock;
jest.mock('../../../common/utils/apollo_context');
const onChangeEditable = jest.fn();
const onSaveContent = jest.fn();

const timelineId = '1e10f150-949b-11ea-b63c-2bc51864784c';
const defaultProps = {
content: `A link to a timeline [timeline](http://localhost:5601/app/siem#/timelines?timeline=(id:'${timelineId}',isOpen:!t))`,
id: 'markdown-id',
isEditable: false,
onChangeEditable,
onSaveContent,
};

describe('UserActionMarkdown ', () => {
const queryTimelineByIdSpy = jest.spyOn(timelineHelpers, 'queryTimelineById');
beforeEach(() => {
mockUseApolloClient.mockClear();
jest.resetAllMocks();
});

it('Opens timeline when timeline link clicked - isEditable: false', async () => {
const wrapper = mount(
<TestProviders>
<Router history={mockHistory}>
<UserActionMarkdown {...defaultProps} />
</Router>
</TestProviders>
);
wrapper
.find(`[data-test-subj="markdown-timeline-link"]`)
.first()
.simulate('click');

expect(queryTimelineByIdSpy).toBeCalledWith({
apolloClient: mockUseApolloClient(),
timelineId,
updateIsLoading: expect.any(Function),
updateTimeline: expect.any(Function),
});
});

it('Opens timeline when timeline link clicked - isEditable: true ', async () => {
const wrapper = mount(
<TestProviders>
<Router history={mockHistory}>
<UserActionMarkdown {...{ ...defaultProps, isEditable: true }} />
</Router>
</TestProviders>
);
wrapper
.find(`[data-test-subj="preview-tab"]`)
.first()
.simulate('click');
wrapper
.find(`[data-test-subj="markdown-timeline-link"]`)
.first()
.simulate('click');
expect(queryTimelineByIdSpy).toBeCalledWith({
apolloClient: mockUseApolloClient(),
timelineId,
updateIsLoading: expect.any(Function),
updateTimeline: expect.any(Function),
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,21 @@ import { EuiFlexGroup, EuiFlexItem, EuiButtonEmpty, EuiButton } from '@elastic/e
import React, { useCallback } from 'react';
import styled, { css } from 'styled-components';

import { useDispatch } from 'react-redux';
import * as i18n from '../case_view/translations';
import { Markdown } from '../../../common/components/markdown';
import { Form, useForm, UseField } from '../../../shared_imports';
import { schema, Content } from './schema';
import { InsertTimelinePopover } from '../../../timelines/components/timeline/insert_timeline_popover';
import { useInsertTimeline } from '../../../timelines/components/timeline/insert_timeline_popover/use_insert_timeline';
import { MarkdownEditorForm } from '../../../common/components//markdown_editor/form';
import {
dispatchUpdateTimeline,
queryTimelineById,
} from '../../../timelines/components/open_timeline/helpers';

import { updateIsLoading as dispatchUpdateIsLoading } from '../../../timelines/store/timeline/actions';
import { useApolloClient } from '../../../common/utils/apollo_context';

const ContentWrapper = styled.div`
${({ theme }) => css`
Expand All @@ -36,6 +44,8 @@ export const UserActionMarkdown = ({
onChangeEditable,
onSaveContent,
}: UserActionMarkdownProps) => {
const dispatch = useDispatch();
const apolloClient = useApolloClient();
const { form } = useForm<Content>({
defaultValue: { content },
options: { stripEmptyFields: false },
Expand All @@ -49,6 +59,24 @@ export const UserActionMarkdown = ({
onChangeEditable(id);
}, [id, onChangeEditable]);

const handleTimelineClick = useCallback(
(timelineId: string) => {
queryTimelineById({
apolloClient,
timelineId,
updateIsLoading: ({
id: currentTimelineId,
isLoading,
}: {
id: string;
isLoading: boolean;
}) => dispatch(dispatchUpdateIsLoading({ id: currentTimelineId, isLoading })),
updateTimeline: dispatchUpdateTimeline(dispatch),
});
},
[apolloClient]
);

const handleSaveAction = useCallback(async () => {
const { isValid, data } = await form.submit();
if (isValid) {
Expand Down Expand Up @@ -98,6 +126,7 @@ export const UserActionMarkdown = ({
cancelAction: handleCancelAction,
saveAction: handleSaveAction,
}),
onClickTimeline: handleTimelineClick,
onCursorPositionUpdate: handleCursorChange,
topRightContent: (
<InsertTimelinePopover
Expand All @@ -111,7 +140,11 @@ export const UserActionMarkdown = ({
</Form>
) : (
<ContentWrapper>
<Markdown raw={content} data-test-subj="user-action-markdown" />
<Markdown
onClickTimeline={handleTimelineClick}
raw={content}
data-test-subj="user-action-markdown"
/>
</ContentWrapper>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -164,5 +164,37 @@ describe('Markdown', () => {

expect(wrapper).toMatchSnapshot();
});

describe('markdown timeline links', () => {
const timelineId = '1e10f150-949b-11ea-b63c-2bc51864784c';
const markdownWithTimelineLink = `A link to a timeline [timeline](http://localhost:5601/app/siem#/timelines?timeline=(id:'${timelineId}',isOpen:!t))`;
const onClickTimeline = jest.fn();
beforeEach(() => {
jest.resetAllMocks();
});
test('it renders a timeline link without href when provided the onClickTimeline argument', () => {
const wrapper = mount(
<Markdown raw={markdownWithTimelineLink} onClickTimeline={onClickTimeline} />
);

expect(
wrapper
.find('[data-test-subj="markdown-timeline-link"]')
.first()
.getDOMNode()
).not.toHaveProperty('href');
});
test('timeline link onClick calls onClickTimeline with timelineId', () => {
const wrapper = mount(
<Markdown raw={markdownWithTimelineLink} onClickTimeline={onClickTimeline} />
);
wrapper
.find('[data-test-subj="markdown-timeline-link"]')
.first()
.simulate('click');

expect(onClickTimeline).toHaveBeenCalledWith(timelineId);
});
});
});
});
43 changes: 30 additions & 13 deletions x-pack/plugins/siem/public/common/components/markdown/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { EuiLink, EuiTableRow, EuiTableRowCell, EuiText, EuiToolTip } from '@ela
import React from 'react';
import ReactMarkdown from 'react-markdown';
import styled, { css } from 'styled-components';
import * as i18n from './translations';

const TableHeader = styled.thead`
font-weight: bold;
Expand Down Expand Up @@ -37,8 +38,9 @@ const REL_NOREFERRER = 'noreferrer';
export const Markdown = React.memo<{
disableLinks?: boolean;
raw?: string;
onClickTimeline?: (timelineId: string) => void;
size?: 'xs' | 's' | 'm';
}>(({ disableLinks = false, raw, size = 's' }) => {
}>(({ disableLinks = false, onClickTimeline, raw, size = 's' }) => {
const markdownRenderers = {
root: ({ children }: { children: React.ReactNode[] }) => (
<EuiText data-test-subj="markdown-root" grow={true} size={size}>
Expand All @@ -59,18 +61,33 @@ export const Markdown = React.memo<{
tableCell: ({ children }: { children: React.ReactNode[] }) => (
<EuiTableRowCell data-test-subj="markdown-table-cell">{children}</EuiTableRowCell>
),
link: ({ children, href }: { children: React.ReactNode[]; href?: string }) => (
<EuiToolTip content={href}>
<EuiLink
href={disableLinks ? undefined : href}
data-test-subj="markdown-link"
rel={`${REL_NOOPENER} ${REL_NOFOLLOW} ${REL_NOREFERRER}`}
target="_blank"
>
{children}
</EuiLink>
</EuiToolTip>
),
link: ({ children, href }: { children: React.ReactNode[]; href?: string }) => {
if (onClickTimeline != null && href != null && href.indexOf(`timelines?timeline=(id:`) > -1) {
const timelineId = href.split('timelines?timeline=(id:')[1].split("'")[1] ?? '';
return (
<EuiToolTip content={i18n.TIMELINE_ID(timelineId)}>
<EuiLink
onClick={() => onClickTimeline(timelineId)}
data-test-subj="markdown-timeline-link"
>
{children}
</EuiLink>
</EuiToolTip>
);
}
return (
<EuiToolTip content={href}>
<EuiLink
href={disableLinks ? undefined : href}
data-test-subj="markdown-link"
rel={`${REL_NOOPENER} ${REL_NOFOLLOW} ${REL_NOREFERRER}`}
target="_blank"
>
{children}
</EuiLink>
</EuiToolTip>
);
},
blockquote: ({ children }: { children: React.ReactNode[] }) => (
<MyBlockquote>{children}</MyBlockquote>
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,11 @@ export const MARKDOWN_HINT_STRIKETHROUGH = i18n.translate(
export const MARKDOWN_HINT_IMAGE_URL = i18n.translate('xpack.siem.markdown.hint.imageUrlLabel', {
defaultMessage: '![image](url)',
});

export const TIMELINE_ID = (timelineId: string) =>
i18n.translate('xpack.siem.markdown.toolTip.timelineId', {
defaultMessage: 'Timeline id: { timelineId }',
values: {
timelineId,
},
});
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ interface IMarkdownEditorForm {
field: FieldHook;
idAria: string;
isDisabled: boolean;
onClickTimeline?: (timelineId: string) => void;
onCursorPositionUpdate?: (cursorPosition: CursorPosition) => void;
placeholder?: string;
topRightContent?: React.ReactNode;
Expand All @@ -26,6 +27,7 @@ export const MarkdownEditorForm = ({
field,
idAria,
isDisabled = false,
onClickTimeline,
onCursorPositionUpdate,
placeholder,
topRightContent,
Expand Down Expand Up @@ -55,6 +57,7 @@ export const MarkdownEditorForm = ({
content={field.value as string}
isDisabled={isDisabled}
onChange={handleContentChange}
onClickTimeline={onClickTimeline}
onCursorPositionUpdate={onCursorPositionUpdate}
placeholder={placeholder}
topRightContent={topRightContent}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ export const MarkdownEditor = React.memo<{
content: string;
isDisabled?: boolean;
onChange: (description: string) => void;
onClickTimeline?: (timelineId: string) => void;
onCursorPositionUpdate?: (cursorPosition: CursorPosition) => void;
placeholder?: string;
}>(
Expand All @@ -83,6 +84,7 @@ export const MarkdownEditor = React.memo<{
content,
isDisabled = false,
onChange,
onClickTimeline,
placeholder,
onCursorPositionUpdate,
}) => {
Expand Down Expand Up @@ -125,9 +127,10 @@ export const MarkdownEditor = React.memo<{
{
id: 'preview',
name: i18n.PREVIEW,
'data-test-subj': 'preview-tab',
content: (
<MarkdownContainer data-test-subj="markdown-container" paddingSize="s">
<Markdown raw={content} />
<Markdown raw={content} onClickTimeline={onClickTimeline} />
</MarkdownContainer>
),
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ export const formatTimelineResultToModel = (

export interface QueryTimelineById<TCache> {
apolloClient: ApolloClient<TCache> | ApolloClient<{}> | undefined;
duplicate: boolean;
duplicate?: boolean;
timelineId: string;
onOpenTimeline?: (timeline: TimelineModel) => void;
openTimeline?: boolean;
Expand Down

0 comments on commit ed187eb

Please sign in to comment.