mirror of
https://github.com/prometheus/prometheus.git
synced 2024-12-24 21:24:05 -08:00
migrate query history (#6193)
* migrate query history Signed-off-by: blalov <boyko.lalov@tick42.com> * update lock file Signed-off-by: blalov <boyko.lalov@tick42.com> * set expression input height when item is selected Signed-off-by: blalov <boyko.lalov@tick42.com> * pr review changes Signed-off-by: blalov <boyko.lalov@tick42.com>
This commit is contained in:
parent
89e610ef4c
commit
e235af9c47
|
@ -11,8 +11,8 @@
|
|||
"@types/node": "^12.11.1",
|
||||
"@types/react": "^16.8.2",
|
||||
"@types/react-dom": "^16.8.0",
|
||||
"@types/sanitize-html": "^1.20.2",
|
||||
"@types/react-resize-detector": "^4.0.2",
|
||||
"@types/sanitize-html": "^1.20.2",
|
||||
"bootstrap": "^4.2.1",
|
||||
"downshift": "^3.2.2",
|
||||
"flot": "^3.2.13",
|
||||
|
@ -26,10 +26,10 @@
|
|||
"popper.js": "^1.14.3",
|
||||
"react": "^16.7.0",
|
||||
"react-dom": "^16.7.0",
|
||||
"sanitize-html": "^1.20.1",
|
||||
"react-resize-detector": "^4.2.1",
|
||||
"react-scripts": "^3.2.0",
|
||||
"reactstrap": "^8.0.1",
|
||||
"sanitize-html": "^1.20.1",
|
||||
"tempusdominus-bootstrap-4": "^5.1.2",
|
||||
"tempusdominus-core": "^5.0.3",
|
||||
"typescript": "^3.3.3"
|
||||
|
|
|
@ -10,6 +10,16 @@ button.classic-ui-btn {
|
|||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
input[type='checkbox']:checked + label {
|
||||
color: #286090;
|
||||
}
|
||||
|
||||
.custom-control-label {
|
||||
cursor: pointer;
|
||||
font-size: .875rem;
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
.expression-input {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
|
|
@ -1,31 +1,16 @@
|
|||
import React, { Component } from 'react';
|
||||
|
||||
import {
|
||||
Button,
|
||||
Container,
|
||||
Col,
|
||||
Row,
|
||||
} from 'reactstrap';
|
||||
|
||||
import PanelList from './PanelList';
|
||||
|
||||
import './App.css';
|
||||
import PanelList from './PanelList';
|
||||
|
||||
class App extends Component {
|
||||
render() {
|
||||
return (
|
||||
<Container fluid={true}>
|
||||
<Row>
|
||||
<Col>
|
||||
<Button
|
||||
className="float-right classic-ui-btn"
|
||||
color="link"
|
||||
onClick={() => {window.location.pathname = "../../graph"}}
|
||||
size="sm">
|
||||
Return to classic UI
|
||||
</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
<PanelList />
|
||||
</Container>
|
||||
);
|
||||
|
|
22
web/ui/react-app/src/Checkbox.tsx
Normal file
22
web/ui/react-app/src/Checkbox.tsx
Normal file
|
@ -0,0 +1,22 @@
|
|||
import React, { FC, HTMLProps, memo } from 'react';
|
||||
import { FormGroup, Label, Input } from 'reactstrap';
|
||||
import { uuidGen } from './utils/func';
|
||||
|
||||
const Checkbox: FC<HTMLProps<HTMLSpanElement>> = ({ children, onChange, style }) => {
|
||||
const id = uuidGen();
|
||||
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}`}>
|
||||
{children}
|
||||
</Label>
|
||||
</FormGroup>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(Checkbox);
|
|
@ -11,12 +11,9 @@ import Downshift, { ControllerStateAndHelpers } from 'downshift';
|
|||
import fuzzy from 'fuzzy';
|
||||
import SanitizeHTML from './components/SanitizeHTML';
|
||||
|
||||
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[];
|
||||
|
@ -59,8 +56,10 @@ class ExpressionInput extends Component<ExpressionInputProps, ExpressionInputSta
|
|||
}, this.setHeight);
|
||||
}
|
||||
|
||||
handleDropdownSelection = (value: string) => this.setState({ value });
|
||||
|
||||
handleDropdownSelection = (value: string) => {
|
||||
this.setState({ value, height: 'auto' }, this.setHeight)
|
||||
};
|
||||
|
||||
handleKeyPress = (event: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (event.key === 'Enter' && !event.shiftKey) {
|
||||
this.props.executeQuery(this.exprInputRef.current!.value);
|
||||
|
@ -73,7 +72,9 @@ class ExpressionInput extends Component<ExpressionInputProps, ExpressionInputSta
|
|||
renderAutosuggest = (downshift: ControllerStateAndHelpers<any>) => {
|
||||
const { inputValue } = downshift
|
||||
if (!inputValue || (this.prevNoMatchValue && inputValue.includes(this.prevNoMatchValue))) {
|
||||
downshift.closeMenu();
|
||||
// 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;
|
||||
}
|
||||
|
||||
|
@ -84,7 +85,7 @@ class ExpressionInput extends Component<ExpressionInputProps, ExpressionInputSta
|
|||
|
||||
if (matches.length === 0) {
|
||||
this.prevNoMatchValue = inputValue;
|
||||
downshift.closeMenu();
|
||||
setTimeout(downshift.closeMenu);
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -128,7 +129,7 @@ class ExpressionInput extends Component<ExpressionInputProps, ExpressionInputSta
|
|||
<InputGroup className="expression-input">
|
||||
<InputGroupAddon addonType="prepend">
|
||||
<InputGroupText>
|
||||
{this.props.loading ? <FontAwesomeIcon icon="spinner" spin/> : <FontAwesomeIcon icon="search"/>}
|
||||
{this.props.loading ? <FontAwesomeIcon icon={faSpinner} spin /> : <FontAwesomeIcon icon={faSearch} />}
|
||||
</InputGroupText>
|
||||
</InputGroupAddon>
|
||||
<Input
|
||||
|
|
|
@ -8,7 +8,6 @@ import {
|
|||
Input,
|
||||
} from 'reactstrap';
|
||||
|
||||
import { library } from '@fortawesome/fontawesome-svg-core';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import {
|
||||
faPlus,
|
||||
|
@ -20,13 +19,6 @@ import {
|
|||
import TimeInput from './TimeInput';
|
||||
import { parseRange, formatRange } from './utils/timeFormat';
|
||||
|
||||
library.add(
|
||||
faPlus,
|
||||
faMinus,
|
||||
faChartArea,
|
||||
faChartLine,
|
||||
);
|
||||
|
||||
interface GraphControlsProps {
|
||||
range: number;
|
||||
endTime: number | null;
|
||||
|
@ -111,7 +103,7 @@ class GraphControls extends Component<GraphControlsProps> {
|
|||
<Form inline className="graph-controls" onSubmit={e => e.preventDefault()}>
|
||||
<InputGroup className="range-input" size="sm">
|
||||
<InputGroupAddon addonType="prepend">
|
||||
<Button title="Decrease range" onClick={this.decreaseRange}><FontAwesomeIcon icon="minus" fixedWidth/></Button>
|
||||
<Button title="Decrease range" onClick={this.decreaseRange}><FontAwesomeIcon icon={faMinus} fixedWidth/></Button>
|
||||
</InputGroupAddon>
|
||||
|
||||
<Input
|
||||
|
@ -121,7 +113,7 @@ class GraphControls extends Component<GraphControlsProps> {
|
|||
/>
|
||||
|
||||
<InputGroupAddon addonType="append">
|
||||
<Button title="Increase range" onClick={this.increaseRange}><FontAwesomeIcon icon="plus" fixedWidth/></Button>
|
||||
<Button title="Increase range" onClick={this.increaseRange}><FontAwesomeIcon icon={faPlus} fixedWidth/></Button>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
|
||||
|
@ -145,8 +137,8 @@ class GraphControls extends Component<GraphControlsProps> {
|
|||
/>
|
||||
|
||||
<ButtonGroup className="stacked-input" size="sm">
|
||||
<Button title="Show unstacked line graph" onClick={() => this.props.onChangeStacking(false)} active={!this.props.stacked}><FontAwesomeIcon icon="chart-line" fixedWidth/></Button>
|
||||
<Button title="Show stacked graph" onClick={() => this.props.onChangeStacking(true)} active={this.props.stacked}><FontAwesomeIcon icon="chart-area" fixedWidth/></Button>
|
||||
<Button title="Show unstacked line graph" onClick={() => this.props.onChangeStacking(false)} active={!this.props.stacked}><FontAwesomeIcon icon={faChartLine} fixedWidth/></Button>
|
||||
<Button title="Show stacked graph" onClick={() => this.props.onChangeStacking(true)} active={this.props.stacked}><FontAwesomeIcon icon={faChartArea} fixedWidth/></Button>
|
||||
</ButtonGroup>
|
||||
</Form>
|
||||
);
|
||||
|
|
|
@ -26,6 +26,7 @@ interface PanelProps {
|
|||
onOptionsChanged: (opts: PanelOptions) => void;
|
||||
metricNames: string[];
|
||||
removePanel: () => void;
|
||||
onExecuteQuery: (query: string) => void;
|
||||
}
|
||||
|
||||
interface PanelState {
|
||||
|
@ -102,6 +103,7 @@ class Panel extends Component<PanelProps, PanelState> {
|
|||
|
||||
executeQuery = (expr: string): void => {
|
||||
const queryStart = Date.now();
|
||||
this.props.onExecuteQuery(expr)
|
||||
if (this.props.options.expr !== expr) {
|
||||
this.setOptions({expr: expr});
|
||||
}
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
import React, { Component } from 'react';
|
||||
import React, { Component, ChangeEvent } from 'react';
|
||||
|
||||
import { Alert, Button, Col, Row } from 'reactstrap';
|
||||
|
||||
import Panel, { PanelOptions, PanelDefaultOptions } from './Panel';
|
||||
import { decodePanelOptionsFromQueryString, encodePanelOptionsToQueryString } from './utils/urlParams';
|
||||
import Checkbox from './Checkbox';
|
||||
|
||||
interface PanelListState {
|
||||
panels: {
|
||||
|
@ -17,7 +18,7 @@ interface PanelListState {
|
|||
|
||||
class PanelList extends Component<any, PanelListState> {
|
||||
private key: number = 0;
|
||||
|
||||
private initialMetricNames: string[] = [];
|
||||
constructor(props: any) {
|
||||
super(props);
|
||||
|
||||
|
@ -45,7 +46,10 @@ class PanelList extends Component<any, PanelListState> {
|
|||
throw new Error('Unexpected response status when fetching metric names: ' + resp.statusText); // TODO extract error
|
||||
}
|
||||
})
|
||||
.then(json => this.setState({ metricNames: json.data }))
|
||||
.then(json => {
|
||||
this.initialMetricNames = json.data;
|
||||
this.setMetrics();
|
||||
})
|
||||
.catch(error => this.setState({ fetchMetricsError: error.message }));
|
||||
|
||||
const browserTime = new Date().getTime() / 1000;
|
||||
|
@ -75,6 +79,40 @@ class PanelList extends Component<any, PanelListState> {
|
|||
}
|
||||
}
|
||||
|
||||
isHistoryEnabled = () => JSON.parse(localStorage.getItem('enable-query-history') || 'false') as boolean;
|
||||
|
||||
getHistoryItems = () => JSON.parse(localStorage.getItem('history') || '[]') as string[];
|
||||
|
||||
toggleQueryHistory = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
localStorage.setItem('enable-query-history', `${e.target.checked}`);
|
||||
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 });
|
||||
}
|
||||
}
|
||||
|
||||
handleQueryHistory = (query: string) => {
|
||||
const isSimpleMetric = this.initialMetricNames.indexOf(query) !== -1;
|
||||
if (isSimpleMetric || !query.length) {
|
||||
return;
|
||||
}
|
||||
const historyItems = this.getHistoryItems();
|
||||
const extendedItems = historyItems.reduce((acc, metric) => {
|
||||
return metric === query ? acc : [...acc, metric]; // Prevent adding query twice.
|
||||
}, [query]);
|
||||
localStorage.setItem('history', JSON.stringify(extendedItems));
|
||||
this.setMetrics();
|
||||
}
|
||||
|
||||
getKey(): string {
|
||||
return (this.key++).toString();
|
||||
}
|
||||
|
@ -116,6 +154,23 @@ class PanelList extends Component<any, PanelListState> {
|
|||
render() {
|
||||
return (
|
||||
<>
|
||||
<Row className="mb-2">
|
||||
<Checkbox
|
||||
style={{ margin: '0 0 0 15px', alignSelf: 'center' }}
|
||||
onChange={this.toggleQueryHistory}
|
||||
checked={this.isHistoryEnabled()}>
|
||||
Enable query history
|
||||
</Checkbox>
|
||||
<Col>
|
||||
<Button
|
||||
className="float-right classic-ui-btn"
|
||||
color="link"
|
||||
onClick={() => { window.location.pathname = "../../graph" }}
|
||||
size="sm">
|
||||
Return to classic UI
|
||||
</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col>
|
||||
{this.state.timeDriftError && <Alert color="danger"><strong>Warning:</strong> Error fetching server time: {this.state.timeDriftError}</Alert>}
|
||||
|
@ -128,6 +183,7 @@ class PanelList extends Component<any, PanelListState> {
|
|||
</Row>
|
||||
{this.state.panels.map(p =>
|
||||
<Panel
|
||||
onExecuteQuery={this.handleQueryHistory}
|
||||
key={p.key}
|
||||
options={p.options}
|
||||
onOptionsChanged={(opts: PanelOptions) => this.handleOptionsChanged(p.key, opts)}
|
||||
|
|
|
@ -20,14 +20,11 @@ import {
|
|||
} from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
library.add(
|
||||
faChevronLeft,
|
||||
faChevronRight,
|
||||
faCalendarCheck,
|
||||
faArrowUp,
|
||||
faArrowDown,
|
||||
faTimes,
|
||||
);
|
||||
|
||||
// Sadly needed to also replace <i> within the date picker, since it's not a React component.
|
||||
dom.watch();
|
||||
|
||||
|
@ -99,7 +96,7 @@ class TimeInput extends Component<TimeInputProps> {
|
|||
return (
|
||||
<InputGroup className="time-input" size="sm">
|
||||
<InputGroupAddon addonType="prepend">
|
||||
<Button title="Decrease time" onClick={this.decreaseTime}><FontAwesomeIcon icon="chevron-left" fixedWidth/></Button>
|
||||
<Button title="Decrease time" onClick={this.decreaseTime}><FontAwesomeIcon icon={faChevronLeft} fixedWidth /></Button>
|
||||
</InputGroupAddon>
|
||||
|
||||
<Input
|
||||
|
@ -114,12 +111,12 @@ class TimeInput extends Component<TimeInputProps> {
|
|||
that functionality is broken, so we create an external solution instead. */}
|
||||
{this.props.time &&
|
||||
<InputGroupAddon addonType="append">
|
||||
<Button className="clear-time-btn" title="Clear time" onClick={this.clearTime}><FontAwesomeIcon icon="times" fixedWidth/></Button>
|
||||
<Button className="clear-time-btn" title="Clear time" onClick={this.clearTime}><FontAwesomeIcon icon={faTimes} fixedWidth /></Button>
|
||||
</InputGroupAddon>
|
||||
}
|
||||
|
||||
<InputGroupAddon addonType="append">
|
||||
<Button title="Increase time" onClick={this.increaseTime}><FontAwesomeIcon icon="chevron-right" fixedWidth/></Button>
|
||||
<Button title="Increase time" onClick={this.increaseTime}><FontAwesomeIcon icon={faChevronRight} fixedWidth /></Button>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
);
|
||||
|
|
1
web/ui/react-app/src/utils/func.ts
Normal file
1
web/ui/react-app/src/utils/func.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export const uuidGen = () => '_' + Math.random().toString(36).substr(2, 9);
|
Loading…
Reference in a new issue