2019-10-17 05:38:09 -07:00
|
|
|
import $ from 'jquery';
|
|
|
|
import React, { Component } from 'react';
|
|
|
|
import {
|
|
|
|
Button,
|
|
|
|
InputGroup,
|
|
|
|
InputGroupAddon,
|
|
|
|
InputGroupText,
|
|
|
|
Input,
|
|
|
|
} from 'reactstrap';
|
|
|
|
|
2019-10-17 07:34:02 -07:00
|
|
|
import Downshift from 'downshift';
|
2019-10-17 05:38:09 -07:00
|
|
|
import fuzzy from 'fuzzy';
|
|
|
|
|
|
|
|
import { library } from '@fortawesome/fontawesome-svg-core';
|
|
|
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
|
|
|
import { faSearch, faSpinner } from '@fortawesome/free-solid-svg-icons';
|
|
|
|
|
|
|
|
library.add(faSearch, faSpinner);
|
|
|
|
|
|
|
|
interface ExpressionInputProps {
|
|
|
|
value: string;
|
|
|
|
metricNames: string[];
|
|
|
|
executeQuery: (expr: string) => void;
|
|
|
|
loading: boolean;
|
|
|
|
}
|
|
|
|
|
|
|
|
class ExpressionInput extends Component<ExpressionInputProps> {
|
|
|
|
prevNoMatchValue: string | null = null;
|
|
|
|
private exprInputRef = React.createRef<HTMLInputElement>();
|
|
|
|
|
|
|
|
handleKeyPress = (event: React.KeyboardEvent<HTMLInputElement>) => {
|
|
|
|
if (event.key === 'Enter' && !event.shiftKey) {
|
|
|
|
this.props.executeQuery(this.exprInputRef.current!.value);
|
|
|
|
event.preventDefault();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
renderAutosuggest = (downshift: any) => {
|
|
|
|
if (!downshift.isOpen) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (this.prevNoMatchValue && downshift.inputValue.includes(this.prevNoMatchValue)) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
let matches = fuzzy.filter(downshift.inputValue.replace(/ /g, ''), this.props.metricNames, {
|
|
|
|
pre: "<strong>",
|
|
|
|
post: "</strong>",
|
|
|
|
});
|
|
|
|
|
|
|
|
if (matches.length === 0) {
|
|
|
|
this.prevNoMatchValue = downshift.inputValue;
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
return (
|
|
|
|
<ul className="autosuggest-dropdown" {...downshift.getMenuProps()}>
|
|
|
|
{
|
|
|
|
matches
|
|
|
|
.slice(0, 200) // Limit DOM rendering to 100 results, as DOM rendering is sloooow.
|
|
|
|
.map((item, index) => (
|
|
|
|
<li
|
|
|
|
{...downshift.getItemProps({
|
|
|
|
key: item.original,
|
|
|
|
index,
|
|
|
|
item: item.original,
|
|
|
|
style: {
|
|
|
|
backgroundColor:
|
|
|
|
downshift.highlightedIndex === index ? 'lightgray' : 'white',
|
|
|
|
fontWeight: downshift.selectedItem === item ? 'bold' : 'normal',
|
|
|
|
},
|
|
|
|
})}
|
|
|
|
>
|
|
|
|
{/* TODO: Find better way than setting inner HTML dangerously. We just want the <strong> to not be escaped.
|
|
|
|
This will be a problem when we save history and the user enters HTML into a query. */}
|
|
|
|
<span dangerouslySetInnerHTML={{__html: item.string}}></span>
|
|
|
|
</li>
|
|
|
|
))
|
|
|
|
}
|
|
|
|
</ul>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
componentDidMount() {
|
|
|
|
const $exprInput = $(this.exprInputRef.current!);
|
|
|
|
const resize = () => {
|
|
|
|
const el = $exprInput.get(0);
|
|
|
|
const offset = el.offsetHeight - el.clientHeight;
|
|
|
|
$exprInput.css('height', 'auto').css('height', el.scrollHeight + offset);
|
|
|
|
};
|
|
|
|
resize();
|
|
|
|
$exprInput.on('input', resize);
|
|
|
|
}
|
|
|
|
|
|
|
|
render() {
|
|
|
|
return (
|
|
|
|
<Downshift
|
|
|
|
//inputValue={this.props.value}
|
|
|
|
//onInputValueChange={this.props.onChange}
|
|
|
|
selectedItem={this.props.value}
|
|
|
|
>
|
|
|
|
{(downshift) => (
|
|
|
|
<div>
|
|
|
|
<InputGroup className="expression-input">
|
|
|
|
<InputGroupAddon addonType="prepend">
|
|
|
|
<InputGroupText>
|
|
|
|
{this.props.loading ? <FontAwesomeIcon icon="spinner" spin/> : <FontAwesomeIcon icon="search"/>}
|
|
|
|
</InputGroupText>
|
|
|
|
</InputGroupAddon>
|
|
|
|
|
|
|
|
<Input
|
|
|
|
autoFocus
|
|
|
|
type="textarea"
|
|
|
|
rows="1"
|
|
|
|
onKeyPress={this.handleKeyPress}
|
|
|
|
placeholder="Expression (press Shift+Enter for newlines)"
|
|
|
|
innerRef={this.exprInputRef}
|
|
|
|
{...downshift.getInputProps({
|
|
|
|
onKeyDown: (event: React.KeyboardEvent): void => {
|
|
|
|
switch (event.key) {
|
|
|
|
case 'Home':
|
|
|
|
case 'End':
|
|
|
|
// We want to be able to jump to the beginning/end of the input field.
|
|
|
|
// By default, Downshift otherwise jumps to the first/last suggestion item instead.
|
|
|
|
(event.nativeEvent as any).preventDownshiftDefault = true;
|
|
|
|
break;
|
|
|
|
case 'ArrowUp':
|
|
|
|
case 'ArrowDown':
|
|
|
|
if (!downshift.isOpen) {
|
|
|
|
(event.nativeEvent as any).preventDownshiftDefault = true;
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
case 'Enter':
|
|
|
|
downshift.closeMenu();
|
|
|
|
break;
|
|
|
|
case 'Escape':
|
|
|
|
if (!downshift.isOpen) {
|
|
|
|
this.exprInputRef.current!.blur();
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} as any)}
|
|
|
|
/>
|
|
|
|
<InputGroupAddon addonType="append">
|
|
|
|
<Button className="execute-btn" color="primary" onClick={() => this.props.executeQuery(this.exprInputRef.current!.value)}>Execute</Button>
|
|
|
|
</InputGroupAddon>
|
|
|
|
</InputGroup>
|
|
|
|
{this.renderAutosuggest(downshift)}
|
|
|
|
</div>
|
|
|
|
)}
|
|
|
|
</Downshift>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
export default ExpressionInput;
|