Explore: highlight typed text in suggestions

- use react-highlight-words
- add highlighting (color and border) to the matching substring of the
  suggested items in the typeahead
- extracted match finding from logging datasource
- created new utils/text.ts class for text-related functions
- added more types
This commit is contained in:
David Kaltschmidt
2018-10-05 13:00:45 +02:00
parent c1164f5c00
commit 76a3b1a793
12 changed files with 116 additions and 98 deletions

View File

@@ -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<LogRow> = 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(
<span className="logs-row-match-highlight" title={`Matching expression: ${match.text}`}>
{entry.substr(match.start, match.length)}
</span>
);
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<LogsProps, any> {
export default class Logs extends PureComponent<LogsProps, {}> {
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<LogsProps, any> {
<div className={row.logLevel ? `logs-row-level logs-row-level-${row.logLevel}` : ''} />
<div title={`${row.timestamp} (${row.timeFromNow})`}>{row.timeLocal}</div>
<div>
<Entry {...row} />
<Highlighter
textToHighlight={row.entry}
searchWords={row.searchWords}
findChunks={findHighlightChunksInText}
highlightClassName="logs-row-match-highlight"
/>
</div>
</Fragment>
))}

View File

@@ -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.PureComponent<PromQueryFieldProps, PromQueryF
<button className="btn navbar-button navbar-button--tight">Log labels</button>
</Cascader>
) : (
<Cascader options={metricsOptions} onChange={this.onChangeMetrics}>
<button className="btn navbar-button navbar-button--tight">Metrics</button>
</Cascader>
)}
<Cascader options={metricsOptions} onChange={this.onChangeMetrics}>
<button className="btn navbar-button navbar-button--tight">Metrics</button>
</Cascader>
)}
</div>
<div className="prom-query-field-wrapper">
<div className="slate-query-field-wrapper">
@@ -586,7 +586,7 @@ class PromQueryField extends React.PureComponent<PromQueryFieldProps, PromQueryF
onWillApplySuggestion={willApplySuggestion}
onValueChanged={this.onChangeQuery}
placeholder="Enter a PromQL query"
portalPrefix="prometheus"
portalOrigin="prometheus"
syntaxLoaded={syntaxLoaded}
/>
</div>

View File

@@ -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<TypeaheadFieldProps, TypeaheadField
};
renderMenu = () => {
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<TypeaheadFieldProps, TypeaheadField
// Create typeahead in DOM root so we can later position it absolutely
return (
<Portal prefix={portalPrefix}>
<Portal origin={portalOrigin}>
<Typeahead
menuRef={this.menuRef}
selectedItem={selectedItem}
onClickItem={this.onClickMenu}
prefix={typeaheadPrefix}
groupedItems={suggestions}
/>
</Portal>
@@ -500,14 +501,14 @@ class QueryField extends React.PureComponent<TypeaheadFieldProps, TypeaheadField
}
}
class Portal extends React.PureComponent<{ index?: number; prefix: string }, {}> {
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);
}

View File

@@ -53,7 +53,6 @@ class QueryRow extends PureComponent<any, {}> {
hint={queryHint}
initialQuery={query}
history={history}
portalPrefix="explore"
onClickHintFix={this.onClickHintFix}
onPressEnter={this.onPressEnter}
onQueryChange={this.onChangeQuery}

View File

@@ -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<TypeaheadItemProps, {}> {
@@ -38,11 +40,12 @@ class TypeaheadItem extends React.PureComponent<TypeaheadItemProps, {}> {
};
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 (
<li ref={this.getRef} className={className} onClick={this.onClick}>
{item.detail || item.label}
<Highlighter textToHighlight={label} searchWords={[prefix]} highlightClassName="typeahead-match" />
{item.documentation && isSelected ? <div className="typeahead-item-hint">{item.documentation}</div> : null}
</li>
);
@@ -54,18 +57,25 @@ interface TypeaheadGroupProps {
label: string;
onClickItem: (Suggestion) => void;
selected: Suggestion;
prefix?: string;
}
class TypeaheadGroup extends React.PureComponent<TypeaheadGroupProps, {}> {
render() {
const { items, label, selected, onClickItem } = this.props;
const { items, label, selected, onClickItem, prefix } = this.props;
return (
<li className="typeahead-group">
<div className="typeahead-group__title">{label}</div>
<ul className="typeahead-group__list">
{items.map(item => {
return (
<TypeaheadItem key={item.label} onClickItem={onClickItem} isSelected={selected === item} item={item} />
<TypeaheadItem
key={item.label}
onClickItem={onClickItem}
isSelected={selected === item}
item={item}
prefix={prefix}
/>
);
})}
</ul>
@@ -79,14 +89,15 @@ interface TypeaheadProps {
menuRef: any;
selectedItem: Suggestion | null;
onClickItem: (Suggestion) => void;
prefix?: string;
}
class Typeahead extends React.PureComponent<TypeaheadProps, {}> {
render() {
const { groupedItems, menuRef, selectedItem, onClickItem } = this.props;
const { groupedItems, menuRef, selectedItem, onClickItem, prefix } = this.props;
return (
<ul className="typeahead" ref={menuRef}>
{groupedItems.map(g => (
<TypeaheadGroup key={g.label} onClickItem={onClickItem} selected={selectedItem} {...g} />
<TypeaheadGroup key={g.label} onClickItem={onClickItem} prefix={prefix} selected={selectedItem} {...g} />
))}
</ul>
);