import React, { PureComponent } from 'react'; import _ from 'lodash'; import Highlighter from 'react-highlight-words'; import classnames from 'classnames'; import { LogRowModel, LogLabelStatsModel, LogsParser, calculateFieldStats, getParser } from 'app/core/logs_model'; import { LogLabels } from './LogLabels'; import { findHighlightChunksInText } from 'app/core/utils/text'; import { LogLabelStats } from './LogLabelStats'; interface Props { highlighterExpressions?: string[]; row: LogRowModel; showDuplicates: boolean; showLabels: boolean | null; // Tristate: null means auto showLocalTime: boolean; showUtc: boolean; getRows: () => LogRowModel[]; onClickLabel?: (label: string, value: string) => void; } interface State { fieldCount: number; fieldLabel: string; fieldStats: LogLabelStatsModel[]; fieldValue: string; parsed: boolean; parser?: LogsParser; parsedFieldHighlights: string[]; showFieldStats: boolean; } /** * Renders a highlighted field. * When hovering, a stats icon is shown. */ const FieldHighlight = onClick => props => { return ( {props.children} onClick(props.children)} /> ); }; /** * 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. */ export class LogRow extends PureComponent { mouseMessageTimer: NodeJS.Timer; state = { fieldCount: 0, fieldLabel: null, fieldStats: null, fieldValue: null, parsed: false, parser: undefined, parsedFieldHighlights: [], showFieldStats: false, }; componentWillUnmount() { clearTimeout(this.mouseMessageTimer); } onClickClose = () => { this.setState({ showFieldStats: false }); }; onClickHighlight = (fieldText: string) => { const { getRows } = this.props; const { parser } = this.state; const allRows = getRows(); // Build value-agnostic row matcher based on the field label const fieldLabel = parser.getLabelFromField(fieldText); const fieldValue = parser.getValueFromField(fieldText); const matcher = parser.buildMatcher(fieldLabel); const fieldStats = calculateFieldStats(allRows, matcher); const fieldCount = fieldStats.reduce((sum, stat) => sum + stat.count, 0); this.setState({ fieldCount, fieldLabel, fieldStats, fieldValue, showFieldStats: true }); }; onMouseOverMessage = () => { // Don't parse right away, user might move along this.mouseMessageTimer = setTimeout(this.parseMessage, 500); }; onMouseOutMessage = () => { clearTimeout(this.mouseMessageTimer); this.setState({ parsed: false }); }; parseMessage = () => { if (!this.state.parsed) { const { row } = this.props; const parser = getParser(row.entry); if (parser) { // Use parser to highlight detected fields const parsedFieldHighlights = parser.getFields(this.props.row.entry); this.setState({ parsedFieldHighlights, parsed: true, parser }); } } }; render() { const { getRows, highlighterExpressions, onClickLabel, row, showDuplicates, showLabels, showLocalTime, showUtc, } = this.props; const { fieldCount, fieldLabel, fieldStats, fieldValue, parsed, parsedFieldHighlights, showFieldStats, } = this.state; const previewHighlights = highlighterExpressions && !_.isEqual(highlighterExpressions, row.searchWords); const highlights = previewHighlights ? highlighterExpressions : row.searchWords; const needsHighlighter = highlights && highlights.length > 0; const highlightClassName = classnames('logs-row__match-highlight', { 'logs-row__match-highlight--preview': previewHighlights, }); return (
{showDuplicates && (
{row.duplicates > 0 ? `${row.duplicates + 1}x` : null}
)}
{showUtc && (
{row.timestamp}
)} {showLocalTime && (
{row.timeLocal}
)} {showLabels && (
)}
{parsed && ( )} {!parsed && needsHighlighter && ( )} {!parsed && !needsHighlighter && row.entry} {showFieldStats && (
)}
); } }