import { cx } from '@emotion/css'; import { debounce } from 'lodash'; import React, { PureComponent } from 'react'; import { Field, LinkModel, LogRowModel, LogsSortOrder, dateTimeFormat, CoreApp, DataFrame } from '@grafana/data'; import { reportInteraction } from '@grafana/runtime'; import { TimeZone } from '@grafana/schema'; import { withTheme2, Themeable2, Icon, Tooltip } from '@grafana/ui'; import { checkLogsError, escapeUnescapedString } from '../utils'; import { LogDetails } from './LogDetails'; import { LogLabels } from './LogLabels'; import { LogRowMessage } from './LogRowMessage'; import { LogRowMessageDisplayedFields } from './LogRowMessageDisplayedFields'; import { getLogLevelStyles, LogRowStyles } from './getLogRowStyles'; interface Props extends Themeable2 { row: LogRowModel; showDuplicates: boolean; showLabels: boolean; showTime: boolean; wrapLogMessage: boolean; prettifyLogMessage: boolean; timeZone: TimeZone; enableLogDetails: boolean; logsSortOrder?: LogsSortOrder | null; forceEscape?: boolean; app?: CoreApp; displayedFields?: string[]; getRows: () => LogRowModel[]; onClickFilterLabel?: (key: string, value: string) => void; onClickFilterOutLabel?: (key: string, value: string) => void; onContextClick?: () => void; getFieldLinks?: (field: Field, rowIndex: number, dataFrame: DataFrame) => Array>; showContextToggle?: (row?: LogRowModel) => boolean; onClickShowField?: (key: string) => void; onClickHideField?: (key: string) => void; onLogRowHover?: (row?: LogRowModel) => void; onOpenContext: (row: LogRowModel, onClose: () => void) => void; onPermalinkClick?: (row: LogRowModel) => Promise; styles: LogRowStyles; permalinkedRowId?: string; scrollIntoView?: (element: HTMLElement) => void; } interface State { highlightBackround: boolean; showDetails: boolean; } /** * Renders a log line. * * When user hovers over it for a certain time, it lazily parses the log line. * Once a parser is found, it will determine fields, that will be highlighted. * When the user requests stats for a field, they will be calculated and rendered below the row. */ class UnThemedLogRow extends PureComponent { state: State = { highlightBackround: false, showDetails: false, }; logLineRef: React.RefObject; constructor(props: Props) { super(props); this.logLineRef = React.createRef(); } // we are debouncing the state change by 3 seconds to highlight the logline after the context closed. debouncedContextClose = debounce(() => { this.setState({ highlightBackround: false }); }, 3000); onOpenContext = (row: LogRowModel) => { this.setState({ highlightBackround: true }); this.props.onOpenContext(row, this.debouncedContextClose); }; toggleDetails = () => { if (!this.props.enableLogDetails) { return; } reportInteraction('grafana_explore_logs_log_details_clicked', { datasourceType: this.props.row.datasourceType, type: this.state.showDetails ? 'close' : 'open', logRowUid: this.props.row.uid, app: this.props.app, }); this.setState((state) => { return { showDetails: !state.showDetails, }; }); }; renderTimeStamp(epochMs: number) { return dateTimeFormat(epochMs, { timeZone: this.props.timeZone, defaultWithMS: true, }); } onMouseEnter = () => { if (this.props.onLogRowHover) { this.props.onLogRowHover(this.props.row); } }; onMouseLeave = () => { if (this.props.onLogRowHover) { this.props.onLogRowHover(undefined); } }; componentDidMount() { this.scrollToLogRow(this.state, true); } componentDidUpdate(_: Props, prevState: State) { this.scrollToLogRow(prevState); } scrollToLogRow = (prevState: State, mounted = false) => { const { row, permalinkedRowId, scrollIntoView } = this.props; if (permalinkedRowId !== row.uid) { // only set the new state if the row is not permalinked anymore or if the component was mounted. if (prevState.highlightBackround || mounted) { this.setState({ highlightBackround: false }); } return; } // at this point this row is the permalinked row, so we need to scroll to it and highlight it if possible. if (this.logLineRef.current && scrollIntoView) { scrollIntoView(this.logLineRef.current); } if (!this.state.highlightBackround) { reportInteraction('grafana_explore_logs_permalink_opened', { datasourceType: row.datasourceType ?? 'unknown', logRowUid: row.uid, }); this.setState({ highlightBackround: true }); } }; render() { const { getRows, onClickFilterLabel, onClickFilterOutLabel, onClickShowField, onClickHideField, enableLogDetails, row, showDuplicates, showContextToggle, showLabels, showTime, displayedFields, wrapLogMessage, prettifyLogMessage, theme, getFieldLinks, forceEscape, app, styles, } = this.props; const { showDetails, highlightBackround } = this.state; const levelStyles = getLogLevelStyles(theme, row.logLevel); const { errorMessage, hasError } = checkLogsError(row); const logRowBackground = cx(styles.logsRow, { [styles.errorLogRow]: hasError, [styles.highlightBackground]: highlightBackround, }); const logRowDetailsBackground = cx(styles.logsRow, { [styles.errorLogRow]: hasError, [styles.highlightBackground]: highlightBackround && !this.state.showDetails, }); const processedRow = row.hasUnescapedContent && forceEscape ? { ...row, entry: escapeUnescapedString(row.entry), raw: escapeUnescapedString(row.raw) } : row; return ( <> {showDuplicates && ( {processedRow.duplicates && processedRow.duplicates > 0 ? `${processedRow.duplicates + 1}x` : null} )} {hasError && ( )} {enableLogDetails && ( )} {showTime && {this.renderTimeStamp(row.timeEpochMs)}} {showLabels && processedRow.uniqueLabels && ( )} {displayedFields && displayedFields.length > 0 ? ( ) : ( )} {this.state.showDetails && ( )} ); } } export const LogRow = withTheme2(UnThemedLogRow); LogRow.displayName = 'LogRow';