mirror of
https://github.com/prometheus/prometheus.git
synced 2025-03-05 20:59:13 -08:00
Start implementing graph controls, add loading spinner
Signed-off-by: Julius Volz <julius.volz@gmail.com>
This commit is contained in:
parent
00d3821218
commit
1df029fdb9
3074
package-lock.json
generated
3074
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -3,10 +3,16 @@
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@fortawesome/fontawesome-svg-core": "^1.2.14",
|
||||||
|
"@fortawesome/free-solid-svg-icons": "^5.7.1",
|
||||||
|
"@fortawesome/react-fontawesome": "^0.1.4",
|
||||||
"bootstrap": "^4.2.1",
|
"bootstrap": "^4.2.1",
|
||||||
"downshift": "^3.2.2",
|
"downshift": "^3.2.2",
|
||||||
|
"i": "^0.3.6",
|
||||||
"jquery": "^3.3.1",
|
"jquery": "^3.3.1",
|
||||||
"jsdom": "^9.6.0",
|
"jsdom": "^9.6.0",
|
||||||
|
"moment": "^2.24.0",
|
||||||
|
"npm": "^6.7.0",
|
||||||
"react": "^16.7.0",
|
"react": "^16.7.0",
|
||||||
"react-dom": "^16.7.0",
|
"react-dom": "^16.7.0",
|
||||||
"react-flot": "^1.3.0",
|
"react-flot": "^1.3.0",
|
||||||
|
|
20
src/App.css
20
src/App.css
|
@ -6,6 +6,10 @@ body {
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
button.execute-btn {
|
||||||
|
width: 84px;
|
||||||
|
}
|
||||||
|
|
||||||
.alert.alert-danger {
|
.alert.alert-danger {
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
|
@ -56,6 +60,22 @@ body {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.graph-controls {
|
||||||
|
padding: 15px 0 5px 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.graph-controls .range-input input {
|
||||||
|
width: 70px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.graph-controls input.resolution-input {
|
||||||
|
width: 90px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.graph-controls .endtime-input, .graph-controls .resolution-input, .graph-controls .stacked-input {
|
||||||
|
margin-left: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
.graph-legend {
|
.graph-legend {
|
||||||
margin: 15px 0 15px 25px;
|
margin: 15px 0 15px 25px;
|
||||||
}
|
}
|
||||||
|
|
212
src/App.js
212
src/App.js
|
@ -2,10 +2,13 @@ import React, { Component } from 'react';
|
||||||
import {
|
import {
|
||||||
Alert,
|
Alert,
|
||||||
Button,
|
Button,
|
||||||
|
ButtonGroup,
|
||||||
Col,
|
Col,
|
||||||
Container,
|
Container,
|
||||||
|
Form,
|
||||||
InputGroup,
|
InputGroup,
|
||||||
InputGroupAddon,
|
InputGroupAddon,
|
||||||
|
InputGroupText,
|
||||||
Input,
|
Input,
|
||||||
Nav,
|
Nav,
|
||||||
NavItem,
|
NavItem,
|
||||||
|
@ -21,6 +24,13 @@ import '../node_modules/react-flot/flot/jquery.flot.crosshair.min';
|
||||||
import '../node_modules/react-flot/flot/jquery.flot.tooltip.min';
|
import '../node_modules/react-flot/flot/jquery.flot.tooltip.min';
|
||||||
import './App.css';
|
import './App.css';
|
||||||
import Downshift from 'downshift';
|
import Downshift from 'downshift';
|
||||||
|
import moment from 'moment'
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
class App extends Component {
|
class App extends Component {
|
||||||
render() {
|
render() {
|
||||||
|
@ -53,7 +63,6 @@ class PanelList extends Component {
|
||||||
if (resp.ok) {
|
if (resp.ok) {
|
||||||
return resp.json();
|
return resp.json();
|
||||||
} else {
|
} else {
|
||||||
console.log(resp);
|
|
||||||
throw new Error('Unexpected response status when fetching metric names: ' + resp.statusText); // TODO extract error
|
throw new Error('Unexpected response status when fetching metric names: ' + resp.statusText); // TODO extract error
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -104,7 +113,8 @@ class Panel extends Component {
|
||||||
type: 'graph', // TODO enum?
|
type: 'graph', // TODO enum?
|
||||||
range: 3600,
|
range: 3600,
|
||||||
endTime: null,
|
endTime: null,
|
||||||
step: null,
|
resolution: null,
|
||||||
|
stacked: false,
|
||||||
data: null,
|
data: null,
|
||||||
loading: false,
|
loading: false,
|
||||||
error: null,
|
error: null,
|
||||||
|
@ -126,30 +136,35 @@ class Panel extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
execute() {
|
execute() {
|
||||||
|
// TODO: Abort existing queries.
|
||||||
if (this.state.expr === "") {
|
if (this.state.expr === "") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setState({loading: true});
|
this.setState({loading: true});
|
||||||
|
|
||||||
|
let endTime = this.getEndTime() / 1000;
|
||||||
|
let resolution = this.state.resolution || Math.max(Math.floor(this.state.range / 250), 1);
|
||||||
|
|
||||||
let url = new URL('http://demo.robustperception.io:9090/');//window.location.href);
|
let url = new URL('http://demo.robustperception.io:9090/');//window.location.href);
|
||||||
let params = {
|
let params = {
|
||||||
'query': this.state.expr,
|
'query': this.state.expr,
|
||||||
};
|
};
|
||||||
|
|
||||||
switch (this.state.type) {
|
switch (this.state.type) {
|
||||||
case 'graph':
|
case 'graph':
|
||||||
url.pathname = '/api/v1/query_range'
|
url.pathname = '/api/v1/query_range'
|
||||||
Object.assign(params, {
|
Object.assign(params, {
|
||||||
start: '1549134688',
|
start: endTime - this.state.range,
|
||||||
end: '1549135688',
|
end: endTime,
|
||||||
step: 10,
|
step: resolution,
|
||||||
})
|
})
|
||||||
// TODO path prefix here and elsewhere.
|
// TODO path prefix here and elsewhere.
|
||||||
break;
|
break;
|
||||||
case 'table':
|
case 'table':
|
||||||
url.pathname = '/api/v1/query'
|
url.pathname = '/api/v1/query'
|
||||||
Object.assign(params, {
|
Object.assign(params, {
|
||||||
time: '1549134688',
|
time: endTime,
|
||||||
})
|
})
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
|
@ -185,6 +200,121 @@ class Panel extends Component {
|
||||||
this.setState({expr: expr});
|
this.setState({expr: expr});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
timeFactors = {
|
||||||
|
"y": 60 * 60 * 24 * 365,
|
||||||
|
"w": 60 * 60 * 24 * 7,
|
||||||
|
"d": 60 * 60 * 24,
|
||||||
|
"h": 60 * 60,
|
||||||
|
"m": 60,
|
||||||
|
"s": 1
|
||||||
|
};
|
||||||
|
|
||||||
|
rangeSteps = [
|
||||||
|
"1s", "10s", "1m", "5m", "15m", "30m", "1h", "2h", "6h", "12h", "1d", "2d",
|
||||||
|
"1w", "2w", "4w", "8w", "1y", "2y"
|
||||||
|
];
|
||||||
|
|
||||||
|
parseDuration(rangeText) {
|
||||||
|
var rangeRE = new RegExp("^([0-9]+)([ywdhms]+)$");
|
||||||
|
var matches = rangeText.match(rangeRE);
|
||||||
|
if (!matches) { return; }
|
||||||
|
if (matches.length !== 3) {
|
||||||
|
return 60;
|
||||||
|
}
|
||||||
|
var value = parseInt(matches[1]);
|
||||||
|
var unit = matches[2];
|
||||||
|
return value * this.timeFactors[unit];
|
||||||
|
}
|
||||||
|
|
||||||
|
increaseRange = (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
for (let range of this.rangeSteps) {
|
||||||
|
let rangeSeconds = this.parseDuration(range);
|
||||||
|
if (this.state.range < rangeSeconds) {
|
||||||
|
this.setState({range: rangeSeconds}, this.execute)
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
decreaseRange = (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
for (let range of this.rangeSteps.slice().reverse()) {
|
||||||
|
let rangeSeconds = this.parseDuration(range);
|
||||||
|
if (this.state.range > rangeSeconds) {
|
||||||
|
this.setState({range: rangeSeconds}, this.execute)
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
changeRange = (event) => {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
increaseEndTime = () => {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
decreaseEndTime = () => {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
getEndTime = () => {
|
||||||
|
if (this.state.endTime === null) {
|
||||||
|
return moment();
|
||||||
|
}
|
||||||
|
return this.state.endTime();
|
||||||
|
}
|
||||||
|
|
||||||
|
changeEndTime = () => {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
changeResolution = () => {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// getEndDate = () => {
|
||||||
|
// var self = this;
|
||||||
|
// if (!self.endDate || !self.endDate.val()) {
|
||||||
|
// return moment();
|
||||||
|
// }
|
||||||
|
// return self.endDate.data('DateTimePicker').date();
|
||||||
|
// };
|
||||||
|
|
||||||
|
// getOrSetEndDate = () => {
|
||||||
|
// var self = this;
|
||||||
|
// var date = self.getEndDate();
|
||||||
|
// self.setEndDate(date);
|
||||||
|
// return date;
|
||||||
|
// };
|
||||||
|
|
||||||
|
// setEndDate = (date) => {
|
||||||
|
// var self = this;
|
||||||
|
// self.endDate.data('DateTimePicker').date(date);
|
||||||
|
// };
|
||||||
|
|
||||||
|
// increaseEnd = () => {
|
||||||
|
// var self = this;
|
||||||
|
// var newDate = moment(self.getOrSetEndDate());
|
||||||
|
// newDate.add(self.parseDuration(self.rangeInput.val()) / 2, 'seconds');
|
||||||
|
// self.setEndDate(newDate);
|
||||||
|
// self.submitQuery();
|
||||||
|
// };
|
||||||
|
|
||||||
|
// decreaseEnd = () => {
|
||||||
|
// var self = this;
|
||||||
|
// var newDate = moment(self.getOrSetEndDate());
|
||||||
|
// newDate.subtract(self.parseDuration(self.rangeInput.val()) / 2, 'seconds');
|
||||||
|
// self.setEndDate(newDate);
|
||||||
|
// self.submitQuery();
|
||||||
|
// };
|
||||||
|
|
||||||
|
changeStacking = (stacked) => {
|
||||||
|
this.setState({stacked: stacked});
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -194,6 +324,7 @@ class Panel extends Component {
|
||||||
value={this.state.expr}
|
value={this.state.expr}
|
||||||
onChange={this.handleExpressionChange}
|
onChange={this.handleExpressionChange}
|
||||||
execute={this.execute}
|
execute={this.execute}
|
||||||
|
loading={this.state.loading}
|
||||||
metrics={this.props.metrics}
|
metrics={this.props.metrics}
|
||||||
/>
|
/>
|
||||||
{/*<Input type="select" name="selectMetric">
|
{/*<Input type="select" name="selectMetric">
|
||||||
|
@ -203,7 +334,7 @@ class Panel extends Component {
|
||||||
</Row>
|
</Row>
|
||||||
<Row>
|
<Row>
|
||||||
<Col>
|
<Col>
|
||||||
{this.state.loading && "Loading..."}
|
{/* {this.state.loading && "Loading..."} */}
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
<Row>
|
<Row>
|
||||||
|
@ -238,7 +369,17 @@ class Panel extends Component {
|
||||||
<GraphControls
|
<GraphControls
|
||||||
range={this.state.range}
|
range={this.state.range}
|
||||||
endTime={this.state.endTime}
|
endTime={this.state.endTime}
|
||||||
step={this.state}
|
resolution={this.state.resolution}
|
||||||
|
stacked={this.state.stacked}
|
||||||
|
|
||||||
|
decreaseRange={this.decreaseRange}
|
||||||
|
increaseRange={this.increaseRange}
|
||||||
|
changeRange={this.changeRange}
|
||||||
|
decreaseEndTime={this.decreaseEndTime}
|
||||||
|
increaseEndTime={this.increaseEndTime}
|
||||||
|
changeEndTime={this.changeEndTime}
|
||||||
|
changeResolution={this.changeResolution}
|
||||||
|
changeStacking={this.changeStacking}
|
||||||
/>
|
/>
|
||||||
<Graph data={this.state.data} />
|
<Graph data={this.state.data} />
|
||||||
</>
|
</>
|
||||||
|
@ -305,6 +446,11 @@ class ExpressionInput extends Component {
|
||||||
{downshift => (
|
{downshift => (
|
||||||
<div>
|
<div>
|
||||||
<InputGroup className="expression-input">
|
<InputGroup className="expression-input">
|
||||||
|
<InputGroupAddon addonType="prepend">
|
||||||
|
<InputGroupText>
|
||||||
|
{this.props.loading ? <FontAwesomeIcon icon="spinner" spin/> : <FontAwesomeIcon icon="search"/>}
|
||||||
|
</InputGroupText>
|
||||||
|
</InputGroupAddon>
|
||||||
|
|
||||||
<Input
|
<Input
|
||||||
autoFocus
|
autoFocus
|
||||||
|
@ -331,7 +477,7 @@ class ExpressionInput extends Component {
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
<InputGroupAddon addonType="append">
|
<InputGroupAddon addonType="append">
|
||||||
<Button color="primary" onClick={this.props.execute}>Execute</Button>
|
<Button className="execute-btn" color="primary" onClick={this.props.execute}>Execute</Button>
|
||||||
</InputGroupAddon>
|
</InputGroupAddon>
|
||||||
</InputGroup>
|
</InputGroup>
|
||||||
{downshift.isOpen &&
|
{downshift.isOpen &&
|
||||||
|
@ -362,16 +508,6 @@ class ExpressionInput extends Component {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Downshift>
|
</Downshift>
|
||||||
|
|
||||||
// <Input
|
|
||||||
// autoFocus
|
|
||||||
// type="textarea"
|
|
||||||
// rows={this.numRows()}
|
|
||||||
// value={this.props.value}
|
|
||||||
// onChange={this.props.onChange}
|
|
||||||
// onKeyPress={this.handleKeyPress}
|
|
||||||
// placeholder="Expression (press Shift+Enter for newlines)" />
|
|
||||||
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -416,9 +552,41 @@ function DataTable(props) {
|
||||||
}
|
}
|
||||||
|
|
||||||
class GraphControls extends Component {
|
class GraphControls extends Component {
|
||||||
// TODO
|
|
||||||
render() {
|
render() {
|
||||||
return null;
|
return (
|
||||||
|
<Form inline className="graph-controls">
|
||||||
|
<InputGroup className="range-input">
|
||||||
|
<InputGroupAddon addonType="prepend">
|
||||||
|
<Button onClick={this.props.decreaseRange}>-</Button>
|
||||||
|
</InputGroupAddon>
|
||||||
|
|
||||||
|
<Input value={this.props.range} onChange={this.props.changeRange}/>
|
||||||
|
|
||||||
|
<InputGroupAddon addonType="append">
|
||||||
|
<Button onClick={this.props.increaseRange}>+</Button>
|
||||||
|
</InputGroupAddon>
|
||||||
|
</InputGroup>
|
||||||
|
|
||||||
|
<InputGroup className="endtime-input">
|
||||||
|
<InputGroupAddon addonType="prepend">
|
||||||
|
<Button onClick={this.props.decreaseEndTime}><<</Button>
|
||||||
|
</InputGroupAddon>
|
||||||
|
|
||||||
|
<Input value={this.props.endTime ? this.props.endTime : ''} onChange={this.props.changeEndTime} />
|
||||||
|
|
||||||
|
<InputGroupAddon addonType="append">
|
||||||
|
<Button onClick={this.props.increaseEndTime}>>></Button>
|
||||||
|
</InputGroupAddon>
|
||||||
|
</InputGroup>
|
||||||
|
|
||||||
|
<Input className="resolution-input" value={this.props.resolution ? this.props.resolution : ''} onChange={this.props.changeResolution} placeholder="Res. (s)"/>
|
||||||
|
|
||||||
|
<ButtonGroup className="stacked-input">
|
||||||
|
<Button onClick={() => this.props.changeStacking(false)} active={this.props.stacked}>stacked</Button>
|
||||||
|
<Button onClick={() => this.props.changeStacking(true)} active={!this.props.stacked}>unstacked</Button>
|
||||||
|
</ButtonGroup>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -538,7 +706,7 @@ class Graph extends Component {
|
||||||
height="500px"
|
height="500px"
|
||||||
width="100%"
|
width="100%"
|
||||||
/>
|
/>
|
||||||
<div className="graph-legend" ref={ref => { this.legend = ref; }}>df</div>
|
<div className="graph-legend" ref={ref => { this.legend = ref; }}></div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue