Ver código fonte

Merge pull request #14277 from grafana/davkal/explore-logging-live-preview

Explore: Logging query live preview of matches
David 7 anos atrás
pai
commit
c4a89eb32d

+ 4 - 13
package.json

@@ -108,18 +108,9 @@
     "precommit": "lint-staged && grunt precommit"
   },
   "lint-staged": {
-    "*.{ts,tsx}": [
-      "prettier --write",
-      "git add"
-    ],
-    "*.scss": [
-      "prettier --write",
-      "git add"
-    ],
-    "*pkg/**/*.go": [
-      "gofmt -w -s",
-      "git add"
-    ]
+    "*.{ts,tsx}": ["prettier --write", "git add"],
+    "*.scss": ["prettier --write", "git add"],
+    "*pkg/**/*.go": ["gofmt -w -s", "git add"]
   },
   "prettier": {
     "trailingComma": "es5",
@@ -156,7 +147,7 @@
     "react-custom-scrollbars": "^4.2.1",
     "react-dom": "^16.5.0",
     "react-grid-layout": "0.16.6",
-    "react-highlight-words": "^0.10.0",
+    "react-highlight-words": "0.11.0",
     "react-popper": "^0.7.5",
     "react-redux": "^5.0.7",
     "react-select": "2.1.0",

+ 16 - 5
public/app/core/utils/text.test.ts

@@ -16,9 +16,20 @@ describe('findMatchesInText()', () => {
     expect(findMatchesInText(' foo ', 'foo')).toEqual([{ length: 3, start: 1, text: 'foo', end: 4 }]);
   });
 
-  expect(findMatchesInText(' foo foo bar ', 'foo|bar')).toEqual([
-    { length: 3, start: 1, text: 'foo', end: 4 },
-    { length: 3, start: 5, text: 'foo', end: 8 },
-    { length: 3, start: 9, text: 'bar', end: 12 },
-  ]);
+  test('should find all matches for a complete regex', () => {
+    expect(findMatchesInText(' foo foo bar ', 'foo|bar')).toEqual([
+      { length: 3, start: 1, text: 'foo', end: 4 },
+      { length: 3, start: 5, text: 'foo', end: 8 },
+      { length: 3, start: 9, text: 'bar', end: 12 },
+    ]);
+  });
+
+  test('not fail on incomplete regex', () => {
+    expect(findMatchesInText(' foo foo bar ', 'foo|')).toEqual([
+      { length: 3, start: 1, text: 'foo', end: 4 },
+      { length: 3, start: 5, text: 'foo', end: 8 },
+    ]);
+    expect(findMatchesInText('foo foo bar', '(')).toEqual([]);
+    expect(findMatchesInText('foo foo bar', '(foo|')).toEqual([]);
+  });
 });

+ 22 - 10
public/app/core/utils/text.ts

@@ -8,6 +8,10 @@ export function findHighlightChunksInText({ searchWords, textToHighlight }) {
   return findMatchesInText(textToHighlight, searchWords.join(' '));
 }
 
+const cleanNeedle = (needle: string): string => {
+  return needle.replace(/[[{(][\w,.-?:*+]+$/, '');
+};
+
 /**
  * Returns a list of substring regexp matches.
  */
@@ -16,17 +20,25 @@ export function findMatchesInText(haystack: string, needle: string): TextMatch[]
   if (!haystack || !needle) {
     return [];
   }
-  const regexp = new RegExp(`(?:${needle})`, 'g');
   const matches = [];
-  let match = regexp.exec(haystack);
-  while (match) {
-    matches.push({
-      text: match[0],
-      start: match.index,
-      length: match[0].length,
-      end: match.index + match[0].length,
-    });
-    match = regexp.exec(haystack);
+  const cleaned = cleanNeedle(needle);
+  let regexp;
+  try {
+    regexp = new RegExp(`(?:${cleaned})`, 'g');
+  } catch (error) {
+    return matches;
   }
+  haystack.replace(regexp, (substring, ...rest) => {
+    if (substring) {
+      const offset = rest[rest.length - 2];
+      matches.push({
+        text: substring,
+        start: offset,
+        length: substring.length,
+        end: offset + substring.length,
+      });
+    }
+    return '';
+  });
   return matches;
 }

+ 23 - 1
public/app/features/explore/Explore.tsx

@@ -253,6 +253,7 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
         datasourceLoading: false,
         datasourceName: datasource.name,
         initialQueries: nextQueries,
+        logsHighlighterExpressions: undefined,
         showingStartPage: Boolean(StartPage),
       },
       () => {
@@ -291,7 +292,11 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
         return qt;
       });
 
-      return { initialQueries: nextQueries, queryTransactions: nextQueryTransactions };
+      return {
+        initialQueries: nextQueries,
+        logsHighlighterExpressions: undefined,
+        queryTransactions: nextQueryTransactions,
+      };
     });
   };
 
