mirror of
https://github.com/grafana/grafana.git
synced 2025-02-16 18:34:52 -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"
|
"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",
|
||||||
|
@ -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([]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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}
|
||||||
|
@ -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}
|
||||||
|
@ -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);
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
18
yarn.lock
18
yarn.lock
@ -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:
|
||||||
|
Loading…
Reference in New Issue
Block a user