From a73e977aa274c92bd0e5e328cc88b96c23115018 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Tue, 20 Oct 2020 11:06:11 +0300 Subject: [PATCH] Open links in new tab & disable links if needed --- .../markdown_editor/markdown_link.tsx | 36 +++++++++++ .../markdown_editor/plugins/index.ts | 2 + .../markdown_editor/renderer.test.tsx | 59 +++++++++++++++++++ .../components/markdown_editor/renderer.tsx | 24 ++++++-- .../components/recent_cases/recent_cases.tsx | 2 +- .../note_previews/note_preview.tsx | 2 +- 6 files changed, 119 insertions(+), 6 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/common/components/markdown_editor/markdown_link.tsx create mode 100644 x-pack/plugins/security_solution/public/common/components/markdown_editor/renderer.test.tsx 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..e8d5e2635de24 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/markdown_editor/markdown_link.tsx @@ -0,0 +1,36 @@ +/* + * 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 } from '@elastic/eui'; + +type MarkdownLinkProps = { disableLinks?: boolean } & EuiLinkAnchorProps; + +const MarkdownLinkComponent: React.FC = ({ + disableLinks, + href, + target, + children, + ...props +}) => ( + <> + {disableLinks ? ( + {children} + ) : ( + + {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 index eda2ee9b9f5f3..b3d91d26e50da 100644 --- 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 @@ -16,4 +16,6 @@ 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..206ab8ddf13d1 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/markdown_editor/renderer.test.tsx @@ -0,0 +1,59 @@ +/* + * 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"]').exists()).toBeFalsy(); + }); + + 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 index 28ca3f075a7ac..7a7693512afbe 100644 --- 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 @@ -4,18 +4,34 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { memo } from 'react'; -import { EuiMarkdownFormat } from '@elastic/eui'; +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 }) => { +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} ); 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 bee247a373d02..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 @@ -52,7 +52,7 @@ const RecentCasesComponent = ({ cases }: { cases: Case[] }) => { {c.description && c.description.length && ( - {c.description} + {c.description} )} diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/note_previews/note_preview.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/note_previews/note_preview.tsx index 32e7ea5968d12..a8e7a2c465e0c 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/note_previews/note_preview.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/note_previews/note_preview.tsx @@ -59,7 +59,7 @@ export const NotePreview = React.memo - {note || ''} + {note ?? ''}