import { cx } from '@emotion/css'; import { MouseEvent, ReactNode, useState, useMemo, useCallback, useRef, useEffect, memo } from 'react'; import { TimeZone, LogsDedupStrategy, LogRowModel, Field, LinkModel, LogsSortOrder, CoreApp, DataFrame, LogRowContextOptions, } from '@grafana/data'; import { config } from '@grafana/runtime'; import { DataQuery } from '@grafana/schema'; import { ConfirmModal, Icon, PopoverContent, useTheme2 } from '@grafana/ui'; import { t, Trans } from 'app/core/internationalization'; import { PopoverMenu } from '../../explore/Logs/PopoverMenu'; import { UniqueKeyMaker } from '../UniqueKeyMaker'; import { disablePopoverMenu, enablePopoverMenu, isPopoverMenuDisabled, sortLogRows, targetIsElement } from '../utils'; //Components import { LogRow } from './LogRow'; import { PreviewLogRow } from './PreviewLogRow'; import { getLogRowStyles } from './getLogRowStyles'; export interface Props { logRows?: LogRowModel[]; deduplicatedRows?: LogRowModel[]; dedupStrategy: LogsDedupStrategy; showLabels: boolean; showTime: boolean; wrapLogMessage: boolean; prettifyLogMessage: boolean; timeZone: TimeZone; enableLogDetails: boolean; logsSortOrder?: LogsSortOrder | null; previewLimit?: number; forceEscape?: boolean; displayedFields?: string[]; app?: CoreApp; showContextToggle?: (row: LogRowModel) => boolean; onClickFilterLabel?: (key: string, value: string, frame?: DataFrame) => void; onClickFilterOutLabel?: (key: string, value: string, frame?: DataFrame) => void; getFieldLinks?: (field: Field, rowIndex: number, dataFrame: DataFrame) => Array>; onClickShowField?: (key: string) => void; onClickHideField?: (key: string) => void; onPinLine?: (row: LogRowModel, allowUnPin?: boolean) => void; onUnpinLine?: (row: LogRowModel) => void; pinLineButtonTooltipTitle?: PopoverContent; onLogRowHover?: (row?: LogRowModel) => void; onOpenContext?: (row: LogRowModel, onClose: () => void) => void; getRowContextQuery?: ( row: LogRowModel, options?: LogRowContextOptions, cacheFilters?: boolean ) => Promise; onPermalinkClick?: (row: LogRowModel) => Promise; permalinkedRowId?: string; scrollIntoView?: (element: HTMLElement) => void; isFilterLabelActive?: (key: string, value: string, refId?: string) => Promise; pinnedLogs?: string[]; /** * If false or undefined, the `contain:strict` css property will be added to the wrapping `` for performance reasons. * Any overflowing content will be clipped at the table boundary. */ overflowingContent?: boolean; onClickFilterString?: (value: string, refId?: string) => void; onClickFilterOutString?: (value: string, refId?: string) => void; logRowMenuIconsBefore?: ReactNode[]; logRowMenuIconsAfter?: ReactNode[]; scrollElement: HTMLDivElement | null; renderPreview?: boolean; } type PopoverStateType = { selection: string; selectedRow: LogRowModel | null; popoverMenuCoordinates: { x: number; y: number }; }; export const LogRows = memo( ({ deduplicatedRows, logRows = [], dedupStrategy, logsSortOrder, previewLimit, pinnedLogs, onOpenContext, onClickFilterOutString, onClickFilterString, scrollElement, renderPreview = false, enableLogDetails, permalinkedRowId, ...props }: Props) => { const [previewSize, setPreviewSize] = useState( /** * If renderPreview is enabled, either half of the log rows or twice the screen size of log rows will be rendered. * The biggest of those values will be used. Else, all rows are rendered. */ renderPreview && !permalinkedRowId ? Math.max(2 * Math.ceil(window.innerHeight / 20), Math.ceil(logRows.length / 3)) : Infinity ); const [popoverState, setPopoverState] = useState({ selection: '', selectedRow: null, popoverMenuCoordinates: { x: 0, y: 0 }, }); const [showDisablePopoverOptions, setShowDisablePopoverOptions] = useState(false); const logRowsRef = useRef(null); const theme = useTheme2(); const styles = getLogRowStyles(theme); const dedupedRows = deduplicatedRows ? deduplicatedRows : logRows; const dedupCount = useMemo( () => dedupedRows.reduce((sum, row) => (row.duplicates ? sum + row.duplicates : sum), 0), [dedupedRows] ); const showDuplicates = dedupStrategy !== LogsDedupStrategy.none && dedupCount > 0; const orderedRows = useMemo( () => (logsSortOrder ? sortLogRows(dedupedRows, logsSortOrder) : dedupedRows), [dedupedRows, logsSortOrder] ); // React profiler becomes unusable if we pass all rows to all rows and their labels, using getter instead const getRows = useMemo(() => () => orderedRows, [orderedRows]); const handleDeselectionRef = useRef<((e: Event) => void) | null>(null); const keyMaker = new UniqueKeyMaker(); useEffect(() => { return () => { if (handleDeselectionRef.current) { document.removeEventListener('click', handleDeselectionRef.current); document.removeEventListener('contextmenu', handleDeselectionRef.current); } }; }, []); useEffect(() => { if (!scrollElement) { return; } function renderAll() { setPreviewSize(Infinity); scrollElement?.removeEventListener('scroll', renderAll); scrollElement?.removeEventListener('wheel', renderAll); } scrollElement.addEventListener('scroll', renderAll); scrollElement.addEventListener('wheel', renderAll); }, [logRows.length, scrollElement]); /** * Toggle the `contextIsOpen` state when a context of one LogRow is opened in order to not show the menu of the other log rows. */ const openContext = useCallback( (row: LogRowModel, onClose: () => void): void => { if (onOpenContext) { onOpenContext(row, onClose); } }, [onOpenContext] ); const popoverMenuSupported = useCallback(() => { if (!config.featureToggles.logRowsPopoverMenu || isPopoverMenuDisabled()) { return false; } return Boolean(onClickFilterOutString || onClickFilterString); }, [onClickFilterOutString, onClickFilterString]); const closePopoverMenu = useCallback(() => { if (handleDeselectionRef.current) { document.removeEventListener('click', handleDeselectionRef.current); document.removeEventListener('contextmenu', handleDeselectionRef.current); handleDeselectionRef.current = null; } setPopoverState({ selection: '', popoverMenuCoordinates: { x: 0, y: 0 }, selectedRow: null, }); }, []); const handleDeselection = useCallback( (e: Event) => { if (targetIsElement(e.target) && !logRowsRef.current?.contains(e.target)) { // The mouseup event comes from outside the log rows, close the menu. closePopoverMenu(); return; } if (document.getSelection()?.toString()) { return; } closePopoverMenu(); }, [closePopoverMenu] ); const handleSelection = useCallback( (e: MouseEvent, row: LogRowModel): boolean => { const selection = document.getSelection()?.toString(); if (!selection) { return false; } if (e.altKey) { enablePopoverMenu(); } if (popoverMenuSupported() === false) { // This signals onRowClick inside LogRow to skip the event because the user is selecting text return selection ? true : false; } if (!logRowsRef.current) { return false; } const MENU_WIDTH = 270; const MENU_HEIGHT = 105; const x = e.clientX + MENU_WIDTH > window.innerWidth ? window.innerWidth - MENU_WIDTH : e.clientX; const y = e.clientY + MENU_HEIGHT > window.innerHeight ? window.innerHeight - MENU_HEIGHT : e.clientY; setPopoverState({ selection, popoverMenuCoordinates: { x, y }, selectedRow: row, }); handleDeselectionRef.current = handleDeselection; document.addEventListener('click', handleDeselection); document.addEventListener('contextmenu', handleDeselection); return true; }, [handleDeselection, popoverMenuSupported] ); const onDisablePopoverMenu = useCallback(() => { setShowDisablePopoverOptions(true); }, []); const onDisableCancel = useCallback(() => { setShowDisablePopoverOptions(false); }, []); const onDisableConfirm = useCallback(() => { disablePopoverMenu(); setShowDisablePopoverOptions(false); }, []); return (
{popoverState.selection && popoverState.selectedRow && ( )} {showDisablePopoverOptions && ( You are about to disable the logs filter menu. To re-enable it, select text in a log line while holding the alt key.
alt+select to enable again
} confirmText={t('logs.log-rows.disable-popover.confirm', 'Confirm')} icon="exclamation-triangle" onConfirm={onDisableConfirm} onDismiss={onDisableCancel} /> )}
{orderedRows.map((row, index) => index < previewSize ? ( logId === row.rowId || logId === row.uid)} isFilterLabelActive={props.isFilterLabelActive} handleTextSelection={handleSelection} enableLogDetails={enableLogDetails} {...props} /> ) : ( ) )}
); } );