mirror of
https://github.com/grafana/grafana.git
synced 2025-02-15 10:03:33 -06:00
Merge pull request #14277 from grafana/davkal/explore-logging-live-preview
Explore: Logging query live preview of matches
This commit is contained in:
commit
c4a89eb32d
17
package.json
17
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,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}
|
||||
|
@ -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}
|
||||
|
@ -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: rgba($typeahead-selected-color, 0.2);
|
||||
border-bottom-style: dotted;
|
||||
}
|
||||
|
||||
.logs-row-level {
|
||||
background-color: transparent;
|
||||
margin: 2px 0;
|
||||
|
18
yarn.lock
18
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:
|
||||
|
Loading…
Reference in New Issue
Block a user