mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Merge pull request #13536 from grafana/davkal/explore-text-match
Explore: highlight typed text in suggestions
This commit is contained in:
@@ -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>
|
||||
))}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user