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