Grpahing, try out echarts

Signed-off-by: Julius Volz <julius.volz@gmail.com>
This commit is contained in:
Julius Volz 2019-02-02 16:48:40 +00:00
parent a125b6756a
commit 8ce943cdfc
5 changed files with 399 additions and 24 deletions

126
package-lock.json generated
View file

@ -2403,6 +2403,11 @@
"resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
"integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24="
},
"bootstrap": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-4.2.1.tgz",
"integrity": "sha512-tt/7vIv3Gm2mnd/WeDx36nfGGHleil0Wg8IeB7eMrVkY0fZ5iTaBisSh8oNANc2IBsCc6vCgCNTIM/IEN0+50Q=="
},
"brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
@ -2872,6 +2877,11 @@
}
}
},
"classnames": {
"version": "2.2.6",
"resolved": "https://registry.npmjs.org/classnames/-/classnames-2.2.6.tgz",
"integrity": "sha512-JR/iSQOSt+LQIWwrwEzJ9uk0xfN3mTVYMwt1Ir5mUcSN6pU+V4zQFFaJsclJbPuAUQH+yfWef6tm7l1quW3C8Q=="
},
"clean-css": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.2.1.tgz",
@ -3973,6 +3983,14 @@
"utila": "~0.4"
}
},
"dom-helpers": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-3.4.0.tgz",
"integrity": "sha512-LnuPJ+dwqKDIyotW1VzmOZ5TONUN7CwkCR5hrgawTUbkBGYdeoNLZo6nNfGkCrjtE1nXXaj7iMMpDa8/d9WoIA==",
"requires": {
"@babel/runtime": "^7.1.2"
}
},
"dom-serializer": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.1.0.tgz",
@ -4067,6 +4085,23 @@
"safer-buffer": "^2.1.0"
}
},
"echarts": {
"version": "4.2.0-rc.2",
"resolved": "https://registry.npmjs.org/echarts/-/echarts-4.2.0-rc.2.tgz",
"integrity": "sha512-5Y4Kyi4eNsRM9Cnl7Q8C6PFVjznBJv1VIiMm/VSQ9zyqeo+ce1695GqUd9v4zfVx+Ow1gnwMJX67h0FNvarScw==",
"requires": {
"zrender": "4.0.5"
}
},
"echarts-for-react": {
"version": "2.0.15-beta.0",
"resolved": "https://registry.npmjs.org/echarts-for-react/-/echarts-for-react-2.0.15-beta.0.tgz",
"integrity": "sha512-brgogznteCirkvOvl/ZOozkpvBt612LBtDiSGE/pANTIO2+YyGpNfFXAIjEIDAY6yTvgdj+Ei7JISAfflXEn5Q==",
"requires": {
"fast-deep-equal": "^2.0.1",
"size-sensor": "^0.2.0"
}
},
"ee-first": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
@ -8329,6 +8364,11 @@
"topo": "2.x.x"
}
},
"jquery": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/jquery/-/jquery-3.3.1.tgz",
"integrity": "sha512-Ubldcmxp5np52/ENotGxlLe6aGMvmF4R8S6tZjsP6Knsaxd/xp3Zrh50cG93lR6nPXyUFwzN3ZSOQI0wRJNdGg=="
},
"js-levenshtein": {
"version": "1.1.6",
"resolved": "https://registry.npmjs.org/js-levenshtein/-/js-levenshtein-1.1.6.tgz",
@ -8650,6 +8690,16 @@
"resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
"integrity": "sha1-gteb/zCmfEAF/9XiUVMArZyk168="
},
"lodash.isfunction": {
"version": "3.0.9",
"resolved": "https://registry.npmjs.org/lodash.isfunction/-/lodash.isfunction-3.0.9.tgz",
"integrity": "sha512-AirXNj15uRIMMPihnkInB4i3NHeb4iBtNg9WRWuK2o31S+ePwwNmDPaTL3o7dTJ+VXNZim7rFs4rxN4YU1oUJw=="
},
"lodash.isobject": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/lodash.isobject/-/lodash.isobject-3.0.2.tgz",
"integrity": "sha1-PI+41bW/S/kK4G4U8qUwpO2TXh0="
},
"lodash.memoize": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz",
@ -8682,6 +8732,11 @@
"lodash._reinterpolate": "~3.0.0"
}
},
"lodash.tonumber": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/lodash.tonumber/-/lodash.tonumber-4.0.3.tgz",
"integrity": "sha1-C5azGzVnJ5Prf1pj7nkfG56QJdk="
},
"lodash.uniq": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz",
@ -9756,6 +9811,11 @@
"resolved": "https://registry.npmjs.org/pnp-webpack-plugin/-/pnp-webpack-plugin-1.1.0.tgz",
"integrity": "sha512-CPCdcFxx7fEcDMWTDjXe2Wypt4JuMt4q5Q2UrpTcyBBkLiCIyPEh/mCGmUWIcNkKGyXwQ9Y2wVhlKm6ketiBNQ=="
},
"popper.js": {
"version": "1.14.7",
"resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.14.7.tgz",
"integrity": "sha512-4q1hNvoUre/8srWsH7hnoSJ5xVmIL4qgz+s4qf2TnJIMyZFUFMGH+9vE7mXynAlHSZ/NdTmmow86muD0myUkVQ=="
},
"portfinder": {
"version": "1.0.20",
"resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.20.tgz",
@ -13601,6 +13661,20 @@
"resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-5.1.2.tgz",
"integrity": "sha512-7kEBKwU9R8fKnZJBRa5RSIfay4KJwnYvKB6gODGicUmDSAhQJ7Tdnll5S0RLtYrzRfMVXlqYw61rzrSpP4ThLQ=="
},
"react-lifecycles-compat": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz",
"integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA=="
},
"react-popper": {
"version": "0.10.4",
"resolved": "https://registry.npmjs.org/react-popper/-/react-popper-0.10.4.tgz",
"integrity": "sha1-rypBXqIike3VBGeNev2opu4ylao=",
"requires": {
"popper.js": "^1.14.1",
"prop-types": "^15.6.1"
}
},
"react-scripts": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-2.1.3.tgz",
@ -13656,6 +13730,48 @@
"workbox-webpack-plugin": "3.6.3"
}
},
"react-transition-group": {
"version": "2.5.3",
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-2.5.3.tgz",
"integrity": "sha512-2DGFck6h99kLNr8pOFk+z4Soq3iISydwOFeeEVPjTN6+Y01CmvbWmnN02VuTWyFdnRtIDPe+wy2q6Ui8snBPZg==",
"requires": {
"dom-helpers": "^3.3.1",
"loose-envify": "^1.4.0",
"prop-types": "^15.6.2",
"react-lifecycles-compat": "^3.0.4"
}
},
"reactstrap": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/reactstrap/-/reactstrap-7.1.0.tgz",
"integrity": "sha512-wtc4RkgnGn1TsZ0AxOZ2OqT+b8YmCWZj/tErPujWLepxzlEEhveZGC+uDerdaHVSAzJUP2DTk605iper7hutQQ==",
"requires": {
"@babel/runtime": "^7.2.0",
"classnames": "^2.2.3",
"lodash.isfunction": "^3.0.9",
"lodash.isobject": "^3.0.2",
"lodash.tonumber": "^4.0.3",
"prop-types": "^15.5.8",
"react-lifecycles-compat": "^3.0.4",
"react-popper": "^0.10.4",
"react-transition-group": "^2.3.1"
},
"dependencies": {
"@babel/runtime": {
"version": "7.3.1",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.3.1.tgz",
"integrity": "sha512-7jGW8ppV0ant637pIqAcFfQDDH1orEPGJb8aXfUozuCU3QqX7rX4DA8iwrbPrR1hcH0FTTHz47yQnk+bl5xHQA==",
"requires": {
"regenerator-runtime": "^0.12.0"
}
},
"regenerator-runtime": {
"version": "0.12.1",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.12.1.tgz",
"integrity": "sha512-odxIc1/vDlo4iZcfXqRYFj0vpXFNoGdKMAUieAlFYO6m/nl5e9KR/beGf41z4a1FI+aQgtjhuaSlDxQ0hmkrHg=="
}
}
},
"read-pkg": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz",
@ -14986,6 +15102,11 @@
"resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-0.1.1.tgz",
"integrity": "sha512-PmGOd02bM9YO5ifxpw36nrNMBTptEtfRl4qUYl9SndkolplkrZZOW7PGHjrZL53QvMVj9nQ+TKqUnRsw4tJa4g=="
},
"size-sensor": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/size-sensor/-/size-sensor-0.2.2.tgz",
"integrity": "sha512-dL/IdBhGDvCHlxQgxryqJNEnSWQz4xvKntsW028CgaJLBLSw8rpi7oUVSM4+xnaHbH+BFkXz6H5aMStfH8v2Pg=="
},
"slash": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/slash/-/slash-1.0.0.tgz",
@ -17137,6 +17258,11 @@
"camelcase": "^5.0.0",
"decamelize": "^1.2.0"
}
},
"zrender": {
"version": "4.0.5",
"resolved": "https://registry.npmjs.org/zrender/-/zrender-4.0.5.tgz",
"integrity": "sha512-SintgipGEJPT9Sz2ABRoE4ZD7Yzy7oR7j7KP6H+C9FlbHWnLUfGVK7E8UV27pGwlxAMB0EsnrqhXx5XjAfv/KA=="
}
}
}

