Start implementing graph controls, add loading spinner

Signed-off-by: Julius Volz <julius.volz@gmail.com>
This commit is contained in:
Julius Volz 2019-02-10 19:32:16 +01:00
parent 00d3821218
commit 1df029fdb9
4 changed files with 3290 additions and 22 deletions

3074
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -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",

View file

@ -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;
} }

View file

@ -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}>&lt;&lt;</Button>
</InputGroupAddon>
<Input value={this.props.endTime ? this.props.endTime : ''} onChange={this.props.changeEndTime} />
<InputGroupAddon addonType="append">
<Button onClick={this.props.increaseEndTime}>&gt;&gt;</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>
); );
} }