diff --git a/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_markdown.tsx b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_markdown.tsx
index 45e46b2d7d2db..5bffebbefa40f 100644
--- a/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_markdown.tsx
+++ b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_markdown.tsx
@@ -4,24 +4,14 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import {
- EuiFlexGroup,
- EuiFlexItem,
- EuiButtonEmpty,
- EuiButton,
- EuiMarkdownFormat,
-} from '@elastic/eui';
+import { EuiFlexGroup, EuiFlexItem, EuiButtonEmpty, EuiButton } from '@elastic/eui';
import React, { useCallback } from 'react';
import styled from 'styled-components';
import * as i18n from '../case_view/translations';
import { Form, useForm, UseField } from '../../../shared_imports';
import { schema, Content } from './schema';
-import {
- MarkdownEditorForm,
- parsingPlugins,
- processingPlugins,
-} from '../../../common/components/markdown_editor/eui_form';
+import { MarkdownRenderer, MarkdownEditorForm } from '../../../common/components/markdown_editor';
const ContentWrapper = styled.div`
padding: ${({ theme }) => `${theme.eui.euiSizeM} ${theme.eui.euiSizeL}`};
@@ -111,12 +101,7 @@ export const UserActionMarkdown = ({
) : (
-
- {content}
-
+ {content}
);
};
diff --git a/x-pack/plugins/security_solution/public/common/components/markdown/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/common/components/markdown/__snapshots__/index.test.tsx.snap
deleted file mode 100644
index 4850547f30c52..0000000000000
--- a/x-pack/plugins/security_solution/public/common/components/markdown/__snapshots__/index.test.tsx.snap
+++ /dev/null
@@ -1,58 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`Markdown markdown links it renders the expected content containing a link 1`] = `
-
-`;
-
-exports[`Markdown markdown tables it renders the expected table content 1`] = `
-
-`;
diff --git a/x-pack/plugins/security_solution/public/common/components/markdown/__snapshots__/markdown_hint.test.tsx.snap b/x-pack/plugins/security_solution/public/common/components/markdown/__snapshots__/markdown_hint.test.tsx.snap
deleted file mode 100644
index 7f350072439c5..0000000000000
--- a/x-pack/plugins/security_solution/public/common/components/markdown/__snapshots__/markdown_hint.test.tsx.snap
+++ /dev/null
@@ -1,55 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`MarkdownHintComponent rendering it renders the expected hints 1`] = `
-
-
- # heading
-
-
- **bold**
-
-
- _italics_
-
-
- \`code\`
-
-
- [link](url)
-
-
- * bullet
-
-
- \`\`\`preformatted\`\`\`
-
-
- >quote
-
- ~~
-
- strikethrough
-
- ~~
-
- 
-
-
-`;
diff --git a/x-pack/plugins/security_solution/public/common/components/markdown/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/markdown/index.test.tsx
deleted file mode 100644
index e30391982ee7a..0000000000000
--- a/x-pack/plugins/security_solution/public/common/components/markdown/index.test.tsx
+++ /dev/null
@@ -1,176 +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;
- * you may not use this file except in compliance with the Elastic License.
- */
-import { mount, shallow } from 'enzyme';
-import React from 'react';
-
-import { Markdown } from '.';
-
-describe('Markdown', () => {
- test(`it renders when raw markdown is NOT provided`, () => {
- const wrapper = mount();
-
- expect(wrapper.find('[data-test-subj="markdown"]').exists()).toEqual(true);
- });
-
- test('it renders plain text', () => {
- const raw = 'this has no special markdown formatting';
- const wrapper = mount();
-
- expect(wrapper.find('[data-test-subj="markdown-root"]').first().text()).toEqual(raw);
- });
-
- test('it applies the EUI text style to all markdown content', () => {
- const wrapper = mount();
-
- expect(
- wrapper.find('[data-test-subj="markdown-root"]').first().childAt(0).hasClass('euiText')
- ).toBe(true);
- });
-
- describe('markdown tables', () => {
- const headerColumns = ['we', 'support', 'markdown', 'tables'];
- const header = `| ${headerColumns[0]} | ${headerColumns[1]} | ${headerColumns[2]} | ${headerColumns[3]} |`;
-
- const rawTable = `${header}\n|---------|---------|------------|--------|\n| because | tables | are | pretty |\n| useful | for | formatting | data |`;
-
- test('it applies EUI table styling to tables', () => {
- const wrapper = mount();
-
- expect(wrapper.find('table').first().childAt(0).hasClass('euiTable')).toBe(true);
- });
-
- headerColumns.forEach((headerText) => {
- test(`it renders the "${headerText}" table header`, () => {
- const wrapper = mount();
-
- expect(wrapper.find('[data-test-subj="markdown-table-header"]').first().text()).toContain(
- headerText
- );
- });
- });
-
- test('it applies EUI table styling to table rows', () => {
- const wrapper = mount();
-
- expect(
- wrapper
- .find('[data-test-subj="markdown-table-row"]')
- .first()
- .childAt(0)
- .hasClass('euiTableRow')
- ).toBe(true);
- });
-
- test('it applies EUI table styling to table cells', () => {
- const wrapper = mount();
-
- expect(
- wrapper
- .find('[data-test-subj="markdown-table-cell"]')
- .first()
- .childAt(0)
- .hasClass('euiTableRowCell')
- ).toBe(true);
- });
-
- test('it renders the expected table content', () => {
- const wrapper = shallow();
-
- expect(wrapper).toMatchSnapshot();
- });
- });
-
- describe('markdown links', () => {
- const markdownWithLink = 'A link to an external site [External Site](https://google.com)';
-
- test('it renders the expected link text', () => {
- const wrapper = mount();
-
- expect(wrapper.find('[data-test-subj="markdown-link"]').first().text()).toEqual(
- 'External Site'
- );
- });
-
- test('it renders the expected href', () => {
- const wrapper = mount();
-
- expect(wrapper.find('[data-test-subj="markdown-link"]').first().getDOMNode()).toHaveProperty(
- 'href',
- 'https://google.com/'
- );
- });
-
- test('it does NOT render the href if links are disabled', () => {
- const wrapper = mount();
-
- expect(
- wrapper.find('[data-test-subj="markdown-link"]').first().getDOMNode()
- ).not.toHaveProperty('href');
- });
-
- test('it opens links in a new tab via target="_blank"', () => {
- const wrapper = mount();
-
- expect(wrapper.find('[data-test-subj="markdown-link"]').first().getDOMNode()).toHaveProperty(
- 'target',
- '_blank'
- );
- });
-
- test('it sets the link `rel` attribute to `noopener` to prevent the new page from accessing `window.opener`, `nofollow` to note the link is not endorsed by us, and noreferrer to prevent the browser from sending the current address', () => {
- const wrapper = mount();
-
- expect(wrapper.find('[data-test-subj="markdown-link"]').first().getDOMNode()).toHaveProperty(
- 'rel',
- 'nofollow noopener noreferrer'
- );
- });
-
- test('it renders the expected content containing a link', () => {
- const wrapper = shallow();
-
- 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, '');
- });
-
- test('timeline link onClick calls onClickTimeline with timelineId and graphEventId', () => {
- const graphEventId = '2bc51864784c';
- const markdownWithTimelineAndGraphEventLink = `A link to a timeline [timeline](http://localhost:5601/app/siem#/timelines?timeline=(id:'${timelineId}',isOpen:!t,graphEventId:'${graphEventId}'))`;
-
- const wrapper = mount(
-
- );
- wrapper.find('[data-test-subj="markdown-timeline-link"]').first().simulate('click');
-
- expect(onClickTimeline).toHaveBeenCalledWith(timelineId, graphEventId);
- });
- });
- });
-});
diff --git a/x-pack/plugins/security_solution/public/common/components/markdown/index.tsx b/x-pack/plugins/security_solution/public/common/components/markdown/index.tsx
deleted file mode 100644
index 1d73c3cb8a2aa..0000000000000
--- a/x-pack/plugins/security_solution/public/common/components/markdown/index.tsx
+++ /dev/null
@@ -1,110 +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;
- * you may not use this file except in compliance with the Elastic License.
- */
-
-/* eslint-disable react/display-name */
-
-import { EuiLink, EuiTableRow, EuiTableRowCell, EuiText, EuiToolTip } from '@elastic/eui';
-import { clone } from 'lodash/fp';
-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;
-`;
-
-const MyBlockquote = styled.div`
- ${({ theme }) => css`
- padding: 0 ${theme.eui.euiSize};
- color: ${theme.eui.euiColorMediumShade};
- border-left: ${theme.eui.euiSizeXS} solid ${theme.eui.euiColorLightShade};
- `}
-`;
-
-TableHeader.displayName = 'TableHeader';
-
-/** prevents links to the new pages from accessing `window.opener` */
-const REL_NOOPENER = 'noopener';
-
-/** prevents search engine manipulation by noting the linked document is not trusted or endorsed by us */
-const REL_NOFOLLOW = 'nofollow';
-
-/** prevents the browser from sending the current address as referrer via the Referer HTTP header */
-const REL_NOREFERRER = 'noreferrer';
-
-export const Markdown = React.memo<{
- disableLinks?: boolean;
- raw?: string;
- onClickTimeline?: (timelineId: string, graphEventId?: string) => void;
- size?: 'xs' | 's' | 'm';
-}>(({ disableLinks = false, onClickTimeline, raw, size = 's' }) => {
- const markdownRenderers = {
- root: ({ children }: { children: React.ReactNode[] }) => (
-
- {children}
-
- ),
- table: ({ children }: { children: React.ReactNode[] }) => (
-
- ),
- tableHead: ({ children }: { children: React.ReactNode[] }) => (
- {children}
- ),
- tableRow: ({ children }: { children: React.ReactNode[] }) => (
- {children}
- ),
- tableCell: ({ children }: { children: React.ReactNode[] }) => (
- {children}
- ),
- link: ({ children, href }: { children: React.ReactNode[]; href?: string }) => {
- if (onClickTimeline != null && href != null && href.indexOf(`timelines?timeline=(id:`) > -1) {
- const timelineId = clone(href).split('timeline=(id:')[1].split("'")[1] ?? '';
- const graphEventId = href.includes('graphEventId:')
- ? clone(href).split('graphEventId:')[1].split("'")[1] ?? ''
- : '';
- return (
-
- onClickTimeline(timelineId, graphEventId)}
- data-test-subj="markdown-timeline-link"
- >
- {children}
-
-
- );
- }
- return (
-
-
- {children}
-
-
- );
- },
- blockquote: ({ children }: { children: React.ReactNode[] }) => (
- {children}
- ),
- };
-
- return (
-
- );
-});
-
-Markdown.displayName = 'Markdown';
diff --git a/x-pack/plugins/security_solution/public/common/components/markdown/markdown_hint.test.tsx b/x-pack/plugins/security_solution/public/common/components/markdown/markdown_hint.test.tsx
deleted file mode 100644
index 5ec37f8aed0cb..0000000000000
--- a/x-pack/plugins/security_solution/public/common/components/markdown/markdown_hint.test.tsx
+++ /dev/null
@@ -1,89 +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;
- * you may not use this file except in compliance with the Elastic License.
- */
-
-import { mount, shallow } from 'enzyme';
-import React from 'react';
-
-import { MarkdownHintComponent } from './markdown_hint';
-
-describe('MarkdownHintComponent ', () => {
- test('it has inline visibility when show is true', () => {
- const wrapper = mount();
-
- expect(wrapper.find('[data-test-subj="markdown-hint"]').first()).toHaveStyleRule(
- 'visibility',
- 'inline'
- );
- });
-
- test('it has hidden visibility when show is false', () => {
- const wrapper = mount();
-
- expect(wrapper.find('[data-test-subj="markdown-hint"]').first()).toHaveStyleRule(
- 'visibility',
- 'hidden'
- );
- });
-
- test('it renders the heading hint', () => {
- const wrapper = mount();
-
- expect(wrapper.find('[data-test-subj="heading-hint"]').first().text()).toEqual('# heading');
- });
-
- test('it renders the bold hint with a bold font-weight', () => {
- const wrapper = mount();
-
- expect(wrapper.find('[data-test-subj="bold-hint"]').first()).toHaveStyleRule(
- 'font-weight',
- 'bold'
- );
- });
-
- test('it renders the italic hint with an italic font-style', () => {
- const wrapper = mount();
-
- expect(wrapper.find('[data-test-subj="italic-hint"]').first()).toHaveStyleRule(
- 'font-style',
- 'italic'
- );
- });
-
- test('it renders the code hint with a monospace font family', () => {
- const wrapper = mount();
-
- expect(wrapper.find('[data-test-subj="code-hint"]').first()).toHaveStyleRule(
- 'font-family',
- 'monospace'
- );
- });
-
- test('it renders the preformatted hint with a monospace font family', () => {
- const wrapper = mount();
-
- expect(wrapper.find('[data-test-subj="preformatted-hint"]').first()).toHaveStyleRule(
- 'font-family',
- 'monospace'
- );
- });
-
- test('it renders the strikethrough hint with a line-through text-decoration', () => {
- const wrapper = mount();
-
- expect(wrapper.find('[data-test-subj="strikethrough-hint"]').first()).toHaveStyleRule(
- 'text-decoration',
- 'line-through'
- );
- });
-
- describe('rendering', () => {
- test('it renders the expected hints', () => {
- const wrapper = shallow();
-
- expect(wrapper).toMatchSnapshot();
- });
- });
-});
diff --git a/x-pack/plugins/security_solution/public/common/components/markdown/markdown_hint.tsx b/x-pack/plugins/security_solution/public/common/components/markdown/markdown_hint.tsx
deleted file mode 100644
index 199059670e4bd..0000000000000
--- a/x-pack/plugins/security_solution/public/common/components/markdown/markdown_hint.tsx
+++ /dev/null
@@ -1,92 +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;
- * you may not use this file except in compliance with the Elastic License.
- */
-
-import { EuiText } from '@elastic/eui';
-import React from 'react';
-import styled from 'styled-components';
-
-import * as i18n from './translations';
-
-const Heading = styled.span`
- margin-right: 5px;
-`;
-
-Heading.displayName = 'Heading';
-
-const Bold = styled.span`
- font-weight: bold;
- margin-right: 5px;
-`;
-
-Bold.displayName = 'Bold';
-
-const MarkdownHintContainer = styled(EuiText)<{ visibility: string }>`
- visibility: ${({ visibility }) => visibility};
-`;
-
-MarkdownHintContainer.displayName = 'MarkdownHintContainer';
-
-const ImageUrl = styled.span`
- margin-left: 5px;
-`;
-
-ImageUrl.displayName = 'ImageUrl';
-
-const Italic = styled.span`
- font-style: italic;
- margin-right: 5px;
-`;
-
-Italic.displayName = 'Italic';
-
-const Strikethrough = styled.span`
- text-decoration: line-through;
-`;
-
-Strikethrough.displayName = 'Strikethrough';
-
-const Code = styled.span`
- font-family: monospace;
- margin-right: 5px;
-`;
-
-Code.displayName = 'Code';
-
-const TrailingWhitespace = styled.span`
- margin-right: 5px;
-`;
-
-TrailingWhitespace.displayName = 'TrailingWhitespace';
-
-export const MarkdownHintComponent = ({ show }: { show: boolean }) => (
-
- {i18n.MARKDOWN_HINT_HEADING}
- {i18n.MARKDOWN_HINT_BOLD}
- {i18n.MARKDOWN_HINT_ITALICS}
- {i18n.MARKDOWN_HINT_CODE}
- {i18n.MARKDOWN_HINT_URL}
- {i18n.MARKDOWN_HINT_BULLET}
- {i18n.MARKDOWN_HINT_PREFORMATTED}
- {i18n.MARKDOWN_HINT_QUOTE}
- {'~~'}
-
- {i18n.MARKDOWN_HINT_STRIKETHROUGH}
-
- {'~~'}
- {i18n.MARKDOWN_HINT_IMAGE_URL}
-
-);
-
-MarkdownHintComponent.displayName = 'MarkdownHintComponent';
-
-export const MarkdownHint = React.memo(MarkdownHintComponent);
-
-MarkdownHint.displayName = 'MarkdownHint';
diff --git a/x-pack/plugins/security_solution/public/common/components/markdown/translations.ts b/x-pack/plugins/security_solution/public/common/components/markdown/translations.ts
deleted file mode 100644
index 98d2e7d47b3fb..0000000000000
--- a/x-pack/plugins/security_solution/public/common/components/markdown/translations.ts
+++ /dev/null
@@ -1,76 +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;
- * you may not use this file except in compliance with the Elastic License.
- */
-
-import { i18n } from '@kbn/i18n';
-
-export const MARKDOWN_HINT_HEADING = i18n.translate(
- 'xpack.securitySolution.markdown.hint.headingLabel',
- {
- defaultMessage: '# heading',
- }
-);
-
-export const MARKDOWN_HINT_BOLD = i18n.translate('xpack.securitySolution.markdown.hint.boldLabel', {
- defaultMessage: '**bold**',
-});
-
-export const MARKDOWN_HINT_ITALICS = i18n.translate(
- 'xpack.securitySolution.markdown.hint.italicsLabel',
- {
- defaultMessage: '_italics_',
- }
-);
-
-export const MARKDOWN_HINT_CODE = i18n.translate('xpack.securitySolution.markdown.hint.codeLabel', {
- defaultMessage: '`code`',
-});
-
-export const MARKDOWN_HINT_URL = i18n.translate('xpack.securitySolution.markdown.hint.urlLabel', {
- defaultMessage: '[link](url)',
-});
-
-export const MARKDOWN_HINT_BULLET = i18n.translate(
- 'xpack.securitySolution.markdown.hint.bulletLabel',
- {
- defaultMessage: '* bullet',
- }
-);
-
-export const MARKDOWN_HINT_PREFORMATTED = i18n.translate(
- 'xpack.securitySolution.markdown.hint.preformattedLabel',
- {
- defaultMessage: '```preformatted```',
- }
-);
-
-export const MARKDOWN_HINT_QUOTE = i18n.translate(
- 'xpack.securitySolution.markdown.hint.quoteLabel',
- {
- defaultMessage: '>quote',
- }
-);
-
-export const MARKDOWN_HINT_STRIKETHROUGH = i18n.translate(
- 'xpack.securitySolution.markdown.hint.strikethroughLabel',
- {
- defaultMessage: 'strikethrough',
- }
-);
-
-export const MARKDOWN_HINT_IMAGE_URL = i18n.translate(
- 'xpack.securitySolution.markdown.hint.imageUrlLabel',
- {
- defaultMessage: '',
- }
-);
-
-export const TIMELINE_ID = (timelineId: string) =>
- i18n.translate('xpack.securitySolution.markdown.toolTip.timelineId', {
- defaultMessage: 'Timeline id: { timelineId }',
- values: {
- timelineId,
- },
- });
diff --git a/x-pack/plugins/security_solution/public/common/components/markdown_editor/editor.tsx b/x-pack/plugins/security_solution/public/common/components/markdown_editor/editor.tsx
new file mode 100644
index 0000000000000..b8632de71abe1
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/common/components/markdown_editor/editor.tsx
@@ -0,0 +1,51 @@
+/*
+ * 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, { memo, useState, useCallback } from 'react';
+import { EuiMarkdownEditor } from '@elastic/eui';
+
+import { uiPlugins, parsingPlugins, processingPlugins } from './plugins';
+
+interface MarkdownEditorProps {
+ onChange: (content: string) => void;
+ value: string;
+ ariaLabel: string;
+ editorId?: string;
+ dataTestSubj?: string;
+ height?: number;
+}
+
+const MarkdownEditorComponent: React.FC = ({
+ onChange,
+ value,
+ ariaLabel,
+ editorId,
+ dataTestSubj,
+ height,
+}) => {
+ const [markdownErrorMessages, setMarkdownErrorMessages] = useState([]);
+ const onParse = useCallback((err, { messages }) => {
+ setMarkdownErrorMessages(err ? [err] : messages);
+ }, []);
+
+ return (
+
+ );
+};
+
+export const MarkdownEditor = memo(MarkdownEditorComponent);
diff --git a/x-pack/plugins/security_solution/public/common/components/markdown_editor/eui_form.tsx b/x-pack/plugins/security_solution/public/common/components/markdown_editor/eui_form.tsx
index 481ed7892a8be..a28abbc8a59e4 100644
--- a/x-pack/plugins/security_solution/public/common/components/markdown_editor/eui_form.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/markdown_editor/eui_form.tsx
@@ -4,20 +4,12 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import React, { useState, useCallback } from 'react';
+import React from 'react';
import styled from 'styled-components';
-import {
- EuiMarkdownEditor,
- EuiMarkdownEditorProps,
- EuiFormRow,
- EuiFlexItem,
- EuiFlexGroup,
- getDefaultEuiMarkdownParsingPlugins,
- getDefaultEuiMarkdownProcessingPlugins,
-} from '@elastic/eui';
+import { EuiMarkdownEditorProps, EuiFormRow, EuiFlexItem, EuiFlexGroup } from '@elastic/eui';
import { FieldHook, getFieldValidityAndErrorMessage } from '../../../shared_imports';
-import * as timelineMarkdownPlugin from './plugins/timeline';
+import { MarkdownEditor } from './editor';
type MarkdownEditorFormProps = EuiMarkdownEditorProps & {
id: string;
@@ -34,12 +26,6 @@ const BottomContentWrapper = styled(EuiFlexGroup)`
`}
`;
-export const parsingPlugins = getDefaultEuiMarkdownParsingPlugins();
-parsingPlugins.push(timelineMarkdownPlugin.parser);
-
-export const processingPlugins = getDefaultEuiMarkdownProcessingPlugins();
-processingPlugins[1][1].components.timeline = timelineMarkdownPlugin.renderer;
-
export const MarkdownEditorForm: React.FC = ({
id,
field,
@@ -48,10 +34,6 @@ export const MarkdownEditorForm: React.FC = ({
bottomRightContent,
}) => {
const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field);
- const [markdownErrorMessages, setMarkdownErrorMessages] = useState([]);
- const onParse = useCallback((err, { messages }) => {
- setMarkdownErrorMessages(err ? [err] : messages);
- }, []);
return (
= ({
labelAppend={field.labelAppend}
>
<>
-
{bottomRightContent && (
diff --git a/x-pack/plugins/security_solution/public/common/components/markdown_editor/index.tsx b/x-pack/plugins/security_solution/public/common/components/markdown_editor/index.tsx
index 9f4141dbcae7d..41f5aab691a7a 100644
--- a/x-pack/plugins/security_solution/public/common/components/markdown_editor/index.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/markdown_editor/index.tsx
@@ -5,3 +5,6 @@
*/
export * from './types';
+export * from './renderer';
+export * from './editor';
+export * from './eui_form';
diff --git a/x-pack/plugins/security_solution/public/common/components/markdown_editor/markdown_link.tsx b/x-pack/plugins/security_solution/public/common/components/markdown_editor/markdown_link.tsx
new file mode 100644
index 0000000000000..f904b63d4bace
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/common/components/markdown_editor/markdown_link.tsx
@@ -0,0 +1,34 @@
+/*
+ * 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, { memo } from 'react';
+import { EuiLink, EuiLinkAnchorProps, EuiToolTip } from '@elastic/eui';
+
+type MarkdownLinkProps = { disableLinks?: boolean } & EuiLinkAnchorProps;
+
+/** prevents search engine manipulation by noting the linked document is not trusted or endorsed by us */
+const REL_NOFOLLOW = 'nofollow';
+
+const MarkdownLinkComponent: React.FC = ({
+ disableLinks,
+ href,
+ target,
+ children,
+ ...props
+}) => (
+
+
+ {children}
+
+
+);
+
+export const MarkdownLink = memo(MarkdownLinkComponent);
diff --git a/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/index.ts b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/index.ts
new file mode 100644
index 0000000000000..b3d91d26e50da
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/index.ts
@@ -0,0 +1,21 @@
+/*
+ * 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 {
+ getDefaultEuiMarkdownParsingPlugins,
+ getDefaultEuiMarkdownProcessingPlugins,
+} from '@elastic/eui';
+
+import * as timelineMarkdownPlugin from './timeline';
+
+export const uiPlugins = [timelineMarkdownPlugin.plugin];
+export const parsingPlugins = getDefaultEuiMarkdownParsingPlugins();
+export const processingPlugins = getDefaultEuiMarkdownProcessingPlugins();
+
+parsingPlugins.push(timelineMarkdownPlugin.parser);
+
+// This line of code is TS-compatible and it will break if [1][1] change in the future.
+processingPlugins[1][1].components.timeline = timelineMarkdownPlugin.renderer;
diff --git a/x-pack/plugins/security_solution/public/common/components/markdown_editor/renderer.test.tsx b/x-pack/plugins/security_solution/public/common/components/markdown_editor/renderer.test.tsx
new file mode 100644
index 0000000000000..e6a38863d7e5f
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/common/components/markdown_editor/renderer.test.tsx
@@ -0,0 +1,61 @@
+/*
+ * 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 { MarkdownRenderer } from './renderer';
+
+describe('Markdown', () => {
+ describe('markdown links', () => {
+ const markdownWithLink = 'A link to an external site [External Site](https://google.com)';
+
+ test('it renders the expected link text', () => {
+ const wrapper = mount({markdownWithLink});
+
+ expect(wrapper.find('[data-test-subj="markdown-link"]').first().text()).toEqual(
+ 'External Site'
+ );
+ });
+
+ test('it renders the expected href', () => {
+ const wrapper = mount({markdownWithLink});
+
+ expect(wrapper.find('[data-test-subj="markdown-link"]').first().getDOMNode()).toHaveProperty(
+ 'href',
+ 'https://google.com/'
+ );
+ });
+
+ test('it does NOT render the href if links are disabled', () => {
+ const wrapper = mount(
+ {markdownWithLink}
+ );
+
+ expect(
+ wrapper.find('[data-test-subj="markdown-link"]').first().getDOMNode()
+ ).not.toHaveProperty('href');
+ });
+
+ test('it opens links in a new tab via target="_blank"', () => {
+ const wrapper = mount({markdownWithLink});
+
+ expect(wrapper.find('[data-test-subj="markdown-link"]').first().getDOMNode()).toHaveProperty(
+ 'target',
+ '_blank'
+ );
+ });
+
+ test('it sets the link `rel` attribute to `noopener` to prevent the new page from accessing `window.opener`, `nofollow` to note the link is not endorsed by us, and noreferrer to prevent the browser from sending the current address', () => {
+ const wrapper = mount({markdownWithLink});
+
+ expect(wrapper.find('[data-test-subj="markdown-link"]').first().getDOMNode()).toHaveProperty(
+ 'rel',
+ 'nofollow noopener noreferrer'
+ );
+ });
+ });
+});
diff --git a/x-pack/plugins/security_solution/public/common/components/markdown_editor/renderer.tsx b/x-pack/plugins/security_solution/public/common/components/markdown_editor/renderer.tsx
new file mode 100644
index 0000000000000..7a7693512afbe
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/common/components/markdown_editor/renderer.tsx
@@ -0,0 +1,40 @@
+/*
+ * 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, { memo, useMemo } from 'react';
+import { cloneDeep } from 'lodash/fp';
+import { EuiMarkdownFormat, EuiLinkAnchorProps } from '@elastic/eui';
+
+import { parsingPlugins, processingPlugins } from './plugins';
+import { MarkdownLink } from './markdown_link';
+
+interface Props {
+ children: string;
+ disableLinks?: boolean;
+}
+
+const MarkdownRendererComponent: React.FC = ({ children, disableLinks }) => {
+ const MarkdownLinkProcessingComponent: React.FC = useMemo(
+ () => (props) => ,
+ [disableLinks]
+ );
+
+ // Deep clone of the processing plugins to prevent affecting the markdown editor.
+ const processingPluginList = cloneDeep(processingPlugins);
+ // This line of code is TS-compatible and it will break if [1][1] change in the future.
+ processingPluginList[1][1].components.a = MarkdownLinkProcessingComponent;
+
+ return (
+
+ {children}
+
+ );
+};
+
+export const MarkdownRenderer = memo(MarkdownRendererComponent);
diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule_details/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule_details/index.test.tsx
index 8e398e6236510..757319e7aa1ae 100644
--- a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule_details/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule_details/index.test.tsx
@@ -154,7 +154,9 @@ describe('StepAboutRuleToggleDetails', () => {
.simulate('change', { target: { value: 'notes' } });
expect(wrapper.find('EuiButtonGroup[idSelected="notes"]').exists()).toBeTruthy();
- expect(wrapper.find('Markdown h1').text()).toEqual('this is some markdown documentation');
+ expect(wrapper.find('.euiMarkdownFormat').text()).toEqual(
+ 'this is some markdown documentation'
+ );
});
});
});
diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule_details/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule_details/index.tsx
index 8604a5293a710..52e9dc7e44ff7 100644
--- a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule_details/index.tsx
+++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule_details/index.tsx
@@ -20,7 +20,7 @@ import styled from 'styled-components';
import { isEmpty } from 'lodash/fp';
import { HeaderSection } from '../../../../common/components/header_section';
-import { Markdown } from '../../../../common/components/markdown';
+import { MarkdownRenderer } from '../../../../common/components/markdown_editor';
import { AboutStepRule, AboutStepRuleDetails } from '../../../pages/detection_engine/rules/types';
import * as i18n from './translations';
import { StepAboutRule } from '../step_about_rule';
@@ -136,7 +136,7 @@ const StepAboutRuleToggleDetailsComponent: React.FC = ({
maxHeight={aboutPanelHeight}
className="eui-yScrollWithShadows"
>
-
+ {stepDataDetails.note}
)}
diff --git a/x-pack/plugins/security_solution/public/overview/components/recent_cases/recent_cases.tsx b/x-pack/plugins/security_solution/public/overview/components/recent_cases/recent_cases.tsx
index 5867a9d859f04..95f0fbb194ca6 100644
--- a/x-pack/plugins/security_solution/public/overview/components/recent_cases/recent_cases.tsx
+++ b/x-pack/plugins/security_solution/public/overview/components/recent_cases/recent_cases.tsx
@@ -10,7 +10,7 @@ import styled from 'styled-components';
import { Case } from '../../../cases/containers/types';
import { getCaseDetailsUrl } from '../../../common/components/link_to/redirect_to_case';
-import { Markdown } from '../../../common/components/markdown';
+import { MarkdownRenderer } from '../../../common/components/markdown_editor';
import { useFormatUrl } from '../../../common/components/link_to';
import { IconWithCount } from '../recent_timelines/counts';
import { LinkAnchor } from '../../../common/components/links';
@@ -52,7 +52,7 @@ const RecentCasesComponent = ({ cases }: { cases: Case[] }) => {
{c.description && c.description.length && (
-
+ {c.description}
)}
diff --git a/x-pack/plugins/security_solution/public/timelines/components/notes/add_note/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/notes/add_note/__snapshots__/index.test.tsx.snap
index 718e7ce1d27a5..53bc76bfeb8e8 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/notes/add_note/__snapshots__/index.test.tsx.snap
+++ b/x-pack/plugins/security_solution/public/timelines/components/notes/add_note/__snapshots__/index.test.tsx.snap
@@ -11,13 +11,6 @@ exports[`AddNote renders correctly 1`] = `
noteInputHeight={200}
updateNewNote={[MockFunction]}
/>
-
-
-
diff --git a/x-pack/plugins/security_solution/public/timelines/components/notes/add_note/__snapshots__/new_note.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/notes/add_note/__snapshots__/new_note.test.tsx.snap
index 9bf2b5c65e829..69e06bc7e0d1b 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/notes/add_note/__snapshots__/new_note.test.tsx.snap
+++ b/x-pack/plugins/security_solution/public/timelines/components/notes/add_note/__snapshots__/new_note.test.tsx.snap
@@ -3,54 +3,13 @@
exports[`NewNote renders correctly 1`] = `
,
- "id": "note",
- "name": "Note",
- }
- }
- tabs={
- Array [
- Object {
- "content": ,
- "id": "note",
- "name": "Note",
- },
- Object {
- "content":
-
- ,
- "id": "preview",
- "name": "Preview (Markdown)",
- },
- ]
- }
-/>
+>
+
+
`;
diff --git a/x-pack/plugins/security_solution/public/timelines/components/notes/add_note/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/notes/add_note/index.test.tsx
index 570c0028e0f51..01dfd72a22db1 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/notes/add_note/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/notes/add_note/index.test.tsx
@@ -74,7 +74,9 @@ describe('AddNote', () => {
test('it renders the contents of the note', () => {
const wrapper = mount();
- expect(wrapper.find('[data-test-subj="add-a-note"]').first().text()).toEqual(note);
+ expect(
+ wrapper.find('[data-test-subj="add-a-note"] .euiMarkdownEditorDropZone').first().text()
+ ).toEqual(note);
});
test('it invokes associateNote when the Add Note button is clicked', () => {
@@ -131,30 +133,4 @@ describe('AddNote', () => {
expect(updateNote).toBeCalled();
});
-
- test('it does NOT display the markdown formatting hint when a note has NOT been entered', () => {
- const testProps = {
- ...props,
- newNote: '',
- };
- const wrapper = mount();
-
- expect(wrapper.find('[data-test-subj="markdown-hint"]').first()).toHaveStyleRule(
- 'visibility',
- 'hidden'
- );
- });
-
- test('it displays the markdown formatting hint when a note has been entered', () => {
- const testProps = {
- ...props,
- newNote: 'We should see a formatting hint now',
- };
- const wrapper = mount();
-
- expect(wrapper.find('[data-test-subj="markdown-hint"]').first()).toHaveStyleRule(
- 'visibility',
- 'inline'
- );
- });
});
diff --git a/x-pack/plugins/security_solution/public/timelines/components/notes/add_note/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/notes/add_note/index.tsx
index 7c211aafdf8c6..6ba62a115917f 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/notes/add_note/index.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/notes/add_note/index.tsx
@@ -8,7 +8,6 @@ import { EuiButton, EuiButtonEmpty, EuiFlexGroup, EuiFlexItem } from '@elastic/e
import React, { useCallback } from 'react';
import styled from 'styled-components';
-import { MarkdownHint } from '../../../../common/components/markdown/markdown_hint';
import {
AssociateNote,
GetNewNoteId,
@@ -64,9 +63,6 @@ export const AddNote = React.memo<{
return (
-
- 0} />
-
{onCancelAddNote != null ? (
diff --git a/x-pack/plugins/security_solution/public/timelines/components/notes/add_note/new_note.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/notes/add_note/new_note.test.tsx
index c85d9b7dca75c..0377653ae3b64 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/notes/add_note/new_note.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/notes/add_note/new_note.test.tsx
@@ -7,8 +7,6 @@
import { mount, shallow } from 'enzyme';
import React from 'react';
-import * as i18n from '../translations';
-
import { NewNote } from './new_note';
describe('NewNote', () => {
@@ -21,36 +19,11 @@ describe('NewNote', () => {
expect(wrapper).toMatchSnapshot();
});
- test('it renders a tab labeled "Note"', () => {
- const wrapper = mount();
-
- expect(wrapper.find('button[role="tab"]').first().text()).toEqual(i18n.NOTE);
- });
-
- test('it renders a tab labeled "Preview (Markdown)"', () => {
- const wrapper = mount();
-
- expect(wrapper.find('button[role="tab"]').at(1).text()).toEqual(i18n.PREVIEW_MARKDOWN);
- });
-
- test('it renders the expected placeholder when a note is NOT provided', () => {
- const wrapper = mount();
-
- expect(wrapper.find(`textarea[placeholder="${i18n.ADD_A_NOTE}"]`).exists()).toEqual(true);
- });
-
test('it renders a text area containing the contents of a new (raw) note', () => {
const wrapper = mount();
- expect(wrapper.find('[data-test-subj="add-a-note"]').first().text()).toEqual(note);
- });
-
- test('it renders a markdown preview when the user clicks Preview (Markdown)', () => {
- const wrapper = mount();
-
- // click the preview tab:
- wrapper.find('button[role="tab"]').at(1).simulate('click');
-
- expect(wrapper.find('[data-test-subj="markdown-root"]').first().text()).toEqual(note);
+ expect(
+ wrapper.find('[data-test-subj="add-a-note"] .euiMarkdownEditorDropZone').first().text()
+ ).toEqual(note);
});
});
diff --git a/x-pack/plugins/security_solution/public/timelines/components/notes/add_note/new_note.tsx b/x-pack/plugins/security_solution/public/timelines/components/notes/add_note/new_note.tsx
index a91c5fc4ecdf3..4b51ab5acce69 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/notes/add_note/new_note.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/notes/add_note/new_note.tsx
@@ -4,74 +4,37 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { EuiPanel, EuiTabbedContent, EuiTextArea } from '@elastic/eui';
+import { EuiFlexItem } from '@elastic/eui';
import React from 'react';
import styled from 'styled-components';
-import { Markdown } from '../../../../common/components/markdown';
+import { MarkdownEditor } from '../../../../common/components/markdown_editor';
import { UpdateInternalNewNote } from '../helpers';
import * as i18n from '../translations';
-const NewNoteTabs = styled(EuiTabbedContent)`
+const NewNoteTabs = styled(EuiFlexItem)`
width: 100%;
`;
NewNoteTabs.displayName = 'NewNoteTabs';
-const MarkdownContainer = styled(EuiPanel)<{ height: number }>`
- height: ${({ height }) => height}px;
- overflow: auto;
-`;
-
-MarkdownContainer.displayName = 'MarkdownContainer';
-
-const TextArea = styled(EuiTextArea)<{ height: number }>`
- min-height: ${({ height }) => `${height}px`};
- width: 100%;
-`;
-
-TextArea.displayName = 'TextArea';
-
/** An input for entering a new note */
export const NewNote = React.memo<{
noteInputHeight: number;
note: string;
updateNewNote: UpdateInternalNewNote;
}>(({ note, noteInputHeight, updateNewNote }) => {
- const tabs = [
- {
- id: 'note',
- name: i18n.NOTE,
- content: (
-
diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json
index d4498a626ab9e..ef1a854c69f46 100644
--- a/x-pack/plugins/translations/translations/ja-JP.json
+++ b/x-pack/plugins/translations/translations/ja-JP.json
@@ -18014,17 +18014,6 @@
"xpack.securitySolution.lists.valueListsUploadError": "値リストのアップロードエラーが発生しました。",
"xpack.securitySolution.lists.valueListsUploadSuccess": "値リスト「{fileName}」はアップロードされませんでした",
"xpack.securitySolution.lists.valueListsUploadSuccessTitle": "値リストがアップロードされました",
- "xpack.securitySolution.markdown.hint.boldLabel": "**太字**",
- "xpack.securitySolution.markdown.hint.bulletLabel": "* ビュレット",
- "xpack.securitySolution.markdown.hint.codeLabel": "`code`",
- "xpack.securitySolution.markdown.hint.headingLabel": "# 見出し",
- "xpack.securitySolution.markdown.hint.imageUrlLabel": "",
- "xpack.securitySolution.markdown.hint.italicsLabel": "_italics_",
- "xpack.securitySolution.markdown.hint.preformattedLabel": "```preformatted```",
- "xpack.securitySolution.markdown.hint.quoteLabel": ">引用",
- "xpack.securitySolution.markdown.hint.strikethroughLabel": "取り消し線",
- "xpack.securitySolution.markdown.hint.urlLabel": "[link](url)",
- "xpack.securitySolution.markdown.toolTip.timelineId": "タイムラインID:{ timelineId }",
"xpack.securitySolution.markdownEditor.markdown": "マークダウン",
"xpack.securitySolution.markdownEditor.markdownInputHelp": "Markdown 構文ヘルプ",
"xpack.securitySolution.markdownEditor.plugins.timeline.insertTimelineButtonLabel": "タイムラインリンクの挿入",
diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json
index 5f1c72929b2c6..be7923a22395e 100644
--- a/x-pack/plugins/translations/translations/zh-CN.json
+++ b/x-pack/plugins/translations/translations/zh-CN.json
@@ -18033,17 +18033,6 @@
"xpack.securitySolution.lists.valueListsUploadError": "上传值列表时出错。",
"xpack.securitySolution.lists.valueListsUploadSuccess": "值列表“{fileName}”已上传",
"xpack.securitySolution.lists.valueListsUploadSuccessTitle": "值列表已上传",
- "xpack.securitySolution.markdown.hint.boldLabel": "**粗体**",
- "xpack.securitySolution.markdown.hint.bulletLabel": "* 项目符号",
- "xpack.securitySolution.markdown.hint.codeLabel": "`code`",
- "xpack.securitySolution.markdown.hint.headingLabel": "# 标题",
- "xpack.securitySolution.markdown.hint.imageUrlLabel": "",
- "xpack.securitySolution.markdown.hint.italicsLabel": "_斜体_",
- "xpack.securitySolution.markdown.hint.preformattedLabel": "```预设格式```",
- "xpack.securitySolution.markdown.hint.quoteLabel": ">引文",
- "xpack.securitySolution.markdown.hint.strikethroughLabel": "删除线",
- "xpack.securitySolution.markdown.hint.urlLabel": "[链接](url)",
- "xpack.securitySolution.markdown.toolTip.timelineId": "时间线 id:{ timelineId }",
"xpack.securitySolution.markdownEditor.markdown": "Markdown",
"xpack.securitySolution.markdownEditor.markdownInputHelp": "Markdown 语法帮助",
"xpack.securitySolution.markdownEditor.plugins.timeline.insertTimelineButtonLabel": "插入时间线链接",