View file

@ -3,9 +3,14 @@
"version": "0.1.0",
"private": true,
"dependencies": {
"bootstrap": "^4.2.1",
"echarts": "^4.2.0-rc.2",
"echarts-for-react": "^2.0.15-beta.0",
"jquery": "^3.3.1",
"react": "^16.7.0",
"react-dom": "^16.7.0",
"react-scripts": "2.1.3"
"react-scripts": "2.1.3",
"reactstrap": "^7.1.0"
},
"scripts": {
"start": "react-scripts start",

30
src/App.css Normal file
View file

@ -0,0 +1,30 @@
body {
padding-top: 10px; /* TODO remove */
}
.expression-input {
margin-bottom: 10px;
}
.alert.alert-danger {
margin-bottom: 10px;
}
.nav-tabs .nav-link {
cursor: pointer;
}
.tab-content {
border-left: 1px solid #dee2e6;
border-right: 1px solid #dee2e6;
border-bottom: 1px solid #dee2e6;
}
.data-table.table {
white-space: pre;
margin: 2px 0 2px 0;
}
.data-table > tbody > tr > td {
padding: 6px 16px 6px 16px;
}

View file

@ -1,9 +1,14 @@
import React, { Component } from 'react';
import { Alert, Button, Col, Container, InputGroup, InputGroupAddon, Input, Nav, NavItem, NavLink, Row, TabContent, TabPane, Table } from 'reactstrap';
import ReactEcharts from 'echarts-for-react';
import './App.css'
class App extends Component {
render() {
return (
<PanelList />
<Container fluid={true}>
<PanelList />
</Container>
);
}
}
@ -46,7 +51,7 @@ class PanelList extends Component {
return (
<>
{this.state.panels}
<button onClick={this.addPanel}>+</button>
<Button color="primary" onClick={this.addPanel}>Add Panel</Button>
</>
);
}
@ -58,7 +63,7 @@ class Panel extends Component {
this.state = {
expr: '',
type: 'graph', // TODO enum?
type: 'table', // TODO enum?
range: '1h',
endTime: null,
data: null,
@ -72,9 +77,38 @@ class Panel extends Component {
}
execute() {
if (this.state.expr === "") {
return;
}
this.setState({loading: true});
fetch('http://demo.robustperception.io:9090/api/v1/query?query=' + encodeURIComponent(this.state.expr))
let url = new URL('http://demo.robustperception.io:9090/');//window.location.href);
let params = {
'query': this.state.expr,
};
switch (this.state.type) {
case 'graph':
url.pathname = '/api/v1/query_range'
Object.assign(params, {
start: '1549118381',
end: '1549119381',
step: 10,
})
// TODO path prefix here and elsewhere.
break;
case 'table':
url.pathname = '/api/v1/query'
Object.assign(params, {
time: '1549119381',
})
break;
default:
// TODO
}
Object.keys(params).forEach(key => url.searchParams.append(key, params[key]))
fetch(url)
.then(resp => {
if (resp.ok) {
return resp.json();
@ -85,6 +119,7 @@ class Panel extends Component {
})
.then(json =>
this.setState({
error: null,
data: json.data,
loading: false,
})
@ -103,13 +138,58 @@ class Panel extends Component {
render() {
return (
<div>
<ExpressionInput value={this.state.expr} onChange={this.handleExpressionChange} execute={this.execute}/>
{this.state.loading && "Loading..."}
{this.state.error}
<DataTable data={this.state.data} />
<button onClick={this.props.removePanel}>-</button>
</div>
<>
<Row>
<Col>
<ExpressionInput value={this.state.expr} onChange={this.handleExpressionChange} execute={this.execute}/>
</Col>
</Row>
<Row>
<Col>
{this.state.loading && "Loading..."}
</Col>
</Row>
<Row>
<Col>
{this.state.error && <Alert color="danger">{this.state.error.toString()}</Alert>}
</Col>
</Row>
<Row>
<Col>
<Nav tabs>
<NavItem>
<NavLink
className={this.state.type === 'graph' ? 'active' : ''}
onClick={() => { this.setState({type: 'graph'}); }}
>
Graph
</NavLink>
</NavItem>
<NavItem>
<NavLink
className={this.state.type === 'table' ? 'active' : ''}
onClick={() => { this.setState({type: 'table'}); }}
>
Table
</NavLink>
</NavItem>
</Nav>
<TabContent activeTab={this.state.type}>
<TabPane tabId="graph">
<Graph data={this.state.data} />
</TabPane>
<TabPane tabId="table">
<DataTable data={this.state.data} />
</TabPane>
</TabContent>
</Col>
</Row>
<Row>
<Col>
<Button className="float-right" color="link" onClick={this.props.removePanel}>Remove Panel</Button>
</Col>
</Row>
</>
);
}
}
@ -128,34 +208,167 @@ class ExpressionInput extends Component {
}
}
numRows() {
// TODO: Not ideal. This doesn't handle long lines.
return this.props.value.split(/\r\n|\r|\n/).length;
}
render() {
return (
<div>
<textarea
<InputGroup className="expression-input">
<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)" />
<button onClick={this.props.execute}>Execute</button>
</div>
<InputGroupAddon addonType="append">
<Button color="primary" onClick={this.props.execute}>Execute</Button>
</InputGroupAddon>
</InputGroup>
);
}
}
function DataTable(props) {
if (!props.data) {
return <div>no data</div>;
const data = props.data;
var rows = <tr><td colSpan="2"><i>no data</i></td></tr>
if (props.data) {
switch(data.resultType) {
case 'vector':
if (data.result === null || data.result.length === 0) {
break;
}
rows = props.data.result.map((s, index) => {
return <tr key={index}><td>{metricToSeriesName(s.metric)}</td><td>{s.value[1]}</td></tr>
});
break;
case 'matrix':
if (data.result === null || data.result.length === 0) {
break;
}
rows = props.data.result.map((s, index) => {
const valueText = s.values.map((v) => {
return [1] + ' @' + v[0];
}).join('\n');
return <tr key={index}><td>{metricToSeriesName(s.metric)}</td><td>{valueText}</td></tr>
});
break;
default:
// TODO
}
}
const rows = props.data.result.map((s, index) => {
return <tr key={index}><th>{metricToSeriesName(s.metric)}</th><td>{s.value[1]} @ {s.value[0]}</td></tr>
});
return (
<table>
<tbody>{rows}</tbody>
</table>
<Table hover size="sm" className="data-table">
<tbody>
{rows}
</tbody>
</Table>
);
}
class Graph extends Component {
componentDidMount() {
this.chart = null;
}
getOption() {
const data = this.transformData(this.props.data);
console.log(data);
return {
legend: {
show: true,
orient: 'vertical',
},
addDataAnimation: false,
xAxis : [
{
type : 'category',
boundaryGap : false,
//data : ['周一','周二','周三','周四','周五','周六','周日']
}
],
yAxis : [
{
type : 'value'
}
],
series: data//[
// {
// 'name': "up{}",
// 'type': "line",
// 'data': [
// [1,1], [2,4], [3,3], [4, -2],
// ],
// },
// {
// 'connectNulls': false,
// 'name': "foo{}",
// 'type': "line",
// 'data': [
// [1,5], [2,5], [3, NaN], [4, -3], [5, 6], [6, 1]
// ],
// },
// {
// 'name': "bar{}",
// 'type': "line",
// 'data': [
// [1,1], [2,3], [3,8], [4, -1],
// ],
// },
// {
// 'name': "down{}",
// 'type': "line",
// 'data': [
// [1,2], [2,0], [3,0], [4, 2]
// ],
// }
//],
};
}
transformData(data) {
if (data.resultType !== 'matrix') {
// TODO self.showError("Result is not of matrix type! Please enter a correct expression.");
return [];
}
return data.result.map(ts => {
return {
name: metricToSeriesName(ts.metric),
data: ts.values.map(v => [v[0], this.parseValue(v[1])]),
type: 'line',
};
})
}
parseValue(value) {
var val = parseFloat(value);
if (isNaN(val)) {
// "+Inf", "-Inf", "+Inf" will be parsed into NaN by parseFloat(). The
// can't be graphed, so show them as gaps (null).
return null;
}
return val;
};
render() {
if (this.props.data === null) {
return null;
}
return (
<div>
<ReactEcharts option={this.getOption()} ref={(ref) => { this.echarts = ref }} />
</div>
);
}
}
function metricToSeriesName(labels) {
var tsName = (labels.__name__ || '') + "{";
var labelStrings = [];

View file

@ -1,5 +1,6 @@
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import 'bootstrap/dist/css/bootstrap.min.css';
ReactDOM.render(<App />, document.getElementById('root'));