mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
VizTooltips: Copy to clipboard functionality (#80761)
This commit is contained in:
parent
29112f8822
commit
e0d0420990
@ -1,9 +1,10 @@
|
||||
import { css, cx } from '@emotion/css';
|
||||
import React, { useState } from 'react';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
|
||||
import { useStyles2 } from '../../themes';
|
||||
import { InlineToast } from '../InlineToast/InlineToast';
|
||||
import { Tooltip } from '../Tooltip';
|
||||
|
||||
import { ColorIndicatorPosition, VizTooltipColorIndicator } from './VizTooltipColorIndicator';
|
||||
@ -16,6 +17,14 @@ interface Props extends LabelValue {
|
||||
isPinned: boolean;
|
||||
}
|
||||
|
||||
enum LabelValueTypes {
|
||||
label = 'label',
|
||||
value = 'value',
|
||||
}
|
||||
|
||||
const SUCCESSFULLY_COPIED_TEXT = 'Copied to clipboard';
|
||||
const SHOW_SUCCESS_DURATION = 2 * 1000;
|
||||
|
||||
export const VizTooltipRow = ({
|
||||
label,
|
||||
value,
|
||||
@ -32,6 +41,61 @@ export const VizTooltipRow = ({
|
||||
const [showLabelTooltip, setShowLabelTooltip] = useState(false);
|
||||
const [showValueTooltip, setShowValueTooltip] = useState(false);
|
||||
|
||||
const [copiedText, setCopiedText] = useState<Record<string, string> | null>(null);
|
||||
const [showCopySuccess, setShowCopySuccess] = useState(false);
|
||||
|
||||
const labelRef = useRef<null | HTMLDivElement>(null);
|
||||
const valueRef = useRef<null | HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
let timeoutId: ReturnType<typeof setTimeout>;
|
||||
|
||||
if (showCopySuccess) {
|
||||
timeoutId = setTimeout(() => {
|
||||
setShowCopySuccess(false);
|
||||
}, SHOW_SUCCESS_DURATION);
|
||||
}
|
||||
|
||||
return () => {
|
||||
window.clearTimeout(timeoutId);
|
||||
};
|
||||
}, [showCopySuccess]);
|
||||
|
||||
const copyToClipboard = async (text: string, type: LabelValueTypes) => {
|
||||
if (!(navigator?.clipboard && window.isSecureContext)) {
|
||||
fallbackCopyToClipboard(text, type);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await navigator.clipboard.writeText(text);
|
||||
setCopiedText({ [`${type}`]: text });
|
||||
setShowCopySuccess(true);
|
||||
} catch (error) {
|
||||
setCopiedText(null);
|
||||
}
|
||||
};
|
||||
|
||||
const fallbackCopyToClipboard = (text: string, type: LabelValueTypes) => {
|
||||
// Use a fallback method for browsers/contexts that don't support the Clipboard API.
|
||||
const textarea = document.createElement('textarea');
|
||||
labelRef.current?.appendChild(textarea);
|
||||
textarea.value = text;
|
||||
textarea.focus();
|
||||
textarea.select();
|
||||
try {
|
||||
const successful = document.execCommand('copy');
|
||||
if (successful) {
|
||||
setCopiedText({ [`${type}`]: text });
|
||||
setShowCopySuccess(true);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Unable to copy to clipboard', err);
|
||||
}
|
||||
|
||||
textarea.remove();
|
||||
};
|
||||
|
||||
const onMouseEnterLabel = (event: React.MouseEvent<HTMLDivElement>) => {
|
||||
if (event.currentTarget.offsetWidth < event.currentTarget.scrollWidth) {
|
||||
setShowLabelTooltip(true);
|
||||
@ -58,15 +122,27 @@ export const VizTooltipRow = ({
|
||||
{!isPinned ? (
|
||||
<div className={cx(styles.label, isActive && styles.activeSeries)}>{label}</div>
|
||||
) : (
|
||||
<Tooltip content={label} interactive={false} show={showLabelTooltip}>
|
||||
<div
|
||||
className={cx(styles.label, isActive && styles.activeSeries)}
|
||||
onMouseEnter={onMouseEnterLabel}
|
||||
onMouseLeave={onMouseLeaveLabel}
|
||||
>
|
||||
{label}
|
||||
</div>
|
||||
</Tooltip>
|
||||
<>
|
||||
<Tooltip content={label} interactive={false} show={showLabelTooltip}>
|
||||
<>
|
||||
{showCopySuccess && copiedText?.label && (
|
||||
<InlineToast placement="top" referenceElement={labelRef.current}>
|
||||
{SUCCESSFULLY_COPIED_TEXT}
|
||||
</InlineToast>
|
||||
)}
|
||||
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions */}
|
||||
<div
|
||||
className={cx(styles.label, isActive && styles.activeSeries, navigator?.clipboard && styles.copy)}
|
||||
onMouseEnter={onMouseEnterLabel}
|
||||
onMouseLeave={onMouseLeaveLabel}
|
||||
onClick={() => copyToClipboard(label, LabelValueTypes.label)}
|
||||
ref={labelRef}
|
||||
>
|
||||
{label}
|
||||
</div>
|
||||
</>
|
||||
</Tooltip>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
@ -84,13 +160,23 @@ export const VizTooltipRow = ({
|
||||
<div className={cx(styles.value, isActive)}>{value}</div>
|
||||
) : (
|
||||
<Tooltip content={value ? value.toString() : ''} interactive={false} show={showValueTooltip}>
|
||||
<div
|
||||
className={cx(styles.value, isActive)}
|
||||
onMouseEnter={onMouseEnterValue}
|
||||
onMouseLeave={onMouseLeaveValue}
|
||||
>
|
||||
{value}
|
||||
</div>
|
||||
<>
|
||||
{showCopySuccess && copiedText?.value && (
|
||||
<InlineToast placement="top" referenceElement={valueRef.current}>
|
||||
{SUCCESSFULLY_COPIED_TEXT}
|
||||
</InlineToast>
|
||||
)}
|
||||
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions */}
|
||||
<div
|
||||
className={cx(styles.value, isActive, navigator?.clipboard && styles.copy)}
|
||||
onMouseEnter={onMouseEnterValue}
|
||||
onMouseLeave={onMouseLeaveValue}
|
||||
onClick={() => copyToClipboard(value ? value.toString() : '', LabelValueTypes.value)}
|
||||
ref={valueRef}
|
||||
>
|
||||
{value}
|
||||
</div>
|
||||
</>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
@ -135,4 +221,7 @@ const getStyles = (theme: GrafanaTheme2, justify: string, marginRight: string) =
|
||||
fontWeight: theme.typography.fontWeightBold,
|
||||
color: theme.colors.text.maxContrast,
|
||||
}),
|
||||
copy: css({
|
||||
cursor: 'pointer',
|
||||
}),
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user