LogRow.tsx 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193
  1. import React, { PureComponent } from 'react';
  2. import _ from 'lodash';
  3. import Highlighter from 'react-highlight-words';
  4. import classnames from 'classnames';
  5. import { LogRowModel, LogsLabelStat, LogsParser, calculateFieldStats, getParser } from 'app/core/logs_model';
  6. import LogLabels, { Stats } from './LogLabels';
  7. import { findHighlightChunksInText } from 'app/core/utils/text';
  8. interface RowProps {
  9. highlighterExpressions?: string[];
  10. row: LogRowModel;
  11. showDuplicates: boolean;
  12. showLabels: boolean | null; // Tristate: null means auto
  13. showLocalTime: boolean;
  14. showUtc: boolean;
  15. getRows: () => LogRowModel[];
  16. onClickLabel?: (label: string, value: string) => void;
  17. }
  18. interface RowState {
  19. fieldCount: number;
  20. fieldLabel: string;
  21. fieldStats: LogsLabelStat[];
  22. fieldValue: string;
  23. parsed: boolean;
  24. parser?: LogsParser;
  25. parsedFieldHighlights: string[];
  26. showFieldStats: boolean;
  27. }
  28. /**
  29. * Renders a highlighted field.
  30. * When hovering, a stats icon is shown.
  31. */
  32. const FieldHighlight = onClick => props => {
  33. return (
  34. <span className={props.className} style={props.style}>
  35. {props.children}
  36. <span className="logs-row__field-highlight--icon fa fa-signal" onClick={() => onClick(props.children)} />
  37. </span>
  38. );
  39. };
  40. /**
  41. * Renders a log line.
  42. *
  43. * When user hovers over it for a certain time, it lazily parses the log line.
  44. * Once a parser is found, it will determine fields, that will be highlighted.
  45. * When the user requests stats for a field, they will be calculated and rendered below the row.
  46. */
  47. export class LogRow extends PureComponent<RowProps, RowState> {
  48. mouseMessageTimer: NodeJS.Timer;
  49. state = {
  50. fieldCount: 0,
  51. fieldLabel: null,
  52. fieldStats: null,
  53. fieldValue: null,
  54. parsed: false,
  55. parser: undefined,
  56. parsedFieldHighlights: [],
  57. showFieldStats: false,
  58. };
  59. componentWillUnmount() {
  60. clearTimeout(this.mouseMessageTimer);
  61. }
  62. onClickClose = () => {
  63. this.setState({ showFieldStats: false });
  64. };
  65. onClickHighlight = (fieldText: string) => {
  66. const { getRows } = this.props;
  67. const { parser } = this.state;
  68. const allRows = getRows();
  69. // Build value-agnostic row matcher based on the field label
  70. const fieldLabel = parser.getLabelFromField(fieldText);
  71. const fieldValue = parser.getValueFromField(fieldText);
  72. const matcher = parser.buildMatcher(fieldLabel);
  73. const fieldStats = calculateFieldStats(allRows, matcher);
  74. const fieldCount = fieldStats.reduce((sum, stat) => sum + stat.count, 0);
  75. this.setState({ fieldCount, fieldLabel, fieldStats, fieldValue, showFieldStats: true });
  76. };
  77. onMouseOverMessage = () => {
  78. // Don't parse right away, user might move along
  79. this.mouseMessageTimer = setTimeout(this.parseMessage, 500);
  80. };
  81. onMouseOutMessage = () => {
  82. clearTimeout(this.mouseMessageTimer);
  83. this.setState({ parsed: false });
  84. };
  85. parseMessage = () => {
  86. if (!this.state.parsed) {
  87. const { row } = this.props;
  88. const parser = getParser(row.entry);
  89. if (parser) {
  90. // Use parser to highlight detected fields
  91. const parsedFieldHighlights = parser.getFields(this.props.row.entry);
  92. this.setState({ parsedFieldHighlights, parsed: true, parser });
  93. }
  94. }
  95. };
  96. render() {
  97. const {
  98. getRows,
  99. highlighterExpressions,
  100. onClickLabel,
  101. row,
  102. showDuplicates,
  103. showLabels,
  104. showLocalTime,
  105. showUtc,
  106. } = this.props;
  107. const {
  108. fieldCount,
  109. fieldLabel,
  110. fieldStats,
  111. fieldValue,
  112. parsed,
  113. parsedFieldHighlights,
  114. showFieldStats,
  115. } = this.state;
  116. const previewHighlights = highlighterExpressions && !_.isEqual(highlighterExpressions, row.searchWords);
  117. const highlights = previewHighlights ? highlighterExpressions : row.searchWords;
  118. const needsHighlighter = highlights && highlights.length > 0;
  119. const highlightClassName = classnames('logs-row__match-highlight', {
  120. 'logs-row__match-highlight--preview': previewHighlights,
  121. });
  122. return (
  123. <div className="logs-row">
  124. {showDuplicates && (
  125. <div className="logs-row__duplicates">{row.duplicates > 0 ? `${row.duplicates + 1}x` : null}</div>
  126. )}
  127. <div className={row.logLevel ? `logs-row__level logs-row__level--${row.logLevel}` : ''} />
  128. {showUtc && (
  129. <div className="logs-row__time" title={`Local: ${row.timeLocal} (${row.timeFromNow})`}>
  130. {row.timestamp}
  131. </div>
  132. )}
  133. {showLocalTime && (
  134. <div className="logs-row__time" title={`${row.timestamp} (${row.timeFromNow})`}>
  135. {row.timeLocal}
  136. </div>
  137. )}
  138. {showLabels && (
  139. <div className="logs-row__labels">
  140. <LogLabels getRows={getRows} labels={row.uniqueLabels} onClickLabel={onClickLabel} />
  141. </div>
  142. )}
  143. <div className="logs-row__message" onMouseEnter={this.onMouseOverMessage} onMouseLeave={this.onMouseOutMessage}>
  144. {parsed && (
  145. <Highlighter
  146. autoEscape
  147. highlightTag={FieldHighlight(this.onClickHighlight)}
  148. textToHighlight={row.entry}
  149. searchWords={parsedFieldHighlights}
  150. highlightClassName="logs-row__field-highlight"
  151. />
  152. )}
  153. {!parsed &&
  154. needsHighlighter && (
  155. <Highlighter
  156. textToHighlight={row.entry}
  157. searchWords={highlights}
  158. findChunks={findHighlightChunksInText}
  159. highlightClassName={highlightClassName}
  160. />
  161. )}
  162. {!parsed && !needsHighlighter && row.entry}
  163. {showFieldStats && (
  164. <div className="logs-row__stats">
  165. <Stats
  166. stats={fieldStats}
  167. label={fieldLabel}
  168. value={fieldValue}
  169. onClickClose={this.onClickClose}
  170. rowCount={fieldCount}
  171. />
  172. </div>
  173. )}
  174. </div>
  175. </div>
  176. );
  177. }
  178. }