[Security Solution] make copytoclipboard component more generic (#170700)

This commit is contained in:
Philippe Oberti 2023-11-14 16:23:51 -06:00 committed by GitHub
parent 480fcef698
commit d1f5a070c8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 119 additions and 324 deletions

View file

@ -7,10 +7,10 @@
import type { VFC } from 'react';
import React, { memo } from 'react';
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { EuiButtonIcon, EuiCopy, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { copyFunction } from '../../../shared/utils/copy_to_clipboard';
import { FLYOUT_URL_PARAM } from '../../shared/hooks/url/use_sync_flyout_state_with_url';
import { CopyToClipboard } from '../../../shared/components/copy_to_clipboard';
import { useGetAlertDetailsFlyoutLink } from '../../../../timelines/components/side_panel/event_details/use_get_alert_details_flyout_link';
import { useBasicDataFromDetailsData } from '../../../../timelines/components/side_panel/event_details/helpers';
import { useRightPanelContext } from '../context';
@ -31,26 +31,32 @@ export const HeaderActions: VFC = memo(() => {
const showShareAlertButton = isAlert && alertDetailsLink;
const modifier = (value: string) => {
const query = new URLSearchParams(window.location.search);
return `${value}&${FLYOUT_URL_PARAM}=${query.get(FLYOUT_URL_PARAM)}`;
};
return (
<EuiFlexGroup direction="row" justifyContent="flexEnd">
{showShareAlertButton && (
<EuiFlexItem grow={false}>
<CopyToClipboard
rawValue={alertDetailsLink}
modifier={(value: string) => {
const query = new URLSearchParams(window.location.search);
return `${value}&${FLYOUT_URL_PARAM}=${query.get(FLYOUT_URL_PARAM)}`;
}}
iconType={'share'}
color={'text'}
ariaLabel={i18n.translate(
'xpack.securitySolution.flyout.right.header.shareButtonAriaLabel',
{
defaultMessage: 'Share Alert',
}
<EuiCopy textToCopy={alertDetailsLink}>
{(copy) => (
<EuiButtonIcon
iconType={'share'}
color={'text'}
aria-label={i18n.translate(
'xpack.securitySolution.flyout.right.header.shareButtonAriaLabel',
{
defaultMessage: 'Share Alert',
}
)}
data-test-subj={SHARE_BUTTON_TEST_ID}
onClick={() => copyFunction(copy, alertDetailsLink, modifier)}
onKeyDown={() => copyFunction(copy, alertDetailsLink, modifier)}
/>
)}
data-test-subj={SHARE_BUTTON_TEST_ID}
/>
</EuiCopy>
</EuiFlexItem>
)}
</EuiFlexGroup>

View file

@ -8,10 +8,10 @@
import type { FC } from 'react';
import React, { memo, useEffect, useRef, useState } from 'react';
import { JsonCodeEditor } from '@kbn/unified-doc-viewer-plugin/public';
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { EuiButtonEmpty, EuiCopy, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import { CopyToClipboard } from '../../../shared/components/copy_to_clipboard';
import { copyFunction } from '../../../shared/utils/copy_to_clipboard';
import { JSON_TAB_CONTENT_TEST_ID, JSON_TAB_COPY_TO_CLIPBOARD_BUTTON_TEST_ID } from './test_ids';
import { useRightPanelContext } from '../context';
@ -48,31 +48,35 @@ export const JsonTab: FC = memo(() => {
return (
<EuiFlexGroup
ref={flexGroupElement}
direction={'column'}
gutterSize={'none'}
direction="column"
gutterSize="none"
data-test-subj={JSON_TAB_CONTENT_TEST_ID}
>
<EuiFlexItem>
<EuiFlexGroup justifyContent={'flexEnd'}>
<EuiFlexItem grow={false}>
<CopyToClipboard
rawValue={jsonValue}
text={
<FormattedMessage
id="xpack.securitySolution.flyout.right.jsonTab.copyToClipboardButtonLabel"
defaultMessage="Copy to clipboard"
/>
}
iconType={'copyClipboard'}
size={'xs'}
ariaLabel={i18n.translate(
'xpack.securitySolution.flyout.right.jsonTab.copyToClipboardButtonAriaLabel',
{
defaultMessage: 'Copy to clipboard',
}
<EuiCopy textToCopy={jsonValue}>
{(copy) => (
<EuiButtonEmpty
iconType={'copyClipboard'}
size={'xs'}
aria-label={i18n.translate(
'xpack.securitySolution.flyout.right.jsonTab.copyToClipboardButtonAriaLabel',
{
defaultMessage: 'Copy to clipboard',
}
)}
data-test-subj={JSON_TAB_COPY_TO_CLIPBOARD_BUTTON_TEST_ID}
onClick={() => copyFunction(copy, jsonValue)}
onKeyDown={() => copyFunction(copy, jsonValue)}
>
<FormattedMessage
id="xpack.securitySolution.flyout.right.jsonTab.copyToClipboardButtonLabel"
defaultMessage="Copy to clipboard"
/>
</EuiButtonEmpty>
)}
data-test-subj={JSON_TAB_COPY_TO_CLIPBOARD_BUTTON_TEST_ID}
/>
</EuiCopy>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>

View file

@ -1,125 +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
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import type { Story } from '@storybook/react';
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { CopyToClipboard } from './copy_to_clipboard';
export default {
component: CopyToClipboard,
title: 'Flyout/CopyToClipboard',
};
const json = JSON.stringify({
foo: 'bar',
});
export const Default: Story<void> = () => {
return (
<CopyToClipboard
rawValue={json}
text={<p>{'Copy'}</p>}
iconType={'copyClipboard'}
ariaLabel={'Copy'}
/>
);
};
export const WithModifier: Story<void> = () => {
return (
<CopyToClipboard
rawValue={json}
modifier={(value) => {
window.alert('modifier');
return value;
}}
text={<p>{'Copy'}</p>}
iconType={'copyClipboard'}
ariaLabel={'Copy'}
/>
);
};
export const MultipleSizes: Story<void> = () => {
return (
<EuiFlexGroup direction="column" gutterSize="none">
<EuiFlexItem grow={false}>
<CopyToClipboard
rawValue={json}
text={<p>{'xs size'}</p>}
iconType={'copyClipboard'}
size={'xs'}
ariaLabel={'Copy'}
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<CopyToClipboard
rawValue={json}
text={<p>{'s size'}</p>}
iconType={'copyClipboard'}
size={'s'}
ariaLabel={'Copy'}
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<CopyToClipboard
rawValue={json}
text={<p>{'m size'}</p>}
iconType={'copyClipboard'}
size={'m'}
ariaLabel={'Copy'}
/>
</EuiFlexItem>
</EuiFlexGroup>
);
};
export const ButtonOnly: Story<void> = () => {
return (
<CopyToClipboard
rawValue={json}
modifier={(value) => {
window.alert('modifier');
return value;
}}
iconType={'copyClipboard'}
ariaLabel={'Copy'}
/>
);
};
export const CustomColor: Story<void> = () => {
return (
<CopyToClipboard
rawValue={json}
modifier={(value) => {
window.alert('modifier');
return value;
}}
iconType={'copyClipboard'}
ariaLabel={'Copy'}
text={<p>{'showing custom color'}</p>}
color={'accent'}
/>
);
};
export const CustomIcon: Story<void> = () => {
return (
<CopyToClipboard
rawValue={json}
modifier={(value) => {
window.alert('modifier');
return value;
}}
iconType={'share'}
ariaLabel={'Share'}
text={<p>{'custom icon'}</p>}
/>
);
};

View file

@ -1,72 +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
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { __IntlProvider as IntlProvider } from '@kbn/i18n-react';
import { render } from '@testing-library/react';
import React from 'react';
import type { CopyToClipboardProps } from './copy_to_clipboard';
import { CopyToClipboard } from './copy_to_clipboard';
jest.mock('@elastic/eui', () => ({
...jest.requireActual('@elastic/eui'),
copyToClipboard: jest.fn(),
EuiCopy: jest.fn(({ children: functionAsChild }) => functionAsChild(jest.fn())),
}));
const renderShareButton = (props: CopyToClipboardProps) =>
render(
<IntlProvider locale="en">
<CopyToClipboard {...props} />
</IntlProvider>
);
describe('ShareButton', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('should render the copy to clipboard button', () => {
const text = 'text';
const props = {
rawValue: 'rawValue',
text: <span>{text}</span>,
iconType: 'iconType',
ariaLabel: 'ariaLabel',
'data-test-subj': 'data-test-subj',
};
const { getByTestId, getByText } = renderShareButton(props);
const button = getByTestId('data-test-subj');
expect(button).toBeInTheDocument();
expect(button).toHaveAttribute('aria-label', props.ariaLabel);
expect(button).toHaveAttribute('type', 'button');
expect(getByText(text)).toBeInTheDocument();
});
it('should use modifier if provided', () => {
const modifiedFc = jest.fn();
const props = {
rawValue: 'rawValue',
modifier: modifiedFc,
text: <span>{'text'}</span>,
iconType: 'iconType',
ariaLabel: 'ariaLabel',
'data-test-subj': 'data-test-subj',
};
const { getByTestId } = renderShareButton(props);
const button = getByTestId('data-test-subj');
button.click();
expect(modifiedFc).toHaveBeenCalledWith(props.rawValue);
});
});

View file

@ -1,88 +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
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import type { EuiButtonEmptyProps } from '@elastic/eui';
import { copyToClipboard, EuiButtonEmpty, EuiCopy } from '@elastic/eui';
import type { FC, ReactElement } from 'react';
import React from 'react';
export interface CopyToClipboardProps {
/**
* Value to save to the clipboard
*/
rawValue: string;
/**
* Function to modify the raw value before saving to the clipboard
*/
modifier?: (rawValue: string) => string;
/**
* Button main text (next to icon)
*/
text?: ReactElement;
/**
* Icon name (value coming from EUI)
*/
iconType: EuiButtonEmptyProps['iconType'];
/**
* Button size (values coming from EUI)
*/
size?: EuiButtonEmptyProps['size'];
/**
* Optional button color
*/
color?: EuiButtonEmptyProps['color'];
/**
* Aria label value for the button
*/
ariaLabel: string;
/**
Data test subject string for testing
*/
['data-test-subj']?: string;
}
/**
* Copy to clipboard component
*/
export const CopyToClipboard: FC<CopyToClipboardProps> = ({
rawValue,
modifier,
text,
iconType,
size = 'm',
color = 'primary',
ariaLabel,
'data-test-subj': dataTestSubj,
}) => {
return (
<EuiCopy textToCopy={rawValue}>
{(copy) => (
<EuiButtonEmpty
onClick={() => {
copy();
if (modifier) {
const modifiedCopyValue = modifier(rawValue);
copyToClipboard(modifiedCopyValue);
} else {
copyToClipboard(rawValue);
}
}}
iconType={iconType}
size={size}
color={color}
aria-label={ariaLabel}
data-test-subj={dataTestSubj}
>
{text}
</EuiButtonEmpty>
)}
</EuiCopy>
);
};
CopyToClipboard.displayName = 'CopyToClipboard';

View file

@ -105,7 +105,7 @@ export const FlyoutNavigation: FC<PanelNavigationProps> = memo(
responsive={false}
css={css`
padding-left: ${euiTheme.size.s};
padding-right: ${euiTheme.size.l};
padding-right: ${euiTheme.size.xl};
height: ${euiTheme.size.xxl};
`}
>

View file

@ -0,0 +1,39 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { copyFunction } from './copy_to_clipboard';
jest.mock('@elastic/eui', () => ({
...jest.requireActual('@elastic/eui'),
copyToClipboard: jest.fn(),
EuiCopy: jest.fn(({ children: functionAsChild }) => functionAsChild(jest.fn())),
}));
describe('copyFunction', () => {
beforeEach(() => {
jest.clearAllMocks();
});
const rawValue = 'rawValue';
it('should call copy function', () => {
const euiCopy = jest.fn();
copyFunction(euiCopy, rawValue);
expect(euiCopy).toHaveBeenCalled();
});
it('should call modifier function if passed', () => {
const euiCopy = jest.fn();
const modifiedFc = jest.fn();
copyFunction(euiCopy, rawValue, modifiedFc);
expect(modifiedFc).toHaveBeenCalledWith(rawValue);
});
});

View file

@ -0,0 +1,31 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { copyToClipboard } from '@elastic/eui';
/**
* Copy to clipboard wrapper component. It allows adding a copy to clipboard functionality to any element.
* It expects the value to be copied with an optional function to modify the value if necessary.
*
* @param copy the copy method from EuiCopy
* @param rawValue the value to save to the clipboard
* @param modifier a function to modify the raw value before saving to the clipboard
*/
export const copyFunction = (
copy: Function,
rawValue: string,
modifier?: (rawValue: string) => string
) => {
copy();
if (modifier) {
const modifiedCopyValue = modifier(rawValue);
copyToClipboard(modifiedCopyValue);
} else {
copyToClipboard(rawValue);
}
};