Commit 6ee7459f by David Kaltschmidt

Explore: Logging query live preview of matches

A logging query has a selector part and a regexp. The regexp matches are highlighted when results return.
This change adds live preview to matches when modifying the regexp in a search field.

- delegate retrieval of match query to datasource
- datasource returns search expressions to be used to highlight a live preview of matches
- logs row now takes preview highlights
- logs row renders preview highlights with dotted line to distinguish from query run matches (solid line)
- fix react-highlight-words version to ensure custom chunk matcher
- custom chunk matcher can now also take incomplete regexps, eg, `(level` without inifinte looping
- perf: debounce of live preview to 500ms
- perf: only top 100 rows get the live preview
- preview is only supported with one query row (multiple rows semantic makes this tricky: regexp for row n should only filter results for query n)
parent 34b8c5bb
......@@ -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,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([]);
});
});
......@@ -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;
}
......@@ -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}
......
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}
......
......@@ -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);
......
......@@ -164,6 +164,7 @@ export interface ExploreState {
graphResult?: any[];
history: HistoryItem[];
initialQueries: DataQuery[];
logsHighlighterExpressions?: string[];
logsResult?: LogsModel;
queryTransactions: QueryTransaction[];
range: RawTimeRange;
......
......@@ -307,6 +307,11 @@
background-color: rgba($typeahead-selected-color, 0.1);
}
.logs-row-match-highlight--preview {
background-color: lighten($typeahead-selected-color, 70%);
border-bottom-style: dotted;
}
.logs-row-level {
background-color: transparent;
margin: 2px 0;
......
......@@ -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:
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment