diff --git a/packages/grafana-ui/src/components/VizTooltip/VizTooltipRow.tsx b/packages/grafana-ui/src/components/VizTooltip/VizTooltipRow.tsx index 3b0f70620f4..e0690ed2fd2 100644 --- a/packages/grafana-ui/src/components/VizTooltip/VizTooltipRow.tsx +++ b/packages/grafana-ui/src/components/VizTooltip/VizTooltipRow.tsx @@ -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 | null>(null); + const [showCopySuccess, setShowCopySuccess] = useState(false); + + const labelRef = useRef(null); + const valueRef = useRef(null); + + useEffect(() => { + let timeoutId: ReturnType; + + 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) => { if (event.currentTarget.offsetWidth < event.currentTarget.scrollWidth) { setShowLabelTooltip(true); @@ -58,15 +122,27 @@ export const VizTooltipRow = ({ {!isPinned ? (
{label}
) : ( - -
- {label} -
-
+ <> + + <> + {showCopySuccess && copiedText?.label && ( + + {SUCCESSFULLY_COPIED_TEXT} + + )} + {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions */} +
copyToClipboard(label, LabelValueTypes.label)} + ref={labelRef} + > + {label} +
+ +
+ )} )} @@ -84,13 +160,23 @@ export const VizTooltipRow = ({
{value}
) : ( -
- {value} -
+ <> + {showCopySuccess && copiedText?.value && ( + + {SUCCESSFULLY_COPIED_TEXT} + + )} + {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions */} +
copyToClipboard(value ? value.toString() : '', LabelValueTypes.value)} + ref={valueRef} + > + {value} +
+
)} @@ -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', + }), });