From 6ee7459f220d0d86ed956cb06e0d362378fcc5e4 Mon Sep 17 00:00:00 2001 From: David Kaltschmidt Date: Sat, 1 Dec 2018 15:26:51 +0100 Subject: [PATCH 1/2] 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) --- package.json | 17 +++------- public/app/core/utils/text.test.ts | 21 +++++++++--- public/app/core/utils/text.ts | 32 +++++++++++++------ public/app/features/explore/Explore.tsx | 24 +++++++++++++- public/app/features/explore/Logs.tsx | 30 ++++++++++++++--- .../plugins/datasource/logging/datasource.ts | 4 +++ public/app/types/explore.ts | 1 + public/sass/pages/_explore.scss | 5 +++ yarn.lock | 18 +++++------ 9 files changed, 109 insertions(+), 43 deletions(-) diff --git a/package.json b/package.json index f5e6e22a8a2..54e9e0284a2 100644 --- a/package.json +++ b/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", diff --git a/public/app/core/utils/text.test.ts b/public/app/core/utils/text.test.ts index 4f9d8367218..206e8507f9d 100644 --- a/public/app/core/utils/text.test.ts +++ b/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([]); + }); }); diff --git a/public/app/core/utils/text.ts b/public/app/core/utils/text.ts index 5d7591a31e2..4e948116dba 100644 --- a/public/app/core/utils/text.ts +++ b/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; } diff --git a/public/app/features/explore/Explore.tsx b/public/app/features/explore/Explore.tsx index 23a06a5acc9..d2588a8ec0b 100644 --- a/public/app/features/explore/Explore.tsx +++ b/public/app/features/explore/Explore.tsx @@ -253,6 +253,7 @@ export class Explore extends React.PureComponent { datasourceLoading: false, datasourceName: datasource.name, initialQueries: nextQueries, + logsHighlighterExpressions: undefined, showingStartPage: Boolean(StartPage), }, () => { @@ -291,7 +292,11 @@ export class Explore extends React.PureComponent { return qt; }); - return { initialQueries: nextQueries, queryTransactions: nextQueryTransactions }; + return { + initialQueries: nextQueries, + logsHighlighterExpressions: undefined, + queryTransactions: nextQueryTransactions, + }; }); }; @@ -337,6 +342,9 @@ export class Explore extends React.PureComponent { 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 { return { ...results, initialQueries: nextQueries, + logsHighlighterExpressions: undefined, queryTransactions: nextQueryTransactions, }; }, @@ -794,6 +803,17 @@ export class Explore extends React.PureComponent { }); } + 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 { graphResult, history, initialQueries, + logsHighlighterExpressions, logsResult, queryTransactions, range, @@ -964,6 +985,7 @@ export class Explore extends React.PureComponent { 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 ( <>
@@ -76,9 +83,9 @@ function Row({ allRows, onClickLabel, row, showLabels, showLocalTime, showUtc }: {needsHighlighter ? ( ) : ( 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 { }; 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 {
{hasData && !deferLogs && + // Only inject highlighterExpression in the first set for performance reasons firstRows.map(row => ( Date: Tue, 4 Dec 2018 15:58:10 +0100 Subject: [PATCH 2/2] Adapt styles --- public/sass/pages/_explore.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/sass/pages/_explore.scss b/public/sass/pages/_explore.scss index b01266a23f3..8ae2cd5456e 100644 --- a/public/sass/pages/_explore.scss +++ b/public/sass/pages/_explore.scss @@ -308,7 +308,7 @@ } .logs-row-match-highlight--preview { - background-color: lighten($typeahead-selected-color, 70%); + background-color: rgba($typeahead-selected-color, 0.2); border-bottom-style: dotted; }