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

Explore: Logging query live preview of matches
This commit is contained in:
David 2018-12-04 16:20:01 +01:00 committed by GitHub
commit c4a89eb32d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 109 additions and 43 deletions

View File

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

View File

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

View File

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

View File

@ -253,6 +253,7 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
datasourceLoading: false, datasourceLoading: false,
datasourceName: datasource.name, datasourceName: datasource.name,
initialQueries: nextQueries, initialQueries: nextQueries,
logsHighlighterExpressions: undefined,
showingStartPage: Boolean(StartPage), showingStartPage: Boolean(StartPage),
}, },
() => { () => {
@ -291,7 +292,11 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
return qt; 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, queryTransactions: nextQueryTransactions,
}; };
}, this.onSubmit); }, 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 { return {
...results, ...results,
initialQueries: nextQueries, initialQueries: nextQueries,
logsHighlighterExpressions: undefined,
queryTransactions: nextQueryTransactions, 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 { cloneState(): ExploreState {
// Copy state, but copy queries including modifications // Copy state, but copy queries including modifications
return { return {
@ -820,6 +840,7 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
graphResult, graphResult,
history, history,
initialQueries, initialQueries,
logsHighlighterExpressions,
logsResult, logsResult,
queryTransactions, queryTransactions,
range, range,
@ -964,6 +985,7 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
<Logs <Logs
data={logsResult} data={logsResult}
key={logsResult.id} key={logsResult.id}
highlighterExpressions={logsHighlighterExpressions}
loading={logsLoading} loading={logsLoading}
position={position} position={position}
onChangeTime={this.onChangeTime} onChangeTime={this.onChangeTime}

View File

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

View File

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

View File

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

View File

@ -307,6 +307,11 @@
background-color: rgba($typeahead-selected-color, 0.1); 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 { .logs-row-level {
background-color: transparent; background-color: transparent;
margin: 2px 0; margin: 2px 0;

View File

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