mirror of https://github.com/grafana/grafana.git
				
				
				
			
		
			
				
	
	
		
			333 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
			
		
		
	
	
			333 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
| import { cx } from '@emotion/css';
 | |
| import { MouseEvent, ReactNode, useState, useMemo, useCallback, useRef, useEffect, memo } from 'react';
 | |
| 
 | |
| import {
 | |
|   TimeZone,
 | |
|   LogsDedupStrategy,
 | |
|   LogRowModel,
 | |
|   LogsSortOrder,
 | |
|   CoreApp,
 | |
|   DataFrame,
 | |
|   LogRowContextOptions,
 | |
| } from '@grafana/data';
 | |
| import { Trans, useTranslate } from '@grafana/i18n';
 | |
| import { config } from '@grafana/runtime';
 | |
| import { DataQuery } from '@grafana/schema';
 | |
| import { ConfirmModal, Icon, PopoverContent, useTheme2 } from '@grafana/ui';
 | |
| import { GetFieldLinksFn } from 'app/plugins/panel/logs/types';
 | |
| 
 | |
| 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?: GetFieldLinksFn;
 | |
|   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<DataQuery | null>;
 | |
|   onPermalinkClick?: (row: LogRowModel) => Promise<void>;
 | |
|   permalinkedRowId?: string;
 | |
|   scrollIntoView?: (element: HTMLElement) => void;
 | |
|   isFilterLabelActive?: (key: string, value: string, refId?: string) => Promise<boolean>;
 | |
|   pinnedLogs?: string[];
 | |
|   /**
 | |
|    * If false or undefined, the `contain:strict` css property will be added to the wrapping `<table>` 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;
 | |
| }
 | |
| 
 | |
| export 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<PopoverStateType>({
 | |
|       selection: '',
 | |
|       selectedRow: null,
 | |
|       popoverMenuCoordinates: { x: 0, y: 0 },
 | |
|     });
 | |
|     const [showDisablePopoverOptions, setShowDisablePopoverOptions] = useState(false);
 | |
|     const logRowsRef = useRef<HTMLDivElement>(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<HTMLElement>, 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);
 | |
|     }, []);
 | |
| 
 | |
|     const { t } = useTranslate();
 | |
| 
 | |
|     return (
 | |
|       <div className={styles.logRows} ref={logRowsRef}>
 | |
|         {popoverState.selection && popoverState.selectedRow && (
 | |
|           <PopoverMenu
 | |
|             close={closePopoverMenu}
 | |
|             row={popoverState.selectedRow}
 | |
|             selection={popoverState.selection}
 | |
|             {...popoverState.popoverMenuCoordinates}
 | |
|             onClickFilterString={onClickFilterString}
 | |
|             onClickFilterOutString={onClickFilterOutString}
 | |
|             onDisable={onDisablePopoverMenu}
 | |
|           />
 | |
|         )}
 | |
|         {showDisablePopoverOptions && (
 | |
|           <ConfirmModal
 | |
|             isOpen
 | |
|             title={t('logs.log-rows.disable-popover.title', 'Disable menu')}
 | |
|             body={
 | |
|               <>
 | |
|                 <Trans i18nKey="logs.log-rows.disable-popover.message">
 | |
|                   You are about to disable the logs filter menu. To re-enable it, select text in a log line while
 | |
|                   holding the alt key.
 | |
|                 </Trans>
 | |
|                 <div className={styles.shortcut}>
 | |
|                   <Icon name="keyboard" />
 | |
|                   <Trans i18nKey="logs.log-rows.disable-popover-message.shortcut">alt+select to enable again</Trans>
 | |
|                 </div>
 | |
|               </>
 | |
|             }
 | |
|             confirmText={t('logs.log-rows.disable-popover.confirm', 'Confirm')}
 | |
|             icon="exclamation-triangle"
 | |
|             onConfirm={onDisableConfirm}
 | |
|             onDismiss={onDisableCancel}
 | |
|           />
 | |
|         )}
 | |
|         <table className={cx(styles.logsRowsTable, props.overflowingContent ? '' : styles.logsRowsTableContain)}>
 | |
|           <tbody>
 | |
|             {orderedRows.map((row, index) =>
 | |
|               index < previewSize ? (
 | |
|                 <LogRow
 | |
|                   key={keyMaker.getKey(row.uid)}
 | |
|                   getRows={getRows}
 | |
|                   row={row}
 | |
|                   showDuplicates={showDuplicates}
 | |
|                   logsSortOrder={logsSortOrder}
 | |
|                   onOpenContext={openContext}
 | |
|                   styles={styles}
 | |
|                   onPermalinkClick={props.onPermalinkClick}
 | |
|                   scrollIntoView={props.scrollIntoView}
 | |
|                   permalinkedRowId={permalinkedRowId}
 | |
|                   onPinLine={props.onPinLine}
 | |
|                   onUnpinLine={props.onUnpinLine}
 | |
|                   pinLineButtonTooltipTitle={props.pinLineButtonTooltipTitle}
 | |
|                   pinned={pinnedLogs?.some((logId) => logId === row.rowId || logId === row.uid)}
 | |
|                   isFilterLabelActive={props.isFilterLabelActive}
 | |
|                   handleTextSelection={handleSelection}
 | |
|                   enableLogDetails={enableLogDetails}
 | |
|                   {...props}
 | |
|                 />
 | |
|               ) : (
 | |
|                 <PreviewLogRow
 | |
|                   key={`preview_${keyMaker.getKey(row.uid)}`}
 | |
|                   enableLogDetails={false}
 | |
|                   getRows={getRows}
 | |
|                   onOpenContext={openContext}
 | |
|                   styles={styles}
 | |
|                   showDuplicates={showDuplicates}
 | |
|                   {...props}
 | |
|                   row={row}
 | |
|                 />
 | |
|               )
 | |
|             )}
 | |
|           </tbody>
 | |
|         </table>
 | |
|       </div>
 | |
|     );
 | |
|   }
 | |
| );
 |