@@ -337,6 +342,9 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
           queryTransactions: nextQueryTransactions,
         };
       }, this.onSubmit);
+    } else if (this.state.datasource.getHighlighterExpression && this.modifiedQueries.length === 1) {
+      // Live preview of log search matches. Can only work on single row query for now
+      this.updateLogsHighlights(value);
     }
   };
 
@@ -529,6 +537,7 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
         return {
           ...results,
           initialQueries: nextQueries,
+          logsHighlighterExpressions: undefined,
           queryTransactions: nextQueryTransactions,
         };
       },
@@ -794,6 +803,17 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
     });
   }
 
+  updateLogsHighlights = _.debounce((value: DataQuery, index: number) => {
+    this.setState(state => {
+      const { datasource } = state;
+      if (datasource.getHighlighterExpression) {
+        const logsHighlighterExpressions = [state.datasource.getHighlighterExpression(value)];
+        return { logsHighlighterExpressions };
+      }
+      return null;
+    });
+  }, 500);
+
   cloneState(): ExploreState {
     // Copy state, but copy queries including modifications
     return {
@@ -820,6 +840,7 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
       graphResult,
       history,
       initialQueries,
+      logsHighlighterExpressions,
       logsResult,
       queryTransactions,
       range,
@@ -964,6 +985,7 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
                         <Logs
                           data={logsResult}
                           key={logsResult.id}
+                          highlighterExpressions={logsHighlighterExpressions}
                           loading={logsLoading}
                           position={position}
                           onChangeTime={this.onChangeTime}

+ 25 - 5
public/app/features/explore/Logs.tsx

@@ -1,6 +1,7 @@
 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 'app/types/series';
@@ -37,6 +38,7 @@ const graphOptions = {
 
 interface RowProps {
   allRows: LogRow[];
+  highlighterExpressions?: string[];
   row: LogRow;
   showLabels: boolean | null; // Tristate: null means auto
   showLocalTime: boolean;
@@ -44,8 +46,13 @@ interface RowProps {
   onClickLabel?: (label: string, value: string) => void;
 }
 
-function Row({ allRows, onClickLabel, row, showLabels, showLocalTime, showUtc }: RowProps) {
-  const needsHighlighter = row.searchWords && row.searchWords.length > 0;
+function Row({ allRows, highlighterExpressions, onClickLabel, row, showLabels, showLocalTime, showUtc }: RowProps) {
+  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={row.logLevel ? `logs-row-level logs-row-level-${row.logLevel}` : ''}>
@@ -76,9 +83,9 @@ function Row({ allRows, onClickLabel, row, showLabels, showLocalTime, showUtc }:
         {needsHighlighter ? (
           <Highlighter
             textToHighlight={row.entry}
-            searchWords={row.searchWords}
+            searchWords={highlights}
             findChunks={findHighlightChunksInText}
-            highlightClassName="logs-row-match-highlight"
+            highlightClassName={highlightClassName}
           />
         ) : (
           row.entry
@@ -102,6 +109,7 @@ function renderMetaItem(value: any, kind: LogsMetaKind) {
 interface LogsProps {
   className?: string;
   data: LogsModel;
+  highlighterExpressions: string[];
   loading: boolean;
   position: string;
   range?: RawTimeRange;
@@ -206,7 +214,17 @@ export default class Logs extends PureComponent<LogsProps, LogsState> {
   };
 
   render() {
-    const { className = '', data, loading = false, onClickLabel, position, range, scanning, scanRange } = this.props;
+    const {
+      className = '',
+      data,
+      highlighterExpressions,
+      loading = false,
+      onClickLabel,
+      position,
+      range,
+      scanning,
+      scanRange,
+    } = this.props;
     const { dedup, deferLogs, hiddenLogLevels, renderAll, showLocalTime, showUtc } = this.state;
     let { showLabels } = this.state;
     const hasData = data && data.rows && data.rows.length > 0;
@@ -316,10 +334,12 @@ export default class Logs extends PureComponent<LogsProps, LogsState> {
         <div className="logs-entries" style={logEntriesStyle}>
           {hasData &&
             !deferLogs &&
+            // Only inject highlighterExpression in the first set for performance reasons
             firstRows.map(row => (
               <Row
                 key={row.key + row.duplicates}
                 allRows={processedRows}
+                highlighterExpressions={highlighterExpressions}
                 row={row}
                 showLabels={showLabels}
                 showLocalTime={showLocalTime}

+ 4 - 0
public/app/plugins/datasource/logging/datasource.ts

@@ -117,6 +117,10 @@ export default class LoggingDatasource {
     return { ...query, expr: expression };
   }
 
+  getHighlighterExpression(query: DataQuery): string {
+    return parseQuery(query.expr).regexp;
+  }
+
   getTime(date, roundUp) {
     if (_.isString(date)) {
       date = dateMath.parse(date, roundUp);

+ 1 - 0
public/app/types/explore.ts

@@ -164,6 +164,7 @@ export interface ExploreState {
   graphResult?: any[];
   history: HistoryItem[];
   initialQueries: DataQuery[];
+  logsHighlighterExpressions?: string[];
   logsResult?: LogsModel;
   queryTransactions: QueryTransaction[];
   range: RawTimeRange;

+ 5 - 0
public/sass/pages/_explore.scss

@@ -307,6 +307,11 @@
       background-color: rgba($typeahead-selected-color, 0.1);
     }
 
+    .logs-row-match-highlight--preview {
+      background-color: rgba($typeahead-selected-color, 0.2);
+      border-bottom-style: dotted;
+    }
+
     .logs-row-level {
       background-color: transparent;
       margin: 2px 0;

+ 9 - 9
yarn.lock

@@ -6230,10 +6230,10 @@ header-case@^1.0.0:
     no-case "^2.2.0"
     upper-case "^1.1.3"
 
-highlight-words-core@^1.1.0:
-  version "1.2.0"
-  resolved "https://registry.yarnpkg.com/highlight-words-core/-/highlight-words-core-1.2.0.tgz#232bec301cbf2a4943d335dc748ce70e9024f3b1"
-  integrity sha512-nu5bMsWIgpsrlXEMNKSvbJMeUPhFxCOVT28DnI8UCVfhm3e98LC8oeyMNrc7E18+QQ4l/PvbeN7ojyN4XsmBdA==
+highlight-words-core@^1.2.0:
+  version "1.2.2"
+  resolved "https://registry.yarnpkg.com/highlight-words-core/-/highlight-words-core-1.2.2.tgz#1eff6d7d9f0a22f155042a00791237791b1eeaaa"
+  integrity sha512-BXUKIkUuh6cmmxzi5OIbUJxrG8OAk2MqoL1DtO3Wo9D2faJg2ph5ntyuQeLqaHJmzER6H5tllCDA9ZnNe9BVGg==
 
 hmac-drbg@^1.0.0:
   version "1.0.1"
@@ -11285,12 +11285,12 @@ react-grid-layout@0.16.6:
     react-draggable "3.x"
     react-resizable "1.x"
 
-react-highlight-words@^0.10.0:
-  version "0.10.0"
-  resolved "https://registry.yarnpkg.com/react-highlight-words/-/react-highlight-words-0.10.0.tgz#2e905c76c11635237f848ecad00600f1b6f6f4a8"
-  integrity sha512-/5jh6a8pir3baCOMC5j88MBmNciSwG5bXWNAAtbtDb3WYJoGn82e2zLCQFnghIBWod1h5y6/LRO8TS6ERbN5aQ==
+react-highlight-words@0.11.0:
+  version "0.11.0"
+  resolved "https://registry.yarnpkg.com/react-highlight-words/-/react-highlight-words-0.11.0.tgz#4f3c2039a8fd275f3ab795e59946b0324d8e6bee"
+  integrity sha512-b+fgdQXNjX6RwHfiBYn6qH2D2mJEDNLuxdsqRseIiQffoCAoj7naMQ5EktUkmo9Bh1mXq/aMpJbdx7Lf2PytcQ==
   dependencies:
-    highlight-words-core "^1.1.0"
+    highlight-words-core "^1.2.0"
     prop-types "^15.5.8"
 
 react-hot-loader@^4.3.6: