diff --git a/packages/grafana-ui/src/components/Logs/LogDetailsRow.test.tsx b/packages/grafana-ui/src/components/Logs/LogDetailsRow.test.tsx index 11d470c0edd..bf5583b6511 100644 --- a/packages/grafana-ui/src/components/Logs/LogDetailsRow.test.tsx +++ b/packages/grafana-ui/src/components/Logs/LogDetailsRow.test.tsx @@ -1,4 +1,5 @@ import { screen, render, fireEvent } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import React, { ComponentProps } from 'react'; import { LogDetailsRow } from './LogDetailsRow'; @@ -105,4 +106,16 @@ describe('LogDetailsRow', () => { expect(screen.getByTestId('logLabelStats')).toBeInTheDocument(); expect(screen.getByTestId('logLabelStats')).toHaveTextContent('another value'); }); + + it('should render clipboard button on hover of log row table value', async () => { + setup({ parsedKey: 'key', parsedValue: 'value' }); + + const valueCell = screen.getByRole('cell', { name: 'value' }); + await userEvent.hover(valueCell); + + expect(screen.getByRole('button', { name: 'Copy value to clipboard' })).toBeInTheDocument(); + await userEvent.unhover(valueCell); + + expect(screen.queryByRole('button', { name: 'Copy value to clipboard' })).not.toBeInTheDocument(); + }); }); diff --git a/packages/grafana-ui/src/components/Logs/LogDetailsRow.tsx b/packages/grafana-ui/src/components/Logs/LogDetailsRow.tsx index d483403f414..e81c565f233 100644 --- a/packages/grafana-ui/src/components/Logs/LogDetailsRow.tsx +++ b/packages/grafana-ui/src/components/Logs/LogDetailsRow.tsx @@ -5,6 +5,7 @@ import { Field, LinkModel, LogLabelStatsModel, GrafanaTheme2 } from '@grafana/da import { withTheme2 } from '../../themes/index'; import { Themeable2 } from '../../types/theme'; +import { ClipboardButton } from '../ClipboardButton/ClipboardButton'; import { DataLinkButton } from '../DataLinks/DataLinkButton'; import { IconButton } from '../IconButton/IconButton'; @@ -31,6 +32,7 @@ interface State { showFieldsStats: boolean; fieldCount: number; fieldStats: LogLabelStatsModel[] | null; + mouseOver: boolean; } const getStyles = (theme: GrafanaTheme2) => { @@ -52,18 +54,27 @@ const getStyles = (theme: GrafanaTheme2) => { showingField: css` color: ${theme.colors.primary.text}; `, + hoverValueCopy: css` + margin: ${theme.spacing(0, 0, 0, 1.2)}; + position: absolute; + top: 0px; + justify-content: center; + border-radius: 20px; + width: 26px; + height: 26px; + `, wrapLine: css` label: wrapLine; white-space: pre-wrap; `, }; }; - class UnThemedLogDetailsRow extends PureComponent { state: State = { showFieldsStats: false, fieldCount: 0, fieldStats: null, + mouseOver: false, }; showField = () => { @@ -112,6 +123,11 @@ class UnThemedLogDetailsRow extends PureComponent { }); } + hoverValueCopy() { + const mouseOver = !this.state.mouseOver; + this.setState({ mouseOver }); + } + render() { const { theme, @@ -126,7 +142,7 @@ class UnThemedLogDetailsRow extends PureComponent { onClickFilterLabel, onClickFilterOutLabel, } = this.props; - const { showFieldsStats, fieldStats, fieldCount } = this.state; + const { showFieldsStats, fieldStats, fieldCount, mouseOver } = this.state; const styles = getStyles(theme); const style = getLogRowStyles(theme); @@ -166,8 +182,23 @@ class UnThemedLogDetailsRow extends PureComponent { {/* Key - value columns */} {parsedKey} - + {parsedValue} + {mouseOver && ( + parsedValue} + title="Copy value to clipboard" + fill="text" + variant="secondary" + icon="copy" + size="sm" + className={styles.hoverValueCopy} + /> + )} {links?.map((link) => (