import React, { Component } from 'react'; import { Button, InputGroup, InputGroupAddon, InputGroupText, Input } from 'reactstrap'; import Downshift, { ControllerStateAndHelpers } from 'downshift'; import fuzzy from 'fuzzy'; import sanitizeHTML from 'sanitize-html'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faSearch, faSpinner, faGlobeEurope } from '@fortawesome/free-solid-svg-icons'; import MetricsExplorer from './MetricsExplorer'; interface ExpressionInputProps { value: string; onExpressionChange: (expr: string) => void; queryHistory: string[]; metricNames: string[]; executeQuery: () => void; loading: boolean; enableAutocomplete: boolean; } interface ExpressionInputState { height: number | string; showMetricsExplorer: boolean; } class ExpressionInput extends Component { private exprInputRef = React.createRef(); constructor(props: ExpressionInputProps) { super(props); this.state = { height: 'auto', showMetricsExplorer: false, }; } componentDidMount() { this.setHeight(); } setHeight = () => { const { offsetHeight, clientHeight, scrollHeight } = this.exprInputRef.current!; const offset = offsetHeight - clientHeight; // Needed in order for the height to be more accurate. this.setState({ height: scrollHeight + offset }); }; handleInput = () => { this.setValue(this.exprInputRef.current!.value); }; setValue = (value: string) => { const { onExpressionChange } = this.props; onExpressionChange(value); this.setState({ height: 'auto' }, this.setHeight); }; componentDidUpdate(prevProps: ExpressionInputProps) { const { value } = this.props; if (value !== prevProps.value) { this.setValue(value); } } handleKeyPress = (event: React.KeyboardEvent) => { const { executeQuery } = this.props; if (event.key === 'Enter' && !event.shiftKey) { executeQuery(); event.preventDefault(); } }; getSearchMatches = (input: string, expressions: string[]) => { return fuzzy.filter(input.replace(/ /g, ''), expressions, { pre: '', post: '', }); }; createAutocompleteSection = (downshift: ControllerStateAndHelpers) => { const { inputValue = '', closeMenu, highlightedIndex } = downshift; const autocompleteSections = { 'Query History': this.props.queryHistory, 'Metric Names': this.props.metricNames, }; let index = 0; const sections = inputValue!.length && this.props.enableAutocomplete ? Object.entries(autocompleteSections).reduce((acc, [title, items]) => { const matches = this.getSearchMatches(inputValue!, items); return !matches.length ? acc : [ ...acc,
  • {title}
  • {matches .slice(0, 100) // Limit DOM rendering to 100 results, as DOM rendering is sloooow. .map(({ original, string: text }) => { const itemProps = downshift.getItemProps({ key: original, index, item: original, style: { backgroundColor: highlightedIndex === index++ ? 'lightgray' : 'white', }, }); return (
  • ); })}
, ]; }, [] as JSX.Element[]) : []; if (!sections.length) { // This is ugly but is needed in order to sync state updates. // This way we force downshift to wait React render call to complete before closeMenu to be triggered. setTimeout(closeMenu); return null; } return (
{sections}
); }; openMetricsExplorer = () => { this.setState({ showMetricsExplorer: true, }); }; updateShowMetricsExplorer = (show: boolean) => { this.setState({ showMetricsExplorer: show, }); }; insertAtCursor = (value: string) => { if (!this.exprInputRef.current) return; const startPosition = this.exprInputRef.current.selectionStart; const endPosition = this.exprInputRef.current.selectionEnd; const previousValue = this.exprInputRef.current.value; let newValue: string; if (startPosition && endPosition) { newValue = previousValue.substring(0, startPosition) + value + previousValue.substring(endPosition, previousValue.length); } else { newValue = previousValue + value; } this.setValue(newValue); }; render() { const { executeQuery, value } = this.props; const { height } = this.state; return ( <> {downshift => (
{this.props.loading ? : } { 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)} value={value} /> {downshift.isOpen && this.createAutocompleteSection(downshift)}
)}
); } } export default ExpressionInput;