mirror of
https://github.com/prometheus/prometheus.git
synced 2025-03-05 20:59:13 -08:00
Fixed and built more features
Signed-off-by: Julius Volz <julius.volz@gmail.com>
This commit is contained in:
parent
285eda3a13
commit
1a3df0b78c
5
package-lock.json
generated
5
package-lock.json
generated
|
@ -6180,6 +6180,11 @@
|
||||||
"resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz",
|
||||||
"integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc="
|
"integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc="
|
||||||
},
|
},
|
||||||
|
"fuzzy": {
|
||||||
|
"version": "0.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/fuzzy/-/fuzzy-0.1.3.tgz",
|
||||||
|
"integrity": "sha1-THbsL/CsGjap3M+aAN+GIweNTtg="
|
||||||
|
},
|
||||||
"get-caller-file": {
|
"get-caller-file": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz",
|
||||||
|
|
|
@ -8,10 +8,12 @@
|
||||||
"@fortawesome/react-fontawesome": "^0.1.4",
|
"@fortawesome/react-fontawesome": "^0.1.4",
|
||||||
"bootstrap": "^4.2.1",
|
"bootstrap": "^4.2.1",
|
||||||
"downshift": "^3.2.2",
|
"downshift": "^3.2.2",
|
||||||
|
"fuzzy": "^0.1.3",
|
||||||
"i": "^0.3.6",
|
"i": "^0.3.6",
|
||||||
"jquery": "^3.3.1",
|
"jquery": "^3.3.1",
|
||||||
"jsdom": "^9.6.0",
|
"jsdom": "^9.6.0",
|
||||||
"moment": "^2.24.0",
|
"moment": "^2.24.0",
|
||||||
|
"moment-timezone": "^0.5.23",
|
||||||
"npm": "^6.7.0",
|
"npm": "^6.7.0",
|
||||||
"react": "^16.7.0",
|
"react": "^16.7.0",
|
||||||
"react-dom": "^16.7.0",
|
"react-dom": "^16.7.0",
|
||||||
|
|
|
@ -70,10 +70,18 @@ button.execute-btn {
|
||||||
padding: 15px 0 10px 10px;
|
padding: 15px 0 10px 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.graph-controls input {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
.graph-controls .range-input input {
|
.graph-controls .range-input input {
|
||||||
width: 50px;
|
width: 50px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.graph-controls .endtime-input input {
|
||||||
|
width: 160px;
|
||||||
|
}
|
||||||
|
|
||||||
.graph-controls input.resolution-input {
|
.graph-controls input.resolution-input {
|
||||||
width: 90px;
|
width: 90px;
|
||||||
}
|
}
|
||||||
|
|
182
src/App.js
182
src/App.js
|
@ -1,4 +1,4 @@
|
||||||
import React, { Component } from 'react';
|
import React, { Component, PureComponent } from 'react';
|
||||||
import {
|
import {
|
||||||
Alert,
|
Alert,
|
||||||
Button,
|
Button,
|
||||||
|
@ -28,7 +28,9 @@ import '../node_modules/react-flot/flot/jquery.flot.stack.min';
|
||||||
import './App.css';
|
import './App.css';
|
||||||
|
|
||||||
import Downshift from 'downshift';
|
import Downshift from 'downshift';
|
||||||
import moment from 'moment';
|
import moment from 'moment-timezone';
|
||||||
|
|
||||||
|
import fuzzy from 'fuzzy';
|
||||||
|
|
||||||
import 'tempusdominus-core';
|
import 'tempusdominus-core';
|
||||||
import 'tempusdominus-bootstrap-4';
|
import 'tempusdominus-bootstrap-4';
|
||||||
|
@ -100,7 +102,7 @@ class PanelList extends Component {
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.addPanel();
|
this.addPanel();
|
||||||
|
|
||||||
fetch("http://demo.robustperception.io:9090/api/v1/label/__name__/values")
|
fetch("http://demo.robustperception.io:9090/api/v1/label/__name__/values", {cache: "no-store"})
|
||||||
.then(resp => {
|
.then(resp => {
|
||||||
if (resp.ok) {
|
if (resp.ok) {
|
||||||
return resp.json();
|
return resp.json();
|
||||||
|
@ -112,7 +114,7 @@ class PanelList extends Component {
|
||||||
.catch(error => this.setState({fetchMetricsError: error.message}));
|
.catch(error => this.setState({fetchMetricsError: error.message}));
|
||||||
|
|
||||||
const browserTime = new Date().getTime() / 1000;
|
const browserTime = new Date().getTime() / 1000;
|
||||||
fetch("http://demo.robustperception.io:9090/api/v1/query?query=time()")
|
fetch("http://demo.robustperception.io:9090/api/v1/query?query=time()", {cache: "no-store"})
|
||||||
.then(resp => {
|
.then(resp => {
|
||||||
if (resp.ok) {
|
if (resp.ok) {
|
||||||
return resp.json();
|
return resp.json();
|
||||||
|
@ -179,11 +181,10 @@ class Panel extends Component {
|
||||||
expr: 'rate(node_cpu_seconds_total[1m])',
|
expr: 'rate(node_cpu_seconds_total[1m])',
|
||||||
type: 'graph', // TODO enum?
|
type: 'graph', // TODO enum?
|
||||||
range: 3600,
|
range: 3600,
|
||||||
endTime: null,
|
endTime: null, // This is in milliseconds.
|
||||||
resolution: null,
|
resolution: null,
|
||||||
stacked: false,
|
stacked: false,
|
||||||
data: null,
|
data: null,
|
||||||
loading: false,
|
|
||||||
error: null,
|
error: null,
|
||||||
stats: null,
|
stats: null,
|
||||||
};
|
};
|
||||||
|
@ -196,6 +197,12 @@ class Panel extends Component {
|
||||||
return prevState[v] !== this.state[v];
|
return prevState[v] !== this.state[v];
|
||||||
})
|
})
|
||||||
if (needsRefresh) {
|
if (needsRefresh) {
|
||||||
|
if (prevState.type !== this.state.type) {
|
||||||
|
// If the other options change, we still want to show the old data until the new
|
||||||
|
// query completes, but this is not a good idea when we actually change between
|
||||||
|
// table and graph view, since not all queries work well in both.
|
||||||
|
this.setState({data: null});
|
||||||
|
}
|
||||||
this.executeQuery();
|
this.executeQuery();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -205,13 +212,19 @@ class Panel extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
executeQuery = ()=> {
|
executeQuery = ()=> {
|
||||||
// TODO: Abort existing queries.
|
if (this.abortInFlightFetch) {
|
||||||
if (this.state.expr === "") {
|
this.abortInFlightFetch();
|
||||||
return;
|
this.abortInFlightFetch = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const abortController = new AbortController();
|
||||||
|
this.abortInFlightFetch = () => abortController.abort();
|
||||||
this.setState({loading: true});
|
this.setState({loading: true});
|
||||||
|
|
||||||
|
if (this.state.expr === '') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let endTime = this.getEndTime() / 1000;
|
let endTime = this.getEndTime() / 1000;
|
||||||
let startTime = endTime - this.state.range;
|
let startTime = endTime - this.state.range;
|
||||||
let resolution = this.state.resolution || Math.max(Math.floor(this.state.range / 250), 1);
|
let resolution = this.state.resolution || Math.max(Math.floor(this.state.range / 250), 1);
|
||||||
|
@ -238,11 +251,11 @@ class Panel extends Component {
|
||||||
})
|
})
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
// TODO
|
throw new Error('Invalid panel type "' + this.state.type + '"');
|
||||||
}
|
}
|
||||||
Object.keys(params).forEach(key => url.searchParams.append(key, params[key]))
|
Object.keys(params).forEach(key => url.searchParams.append(key, params[key]))
|
||||||
|
|
||||||
fetch(url)
|
fetch(url, {cache: 'no-store', signal: abortController.signal})
|
||||||
.then(resp => resp.json())
|
.then(resp => resp.json())
|
||||||
.then(json => {
|
.then(json => {
|
||||||
if (json.status !== 'success') {
|
if (json.status !== 'success') {
|
||||||
|
@ -258,9 +271,14 @@ class Panel extends Component {
|
||||||
resolution: resolution,
|
resolution: resolution,
|
||||||
},
|
},
|
||||||
loading: false,
|
loading: false,
|
||||||
})
|
});
|
||||||
|
this.abortInFlightFetch = null;
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
|
if (error.name === 'AbortError') {
|
||||||
|
// Aborts are expected, don't show an error for them.
|
||||||
|
return
|
||||||
|
}
|
||||||
this.setState({
|
this.setState({
|
||||||
error: 'Error executing query: ' + error.message,
|
error: 'Error executing query: ' + error.message,
|
||||||
loading: false,
|
loading: false,
|
||||||
|
@ -289,8 +307,12 @@ class Panel extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
handleChangeResolution = (resolution) => {
|
handleChangeResolution = (resolution) => {
|
||||||
|
// TODO: Where should we validate domain model constraints? In the parent's
|
||||||
|
// change handler like here, or in the calling component?
|
||||||
|
if (resolution > 0) {
|
||||||
this.setState({resolution: resolution});
|
this.setState({resolution: resolution});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// getEndDate = () => {
|
// getEndDate = () => {
|
||||||
// var self = this;
|
// var self = this;
|
||||||
|
@ -344,14 +366,6 @@ class Panel extends Component {
|
||||||
loading={this.state.loading}
|
loading={this.state.loading}
|
||||||
metrics={this.props.metrics}
|
metrics={this.props.metrics}
|
||||||
/>
|
/>
|
||||||
{/*<Input type="select" name="selectMetric">
|
|
||||||
{this.props.metrics.map(m => <option key={m}>{m}</option>)}
|
|
||||||
</Input>*/}
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
<Row>
|
|
||||||
<Col>
|
|
||||||
{/* {this.state.loading && "Loading..."} */}
|
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
<Row>
|
<Row>
|
||||||
|
@ -426,38 +440,52 @@ class ExpressionInput extends Component {
|
||||||
|
|
||||||
stateReducer = (state, changes) => {
|
stateReducer = (state, changes) => {
|
||||||
return changes;
|
return changes;
|
||||||
// TODO: Remove this whole function if I don't notice any odd behavior without it.
|
// // TODO: Remove this whole function if I don't notice any odd behavior without it.
|
||||||
// I don't remember why I had to add this and currently things seem fine without it.
|
// // I don't remember why I had to add this and currently things seem fine without it.
|
||||||
switch (changes.type) {
|
// switch (changes.type) {
|
||||||
case Downshift.stateChangeTypes.keyDownEnter:
|
// case Downshift.stateChangeTypes.keyDownEnter:
|
||||||
case Downshift.stateChangeTypes.clickItem:
|
// case Downshift.stateChangeTypes.clickItem:
|
||||||
case Downshift.stateChangeTypes.changeInput:
|
// case Downshift.stateChangeTypes.changeInput:
|
||||||
return {
|
// return {
|
||||||
...changes,
|
// ...changes,
|
||||||
selectedItem: changes.inputValue,
|
// selectedItem: changes.inputValue,
|
||||||
};
|
// };
|
||||||
default:
|
// default:
|
||||||
return changes;
|
// return changes;
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
renderAutosuggest = (downshift) => {
|
renderAutosuggest = (downshift) => {
|
||||||
let matches = this.props.metrics.filter(item => !downshift.inputValue || item.includes(downshift.inputValue));
|
if (this.prevNoMatchValue && downshift.inputValue.includes(this.prevNoMatchValue)) {
|
||||||
if (matches.length === 0 || !downshift.isOpen) {
|
// TODO: Is this still correct with fuzzy?
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let matches = fuzzy.filter(downshift.inputValue.replace(/ /g, ''), this.props.metrics, {
|
||||||
|
pre: "<strong>",
|
||||||
|
post: "</strong>",
|
||||||
|
});
|
||||||
|
|
||||||
|
if (matches.length === 0) {
|
||||||
|
this.prevNoMatchValue = downshift.inputValue;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!downshift.isOpen) {
|
||||||
|
return null; // TODO CHECK NEED FOR THIS
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ul className="autosuggest-dropdown" {...downshift.getMenuProps()}>
|
<ul className="autosuggest-dropdown" {...downshift.getMenuProps()}>
|
||||||
{
|
{
|
||||||
matches
|
matches
|
||||||
.slice(0, 100) // Limit DOM rendering to 100 results, as DOM rendering is sloooow.
|
.slice(0, 200) // Limit DOM rendering to 100 results, as DOM rendering is sloooow.
|
||||||
.map((item, index) => (
|
.map((item, index) => (
|
||||||
<li
|
<li
|
||||||
{...downshift.getItemProps({
|
{...downshift.getItemProps({
|
||||||
key: item,
|
key: item.original,
|
||||||
index,
|
index,
|
||||||
item,
|
item: item.original,
|
||||||
style: {
|
style: {
|
||||||
backgroundColor:
|
backgroundColor:
|
||||||
downshift.highlightedIndex === index ? 'lightgray' : 'white',
|
downshift.highlightedIndex === index ? 'lightgray' : 'white',
|
||||||
|
@ -465,7 +493,9 @@ class ExpressionInput extends Component {
|
||||||
},
|
},
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{item}
|
{/* TODO: Find better way than setting inner HTML dangerously. We just want the <strong> to not be escaped.
|
||||||
|
This will be a problem when we save history and the user enters HTML into a query. q*/}
|
||||||
|
<span dangerouslySetInnerHTML={{__html: item.string}}></span>
|
||||||
</li>
|
</li>
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
@ -536,20 +566,28 @@ class ExpressionInput extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
function TabPaneAlert(props) {
|
function TabPaneAlert(props) {
|
||||||
const { color, message } = props;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* Without the following <div> hack, giving the <Alert> any top margin
|
{/* Without the following <div> hack, giving the <Alert> any top margin
|
||||||
will make the entire tab pane look detached. */}
|
makes the entire tab pane look detached by that margin. */}
|
||||||
<div style={{height: '1px'}}></div>
|
<div style={{height: '1px'}}></div>
|
||||||
<Alert className="tabpane-alert" color={color}>{props.children}</Alert>
|
<Alert className="tabpane-alert" color={props.color}>{props.children}</Alert>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function DataTable(props) {
|
class DataTable extends PureComponent {
|
||||||
const data = props.data;
|
limitSeries(series) {
|
||||||
|
const maxSeries = 10000;
|
||||||
|
|
||||||
|
if (series.length > maxSeries) {
|
||||||
|
return series.slice(0, maxSeries);
|
||||||
|
}
|
||||||
|
return series;
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const data = this.props.data;
|
||||||
|
|
||||||
if (data === null) {
|
if (data === null) {
|
||||||
return <TabPaneAlert color="light">No data queried yet</TabPaneAlert>;
|
return <TabPaneAlert color="light">No data queried yet</TabPaneAlert>;
|
||||||
|
@ -560,37 +598,48 @@ function DataTable(props) {
|
||||||
}
|
}
|
||||||
|
|
||||||
let rows = [];
|
let rows = [];
|
||||||
if (props.data) {
|
let limitedSeries = this.limitSeries(data.result);
|
||||||
|
if (data) {
|
||||||
switch(data.resultType) {
|
switch(data.resultType) {
|
||||||
case 'vector':
|
case 'vector':
|
||||||
rows = props.data.result.map((s, index) => {
|
rows = limitedSeries.map((s, index) => {
|
||||||
return <tr key={index}><td>{metricToSeriesName(s.metric)}</td><td>{s.value[1]}</td></tr>
|
return <tr key={index}><td>{metricToSeriesName(s.metric)}</td><td>{s.value[1]}</td></tr>
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case 'matrix':
|
case 'matrix':
|
||||||
rows = props.data.result.map((s, index) => {
|
rows = limitedSeries.map((s, index) => {
|
||||||
const valueText = s.values.map((v) => {
|
const valueText = s.values.map((v) => {
|
||||||
return [1] + ' @' + v[0];
|
return [1] + ' @' + v[0];
|
||||||
}).join('\n');
|
}).join('\n');
|
||||||
return <tr style={{'white-space': 'pre'}} key={index}><td>{metricToSeriesName(s.metric)}</td><td>{valueText}</td></tr>
|
return <tr style={{whiteSpace: 'pre'}} key={index}><td>{metricToSeriesName(s.metric)}</td><td>{valueText}</td></tr>
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case 'scalar':
|
case 'scalar':
|
||||||
rows.push(<tr><td>scalar</td><td>{data.result[1]}</td></tr>);
|
rows.push(<tr><td>scalar</td><td>{data.result[1]}</td></tr>);
|
||||||
|
break;
|
||||||
case 'string':
|
case 'string':
|
||||||
rows.push(<tr><td>scalar</td><td>{data.result[1]}</td></tr>);
|
rows.push(<tr><td>scalar</td><td>{data.result[1]}</td></tr>);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
return <TabPaneAlert color="danger">Unsupported result value type '{data.resultType}'</TabPaneAlert>;
|
return <TabPaneAlert color="danger">Unsupported result value type '{data.resultType}'</TabPaneAlert>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
|
{data.result.length !== limitedSeries.length &&
|
||||||
|
<TabPaneAlert color="danger">
|
||||||
|
<strong>Warning:</strong> Fetched {data.result.length} metrics, only displaying first {limitedSeries.length}.
|
||||||
|
</TabPaneAlert>
|
||||||
|
}
|
||||||
<Table hover size="sm" className="data-table">
|
<Table hover size="sm" className="data-table">
|
||||||
<tbody>
|
<tbody>
|
||||||
{rows}
|
{rows}
|
||||||
</tbody>
|
</tbody>
|
||||||
</Table>
|
</Table>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
class GraphControls extends Component {
|
class GraphControls extends Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
|
@ -602,6 +651,7 @@ class GraphControls extends Component {
|
||||||
|
|
||||||
this.rangeRef = React.createRef();
|
this.rangeRef = React.createRef();
|
||||||
this.endTimeRef = React.createRef();
|
this.endTimeRef = React.createRef();
|
||||||
|
this.resolutionRef = React.createRef();
|
||||||
}
|
}
|
||||||
|
|
||||||
rangeUnits = {
|
rangeUnits = {
|
||||||
|
@ -638,7 +688,7 @@ class GraphControls extends Component {
|
||||||
return range + 's';
|
return range + 's';
|
||||||
}
|
}
|
||||||
|
|
||||||
onRangeInputChanged = (rangeText) => {
|
onChangeRangeInput = (rangeText) => {
|
||||||
const range = this.parseRange(rangeText);
|
const range = this.parseRange(rangeText);
|
||||||
if (range === null) {
|
if (range === null) {
|
||||||
this.changeRangeInput(this.formatRange(this.props.range));
|
this.changeRangeInput(this.formatRange(this.props.range));
|
||||||
|
@ -689,7 +739,6 @@ class GraphControls extends Component {
|
||||||
this.$endTime.datetimepicker('date', endTime);
|
this.$endTime.datetimepicker('date', endTime);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Handle manual textual changes to datetime input.
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.$endTime = window.$(this.endTimeRef.current);
|
this.$endTime = window.$(this.endTimeRef.current);
|
||||||
|
|
||||||
|
@ -707,10 +756,15 @@ class GraphControls extends Component {
|
||||||
format: 'YYYY-MM-DD HH:mm:ss',
|
format: 'YYYY-MM-DD HH:mm:ss',
|
||||||
locale: 'en',
|
locale: 'en',
|
||||||
timeZone: 'UTC',
|
timeZone: 'UTC',
|
||||||
|
defaultDate: this.props.endTime,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.$endTime.on('change.datetimepicker', e => {
|
this.$endTime.on('change.datetimepicker', e => {
|
||||||
|
if (e.date) {
|
||||||
this.props.onChangeEndTime(e.date);
|
this.props.onChangeEndTime(e.date);
|
||||||
|
} else {
|
||||||
|
this.$endTime.datetimepicker('date', e.target.value);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -723,7 +777,11 @@ class GraphControls extends Component {
|
||||||
</InputGroupAddon>
|
</InputGroupAddon>
|
||||||
|
|
||||||
{/* <Input value={this.state.rangeInput} onChange={(e) => this.changeRangeInput(e.target.value)}/> */}
|
{/* <Input value={this.state.rangeInput} onChange={(e) => this.changeRangeInput(e.target.value)}/> */}
|
||||||
<Input defaultValue={this.formatRange(this.props.range)} innerRef={this.rangeRef} onBlur={() => this.onRangeInputChanged(this.rangeRef.current.value)}/>
|
<Input
|
||||||
|
defaultValue={this.formatRange(this.props.range)}
|
||||||
|
innerRef={this.rangeRef}
|
||||||
|
onBlur={() => this.onChangeRangeInput(this.rangeRef.current.value)}
|
||||||
|
/>
|
||||||
|
|
||||||
<InputGroupAddon addonType="append">
|
<InputGroupAddon addonType="append">
|
||||||
<Button title="Increase range" onClick={this.increaseRange}><FontAwesomeIcon icon="plus" fixedWidth/></Button>
|
<Button title="Increase range" onClick={this.increaseRange}><FontAwesomeIcon icon="plus" fixedWidth/></Button>
|
||||||
|
@ -752,7 +810,14 @@ class GraphControls extends Component {
|
||||||
</InputGroup>
|
</InputGroup>
|
||||||
|
|
||||||
{/* TODO: validate resolution and only update when valid */}
|
{/* TODO: validate resolution and only update when valid */}
|
||||||
<Input className="resolution-input" value={this.props.resolution ? this.props.resolution : ''} onChange={(e) => this.props.onChangeResolution(e.target.value)} placeholder="Res. (s)" bsSize="sm"/>
|
<Input
|
||||||
|
placeholder="Res. (s)"
|
||||||
|
className="resolution-input"
|
||||||
|
defaultValue={this.props.resolution !== null ? this.props.resolution : ''}
|
||||||
|
innerRef={this.resolutionRef}
|
||||||
|
onBlur={() => this.props.onChangeResolution(parseInt(this.resolutionRef.current.value))}
|
||||||
|
bsSize="sm"
|
||||||
|
/>
|
||||||
|
|
||||||
<ButtonGroup className="stacked-input" size="sm">
|
<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 unstacked line graph" onClick={() => this.props.onChangeStacking(false)} active={!this.props.stacked}><FontAwesomeIcon icon="chart-line" fixedWidth/></Button>
|
||||||
|
@ -769,12 +834,13 @@ function getGraphID() {
|
||||||
return graphID++;
|
return graphID++;
|
||||||
}
|
}
|
||||||
|
|
||||||
class Graph extends Component {
|
class Graph extends PureComponent {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = {
|
this.state = {
|
||||||
legendRef: null,
|
legendRef: null,
|
||||||
};
|
};
|
||||||
|
this.id = getGraphID();
|
||||||
}
|
}
|
||||||
|
|
||||||
escapeHTML(string) {
|
escapeHTML(string) {
|
||||||
|
@ -866,7 +932,6 @@ class Graph extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
getOptions() {
|
getOptions() {
|
||||||
console.log(this.props);
|
|
||||||
return {
|
return {
|
||||||
// colors: [
|
// colors: [
|
||||||
// '#7EB26D', // 0: pale green
|
// '#7EB26D', // 0: pale green
|
||||||
|
@ -976,11 +1041,6 @@ class Graph extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
getData() {
|
getData() {
|
||||||
if (this.props.data.resultType !== 'matrix') {
|
|
||||||
// TODO self.showError("Result is not of matrix type! Please enter a correct expression.");
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.props.data.result.map(ts => {
|
return this.props.data.result.map(ts => {
|
||||||
// Insert nulls for all missing steps.
|
// Insert nulls for all missing steps.
|
||||||
let data = [];
|
let data = [];
|
||||||
|
@ -1031,7 +1091,7 @@ class Graph extends Component {
|
||||||
<div className="graph">
|
<div className="graph">
|
||||||
{this.state.legendRef &&
|
{this.state.legendRef &&
|
||||||
<ReactFlot
|
<ReactFlot
|
||||||
id={getGraphID().toString()}
|
id={this.id.toString()}
|
||||||
data={this.getData()}
|
data={this.getData()}
|
||||||
options={this.getOptions()}
|
options={this.getOptions()}
|
||||||
height="500px"
|
height="500px"
|
||||||
|
|
Loading…
Reference in a new issue