import { memo, FocusEvent, SyntheticEvent, useCallback, ReactNode, useMemo, cloneElement, isValidElement, MouseEvent, } from 'react'; import { LogRowContextOptions, LogRowModel } from '@grafana/data'; import { DataQuery } from '@grafana/schema'; import { ClipboardButton, IconButton, PopoverContent } from '@grafana/ui'; import { handleOpenLogsContextClick } from '../utils'; import { LogRowStyles } from './getLogRowStyles'; interface Props { logText: string; row: LogRowModel; showContextToggle?: (row: LogRowModel) => boolean; onOpenContext: (row: LogRowModel) => void; getRowContextQuery?: ( row: LogRowModel, options?: LogRowContextOptions, cacheFilters?: boolean ) => Promise; onPermalinkClick?: (row: LogRowModel) => Promise; onPinLine?: (row: LogRowModel) => void; onUnpinLine?: (row: LogRowModel) => void; pinLineButtonTooltipTitle?: PopoverContent; pinned?: boolean; styles: LogRowStyles; mouseIsOver: boolean; onBlur: () => void; addonBefore?: ReactNode[]; addonAfter?: ReactNode[]; } export const LogRowMenuCell = memo( ({ logText, onOpenContext, onPermalinkClick, onPinLine, onUnpinLine, pinLineButtonTooltipTitle, pinned, row, showContextToggle, styles, mouseIsOver, onBlur, getRowContextQuery, addonBefore, addonAfter, }: Props) => { const shouldShowContextToggle = useMemo( () => (showContextToggle ? showContextToggle(row) : false), [row, showContextToggle] ); const onLogRowClick = useCallback((e: SyntheticEvent) => { e.stopPropagation(); }, []); const onShowContextClick = useCallback( async (event: MouseEvent) => { event.stopPropagation(); handleOpenLogsContextClick(event, row, getRowContextQuery, onOpenContext); }, [onOpenContext, getRowContextQuery, row] ); /** * For better accessibility support, we listen to the onBlur event here (to hide this component), and * to onFocus in LogRow (to show this component). */ const handleBlur = useCallback( (e: FocusEvent) => { if (!e.currentTarget.contains(e.relatedTarget) && onBlur) { onBlur(); } }, [onBlur] ); const getLogText = useCallback(() => logText, [logText]); const beforeContent = useMemo(() => { if (!addonBefore) { return null; } return addClickListenersToNode(addonBefore, row); }, [addonBefore, row]); const afterContent = useMemo(() => { if (!addonAfter) { return null; } return addClickListenersToNode(addonAfter, row); }, [addonAfter, row]); return ( // We keep this click listener here to prevent the row from being selected when clicking on the menu. // eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions {pinned && !mouseIsOver && ( onUnpinLine && onUnpinLine(row)} tooltip="Unpin line" tooltipPlacement="top" aria-label="Unpin line" tabIndex={0} /> )} {mouseIsOver && ( <> {beforeContent} {shouldShowContextToggle && ( )} {pinned && onUnpinLine && ( onUnpinLine && onUnpinLine(row)} tooltip="Unpin line" tooltipPlacement="top" aria-label="Unpin line" tabIndex={0} /> )} {!pinned && onPinLine && ( onPinLine && onPinLine(row)} tooltip={pinLineButtonTooltipTitle ?? 'Pin line'} tooltipPlacement="top" aria-label="Pin line" tabIndex={0} /> )} {onPermalinkClick && row.rowId !== undefined && row.uid && ( onPermalinkClick(row)} tabIndex={0} /> )} {afterContent} )} ); } ); type AddonOnClickListener = (event: MouseEvent, row: LogRowModel) => void | undefined; function addClickListenersToNode(nodes: ReactNode[], row: LogRowModel) { return nodes.map((node, index) => { if (isValidElement(node)) { const onClick: AddonOnClickListener = node.props.onClick; if (!onClick) { return node; } return cloneElement(node, { // @ts-expect-error onClick: (event: MouseEvent) => { onClick(event, row); }, key: index, }); } return node; }); } LogRowMenuCell.displayName = 'LogRowMenuCell';