UI/ClipboardButton: Remove ClipboardJS in favor of native Clipboard API (#42996)

* UI/ClipboardButton: Remove ClipboardJS in favor of native Clipboard API
Closes #42365

* Add backwards compatibility
This commit is contained in:
kay delaney 2022-01-07 13:41:09 +00:00 committed by GitHub
parent 4eacdf5f9e
commit 36e4a871f2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 55 additions and 224 deletions

View File

@ -102,7 +102,6 @@
"@types/angular": "1.8.3",
"@types/angular-route": "1.7.0",
"@types/classnames": "2.3.0",
"@types/clipboard": "2.0.1",
"@types/common-tags": "^1.8.0",
"@types/d3": "7.1.0",
"@types/d3-force": "^2.1.0",
@ -277,7 +276,6 @@
"calculate-size": "1.1.1",
"centrifuge": "2.8.4",
"classnames": "2.3.1",
"clipboard": "2.0.4",
"comlink": "4.3.1",
"common-tags": "^1.8.0",
"core-js": "3.20.0",

View File

@ -48,7 +48,6 @@
"ansicolor": "1.1.95",
"calculate-size": "1.1.1",
"classnames": "2.3.1",
"clipboard": "2.0.4",
"core-js": "3.20.0",
"d3": "5.15.0",
"date-fns": "2.28.0",
@ -119,7 +118,6 @@
"@testing-library/react-hooks": "7.0.2",
"@testing-library/user-event": "13.5.0",
"@types/classnames": "2.3.0",
"@types/clipboard": "2.0.1",
"@types/common-tags": "^1.8.0",
"@types/d3": "7.1.0",
"@types/enzyme": "3.10.5",

View File

@ -1,51 +1,44 @@
import React, { PureComponent } from 'react';
import Clipboard from 'clipboard';
import React, { useCallback, useRef } from 'react';
import { Button, ButtonProps } from '../Button';
/** @deprecated Will be removed in next major release */
interface ClipboardEvent {
action: string;
text: string;
trigger: Element;
clearSelection(): void;
}
export interface Props extends ButtonProps {
/** A function that returns text to be copied */
getText(): string;
/** Callback when the text has been successfully copied */
onClipboardCopy?(e: Clipboard.Event): void;
onClipboardCopy?(e: ClipboardEvent): void;
/** Callback when there was an error copying the text */
onClipboardError?(e: Clipboard.Event): void;
onClipboardError?(e: ClipboardEvent): void;
}
export class ClipboardButton extends PureComponent<Props> {
private clipboard!: Clipboard;
private elem!: HTMLButtonElement;
const dummyClearFunc = () => {};
setRef = (elem: HTMLButtonElement) => {
this.elem = elem;
};
export function ClipboardButton({ onClipboardCopy, onClipboardError, children, getText, ...buttonProps }: Props) {
// Can be removed in 9.x
const buttonRef = useRef<null | HTMLButtonElement>(null);
const copyText = useCallback(() => {
const copiedText = getText();
const dummyEvent: ClipboardEvent = {
action: 'copy',
clearSelection: dummyClearFunc,
text: copiedText,
trigger: buttonRef.current!,
};
navigator.clipboard
.writeText(copiedText)
.then(() => (onClipboardCopy?.(dummyEvent), () => onClipboardError?.(dummyEvent)));
}, [getText, onClipboardCopy, onClipboardError]);
componentDidMount() {
const { getText, onClipboardCopy, onClipboardError } = this.props;
this.clipboard = new Clipboard(this.elem, {
text: () => getText(),
});
this.clipboard.on('success', (e: Clipboard.Event) => {
onClipboardCopy && onClipboardCopy(e);
});
this.clipboard.on('error', (e: Clipboard.Event) => {
onClipboardError && onClipboardError(e);
});
}
componentWillUnmount() {
this.clipboard.destroy();
}
render() {
const { getText, onClipboardCopy, onClipboardError, children, ...buttonProps } = this.props;
return (
<Button {...buttonProps} ref={this.setRef}>
{children}
</Button>
);
}
return (
<Button onClick={copyText} {...buttonProps} ref={buttonRef}>
{children}
</Button>
);
}

View File

@ -7,6 +7,7 @@ import EmptyListCTA from '../core/components/EmptyListCTA/EmptyListCTA';
import { TagFilter } from '../core/components/TagFilter/TagFilter';
import { MetricSelect } from '../core/components/Select/MetricSelect';
import {
ClipboardButton,
ColorPicker,
DataLinksInlineEditor,
DataSourceHttpSettings,
@ -200,4 +201,8 @@ export function registerAngularDirectives() {
['datasource', { watchDepth: 'reference' }],
'onChange',
]);
react2AngularDirective('clipboardButton', ClipboardButton, [
['getText', { watchDepth: 'reference', wrapApply: true }],
]);
}

View File

@ -1,8 +1,5 @@
import angular from 'angular';
import Clipboard from 'clipboard';
import coreModule from './core_module';
import { appEvents } from 'app/core/core';
import { AppEvents } from '@grafana/data';
/** @ngInject */
function tip($compile: any) {
@ -23,31 +20,6 @@ function tip($compile: any) {
};
}
function clipboardButton() {
return {
scope: {
getText: '&clipboardButton',
},
link: (scope: any, elem: any) => {
scope.clipboard = new Clipboard(elem[0], {
text: () => {
return scope.getText();
},
});
scope.clipboard.on('success', () => {
appEvents.emit(AppEvents.alertSuccess, ['Content copied to clipboard']);
});
scope.$on('$destroy', () => {
if (scope.clipboard) {
scope.clipboard.destroy();
}
});
},
};
}
/** @ngInject */
function compile($compile: any) {
return {
@ -209,7 +181,6 @@ function gfDropdown($parse: any, $compile: any, $timeout: any) {
}
coreModule.directive('tip', tip);
coreModule.directive('clipboardButton', clipboardButton);
coreModule.directive('compile', compile);
coreModule.directive('watchChange', watchChange);
coreModule.directive('editorOptBool', editorOptBool);

View File

@ -1,78 +0,0 @@
import React, { PureComponent, ReactNode } from 'react';
import ClipboardJS from 'clipboard';
interface Props {
text: () => string;
elType?: string | React.ForwardRefExoticComponent<any>;
onSuccess?: (evt: any) => void;
onError?: (evt: any) => void;
className?: string;
children?: ReactNode;
}
export class CopyToClipboard extends PureComponent<Props> {
clipboardjs?: ClipboardJS;
myRef: any;
constructor(props: Props) {
super(props);
this.myRef = React.createRef();
}
componentDidMount() {
this.initClipboardJS();
}
componentDidUpdate() {
if (this.clipboardjs) {
this.clipboardjs.destroy();
}
this.initClipboardJS();
}
initClipboardJS = () => {
const { text, onSuccess, onError } = this.props;
this.clipboardjs = new ClipboardJS(this.myRef.current, {
text: text,
});
if (onSuccess) {
this.clipboardjs.on('success', (evt) => {
evt.clearSelection();
onSuccess(evt);
});
}
if (onError) {
this.clipboardjs.on('error', (evt) => {
console.error('Action:', evt.action);
console.error('Trigger:', evt.trigger);
onError(evt);
});
}
};
componentWillUnmount() {
if (this.clipboardjs) {
this.clipboardjs.destroy();
}
}
getElementType = () => {
return this.props.elType || 'button';
};
render() {
const { elType, text, children, onError, onSuccess, ...restProps } = this.props;
return React.createElement(
this.getElementType(),
{
ref: this.myRef,
...restProps,
},
this.props.children
);
}
}

View File

@ -1,7 +1,6 @@
import React, { PureComponent } from 'react';
import { LoadingPlaceholder, JSONFormatter, Icon, HorizontalGroup } from '@grafana/ui';
import { LoadingPlaceholder, JSONFormatter, Icon, HorizontalGroup, ClipboardButton } from '@grafana/ui';
import appEvents from 'app/core/app_events';
import { CopyToClipboard } from 'app/core/components/CopyToClipboard/CopyToClipboard';
import { DashboardModel, PanelModel } from '../dashboard/state';
import { getBackendSrv } from '@grafana/runtime';
import { AppEvents } from '@grafana/data';
@ -107,9 +106,9 @@ export class TestRuleResult extends PureComponent<Props, State> {
<div className="pull-right">
<HorizontalGroup spacing="md">
<div onClick={this.onToggleExpand}>{this.renderExpandCollapse()}</div>
<CopyToClipboard elType="div" text={this.getTextForClipboard} onSuccess={this.onClipboardSuccess}>
<Icon name="copy" /> Copy to Clipboard
</CopyToClipboard>
<ClipboardButton getText={this.getTextForClipboard} onClipboardCopy={this.onClipboardSuccess} icon="copy">
Copy to Clipboard
</ClipboardButton>
</HorizontalGroup>
</div>

View File

@ -1,8 +1,7 @@
import React, { useCallback, useState } from 'react';
import { css } from '@emotion/css';
import { saveAs } from 'file-saver';
import { Button, Modal, stylesFactory, TextArea, useTheme } from '@grafana/ui';
import { CopyToClipboard } from 'app/core/components/CopyToClipboard/CopyToClipboard';
import { Button, ClipboardButton, Modal, stylesFactory, TextArea, useTheme } from '@grafana/ui';
import { SaveDashboardFormProps } from '../types';
import { AppEvents, GrafanaTheme } from '@grafana/data';
import appEvents from '../../../../../core/app_events';
@ -61,9 +60,9 @@ export const SaveProvisionedDashboardForm: React.FC<SaveDashboardFormProps> = ({
<Button variant="secondary" onClick={onCancel} fill="outline">
Cancel
</Button>
<CopyToClipboard text={() => dashboardJSON} elType={Button} onSuccess={onCopyToClipboardSuccess}>
<ClipboardButton getText={() => dashboardJSON} onClipboardCopy={onCopyToClipboardSuccess}>
Copy JSON to clipboard
</CopyToClipboard>
</ClipboardButton>
<Button onClick={saveToFile}>Save JSON to file</Button>
</Modal.ButtonRow>
</div>

View File

@ -1,10 +1,9 @@
import React, { PureComponent } from 'react';
import { Button, JSONFormatter, LoadingPlaceholder } from '@grafana/ui';
import { Button, ClipboardButton, JSONFormatter, LoadingPlaceholder } from '@grafana/ui';
import { selectors } from '@grafana/e2e-selectors';
import { AppEvents, DataFrame } from '@grafana/data';
import appEvents from 'app/core/app_events';
import { CopyToClipboard } from 'app/core/components/CopyToClipboard/CopyToClipboard';
import { PanelModel } from 'app/features/dashboard/state';
import { getPanelInspectorStyles } from './styles';
import { supportsDataQuery } from 'app/features/dashboard/components/PanelEditor/utils';
@ -294,16 +293,15 @@ export class QueryInspector extends PureComponent<Props, State> {
)}
{haveData && (
<CopyToClipboard
text={this.getTextForClipboard}
onSuccess={this.onClipboardSuccess}
elType="div"
<ClipboardButton
getText={this.getTextForClipboard}
onClipboardCopy={this.onClipboardSuccess}
className={styles.toolbarItem}
icon="copy"
variant="secondary"
>
<Button icon="copy" variant="secondary">
Copy to clipboard
</Button>
</CopyToClipboard>
Copy to clipboard
</ClipboardButton>
)}
<div className="flex-grow-1" />
</div>

View File

@ -3808,7 +3808,6 @@ __metadata:
"@testing-library/react-hooks": 7.0.2
"@testing-library/user-event": 13.5.0
"@types/classnames": 2.3.0
"@types/clipboard": 2.0.1
"@types/common-tags": ^1.8.0
"@types/d3": 7.1.0
"@types/enzyme": 3.10.5
@ -3844,7 +3843,6 @@ __metadata:
babel-loader: 8.2.2
calculate-size: 1.1.1
classnames: 2.3.1
clipboard: 2.0.4
common-tags: ^1.8.0
core-js: 3.20.0
css-loader: 6.5.1
@ -5412,7 +5410,7 @@ __metadata:
ramda: ^0.27.1
peerDependencies:
"@babel/core": ^7.0.0
babel-plugin-macros: 2 || 3
babel-plugin-macros: 2 || 3
typescript: 2 || 3 || 4
bin:
lingui: lingui.js
@ -5467,7 +5465,7 @@ __metadata:
"@lingui/conf": ^3.12.1
ramda: ^0.27.1
peerDependencies:
babel-plugin-macros: 2 || 3
babel-plugin-macros: 2 || 3
checksum: 83384f2d796dbc7ecadb74a1c6159ccad82976c194f97b82c2ba72cc8113f95bf0193aa6a4e9596db25b713129a9037a873bf1282bc30e197c2a72855707162f
languageName: node
linkType: hard
@ -5480,7 +5478,7 @@ __metadata:
"@lingui/conf": ^3.13.0
ramda: ^0.27.1
peerDependencies:
babel-plugin-macros: 2 || 3
babel-plugin-macros: 2 || 3
checksum: 5b80f4fe162e2be76462a124d267d97a7b7c5eacbf966b603e44b06010b016235366ebfa426473262bd72caffc1b03b351743a4cf909c8a77e4319e419abab07
languageName: node
linkType: hard
@ -8676,13 +8674,6 @@ __metadata:
languageName: node
linkType: hard
"@types/clipboard@npm:2.0.1":
version: 2.0.1
resolution: "@types/clipboard@npm:2.0.1"
checksum: 4ef5af19ac1055693a313194396e00a3f42326b45dd5ae90a2bb47a643957f313ce864c430f387fb16c78c9cd2cde44af84f0f7fd9f2695674a75161a7016937
languageName: node
linkType: hard
"@types/color-convert@npm:^2.0.0":
version: 2.0.0
resolution: "@types/color-convert@npm:2.0.0"
@ -14036,17 +14027,6 @@ __metadata:
languageName: node
linkType: hard
"clipboard@npm:2.0.4":
version: 2.0.4
resolution: "clipboard@npm:2.0.4"
dependencies:
good-listener: ^1.2.2
select: ^1.1.2
tiny-emitter: ^2.0.0
checksum: 2f44556f713e940a69a20f95dde9ebc4dc7f6ec121562496a5358eb1893583f6554c64b8a54051cb1d3f637df71efd035afd9baf0b28d8b8799803e8556545db
languageName: node
linkType: hard
"cliui@npm:^6.0.0":
version: 6.0.0
resolution: "cliui@npm:6.0.0"
@ -16330,13 +16310,6 @@ __metadata:
languageName: node
linkType: hard
"delegate@npm:^3.1.2":
version: 3.2.0
resolution: "delegate@npm:3.2.0"
checksum: d943058fe05897228b158cbd1bab05164df28c8f54127873231d6b03b0a5acc1b3ee1f98ac70ccc9b79cd84aa47118a7de111fee2923753491583905069da27d
languageName: node
linkType: hard
"delegates@npm:^1.0.0":
version: 1.0.0
resolution: "delegates@npm:1.0.0"
@ -19486,15 +19459,6 @@ __metadata:
languageName: node
linkType: hard
"good-listener@npm:^1.2.2":
version: 1.2.2
resolution: "good-listener@npm:1.2.2"
dependencies:
delegate: ^3.1.2
checksum: f39fb82c4e41524f56104cfd2d7aef1a88e72f3f75139115fbdf98cc7d844e0c1b39218b2e83438c6188727bf904ed78c7f0f2feff67b32833bc3af7f0202b33
languageName: node
linkType: hard
"got@npm:^6.7.1":
version: 6.7.1
resolution: "got@npm:6.7.1"
@ -19592,7 +19556,6 @@ __metadata:
"@types/angular": 1.8.3
"@types/angular-route": 1.7.0
"@types/classnames": 2.3.0
"@types/clipboard": 2.0.1
"@types/common-tags": ^1.8.0
"@types/d3": 7.1.0
"@types/d3-force": ^2.1.0
@ -19667,7 +19630,6 @@ __metadata:
calculate-size: 1.1.1
centrifuge: 2.8.4
classnames: 2.3.1
clipboard: 2.0.4
comlink: 4.3.1
common-tags: ^1.8.0
copy-webpack-plugin: 9.0.1
@ -31573,13 +31535,6 @@ __metadata:
languageName: node
linkType: hard
"select@npm:^1.1.2":
version: 1.1.2
resolution: "select@npm:1.1.2"
checksum: 4346151e94f226ea6131e44e68e6d837f3fdee64831b756dd657cc0b02f4cb5107f867cb34a1d1216ab7737d0bf0645d44546afb030bbd8d64e891f5e4c4814e
languageName: node
linkType: hard
"selection-is-backward@npm:^1.0.0":
version: 1.0.0
resolution: "selection-is-backward@npm:1.0.0"
@ -33749,13 +33704,6 @@ __metadata:
languageName: node
linkType: hard
"tiny-emitter@npm:^2.0.0":
version: 2.1.0
resolution: "tiny-emitter@npm:2.1.0"
checksum: fbcfb5145751a0e3b109507a828eb6d6d4501352ab7bb33eccef46e22e9d9ad3953158870a6966a59e57ab7c3f9cfac7cab8521db4de6a5e757012f4677df2dd
languageName: node
linkType: hard
"tiny-invariant@npm:^1.0.1, tiny-invariant@npm:^1.0.2, tiny-invariant@npm:^1.0.6":
version: 1.1.0
resolution: "tiny-invariant@npm:1.1.0"