Skip to content

Commit 5bf7966

Browse files
authored
[Security Solution][Resolver] Replace copy-to-clipboard with native navigator.clipboard (#80193)
1 parent 6ab4be5 commit 5bf7966

File tree

3 files changed

+47
-19
lines changed

3 files changed

+47
-19
lines changed

x-pack/plugins/security_solution/public/resolver/view/panel.test.tsx

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
* you may not use this file except in compliance with the Elastic License.
55
*/
66
import { createMemoryHistory, History as HistoryPackageHistoryInterface } from 'history';
7-
import copy from 'copy-to-clipboard';
87
import { noAncestorsTwoChildrenWithRelatedEventsOnOrigin } from '../data_access_layer/mocks/no_ancestors_two_children_with_related_events_on_origin';
98
import { Simulator } from '../test_utilities/simulator';
109
// Extend jest with a custom matcher
@@ -14,10 +13,6 @@ import { urlSearch } from '../test_utilities/url_search';
1413
// the resolver component instance ID, used by the react code to distinguish piece of global state from those used by other resolver instances
1514
const resolverComponentInstanceID = 'resolverComponentInstanceID';
1615

17-
jest.mock('copy-to-clipboard', () => {
18-
return jest.fn();
19-
});
20-
2116
describe(`Resolver: when analyzing a tree with no ancestors and two children and two related registry event on the origin, and when the component instance ID is ${resolverComponentInstanceID}`, () => {
2217
/**
2318
* Get (or lazily create and get) the simulator.
@@ -121,8 +116,8 @@ describe(`Resolver: when analyzing a tree with no ancestors and two children and
121116

122117
copyableFields?.map((copyableField) => {
123118
copyableField.simulate('mouseenter');
124-
simulator().testSubject('clipboard').last().simulate('click');
125-
expect(copy).toHaveBeenLastCalledWith(copyableField.text(), expect.any(Object));
119+
simulator().testSubject('resolver:panel:clipboard').last().simulate('click');
120+
expect(navigator.clipboard.writeText).toHaveBeenCalledWith(copyableField.text());
126121
copyableField.simulate('mouseleave');
127122
});
128123
});
@@ -179,8 +174,8 @@ describe(`Resolver: when analyzing a tree with no ancestors and two children and
179174

180175
copyableFields?.map((copyableField) => {
181176
copyableField.simulate('mouseenter');
182-
simulator().testSubject('clipboard').last().simulate('click');
183-
expect(copy).toHaveBeenLastCalledWith(copyableField.text(), expect.any(Object));
177+
simulator().testSubject('resolver:panel:clipboard').last().simulate('click');
178+
expect(navigator.clipboard.writeText).toHaveBeenCalledWith(copyableField.text());
184179
copyableField.simulate('mouseleave');
185180
});
186181
});
@@ -288,8 +283,8 @@ describe(`Resolver: when analyzing a tree with no ancestors and two children and
288283

289284
copyableFields?.map((copyableField) => {
290285
copyableField.simulate('mouseenter');
291-
simulator().testSubject('clipboard').last().simulate('click');
292-
expect(copy).toHaveBeenLastCalledWith(copyableField.text(), expect.any(Object));
286+
simulator().testSubject('resolver:panel:clipboard').last().simulate('click');
287+
expect(navigator.clipboard.writeText).toHaveBeenCalledWith(copyableField.text());
293288
copyableField.simulate('mouseleave');
294289
});
295290
});

x-pack/plugins/security_solution/public/resolver/view/panels/copyable_panel_field.tsx

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,11 @@
66

77
/* eslint-disable react/display-name */
88

9-
import { EuiToolTip, EuiPopover } from '@elastic/eui';
9+
import { EuiToolTip, EuiButtonIcon, EuiPopover } from '@elastic/eui';
1010
import { i18n } from '@kbn/i18n';
1111
import styled from 'styled-components';
12-
import React, { memo, useState } from 'react';
13-
import { WithCopyToClipboard } from '../../../common/lib/clipboard/with_copy_to_clipboard';
12+
import React, { memo, useState, useCallback } from 'react';
13+
import { useKibana } from '../../../../../../../src/plugins/kibana_react/public';
1414
import { useColors } from '../use_colors';
1515
import { StyledPanel } from '../styles';
1616

@@ -43,8 +43,10 @@ export const CopyablePanelField = memo(
4343
({ textToCopy, content }: { textToCopy: string; content: JSX.Element | string }) => {
4444
const { linkColor, copyableFieldBackground } = useColors();
4545
const [isOpen, setIsOpen] = useState(false);
46+
const toasts = useKibana().services.notifications?.toasts;
4647

4748
const onMouseEnter = () => setIsOpen(true);
49+
const onMouseLeave = () => setIsOpen(false);
4850

4951
const ButtonContent = memo(() => (
5052
<StyledCopyableField
@@ -57,7 +59,22 @@ export const CopyablePanelField = memo(
5759
</StyledCopyableField>
5860
));
5961

60-
const onMouseLeave = () => setIsOpen(false);
62+
const onClick = useCallback(
63+
async (event: React.MouseEvent<HTMLButtonElement>) => {
64+
try {
65+
await navigator.clipboard.writeText(textToCopy);
66+
} catch (error) {
67+
if (toasts) {
68+
toasts.addError(error, {
69+
title: i18n.translate('xpack.securitySolution.resolver.panel.copyFailureTitle', {
70+
defaultMessage: 'Copy Failure',
71+
}),
72+
});
73+
}
74+
}
75+
},
76+
[textToCopy, toasts]
77+
);
6178

6279
return (
6380
<div onMouseLeave={onMouseLeave}>
@@ -74,10 +91,14 @@ export const CopyablePanelField = memo(
7491
defaultMessage: 'Copy to Clipboard',
7592
})}
7693
>
77-
<WithCopyToClipboard
78-
data-test-subj="resolver:panel:copy-to-clipboard"
79-
text={textToCopy}
80-
titleSummary={textToCopy}
94+
<EuiButtonIcon
95+
aria-label={i18n.translate('xpack.securitySolution.resolver.panel.copyToClipboard', {
96+
defaultMessage: 'Copy to Clipboard',
97+
})}
98+
color="text"
99+
data-test-subj="resolver:panel:clipboard"
100+
iconType="copyClipboard"
101+
onClick={onClick}
81102
/>
82103
</EuiToolTip>
83104
</EuiPopover>

x-pack/plugins/security_solution/public/resolver/view/side_effect_simulator_factory.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,18 @@ export const sideEffectSimulatorFactory: () => SideEffectSimulator = () => {
6666
return contentRectForElement(this);
6767
});
6868

69+
/**
70+
* Mock the global writeText method as it is not available in jsDOM and alows us to track what was copied
71+
*/
72+
const MockClipboard: Clipboard = {
73+
writeText: jest.fn(),
74+
readText: jest.fn(),
75+
addEventListener: jest.fn(),
76+
dispatchEvent: jest.fn(),
77+
removeEventListener: jest.fn(),
78+
};
79+
// @ts-ignore navigator doesn't natively exist on global
80+
global.navigator.clipboard = MockClipboard;
6981
/**
7082
* A mock implementation of `ResizeObserver` that works with our fake `getBoundingClientRect` and `simulateElementResize`
7183
*/

0 commit comments

Comments
 (0)