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:
commit
39b25e0596
@ -11,7 +11,7 @@ export enum LogLevel {
|
|||||||
export interface LogSearchMatch {
|
export interface LogSearchMatch {
|
||||||
start: number;
|
start: number;
|
||||||
length: number;
|
length: number;
|
||||||
text?: string;
|
text: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LogRow {
|
export interface LogRow {
|
||||||
@ -21,7 +21,7 @@ export interface LogRow {
|
|||||||
timestamp: string;
|
timestamp: string;
|
||||||
timeFromNow: string;
|
timeFromNow: string;
|
||||||
timeLocal: string;
|
timeLocal: string;
|
||||||
searchMatches?: LogSearchMatch[];
|
searchWords?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LogsModel {
|
export interface LogsModel {
|
||||||
|
24
public/app/core/utils/text.test.ts
Normal file
24
public/app/core/utils/text.test.ts
Normal file
@ -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 },
|
||||||
|
]);
|
||||||
|
});
|
32
public/app/core/utils/text.ts
Normal file
32
public/app/core/utils/text.ts
Normal file
@ -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;
|
||||||
|
}
|
@ -1,6 +1,8 @@
|
|||||||
import React, { Fragment, PureComponent } from 'react';
|
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 {
|
interface LogsProps {
|
||||||
className?: string;
|
className?: string;
|
||||||
@ -10,34 +12,7 @@ interface LogsProps {
|
|||||||
|
|
||||||
const EXAMPLE_QUERY = '{job="default/prometheus"}';
|
const EXAMPLE_QUERY = '{job="default/prometheus"}';
|
||||||
|
|
||||||
const Entry: React.SFC<LogRow> = props => {
|
export default class Logs extends PureComponent<LogsProps, {}> {
|
||||||
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> {
|
|
||||||
render() {
|
render() {
|
||||||
const { className = '', data } = this.props;
|
const { className = '', data } = this.props;
|
||||||
const hasData = data && data.rows && data.rows.length > 0;
|
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 className={row.logLevel ? `logs-row-level logs-row-level-${row.logLevel}` : ''} />
|
||||||
<div title={`${row.timestamp} (${row.timeFromNow})`}>{row.timeLocal}</div>
|
<div title={`${row.timestamp} (${row.timeFromNow})`}>{row.timeLocal}</div>
|
||||||
<div>
|
<div>
|
||||||
<Entry {...row} />
|
<Highlighter
|
||||||
|
textToHighlight={row.entry}
|
||||||
|
searchWords={row.searchWords}
|
||||||
|
findChunks={findHighlightChunksInText}
|
||||||
|
highlightClassName="logs-row-match-highlight"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
))}
|
))}
|
||||||
|
@ -145,7 +145,7 @@ interface PromQueryFieldProps {
|
|||||||
onClickHintFix?: (action: any) => void;
|
onClickHintFix?: (action: any) => void;
|
||||||
onPressEnter?: () => void;
|
onPressEnter?: () => void;
|
||||||
onQueryChange?: (value: string, override?: boolean) => void;
|
onQueryChange?: (value: string, override?: boolean) => void;
|
||||||
portalPrefix?: string;
|
portalOrigin?: string;
|
||||||
request?: (url: string) => any;
|
request?: (url: string) => any;
|
||||||
supportsLogs?: boolean; // To be removed after Logging gets its own query field
|
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>
|
<button className="btn navbar-button navbar-button--tight">Log labels</button>
|
||||||
</Cascader>
|
</Cascader>
|
||||||
) : (
|
) : (
|
||||||
<Cascader options={metricsOptions} onChange={this.onChangeMetrics}>
|
<Cascader options={metricsOptions} onChange={this.onChangeMetrics}>
|
||||||
<button className="btn navbar-button navbar-button--tight">Metrics</button>
|
<button className="btn navbar-button navbar-button--tight">Metrics</button>
|
||||||
</Cascader>
|
</Cascader>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="prom-query-field-wrapper">
|
<div className="prom-query-field-wrapper">
|
||||||
<div className="slate-query-field-wrapper">
|
<div className="slate-query-field-wrapper">
|
||||||
@ -586,7 +586,7 @@ class PromQueryField extends React.PureComponent<PromQueryFieldProps, PromQueryF
|
|||||||
onWillApplySuggestion={willApplySuggestion}
|
onWillApplySuggestion={willApplySuggestion}
|
||||||
onValueChanged={this.onChangeQuery}
|
onValueChanged={this.onChangeQuery}
|
||||||
placeholder="Enter a PromQL query"
|
placeholder="Enter a PromQL query"
|
||||||
portalPrefix="prometheus"
|
portalOrigin="prometheus"
|
||||||
syntaxLoaded={syntaxLoaded}
|
syntaxLoaded={syntaxLoaded}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -104,7 +104,7 @@ interface TypeaheadFieldProps {
|
|||||||
onValueChanged?: (value: Value) => void;
|
onValueChanged?: (value: Value) => void;
|
||||||
onWillApplySuggestion?: (suggestion: string, state: TypeaheadFieldState) => string;
|
onWillApplySuggestion?: (suggestion: string, state: TypeaheadFieldState) => string;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
portalPrefix?: string;
|
portalOrigin?: string;
|
||||||
syntax?: string;
|
syntax?: string;
|
||||||
syntaxLoaded?: boolean;
|
syntaxLoaded?: boolean;
|
||||||
}
|
}
|
||||||
@ -459,8 +459,8 @@ class QueryField extends React.PureComponent<TypeaheadFieldProps, TypeaheadField
|
|||||||
};
|
};
|
||||||
|
|
||||||
renderMenu = () => {
|
renderMenu = () => {
|
||||||
const { portalPrefix } = this.props;
|
const { portalOrigin } = this.props;
|
||||||
const { suggestions, typeaheadIndex } = this.state;
|
const { suggestions, typeaheadIndex, typeaheadPrefix } = this.state;
|
||||||
if (!hasSuggestions(suggestions)) {
|
if (!hasSuggestions(suggestions)) {
|
||||||
return null;
|
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
|
// Create typeahead in DOM root so we can later position it absolutely
|
||||||
return (
|
return (
|
||||||
<Portal prefix={portalPrefix}>
|
<Portal origin={portalOrigin}>
|
||||||
<Typeahead
|
<Typeahead
|
||||||
menuRef={this.menuRef}
|
menuRef={this.menuRef}
|
||||||
selectedItem={selectedItem}
|
selectedItem={selectedItem}
|
||||||
onClickItem={this.onClickMenu}
|
onClickItem={this.onClickMenu}
|
||||||
|
prefix={typeaheadPrefix}
|
||||||
groupedItems={suggestions}
|
groupedItems={suggestions}
|
||||||
/>
|
/>
|
||||||
</Portal>
|
</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;
|
node: HTMLElement;
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
const { index = 0, prefix = 'query' } = props;
|
const { index = 0, origin = 'query' } = props;
|
||||||
this.node = document.createElement('div');
|
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);
|
document.body.appendChild(this.node);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -53,7 +53,6 @@ class QueryRow extends PureComponent<any, {}> {
|
|||||||
hint={queryHint}
|
hint={queryHint}
|
||||||
initialQuery={query}
|
initialQuery={query}
|
||||||
history={history}
|
history={history}
|
||||||
portalPrefix="explore"
|
|
||||||
onClickHintFix={this.onClickHintFix}
|
onClickHintFix={this.onClickHintFix}
|
||||||
onPressEnter={this.onPressEnter}
|
onPressEnter={this.onPressEnter}
|
||||||
onQueryChange={this.onChangeQuery}
|
onQueryChange={this.onChangeQuery}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import Highlighter from 'react-highlight-words';
|
||||||
|
|
||||||
import { Suggestion, SuggestionGroup } from './QueryField';
|
import { Suggestion, SuggestionGroup } from './QueryField';
|
||||||
|
|
||||||
@ -16,6 +17,7 @@ interface TypeaheadItemProps {
|
|||||||
isSelected: boolean;
|
isSelected: boolean;
|
||||||
item: Suggestion;
|
item: Suggestion;
|
||||||
onClickItem: (Suggestion) => void;
|
onClickItem: (Suggestion) => void;
|
||||||
|
prefix?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
class TypeaheadItem extends React.PureComponent<TypeaheadItemProps, {}> {
|
class TypeaheadItem extends React.PureComponent<TypeaheadItemProps, {}> {
|
||||||
@ -38,11 +40,12 @@ class TypeaheadItem extends React.PureComponent<TypeaheadItemProps, {}> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { isSelected, item } = this.props;
|
const { isSelected, item, prefix } = this.props;
|
||||||
const className = isSelected ? 'typeahead-item typeahead-item__selected' : 'typeahead-item';
|
const className = isSelected ? 'typeahead-item typeahead-item__selected' : 'typeahead-item';
|
||||||
|
const { label } = item;
|
||||||
return (
|
return (
|
||||||
<li ref={this.getRef} className={className} onClick={this.onClick}>
|
<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}
|
{item.documentation && isSelected ? <div className="typeahead-item-hint">{item.documentation}</div> : null}
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
@ -54,18 +57,25 @@ interface TypeaheadGroupProps {
|
|||||||
label: string;
|
label: string;
|
||||||
onClickItem: (Suggestion) => void;
|
onClickItem: (Suggestion) => void;
|
||||||
selected: Suggestion;
|
selected: Suggestion;
|
||||||
|
prefix?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
class TypeaheadGroup extends React.PureComponent<TypeaheadGroupProps, {}> {
|
class TypeaheadGroup extends React.PureComponent<TypeaheadGroupProps, {}> {
|
||||||
render() {
|
render() {
|
||||||
const { items, label, selected, onClickItem } = this.props;
|
const { items, label, selected, onClickItem, prefix } = this.props;
|
||||||
return (
|
return (
|
||||||
<li className="typeahead-group">
|
<li className="typeahead-group">
|
||||||
<div className="typeahead-group__title">{label}</div>
|
<div className="typeahead-group__title">{label}</div>
|
||||||
<ul className="typeahead-group__list">
|
<ul className="typeahead-group__list">
|
||||||
{items.map(item => {
|
{items.map(item => {
|
||||||
return (
|
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>
|
</ul>
|
||||||
@ -79,14 +89,15 @@ interface TypeaheadProps {
|
|||||||
menuRef: any;
|
menuRef: any;
|
||||||
selectedItem: Suggestion | null;
|
selectedItem: Suggestion | null;
|
||||||
onClickItem: (Suggestion) => void;
|
onClickItem: (Suggestion) => void;
|
||||||
|
prefix?: string;
|
||||||
}
|
}
|
||||||
class Typeahead extends React.PureComponent<TypeaheadProps, {}> {
|
class Typeahead extends React.PureComponent<TypeaheadProps, {}> {
|
||||||
render() {
|
render() {
|
||||||
const { groupedItems, menuRef, selectedItem, onClickItem } = this.props;
|
const { groupedItems, menuRef, selectedItem, onClickItem, prefix } = this.props;
|
||||||
return (
|
return (
|
||||||
<ul className="typeahead" ref={menuRef}>
|
<ul className="typeahead" ref={menuRef}>
|
||||||
{groupedItems.map(g => (
|
{groupedItems.map(g => (
|
||||||
<TypeaheadGroup key={g.label} onClickItem={onClickItem} selected={selectedItem} {...g} />
|
<TypeaheadGroup key={g.label} onClickItem={onClickItem} prefix={prefix} selected={selectedItem} {...g} />
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
);
|
);
|
||||||
|
@ -1,29 +1,6 @@
|
|||||||
import { LogLevel } from 'app/core/logs_model';
|
import { LogLevel } from 'app/core/logs_model';
|
||||||
|
|
||||||
import { getLogLevel, getSearchMatches } from './result_transformer';
|
import { getLogLevel } 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' },
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('getLoglevel()', () => {
|
describe('getLoglevel()', () => {
|
||||||
it('returns no log level on empty line', () => {
|
it('returns no log level on empty line', () => {
|
||||||
|
@ -19,25 +19,6 @@ export function getLogLevel(line: string): LogLevel {
|
|||||||
return level;
|
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 {
|
export function processEntry(entry: { line: string; timestamp: string }, stream): LogRow {
|
||||||
const { line, timestamp } = entry;
|
const { line, timestamp } = entry;
|
||||||
const { labels } = stream;
|
const { labels } = stream;
|
||||||
@ -45,16 +26,15 @@ export function processEntry(entry: { line: string; timestamp: string }, stream)
|
|||||||
const time = moment(timestamp);
|
const time = moment(timestamp);
|
||||||
const timeFromNow = time.fromNow();
|
const timeFromNow = time.fromNow();
|
||||||
const timeLocal = time.format('YYYY-MM-DD HH:mm:ss');
|
const timeLocal = time.format('YYYY-MM-DD HH:mm:ss');
|
||||||
const searchMatches = getSearchMatches(line, stream.search);
|
|
||||||
const logLevel = getLogLevel(line);
|
const logLevel = getLogLevel(line);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
key,
|
key,
|
||||||
logLevel,
|
logLevel,
|
||||||
searchMatches,
|
|
||||||
timeFromNow,
|
timeFromNow,
|
||||||
timeLocal,
|
timeLocal,
|
||||||
entry: line,
|
entry: line,
|
||||||
|
searchWords: [stream.search],
|
||||||
timestamp: timestamp,
|
timestamp: timestamp,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -13,6 +13,13 @@ export interface Query {
|
|||||||
key?: string;
|
key?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface TextMatch {
|
||||||
|
text: string;
|
||||||
|
start: number;
|
||||||
|
length: number;
|
||||||
|
end: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface ExploreState {
|
export interface ExploreState {
|
||||||
datasource: any;
|
datasource: any;
|
||||||
datasourceError: any;
|
datasourceError: any;
|
||||||
|
@ -66,7 +66,6 @@
|
|||||||
|
|
||||||
.typeahead-item__selected {
|
.typeahead-item__selected {
|
||||||
background-color: $typeahead-selected-bg;
|
background-color: $typeahead-selected-bg;
|
||||||
color: $typeahead-selected-color;
|
|
||||||
|
|
||||||
.typeahead-item-hint {
|
.typeahead-item-hint {
|
||||||
font-size: $font-size-xs;
|
font-size: $font-size-xs;
|
||||||
@ -74,6 +73,14 @@
|
|||||||
white-space: normal;
|
white-space: normal;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.typeahead-match {
|
||||||
|
color: $typeahead-selected-color;
|
||||||
|
border-bottom: 1px solid $typeahead-selected-color;
|
||||||
|
// Undoing mark styling
|
||||||
|
padding: inherit;
|
||||||
|
background: inherit;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* SYNTAX */
|
/* SYNTAX */
|
||||||
|
Loading…
Reference in New Issue
Block a user