LogRow.tsx 6.0 KB

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