Browse Source

Refactored out LogRow to a separate file

Hugo Häggmark 7 years ago
parent
commit
23202ab130

+ 10 - 10
public/app/core/logs_model.ts

@@ -42,7 +42,7 @@ export interface LogSearchMatch {
   text: string;
 }
 
-export interface LogRow {
+export interface LogRowModel {
   duplicates?: number;
   entry: string;
   key: string; // timestamp + labels
@@ -78,7 +78,7 @@ export interface LogsMetaItem {
 export interface LogsModel {
   id: string; // Identify one logs result from another
   meta?: LogsMetaItem[];
-  rows: LogRow[];
+  rows: LogRowModel[];
   series?: TimeSeries[];
 }
 
@@ -188,13 +188,13 @@ export const LogsParsers: { [name: string]: LogsParser } = {
   },
 };
 
-export function calculateFieldStats(rows: LogRow[], extractor: RegExp): LogsLabelStat[] {
+export function calculateFieldStats(rows: LogRowModel[], extractor: RegExp): LogsLabelStat[] {
   // Consider only rows that satisfy the matcher
   const rowsWithField = rows.filter(row => extractor.test(row.entry));
   const rowCount = rowsWithField.length;
 
   // Get field value counts for eligible rows
-  const countsByValue = _.countBy(rowsWithField, row => (row as LogRow).entry.match(extractor)[1]);
+  const countsByValue = _.countBy(rowsWithField, row => (row as LogRowModel).entry.match(extractor)[1]);
   const sortedCounts = _.chain(countsByValue)
     .map((count, value) => ({ count, value, proportion: count / rowCount }))
     .sortBy('count')
@@ -204,13 +204,13 @@ export function calculateFieldStats(rows: LogRow[], extractor: RegExp): LogsLabe
   return sortedCounts;
 }
 
-export function calculateLogsLabelStats(rows: LogRow[], label: string): LogsLabelStat[] {
+export function calculateLogsLabelStats(rows: LogRowModel[], label: string): LogsLabelStat[] {
   // Consider only rows that have the given label
   const rowsWithLabel = rows.filter(row => row.labels[label] !== undefined);
   const rowCount = rowsWithLabel.length;
 
   // Get label value counts for eligible rows
-  const countsByValue = _.countBy(rowsWithLabel, row => (row as LogRow).labels[label]);
+  const countsByValue = _.countBy(rowsWithLabel, row => (row as LogRowModel).labels[label]);
   const sortedCounts = _.chain(countsByValue)
     .map((count, value) => ({ count, value, proportion: count / rowCount }))
     .sortBy('count')
@@ -221,7 +221,7 @@ export function calculateLogsLabelStats(rows: LogRow[], label: string): LogsLabe
 }
 
 const isoDateRegexp = /\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-6]\d[,\.]\d+([+-][0-2]\d:[0-5]\d|Z)/g;
-function isDuplicateRow(row: LogRow, other: LogRow, strategy: LogsDedupStrategy): boolean {
+function isDuplicateRow(row: LogRowModel, other: LogRowModel, strategy: LogsDedupStrategy): boolean {
   switch (strategy) {
     case LogsDedupStrategy.exact:
       // Exact still strips dates
@@ -243,7 +243,7 @@ export function dedupLogRows(logs: LogsModel, strategy: LogsDedupStrategy): Logs
     return logs;
   }
 
-  const dedupedRows = logs.rows.reduce((result: LogRow[], row: LogRow, index, list) => {
+  const dedupedRows = logs.rows.reduce((result: LogRowModel[], row: LogRowModel, index, list) => {
     const previous = result[result.length - 1];
     if (index > 0 && isDuplicateRow(row, previous, strategy)) {
       previous.duplicates++;
@@ -278,7 +278,7 @@ export function filterLogLevels(logs: LogsModel, hiddenLogLevels: Set<LogLevel>)
     return logs;
   }
 
-  const filteredRows = logs.rows.reduce((result: LogRow[], row: LogRow, index, list) => {
+  const filteredRows = logs.rows.reduce((result: LogRowModel[], row: LogRowModel, index, list) => {
     if (!hiddenLogLevels.has(row.logLevel)) {
       result.push(row);
     }
@@ -291,7 +291,7 @@ export function filterLogLevels(logs: LogsModel, hiddenLogLevels: Set<LogLevel>)
   };
 }
 
-export function makeSeriesForLogs(rows: LogRow[], intervalMs: number): TimeSeries[] {
+export function makeSeriesForLogs(rows: LogRowModel[], intervalMs: number): TimeSeries[] {
   // currently interval is rangeMs / resolution, which is too low for showing series as bars.
   // need at least 10px per bucket, so we multiply interval by 10. Should be solved higher up the chain
   // when executing queries & interval calculated and not here but this is a temporary fix.

+ 3 - 3
public/app/features/explore/LogLabels.tsx

@@ -1,7 +1,7 @@
 import React, { PureComponent } from 'react';
 import classnames from 'classnames';
 
-import { calculateLogsLabelStats, LogsLabelStat, LogsStreamLabels, LogRow } from 'app/core/logs_model';
+import { calculateLogsLabelStats, LogsLabelStat, LogsStreamLabels, LogRowModel } from 'app/core/logs_model';
 
 function StatsRow({ active, count, proportion, value }: LogsLabelStat) {
   const percent = `${Math.round(proportion * 100)}%`;
@@ -68,7 +68,7 @@ export class Stats extends PureComponent<{
 
 class Label extends PureComponent<
   {
-    getRows?: () => LogRow[];
+    getRows?: () => LogRowModel[];
     label: string;
     plain?: boolean;
     value: string;
@@ -133,7 +133,7 @@ class Label extends PureComponent<
 }
 
 export default class LogLabels extends PureComponent<{
-  getRows?: () => LogRow[];
+  getRows?: () => LogRowModel[];
   labels: LogsStreamLabels;
   plain?: boolean;
   onClickLabel?: (label: string, value: string) => void;

+ 193 - 0
public/app/features/explore/LogRow.tsx

@@ -0,0 +1,193 @@
+import React, { PureComponent } from 'react';
+import _ from 'lodash';
+import Highlighter from 'react-highlight-words';
+import classnames from 'classnames';
+
+import { LogRowModel, LogsLabelStat, LogsParser, calculateFieldStats, getParser } from 'app/core/logs_model';
+import LogLabels, { Stats } from './LogLabels';
+import { findHighlightChunksInText } from 'app/core/utils/text';
+
+interface RowProps {
+  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 RowState {
+  fieldCount: number;
+  fieldLabel: string;
+  fieldStats: LogsLabelStat[];
+  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 (
+    <span className={props.className} style={props.style}>
+      {props.children}
+      <span className="logs-row__field-highlight--icon fa fa-signal" onClick={() => onClick(props.children)} />
+    </span>
+  );
+};
+
+/**
+ * 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<RowProps, RowState> {
+  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 (
+      <div className="logs-row">
+        {showDuplicates && (
+          <div className="logs-row__duplicates">{row.duplicates > 0 ? `${row.duplicates + 1}x` : null}</div>
+        )}
+        <div className={row.logLevel ? `logs-row__level logs-row__level--${row.logLevel}` : ''} />
+        {showUtc && (
+          <div className="logs-row__time" title={`Local: ${row.timeLocal} (${row.timeFromNow})`}>
+            {row.timestamp}
+          </div>
+        )}
+        {showLocalTime && (
+          <div className="logs-row__time" title={`${row.timestamp} (${row.timeFromNow})`}>
+            {row.timeLocal}
+          </div>
+        )}
+        {showLabels && (
+          <div className="logs-row__labels">
+            <LogLabels getRows={getRows} labels={row.uniqueLabels} onClickLabel={onClickLabel} />
+          </div>
+        )}
+        <div className="logs-row__message" onMouseEnter={this.onMouseOverMessage} onMouseLeave={this.onMouseOutMessage}>
+          {parsed && (
+            <Highlighter
+              autoEscape
+              highlightTag={FieldHighlight(this.onClickHighlight)}
+              textToHighlight={row.entry}
+              searchWords={parsedFieldHighlights}
+              highlightClassName="logs-row__field-highlight"
+            />
+          )}
+          {!parsed &&
+            needsHighlighter && (
+              <Highlighter
+                textToHighlight={row.entry}
+                searchWords={highlights}
+                findChunks={findHighlightChunksInText}
+                highlightClassName={highlightClassName}
+              />
+            )}
+          {!parsed && !needsHighlighter && row.entry}
+          {showFieldStats && (
+            <div className="logs-row__stats">
+              <Stats
+                stats={fieldStats}
+                label={fieldLabel}
+                value={fieldValue}
+                onClickClose={this.onClickClose}
+                rowCount={fieldCount}
+              />
+            </div>
+          )}
+        </div>
+      </div>
+    );
+  }
+}

+ 6 - 198
public/app/features/explore/Logs.tsx

@@ -1,7 +1,5 @@
 import _ from 'lodash';
 import React, { PureComponent } from 'react';
-import Highlighter from 'react-highlight-words';
-import classnames from 'classnames';
 
 import * as rangeUtil from 'app/core/utils/rangeutil';
 import { RawTimeRange } from '@grafana/ui';
@@ -11,20 +9,16 @@ import {
   LogsModel,
   dedupLogRows,
   filterLogLevels,
-  getParser,
   LogLevel,
   LogsMetaKind,
-  LogsLabelStat,
-  LogsParser,
-  LogRow,
-  calculateFieldStats,
 } from 'app/core/logs_model';
-import { findHighlightChunksInText } from 'app/core/utils/text';
+
 import { Switch } from 'app/core/components/Switch/Switch';
 import ToggleButtonGroup, { ToggleButton } from 'app/core/components/ToggleButtonGroup/ToggleButtonGroup';
 
 import Graph from './Graph';
-import LogLabels, { Stats } from './LogLabels';
+import LogLabels from './LogLabels';
+import { LogRow } from './LogRow';
 
 const PREVIEW_LIMIT = 100;
 
@@ -43,191 +37,6 @@ const graphOptions = {
   },
 };
 
-/**
- * Renders a highlighted field.
- * When hovering, a stats icon is shown.
- */
-const FieldHighlight = onClick => props => {
-  return (
-    <span className={props.className} style={props.style}>
-      {props.children}
-      <span className="logs-row__field-highlight--icon fa fa-signal" onClick={() => onClick(props.children)} />
-    </span>
-  );
-};
-
-interface RowProps {
-  highlighterExpressions?: string[];
-  row: LogRow;
-  showDuplicates: boolean;
-  showLabels: boolean | null; // Tristate: null means auto
-  showLocalTime: boolean;
-  showUtc: boolean;
-  getRows: () => LogRow[];
-  onClickLabel?: (label: string, value: string) => void;
-}
-
-interface RowState {
-  fieldCount: number;
-  fieldLabel: string;
-  fieldStats: LogsLabelStat[];
-  fieldValue: string;
-  parsed: boolean;
-  parser?: LogsParser;
-  parsedFieldHighlights: string[];
-  showFieldStats: 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 Row extends PureComponent<RowProps, RowState> {
-  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 (
-      <div className="logs-row">
-        {showDuplicates && (
-          <div className="logs-row__duplicates">{row.duplicates > 0 ? `${row.duplicates + 1}x` : null}</div>
-        )}
-        <div className={row.logLevel ? `logs-row__level logs-row__level--${row.logLevel}` : ''} />
-        {showUtc && (
-          <div className="logs-row__time" title={`Local: ${row.timeLocal} (${row.timeFromNow})`}>
-            {row.timestamp}
-          </div>
-        )}
-        {showLocalTime && (
-          <div className="logs-row__time" title={`${row.timestamp} (${row.timeFromNow})`}>
-            {row.timeLocal}
-          </div>
-        )}
-        {showLabels && (
-          <div className="logs-row__labels">
-            <LogLabels getRows={getRows} labels={row.uniqueLabels} onClickLabel={onClickLabel} />
-          </div>
-        )}
-        <div className="logs-row__message" onMouseEnter={this.onMouseOverMessage} onMouseLeave={this.onMouseOutMessage}>
-          {parsed && (
-            <Highlighter
-              autoEscape
-              highlightTag={FieldHighlight(this.onClickHighlight)}
-              textToHighlight={row.entry}
-              searchWords={parsedFieldHighlights}
-              highlightClassName="logs-row__field-highlight"
-            />
-          )}
-          {!parsed &&
-            needsHighlighter && (
-              <Highlighter
-                textToHighlight={row.entry}
-                searchWords={highlights}
-                findChunks={findHighlightChunksInText}
-                highlightClassName={highlightClassName}
-              />
-            )}
-          {!parsed && !needsHighlighter && row.entry}
-          {showFieldStats && (
-            <div className="logs-row__stats">
-              <Stats
-                stats={fieldStats}
-                label={fieldLabel}
-                value={fieldValue}
-                onClickClose={this.onClickClose}
-                rowCount={fieldCount}
-              />
-            </div>
-          )}
-        </div>
-      </div>
-    );
-  }
-}
-
 function renderMetaItem(value: any, kind: LogsMetaKind) {
   if (kind === LogsMetaKind.LabelsMap) {
     return (
@@ -441,10 +250,9 @@ export default class Logs extends PureComponent<LogsProps, LogsState> {
 
         <div className="logs-rows">
           {hasData &&
-            !deferLogs &&
-            // Only inject highlighterExpression in the first set for performance reasons
+          !deferLogs && // Only inject highlighterExpression in the first set for performance reasons
             firstRows.map(row => (
-              <Row
+              <LogRow
                 key={row.key + row.duplicates}
                 getRows={getRows}
                 highlighterExpressions={highlighterExpressions}
@@ -460,7 +268,7 @@ export default class Logs extends PureComponent<LogsProps, LogsState> {
             !deferLogs &&
             renderAll &&
             lastRows.map(row => (
-              <Row
+              <LogRow
                 key={row.key + row.duplicates}
                 getRows={getRows}
                 row={row}

+ 4 - 4
public/app/plugins/datasource/loki/result_transformer.ts

@@ -5,7 +5,7 @@ import {
   LogLevel,
   LogsMetaItem,
   LogsModel,
-  LogRow,
+  LogRowModel,
   LogsStream,
   LogsStreamEntry,
   LogsStreamLabels,
@@ -115,7 +115,7 @@ export function processEntry(
   parsedLabels: LogsStreamLabels,
   uniqueLabels: LogsStreamLabels,
   search: string
-): LogRow {
+): LogRowModel {
   const { line } = entry;
   const ts = entry.ts || entry.timestamp;
   // Assumes unique-ness, needs nanosec precision for timestamp
@@ -156,9 +156,9 @@ export function mergeStreamsToLogs(streams: LogsStream[], limit = DEFAULT_MAX_LI
   }));
 
   // Merge stream entries into single list of log rows
-  const sortedRows: LogRow[] = _.chain(streams)
+  const sortedRows: LogRowModel[] = _.chain(streams)
     .reduce(
-      (acc: LogRow[], stream: LogsStream) => [
+      (acc: LogRowModel[], stream: LogsStream) => [
         ...acc,
         ...stream.entries.map(entry =>
           processEntry(entry, stream.labels, stream.parsedLabels, stream.uniqueLabels, stream.search)