LogRow.tsx 6.2 KB

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