split autocomplete dropdown in to groups (#6211)
* split autocomplete dropdown in to groups Signed-off-by: blalov <boyko.lalov@tick42.com> * fix autocomplete flickering Signed-off-by: blalov <boyko.lalov@tick42.com> * fix expression input issue. Signed-off-by: blalov <boyko.lalov@tick42.com> * select autocomplete item issue fix Signed-off-by: blalov <boyko.lalov@tick42.com> * remove metric group abstraction Signed-off-by: blalov <boyko.lalov@tick42.com>
This commit is contained in:
parent
1afa476b8a
commit
dab87ca281
|
@ -75,7 +75,6 @@ button.execute-btn {
|
|||
top: 100%;
|
||||
left: 56px;
|
||||
float: left;
|
||||
padding: .5rem 1px .5rem 1px;
|
||||
margin: -5px;
|
||||
list-style: none;
|
||||
}
|
||||
|
@ -90,6 +89,14 @@ button.execute-btn {
|
|||
display: block;
|
||||
}
|
||||
|
||||
.autosuggest-dropdown .card-header {
|
||||
background: rgba(240, 230, 140, 0.4);
|
||||
height: 30px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.graph-controls, .table-controls {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
|
|
@ -1,18 +1,15 @@
|
|||
import React, { FC, HTMLProps, memo } from 'react';
|
||||
import { FormGroup, Label, Input } from 'reactstrap';
|
||||
import { uuidGen } from './utils/func';
|
||||
import React, { FC, memo, CSSProperties } from 'react';
|
||||
import { FormGroup, Label, Input, InputProps } from 'reactstrap';
|
||||
|
||||
const Checkbox: FC<HTMLProps<HTMLSpanElement>> = ({ children, onChange, style }) => {
|
||||
const id = uuidGen();
|
||||
interface CheckboxProps extends InputProps {
|
||||
wrapperStyles?: CSSProperties;
|
||||
}
|
||||
|
||||
const Checkbox: FC<CheckboxProps> = ({ children, wrapperStyles, id, ...rest }) => {
|
||||
return (
|
||||
<FormGroup className="custom-control custom-checkbox" style={style}>
|
||||
<Input
|
||||
onChange={onChange}
|
||||
type="checkbox"
|
||||
className="custom-control-input"
|
||||
id={`checkbox_${id}`}
|
||||
placeholder="password placeholder" />
|
||||
<Label style={{ userSelect: 'none' }} className="custom-control-label" for={`checkbox_${id}`}>
|
||||
<FormGroup className="custom-control custom-checkbox" style={wrapperStyles}>
|
||||
<Input {...rest} id={id} type="checkbox" className="custom-control-input" />
|
||||
<Label style={{ userSelect: 'none' }} className="custom-control-label" for={id}>
|
||||
{children}
|
||||
</Label>
|
||||
</FormGroup>
|
||||
|
|
|
@ -5,6 +5,10 @@ import {
|
|||
InputGroupAddon,
|
||||
InputGroupText,
|
||||
Input,
|
||||
ListGroup,
|
||||
ListGroupItem,
|
||||
Card,
|
||||
CardHeader,
|
||||
} from 'reactstrap';
|
||||
|
||||
import Downshift, { ControllerStateAndHelpers } from 'downshift';
|
||||
|
@ -16,7 +20,7 @@ import { faSearch, faSpinner } from '@fortawesome/free-solid-svg-icons';
|
|||
|
||||
interface ExpressionInputProps {
|
||||
value: string;
|
||||
metricNames: string[];
|
||||
autocompleteSections: { [key: string]: string[] };
|
||||
executeQuery: (expr: string) => void;
|
||||
loading: boolean;
|
||||
}
|
||||
|
@ -27,7 +31,6 @@ interface ExpressionInputState {
|
|||
}
|
||||
|
||||
class ExpressionInput extends Component<ExpressionInputProps, ExpressionInputState> {
|
||||
private prevNoMatchValue: string | null = null;
|
||||
private exprInputRef = React.createRef<HTMLInputElement>();
|
||||
|
||||
constructor(props: ExpressionInputProps) {
|
||||
|
@ -42,7 +45,6 @@ class ExpressionInput extends Component<ExpressionInputProps, ExpressionInputSta
|
|||
this.setHeight();
|
||||
}
|
||||
|
||||
|
||||
setHeight = () => {
|
||||
const { offsetHeight, clientHeight, scrollHeight } = this.exprInputRef.current!;
|
||||
const offset = offsetHeight - clientHeight; // Needed in order for the height to be more accurate.
|
||||
|
@ -57,7 +59,7 @@ class ExpressionInput extends Component<ExpressionInputProps, ExpressionInputSta
|
|||
}
|
||||
|
||||
handleDropdownSelection = (value: string) => {
|
||||
this.setState({ value, height: 'auto' }, this.setHeight)
|
||||
this.setState({ value, height: 'auto' }, this.setHeight);
|
||||
};
|
||||
|
||||
handleKeyPress = (event: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
|
@ -67,53 +69,60 @@ class ExpressionInput extends Component<ExpressionInputProps, ExpressionInputSta
|
|||
}
|
||||
}
|
||||
|
||||
executeQuery = () => this.props.executeQuery(this.exprInputRef.current!.value)
|
||||
executeQuery = () => this.props.executeQuery(this.exprInputRef.current!.value);
|
||||
|
||||
renderAutosuggest = (downshift: ControllerStateAndHelpers<any>) => {
|
||||
const { inputValue } = downshift
|
||||
if (!inputValue || (this.prevNoMatchValue && inputValue.includes(this.prevNoMatchValue))) {
|
||||
// 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(downshift.closeMenu);
|
||||
return null;
|
||||
}
|
||||
|
||||
const matches = fuzzy.filter(inputValue.replace(/ /g, ''), this.props.metricNames, {
|
||||
getSearchMatches = (input: string, expressions: string[]) => {
|
||||
return fuzzy.filter(input.replace(/ /g, ''), expressions, {
|
||||
pre: "<strong>",
|
||||
post: "</strong>",
|
||||
});
|
||||
}
|
||||
|
||||
if (matches.length === 0) {
|
||||
this.prevNoMatchValue = inputValue;
|
||||
setTimeout(downshift.closeMenu);
|
||||
createAutocompleteSection = (downshift: ControllerStateAndHelpers<any>) => {
|
||||
const { inputValue = '', closeMenu, highlightedIndex } = downshift;
|
||||
const { autocompleteSections } = this.props;
|
||||
let index = 0;
|
||||
const sections = inputValue!.length ?
|
||||
Object.entries(autocompleteSections).reduce((acc, [title, items]) => {
|
||||
const matches = this.getSearchMatches(inputValue!, items);
|
||||
return !matches.length ? acc : [
|
||||
...acc,
|
||||
<Card tag={ListGroup} key={title}>
|
||||
<CardHeader style={{ fontSize: 13 }}>{title}</CardHeader>
|
||||
{
|
||||
matches
|
||||
.slice(0, 100) // Limit DOM rendering to 100 results, as DOM rendering is sloooow.
|
||||
.map(({ original, string }) => {
|
||||
const itemProps = downshift.getItemProps({
|
||||
key: original,
|
||||
index,
|
||||
item: original,
|
||||
style: {
|
||||
backgroundColor: highlightedIndex === index++ ? 'lightgray' : 'white',
|
||||
},
|
||||
})
|
||||
return (
|
||||
<SanitizeHTML tag={ListGroupItem} {...itemProps} allowedTags={['strong']}>
|
||||
{string}
|
||||
</SanitizeHTML>
|
||||
);
|
||||
})
|
||||
}
|
||||
</Card>
|
||||
]
|
||||
}, [] 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 (
|
||||
<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',
|
||||
},
|
||||
})}
|
||||
>
|
||||
<SanitizeHTML inline={true} allowedTags={['strong']}>
|
||||
{item.string}
|
||||
</SanitizeHTML>
|
||||
</li>
|
||||
))
|
||||
}
|
||||
</ul>
|
||||
<div {...downshift.getMenuProps()} className="autosuggest-dropdown">
|
||||
{ sections }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -121,8 +130,8 @@ class ExpressionInput extends Component<ExpressionInputProps, ExpressionInputSta
|
|||
const { value, height } = this.state;
|
||||
return (
|
||||
<Downshift
|
||||
onChange={this.handleDropdownSelection}
|
||||
inputValue={value}
|
||||
onSelect={this.handleDropdownSelection}
|
||||
>
|
||||
{(downshift) => (
|
||||
<div>
|
||||
|
@ -179,7 +188,7 @@ class ExpressionInput extends Component<ExpressionInputProps, ExpressionInputSta
|
|||
</Button>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
{downshift.isOpen && this.renderAutosuggest(downshift)}
|
||||
{downshift.isOpen && this.createAutocompleteSection(downshift)}
|
||||
</div>
|
||||
)}
|
||||
</Downshift>
|
||||
|
|
|
@ -24,6 +24,7 @@ import QueryStatsView, { QueryStats } from './QueryStatsView';
|
|||
interface PanelProps {
|
||||
options: PanelOptions;
|
||||
onOptionsChanged: (opts: PanelOptions) => void;
|
||||
pastQueries: string[];
|
||||
metricNames: string[];
|
||||
removePanel: () => void;
|
||||
onExecuteQuery: (query: string) => void;
|
||||
|
@ -228,15 +229,19 @@ class Panel extends Component<PanelProps, PanelState> {
|
|||
}
|
||||
|
||||
render() {
|
||||
const { pastQueries, metricNames, options } = this.props;
|
||||
return (
|
||||
<div className="panel">
|
||||
<Row>
|
||||
<Col>
|
||||
<ExpressionInput
|
||||
value={this.props.options.expr}
|
||||
value={options.expr}
|
||||
executeQuery={this.executeQuery}
|
||||
loading={this.state.loading}
|
||||
metricNames={this.props.metricNames}
|
||||
autocompleteSections={{
|
||||
'Query History': pastQueries,
|
||||
'Metric Names': metricNames,
|
||||
}}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
|
@ -250,7 +255,7 @@ class Panel extends Component<PanelProps, PanelState> {
|
|||
<Nav tabs>
|
||||
<NavItem>
|
||||
<NavLink
|
||||
className={this.props.options.type === 'table' ? 'active' : ''}
|
||||
className={options.type === 'table' ? 'active' : ''}
|
||||
onClick={() => { this.setOptions({type: 'table'}); }}
|
||||
>
|
||||
Table
|
||||
|
@ -258,7 +263,7 @@ class Panel extends Component<PanelProps, PanelState> {
|
|||
</NavItem>
|
||||
<NavItem>
|
||||
<NavLink
|
||||
className={this.props.options.type === 'graph' ? 'active' : ''}
|
||||
className={options.type === 'graph' ? 'active' : ''}
|
||||
onClick={() => { this.setOptions({type: 'graph'}); }}
|
||||
>
|
||||
Graph
|
||||
|
@ -269,14 +274,14 @@ class Panel extends Component<PanelProps, PanelState> {
|
|||
<QueryStatsView {...this.state.stats} />
|
||||
}
|
||||
</Nav>
|
||||
<TabContent activeTab={this.props.options.type}>
|
||||
<TabContent activeTab={options.type}>
|
||||
<TabPane tabId="table">
|
||||
{this.props.options.type === 'table' &&
|
||||
{options.type === 'table' &&
|
||||
<>
|
||||
<div className="table-controls">
|
||||
<TimeInput
|
||||
time={this.props.options.endTime}
|
||||
range={this.props.options.range}
|
||||
time={options.endTime}
|
||||
range={options.range}
|
||||
placeholder="Evaluation time"
|
||||
onChangeTime={this.handleChangeEndTime}
|
||||
/>
|
||||
|
@ -289,17 +294,17 @@ class Panel extends Component<PanelProps, PanelState> {
|
|||
{this.props.options.type === 'graph' &&
|
||||
<>
|
||||
<GraphControls
|
||||
range={this.props.options.range}
|
||||
endTime={this.props.options.endTime}
|
||||
resolution={this.props.options.resolution}
|
||||
stacked={this.props.options.stacked}
|
||||
range={options.range}
|
||||
endTime={options.endTime}
|
||||
resolution={options.resolution}
|
||||
stacked={options.stacked}
|
||||
|
||||
onChangeRange={this.handleChangeRange}
|
||||
onChangeEndTime={this.handleChangeEndTime}
|
||||
onChangeResolution={this.handleChangeResolution}
|
||||
onChangeStacking={this.handleChangeStacking}
|
||||
/>
|
||||
<Graph data={this.state.data} stacked={this.props.options.stacked} queryParams={this.state.lastQueryParams} />
|
||||
<Graph data={this.state.data} stacked={options.stacked} queryParams={this.state.lastQueryParams} />
|
||||
</>
|
||||
}
|
||||
</TabPane>
|
||||
|
|
|
@ -6,11 +6,14 @@ import Panel, { PanelOptions, PanelDefaultOptions } from './Panel';
|
|||
import { decodePanelOptionsFromQueryString, encodePanelOptionsToQueryString } from './utils/urlParams';
|
||||
import Checkbox from './Checkbox';
|
||||
|
||||
export type MetricGroup = { title: string; items: string[] };
|
||||
|
||||
interface PanelListState {
|
||||
panels: {
|
||||
key: string;
|
||||
options: PanelOptions;
|
||||
}[],
|
||||
}[];
|
||||
pastQueries: string[];
|
||||
metricNames: string[];
|
||||
fetchMetricsError: string | null;
|
||||
timeDriftError: string | null;
|
||||
|
@ -18,7 +21,6 @@ interface PanelListState {
|
|||
|
||||
class PanelList extends Component<any, PanelListState> {
|
||||
private key: number = 0;
|
||||
private initialMetricNames: string[] = [];
|
||||
constructor(props: any) {
|
||||
super(props);
|
||||
|
||||
|
@ -31,6 +33,7 @@ class PanelList extends Component<any, PanelListState> {
|
|||
options: PanelDefaultOptions,
|
||||
},
|
||||
],
|
||||
pastQueries: [],
|
||||
metricNames: [],
|
||||
fetchMetricsError: null,
|
||||
timeDriftError: null,
|
||||
|
@ -47,8 +50,7 @@ class PanelList extends Component<any, PanelListState> {
|
|||
}
|
||||
})
|
||||
.then(json => {
|
||||
this.initialMetricNames = json.data;
|
||||
this.setMetrics();
|
||||
this.setMetrics(json.data);
|
||||
})
|
||||
.catch(error => this.setState({ fetchMetricsError: error.message }));
|
||||
|
||||
|
@ -88,20 +90,15 @@ class PanelList extends Component<any, PanelListState> {
|
|||
this.setMetrics();
|
||||
}
|
||||
|
||||
setMetrics = () => {
|
||||
if (this.isHistoryEnabled()) {
|
||||
const historyItems = this.getHistoryItems();
|
||||
const { length } = historyItems;
|
||||
this.setState({
|
||||
metricNames: [...historyItems.slice(length - 50, length), ...this.initialMetricNames],
|
||||
});
|
||||
} else {
|
||||
this.setState({ metricNames: this.initialMetricNames });
|
||||
}
|
||||
setMetrics = (metricNames: string[] = this.state.metricNames) => {
|
||||
this.setState({
|
||||
pastQueries: this.isHistoryEnabled() ? this.getHistoryItems() : [],
|
||||
metricNames
|
||||
});
|
||||
}
|
||||
|
||||
handleQueryHistory = (query: string) => {
|
||||
const isSimpleMetric = this.initialMetricNames.indexOf(query) !== -1;
|
||||
const isSimpleMetric = this.state.metricNames.indexOf(query) !== -1;
|
||||
if (isSimpleMetric || !query.length) {
|
||||
return;
|
||||
}
|
||||
|
@ -109,7 +106,7 @@ class PanelList extends Component<any, PanelListState> {
|
|||
const extendedItems = historyItems.reduce((acc, metric) => {
|
||||
return metric === query ? acc : [...acc, metric]; // Prevent adding query twice.
|
||||
}, [query]);
|
||||
localStorage.setItem('history', JSON.stringify(extendedItems));
|
||||
localStorage.setItem('history', JSON.stringify(extendedItems.slice(0, 50)));
|
||||
this.setMetrics();
|
||||
}
|
||||
|
||||
|
@ -152,13 +149,15 @@ class PanelList extends Component<any, PanelListState> {
|
|||
}
|
||||
|
||||
render() {
|
||||
const { metricNames, pastQueries, timeDriftError, fetchMetricsError } = this.state;
|
||||
return (
|
||||
<>
|
||||
<Row className="mb-2">
|
||||
<Checkbox
|
||||
style={{ margin: '0 0 0 15px', alignSelf: 'center' }}
|
||||
id="query-history-checkbox"
|
||||
wrapperStyles={{ margin: '0 0 0 15px', alignSelf: 'center' }}
|
||||
onChange={this.toggleQueryHistory}
|
||||
checked={this.isHistoryEnabled()}>
|
||||
defaultChecked={this.isHistoryEnabled()}>
|
||||
Enable query history
|
||||
</Checkbox>
|
||||
<Col>
|
||||
|
@ -173,12 +172,12 @@ class PanelList extends Component<any, PanelListState> {
|
|||
</Row>
|
||||
<Row>
|
||||
<Col>
|
||||
{this.state.timeDriftError && <Alert color="danger"><strong>Warning:</strong> Error fetching server time: {this.state.timeDriftError}</Alert>}
|
||||
{timeDriftError && <Alert color="danger"><strong>Warning:</strong> Error fetching server time: {this.state.timeDriftError}</Alert>}
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col>
|
||||
{this.state.fetchMetricsError && <Alert color="danger"><strong>Warning:</strong> Error fetching metrics list: {this.state.fetchMetricsError}</Alert>}
|
||||
{fetchMetricsError && <Alert color="danger"><strong>Warning:</strong> Error fetching metrics list: {this.state.fetchMetricsError}</Alert>}
|
||||
</Col>
|
||||
</Row>
|
||||
{this.state.panels.map(p =>
|
||||
|
@ -188,7 +187,8 @@ class PanelList extends Component<any, PanelListState> {
|
|||
options={p.options}
|
||||
onOptionsChanged={(opts: PanelOptions) => this.handleOptionsChanged(p.key, opts)}
|
||||
removePanel={() => this.removePanel(p.key)}
|
||||
metricNames={this.state.metricNames}
|
||||
metricNames={metricNames}
|
||||
pastQueries={pastQueries}
|
||||
/>
|
||||
)}
|
||||
<Button color="primary" className="add-panel-btn" onClick={this.addPanel}>Add Panel</Button>
|
||||
|
|
|
@ -1,30 +1,17 @@
|
|||
/**
|
||||
* SanitizeHTML to render HTML, this takes care of sanitizing HTML.
|
||||
*/
|
||||
import React, { PureComponent } from 'react';
|
||||
import React, { memo } from 'react';
|
||||
import sanitizeHTML from 'sanitize-html';
|
||||
|
||||
interface SanitizeHTMLProps {
|
||||
inline: Boolean;
|
||||
allowedTags: string[];
|
||||
children: Element | string;
|
||||
children: string;
|
||||
tag?: keyof JSX.IntrinsicElements;
|
||||
}
|
||||
|
||||
class SanitizeHTML extends PureComponent<SanitizeHTMLProps> {
|
||||
sanitize = (html: any) => {
|
||||
return sanitizeHTML(html, {
|
||||
allowedTags: this.props.allowedTags
|
||||
});
|
||||
};
|
||||
const SanitizeHTML = ({ tag: Tag = 'div', children, allowedTags, ...rest }: SanitizeHTMLProps) => (
|
||||
<Tag {...rest} dangerouslySetInnerHTML={{ __html: sanitizeHTML(children, { allowedTags }) }} />
|
||||
);
|
||||
|
||||
render() {
|
||||
const { inline, children } = this.props;
|
||||
return inline ? (
|
||||
<span dangerouslySetInnerHTML={{ __html: this.sanitize(children) }} />
|
||||
) : (
|
||||
<div dangerouslySetInnerHTML={{ __html: this.sanitize(children) }} />
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default SanitizeHTML;
|
||||
export default memo(SanitizeHTML);
|
||||
|
|
Loading…
Reference in New Issue