From 6af20438ab9733a1c21569da6a460ad2f2aec4d6 Mon Sep 17 00:00:00 2001 From: stephmilovic Date: Tue, 12 May 2020 10:55:57 -0600 Subject: [PATCH 1/6] init commit --- .../user_action_tree/user_action_markdown.tsx | 6 ++- .../common/components/markdown/index.tsx | 46 +++++++++++++------ .../components/markdown/translations.ts | 8 ++++ 3 files changed, 45 insertions(+), 15 deletions(-) diff --git a/x-pack/plugins/siem/public/cases/components/user_action_tree/user_action_markdown.tsx b/x-pack/plugins/siem/public/cases/components/user_action_tree/user_action_markdown.tsx index 23d8d8f1a7e68..1e8a66ab4f9cf 100644 --- a/x-pack/plugins/siem/public/cases/components/user_action_tree/user_action_markdown.tsx +++ b/x-pack/plugins/siem/public/cases/components/user_action_tree/user_action_markdown.tsx @@ -111,7 +111,11 @@ export const UserActionMarkdown = ({ ) : ( - + console.log('zomg', timelineId)} + raw={content} + data-test-subj="user-action-markdown" + /> ); }; diff --git a/x-pack/plugins/siem/public/common/components/markdown/index.tsx b/x-pack/plugins/siem/public/common/components/markdown/index.tsx index 8e051685af56d..ba513fcba6005 100644 --- a/x-pack/plugins/siem/public/common/components/markdown/index.tsx +++ b/x-pack/plugins/siem/public/common/components/markdown/index.tsx @@ -6,10 +6,17 @@ /* eslint-disable react/display-name */ -import { EuiLink, EuiTableRow, EuiTableRowCell, EuiText, EuiToolTip } from '@elastic/eui'; +import { + EuiLink, + EuiTableRow, + EuiTableRowCell, + EuiText, + EuiToolTip, +} from '@elastic/eui'; 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; @@ -37,8 +44,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[] }) => ( @@ -59,18 +67,28 @@ export const Markdown = React.memo<{ tableCell: ({ children }: { children: React.ReactNode[] }) => ( {children} ), - link: ({ children, href }: { children: React.ReactNode[]; href?: string }) => ( - - - {children} - - - ), + 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 ( + + onClickTimeline(timelineId)}>{children} + + ); + } + return ( + + + {children} + + + ); + }, blockquote: ({ children }: { children: React.ReactNode[] }) => ( {children} ), diff --git a/x-pack/plugins/siem/public/common/components/markdown/translations.ts b/x-pack/plugins/siem/public/common/components/markdown/translations.ts index cfd9e9ef1b106..4524d27739ea8 100644 --- a/x-pack/plugins/siem/public/common/components/markdown/translations.ts +++ b/x-pack/plugins/siem/public/common/components/markdown/translations.ts @@ -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, + }, + }); From 57d8a80c16eb74fb629c029ee5f4adce4d844513 Mon Sep 17 00:00:00 2001 From: stephmilovic Date: Tue, 12 May 2020 16:33:38 -0600 Subject: [PATCH 2/6] working --- .../user_action_tree/user_action_markdown.tsx | 30 ++++++++++++++++++- .../components/open_timeline/helpers.ts | 2 +- 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/siem/public/cases/components/user_action_tree/user_action_markdown.tsx b/x-pack/plugins/siem/public/cases/components/user_action_tree/user_action_markdown.tsx index 1e8a66ab4f9cf..2dfe9471ba7b7 100644 --- a/x-pack/plugins/siem/public/cases/components/user_action_tree/user_action_markdown.tsx +++ b/x-pack/plugins/siem/public/cases/components/user_action_tree/user_action_markdown.tsx @@ -8,6 +8,7 @@ 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'; @@ -15,6 +16,13 @@ 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` @@ -36,6 +44,8 @@ export const UserActionMarkdown = ({ onChangeEditable, onSaveContent, }: UserActionMarkdownProps) => { + const dispatch = useDispatch(); + const apolloClient = useApolloClient(); const { form } = useForm({ defaultValue: { content }, options: { stripEmptyFields: false }, @@ -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) { @@ -112,7 +140,7 @@ export const UserActionMarkdown = ({ ) : ( console.log('zomg', timelineId)} + onClickTimeline={handleTimelineClick} raw={content} data-test-subj="user-action-markdown" /> diff --git a/x-pack/plugins/siem/public/timelines/components/open_timeline/helpers.ts b/x-pack/plugins/siem/public/timelines/components/open_timeline/helpers.ts index df433f147490e..30a88c58afff8 100644 --- a/x-pack/plugins/siem/public/timelines/components/open_timeline/helpers.ts +++ b/x-pack/plugins/siem/public/timelines/components/open_timeline/helpers.ts @@ -189,7 +189,7 @@ export const formatTimelineResultToModel = ( export interface QueryTimelineById { apolloClient: ApolloClient | ApolloClient<{}> | undefined; - duplicate: boolean; + duplicate?: boolean; timelineId: string; onOpenTimeline?: (timeline: TimelineModel) => void; openTimeline?: boolean; From 98a4bc2c2f1a72edea987d8cc50f08485a6d33ea Mon Sep 17 00:00:00 2001 From: stephmilovic Date: Wed, 13 May 2020 08:48:30 -0600 Subject: [PATCH 3/6] tests --- .../user_action_markdown.test.tsx | 55 +++++++++++++++++++ .../common/components/markdown/index.test.tsx | 32 +++++++++++ .../common/components/markdown/index.tsx | 15 +++-- 3 files changed, 94 insertions(+), 8 deletions(-) create mode 100644 x-pack/plugins/siem/public/cases/components/user_action_tree/user_action_markdown.test.tsx diff --git a/x-pack/plugins/siem/public/cases/components/user_action_tree/user_action_markdown.test.tsx b/x-pack/plugins/siem/public/cases/components/user_action_tree/user_action_markdown.test.tsx new file mode 100644 index 0000000000000..338a3d5661274 --- /dev/null +++ b/x-pack/plugins/siem/public/cases/components/user_action_tree/user_action_markdown.test.tsx @@ -0,0 +1,55 @@ +/* + * 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', async () => { + const wrapper = mount( + + + + + + ); + 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), + }); + }); +}); diff --git a/x-pack/plugins/siem/public/common/components/markdown/index.test.tsx b/x-pack/plugins/siem/public/common/components/markdown/index.test.tsx index 89af9202a597e..bbf59177bcf04 100644 --- a/x-pack/plugins/siem/public/common/components/markdown/index.test.tsx +++ b/x-pack/plugins/siem/public/common/components/markdown/index.test.tsx @@ -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( + + ); + + 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( + + ); + wrapper + .find('[data-test-subj="markdown-timeline-link"]') + .first() + .simulate('click'); + + expect(onClickTimeline).toHaveBeenCalledWith(timelineId); + }); + }); }); }); diff --git a/x-pack/plugins/siem/public/common/components/markdown/index.tsx b/x-pack/plugins/siem/public/common/components/markdown/index.tsx index ba513fcba6005..1a4c9cb71a77e 100644 --- a/x-pack/plugins/siem/public/common/components/markdown/index.tsx +++ b/x-pack/plugins/siem/public/common/components/markdown/index.tsx @@ -6,13 +6,7 @@ /* eslint-disable react/display-name */ -import { - EuiLink, - EuiTableRow, - EuiTableRowCell, - EuiText, - EuiToolTip, -} from '@elastic/eui'; +import { EuiLink, EuiTableRow, EuiTableRowCell, EuiText, EuiToolTip } from '@elastic/eui'; import React from 'react'; import ReactMarkdown from 'react-markdown'; import styled, { css } from 'styled-components'; @@ -72,7 +66,12 @@ export const Markdown = React.memo<{ const timelineId = href.split('timelines?timeline=(id:')[1].split("'")[1] ?? ''; return ( - onClickTimeline(timelineId)}>{children} + onClickTimeline(timelineId)} + data-test-subj="markdown-timeline-link" + > + {children} + ); } From 0f64c89267d199c3e084cc5283449786ed63fa7d Mon Sep 17 00:00:00 2001 From: stephmilovic Date: Wed, 13 May 2020 09:06:22 -0600 Subject: [PATCH 4/6] fix preview mode --- .../components/user_action_tree/user_action_markdown.tsx | 1 + .../siem/public/common/components/markdown_editor/form.tsx | 3 +++ .../siem/public/common/components/markdown_editor/index.tsx | 4 +++- 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/siem/public/cases/components/user_action_tree/user_action_markdown.tsx b/x-pack/plugins/siem/public/cases/components/user_action_tree/user_action_markdown.tsx index 2dfe9471ba7b7..03dd599da88e5 100644 --- a/x-pack/plugins/siem/public/cases/components/user_action_tree/user_action_markdown.tsx +++ b/x-pack/plugins/siem/public/cases/components/user_action_tree/user_action_markdown.tsx @@ -126,6 +126,7 @@ export const UserActionMarkdown = ({ cancelAction: handleCancelAction, saveAction: handleSaveAction, }), + onClickTimeline: handleTimelineClick, onCursorPositionUpdate: handleCursorChange, topRightContent: ( void; onCursorPositionUpdate?: (cursorPosition: CursorPosition) => void; placeholder?: string; topRightContent?: React.ReactNode; @@ -26,6 +27,7 @@ export const MarkdownEditorForm = ({ field, idAria, isDisabled = false, + onClickTimeline, onCursorPositionUpdate, placeholder, topRightContent, @@ -55,6 +57,7 @@ export const MarkdownEditorForm = ({ content={field.value as string} isDisabled={isDisabled} onChange={handleContentChange} + onClickTimeline={onClickTimeline} onCursorPositionUpdate={onCursorPositionUpdate} placeholder={placeholder} topRightContent={topRightContent} diff --git a/x-pack/plugins/siem/public/common/components/markdown_editor/index.tsx b/x-pack/plugins/siem/public/common/components/markdown_editor/index.tsx index 4fb7086e82b28..0666ba8b208a9 100644 --- a/x-pack/plugins/siem/public/common/components/markdown_editor/index.tsx +++ b/x-pack/plugins/siem/public/common/components/markdown_editor/index.tsx @@ -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; }>( @@ -83,6 +84,7 @@ export const MarkdownEditor = React.memo<{ content, isDisabled = false, onChange, + onClickTimeline, placeholder, onCursorPositionUpdate, }) => { @@ -127,7 +129,7 @@ export const MarkdownEditor = React.memo<{ name: i18n.PREVIEW, content: ( - + ), }, From 3d65ba8c3d72156f25d836c975ca6c125ac14911 Mon Sep 17 00:00:00 2001 From: stephmilovic Date: Wed, 13 May 2020 10:37:41 -0600 Subject: [PATCH 5/6] added another test --- .../user_action_markdown.test.tsx | 26 ++++++++++++++++++- .../components/markdown_editor/index.tsx | 1 + 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/siem/public/cases/components/user_action_tree/user_action_markdown.test.tsx b/x-pack/plugins/siem/public/cases/components/user_action_tree/user_action_markdown.test.tsx index 338a3d5661274..27438207bed97 100644 --- a/x-pack/plugins/siem/public/cases/components/user_action_tree/user_action_markdown.test.tsx +++ b/x-pack/plugins/siem/public/cases/components/user_action_tree/user_action_markdown.test.tsx @@ -32,7 +32,7 @@ describe('UserActionMarkdown ', () => { jest.resetAllMocks(); }); - it('Opens timeline when timeline link clicked', async () => { + it('Opens timeline when timeline link clicked - isEditable: false', async () => { const wrapper = mount( @@ -52,4 +52,28 @@ describe('UserActionMarkdown ', () => { updateTimeline: expect.any(Function), }); }); + + it('Opens timeline when timeline link clicked - isEditable: true ', async () => { + const wrapper = mount( + + + + + + ); + 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), + }); + }); }); diff --git a/x-pack/plugins/siem/public/common/components/markdown_editor/index.tsx b/x-pack/plugins/siem/public/common/components/markdown_editor/index.tsx index 0666ba8b208a9..b0df2b6b5b60f 100644 --- a/x-pack/plugins/siem/public/common/components/markdown_editor/index.tsx +++ b/x-pack/plugins/siem/public/common/components/markdown_editor/index.tsx @@ -127,6 +127,7 @@ export const MarkdownEditor = React.memo<{ { id: 'preview', name: i18n.PREVIEW, + 'data-test-subj': 'preview-tab', content: ( From 123d5db7471e326e3168b40a5ee878acb9abbefb Mon Sep 17 00:00:00 2001 From: stephmilovic Date: Thu, 14 May 2020 11:53:13 -0600 Subject: [PATCH 6/6] fix cypress tests --- .../plugins/siem/cypress/integration/cases.spec.ts | 12 ++++-------- x-pack/plugins/siem/cypress/screens/case_details.ts | 2 +- x-pack/plugins/siem/cypress/tasks/case_details.ts | 7 +++---- 3 files changed, 8 insertions(+), 13 deletions(-) diff --git a/x-pack/plugins/siem/cypress/integration/cases.spec.ts b/x-pack/plugins/siem/cypress/integration/cases.spec.ts index f541555d56440..93b7f28290143 100644 --- a/x-pack/plugins/siem/cypress/integration/cases.spec.ts +++ b/x-pack/plugins/siem/cypress/integration/cases.spec.ts @@ -30,7 +30,6 @@ import { CASE_DETAILS_PUSH_AS_SERVICE_NOW_BTN, CASE_DETAILS_STATUS, CASE_DETAILS_TAGS, - CASE_DETAILS_TIMELINE_MARKDOWN, CASE_DETAILS_USER_ACTION, CASE_DETAILS_USERNAMES, PARTICIPANTS, @@ -103,13 +102,10 @@ describe('Cases', () => { .should('have.text', case1.reporter); cy.get(CASE_DETAILS_TAGS).should('have.text', expectedTags); cy.get(CASE_DETAILS_PUSH_AS_SERVICE_NOW_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); }); }); diff --git a/x-pack/plugins/siem/cypress/screens/case_details.ts b/x-pack/plugins/siem/cypress/screens/case_details.ts index dadc1bdff1933..e602f5c6488ed 100644 --- a/x-pack/plugins/siem/cypress/screens/case_details.ts +++ b/x-pack/plugins/siem/cypress/screens/case_details.ts @@ -16,7 +16,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'; diff --git a/x-pack/plugins/siem/cypress/tasks/case_details.ts b/x-pack/plugins/siem/cypress/tasks/case_details.ts index a28f8b8010adb..976d568ab3a91 100644 --- a/x-pack/plugins/siem/cypress/tasks/case_details.ts +++ b/x-pack/plugins/siem/cypress/tasks/case_details.ts @@ -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'); };