diff --git a/public/app/core/logs_model.ts b/public/app/core/logs_model.ts index 46e95a471ce..8848a929359 100644 --- a/public/app/core/logs_model.ts +++ b/public/app/core/logs_model.ts @@ -11,7 +11,7 @@ export enum LogLevel { export interface LogSearchMatch { start: number; length: number; - text?: string; + text: string; } export interface LogRow { @@ -21,7 +21,7 @@ export interface LogRow { timestamp: string; timeFromNow: string; timeLocal: string; - searchMatches?: LogSearchMatch[]; + searchWords?: string[]; } export interface LogsModel { diff --git a/public/app/core/utils/text.test.ts b/public/app/core/utils/text.test.ts new file mode 100644 index 00000000000..4f9d8367218 --- /dev/null +++ b/public/app/core/utils/text.test.ts @@ -0,0 +1,24 @@ +import { findMatchesInText } from './text'; + +describe('findMatchesInText()', () => { + it('gets no matches for when search and or line are empty', () => { + expect(findMatchesInText('', '')).toEqual([]); + expect(findMatchesInText('foo', '')).toEqual([]); + expect(findMatchesInText('', 'foo')).toEqual([]); + }); + + it('gets no matches for unmatched search string', () => { + expect(findMatchesInText('foo', 'bar')).toEqual([]); + }); + + it('gets matches for matched search string', () => { + expect(findMatchesInText('foo', 'foo')).toEqual([{ length: 3, start: 0, text: 'foo', end: 3 }]); + 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 }, + ]); +}); diff --git a/public/app/core/utils/text.ts b/public/app/core/utils/text.ts new file mode 100644 index 00000000000..5d7591a31e2 --- /dev/null +++ b/public/app/core/utils/text.ts @@ -0,0 +1,32 @@ +import { TextMatch } from 'app/types/explore'; + +/** + * Adapt findMatchesInText for react-highlight-words findChunks handler. + * See https://github.com/bvaughn/react-highlight-words#props + */ +export function findHighlightChunksInText({ searchWords, textToHighlight }) { + return findMatchesInText(textToHighlight, searchWords.join(' ')); +} + +/** + * Returns a list of substring regexp matches. + */ +export function findMatchesInText(haystack: string, needle: string): TextMatch[] { + // Empty search can send re.exec() into infinite loop, exit early + 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); + } + return matches; +} diff --git a/public/app/features/explore/Logs.tsx b/public/app/features/explore/Logs.tsx index ae2d5e2daa6..cc8f9be48fd 100644 --- a/public/app/features/explore/Logs.tsx +++ b/public/app/features/explore/Logs.tsx @@ -1,6 +1,8 @@ import React, { Fragment, PureComponent } from 'react'; +import Highlighter from 'react-highlight-words'; -import { LogsModel, LogRow } from 'app/core/logs_model'; +import { LogsModel } from 'app/core/logs_model'; +import { findHighlightChunksInText } from 'app/core/utils/text'; interface LogsProps { className?: string; @@ -10,34 +12,7 @@ interface LogsProps { const EXAMPLE_QUERY = '{job="default/prometheus"}'; -const Entry: React.SFC = props => { - const { entry, searchMatches } = props; - if (searchMatches && searchMatches.length > 0) { - let lastMatchEnd = 0; - const spans = searchMatches.reduce((acc, match, i) => { - // Insert non-match - if (match.start !== lastMatchEnd) { - acc.push(<>{entry.slice(lastMatchEnd, match.start)}); - } - // Match - acc.push( - - {entry.substr(match.start, match.length)} - - ); - lastMatchEnd = match.start + match.length; - // Non-matching end - if (i === searchMatches.length - 1) { - acc.push(<>{entry.slice(lastMatchEnd)}); - } - return acc; - }, []); - return <>{spans}; - } - return <>{props.entry}; -}; - -export default class Logs extends PureComponent { +export default class Logs extends PureComponent { render() { const { className = '', data } = this.props; const hasData = data && data.rows && data.rows.length > 0; @@ -50,7 +25,12 @@ export default class Logs extends PureComponent {
{row.timeLocal}
- +
))} diff --git a/public/app/features/explore/PromQueryField.tsx b/public/app/features/explore/PromQueryField.tsx index 8e74b34fee2..889666c5e35 100644 --- a/public/app/features/explore/PromQueryField.tsx +++ b/public/app/features/explore/PromQueryField.tsx @@ -145,7 +145,7 @@ interface PromQueryFieldProps { onClickHintFix?: (action: any) => void; onPressEnter?: () => void; onQueryChange?: (value: string, override?: boolean) => void; - portalPrefix?: string; + portalOrigin?: string; request?: (url: string) => any; supportsLogs?: boolean; // To be removed after Logging gets its own query field } @@ -571,10 +571,10 @@ class PromQueryField extends React.PureComponentLog labels ) : ( - - - - )} + + + + )}
@@ -586,7 +586,7 @@ class PromQueryField extends React.PureComponent
diff --git a/public/app/features/explore/QueryField.tsx b/public/app/features/explore/QueryField.tsx index 237ad6a82e8..c89893b4f28 100644 --- a/public/app/features/explore/QueryField.tsx +++ b/public/app/features/explore/QueryField.tsx @@ -104,7 +104,7 @@ interface TypeaheadFieldProps { onValueChanged?: (value: Value) => void; onWillApplySuggestion?: (suggestion: string, state: TypeaheadFieldState) => string; placeholder?: string; - portalPrefix?: string; + portalOrigin?: string; syntax?: string; syntaxLoaded?: boolean; } @@ -459,8 +459,8 @@ class QueryField extends React.PureComponent { - const { portalPrefix } = this.props; - const { suggestions, typeaheadIndex } = this.state; + const { portalOrigin } = this.props; + const { suggestions, typeaheadIndex, typeaheadPrefix } = this.state; if (!hasSuggestions(suggestions)) { return null; } @@ -469,11 +469,12 @@ class QueryField extends React.PureComponent + @@ -500,14 +501,14 @@ class QueryField extends React.PureComponent { +class Portal extends React.PureComponent<{ index?: number; origin: string }, {}> { node: HTMLElement; constructor(props) { super(props); - const { index = 0, prefix = 'query' } = props; + const { index = 0, origin = 'query' } = props; this.node = document.createElement('div'); - this.node.classList.add(`slate-typeahead`, `slate-typeahead-${prefix}-${index}`); + this.node.classList.add(`slate-typeahead`, `slate-typeahead-${origin}-${index}`); document.body.appendChild(this.node); } diff --git a/public/app/features/explore/QueryRows.tsx b/public/app/features/explore/QueryRows.tsx index 4dee3813ca8..3d71c2f3566 100644 --- a/public/app/features/explore/QueryRows.tsx +++ b/public/app/features/explore/QueryRows.tsx @@ -53,7 +53,6 @@ class QueryRow extends PureComponent { hint={queryHint} initialQuery={query} history={history} - portalPrefix="explore" onClickHintFix={this.onClickHintFix} onPressEnter={this.onPressEnter} onQueryChange={this.onChangeQuery} diff --git a/public/app/features/explore/Typeahead.tsx b/public/app/features/explore/Typeahead.tsx index 999e5003eaf..0c01cbe01ba 100644 --- a/public/app/features/explore/Typeahead.tsx +++ b/public/app/features/explore/Typeahead.tsx @@ -1,4 +1,5 @@ import React from 'react'; +import Highlighter from 'react-highlight-words'; import { Suggestion, SuggestionGroup } from './QueryField'; @@ -16,6 +17,7 @@ interface TypeaheadItemProps { isSelected: boolean; item: Suggestion; onClickItem: (Suggestion) => void; + prefix?: string; } class TypeaheadItem extends React.PureComponent { @@ -38,11 +40,12 @@ class TypeaheadItem extends React.PureComponent { }; render() { - const { isSelected, item } = this.props; + const { isSelected, item, prefix } = this.props; const className = isSelected ? 'typeahead-item typeahead-item__selected' : 'typeahead-item'; + const { label } = item; return (
  • - {item.detail || item.label} + {item.documentation && isSelected ?
    {item.documentation}
    : null}
  • ); @@ -54,18 +57,25 @@ interface TypeaheadGroupProps { label: string; onClickItem: (Suggestion) => void; selected: Suggestion; + prefix?: string; } class TypeaheadGroup extends React.PureComponent { render() { - const { items, label, selected, onClickItem } = this.props; + const { items, label, selected, onClickItem, prefix } = this.props; return (
  • {label}
      {items.map(item => { return ( - + ); })}
    @@ -79,14 +89,15 @@ interface TypeaheadProps { menuRef: any; selectedItem: Suggestion | null; onClickItem: (Suggestion) => void; + prefix?: string; } class Typeahead extends React.PureComponent { render() { - const { groupedItems, menuRef, selectedItem, onClickItem } = this.props; + const { groupedItems, menuRef, selectedItem, onClickItem, prefix } = this.props; return (
      {groupedItems.map(g => ( - + ))}
    ); diff --git a/public/app/plugins/datasource/logging/result_transformer.test.ts b/public/app/plugins/datasource/logging/result_transformer.test.ts index 0d203f748ba..c1e6913a388 100644 --- a/public/app/plugins/datasource/logging/result_transformer.test.ts +++ b/public/app/plugins/datasource/logging/result_transformer.test.ts @@ -1,29 +1,6 @@ import { LogLevel } from 'app/core/logs_model'; -import { getLogLevel, getSearchMatches } from './result_transformer'; - -describe('getSearchMatches()', () => { - it('gets no matches for when search and or line are empty', () => { - expect(getSearchMatches('', '')).toEqual([]); - expect(getSearchMatches('foo', '')).toEqual([]); - expect(getSearchMatches('', 'foo')).toEqual([]); - }); - - it('gets no matches for unmatched search string', () => { - expect(getSearchMatches('foo', 'bar')).toEqual([]); - }); - - it('gets matches for matched search string', () => { - expect(getSearchMatches('foo', 'foo')).toEqual([{ length: 3, start: 0, text: 'foo' }]); - expect(getSearchMatches(' foo ', 'foo')).toEqual([{ length: 3, start: 1, text: 'foo' }]); - }); - - expect(getSearchMatches(' foo foo bar ', 'foo|bar')).toEqual([ - { length: 3, start: 1, text: 'foo' }, - { length: 3, start: 5, text: 'foo' }, - { length: 3, start: 9, text: 'bar' }, - ]); -}); +import { getLogLevel } from './result_transformer'; describe('getLoglevel()', () => { it('returns no log level on empty line', () => { diff --git a/public/app/plugins/datasource/logging/result_transformer.ts b/public/app/plugins/datasource/logging/result_transformer.ts index 891f9268068..526a9c7da2c 100644 --- a/public/app/plugins/datasource/logging/result_transformer.ts +++ b/public/app/plugins/datasource/logging/result_transformer.ts @@ -19,25 +19,6 @@ export function getLogLevel(line: string): LogLevel { return level; } -export function getSearchMatches(line: string, search: string) { - // Empty search can send re.exec() into infinite loop, exit early - if (!line || !search) { - return []; - } - const regexp = new RegExp(`(?:${search})`, 'g'); - const matches = []; - let match = regexp.exec(line); - while (match) { - matches.push({ - text: match[0], - start: match.index, - length: match[0].length, - }); - match = regexp.exec(line); - } - return matches; -} - export function processEntry(entry: { line: string; timestamp: string }, stream): LogRow { const { line, timestamp } = entry; const { labels } = stream; @@ -45,16 +26,15 @@ export function processEntry(entry: { line: string; timestamp: string }, stream) const time = moment(timestamp); const timeFromNow = time.fromNow(); const timeLocal = time.format('YYYY-MM-DD HH:mm:ss'); - const searchMatches = getSearchMatches(line, stream.search); const logLevel = getLogLevel(line); return { key, logLevel, - searchMatches, timeFromNow, timeLocal, entry: line, + searchWords: [stream.search], timestamp: timestamp, }; } diff --git a/public/app/types/explore.ts b/public/app/types/explore.ts index d72020b6eb7..f6072becec4 100644 --- a/public/app/types/explore.ts +++ b/public/app/types/explore.ts @@ -13,6 +13,13 @@ export interface Query { key?: string; } +export interface TextMatch { + text: string; + start: number; + length: number; + end: number; +} + export interface ExploreState { datasource: any; datasourceError: any; diff --git a/public/sass/components/_slate_editor.scss b/public/sass/components/_slate_editor.scss index 10b2238f4b8..a8291aa1a87 100644 --- a/public/sass/components/_slate_editor.scss +++ b/public/sass/components/_slate_editor.scss @@ -66,7 +66,6 @@ .typeahead-item__selected { background-color: $typeahead-selected-bg; - color: $typeahead-selected-color; .typeahead-item-hint { font-size: $font-size-xs; @@ -74,6 +73,14 @@ white-space: normal; } } + + .typeahead-match { + color: $typeahead-selected-color; + border-bottom: 1px solid $typeahead-selected-color; + // Undoing mark styling + padding: inherit; + background: inherit; + } } /* SYNTAX */