diff --git a/package-lock.json b/package-lock.json
index 80bfc3e456..1123925991 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -6180,6 +6180,11 @@
"resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz",
"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": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz",
diff --git a/package.json b/package.json
index 227219a16a..b4085f46d8 100644
--- a/package.json
+++ b/package.json
@@ -8,10 +8,12 @@
"@fortawesome/react-fontawesome": "^0.1.4",
"bootstrap": "^4.2.1",
"downshift": "^3.2.2",
+ "fuzzy": "^0.1.3",
"i": "^0.3.6",
"jquery": "^3.3.1",
"jsdom": "^9.6.0",
"moment": "^2.24.0",
+ "moment-timezone": "^0.5.23",
"npm": "^6.7.0",
"react": "^16.7.0",
"react-dom": "^16.7.0",
diff --git a/src/App.css b/src/App.css
index 38e4602c83..5a6601b306 100644
--- a/src/App.css
+++ b/src/App.css
@@ -70,10 +70,18 @@ button.execute-btn {
padding: 15px 0 10px 10px;
}
+.graph-controls input {
+ text-align: center;
+}
+
.graph-controls .range-input input {
width: 50px;
}
+.graph-controls .endtime-input input {
+ width: 160px;
+}
+
.graph-controls input.resolution-input {
width: 90px;
}
diff --git a/src/App.js b/src/App.js
index a6472d6089..a98034dbc2 100755
--- a/src/App.js
+++ b/src/App.js
@@ -1,4 +1,4 @@
-import React, { Component } from 'react';
+import React, { Component, PureComponent } from 'react';
import {
Alert,
Button,
@@ -28,7 +28,9 @@ import '../node_modules/react-flot/flot/jquery.flot.stack.min';
import './App.css';
import Downshift from 'downshift';
-import moment from 'moment';
+import moment from 'moment-timezone';
+
+import fuzzy from 'fuzzy';
import 'tempusdominus-core';
import 'tempusdominus-bootstrap-4';
@@ -100,7 +102,7 @@ class PanelList extends Component {
componentDidMount() {
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 => {
if (resp.ok) {
return resp.json();
@@ -112,7 +114,7 @@ class PanelList extends Component {
.catch(error => this.setState({fetchMetricsError: error.message}));
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 => {
if (resp.ok) {
return resp.json();
@@ -179,11 +181,10 @@ class Panel extends Component {
expr: 'rate(node_cpu_seconds_total[1m])',
type: 'graph', // TODO enum?
range: 3600,
- endTime: null,
+ endTime: null, // This is in milliseconds.
resolution: null,
stacked: false,
data: null,
- loading: false,
error: null,
stats: null,
};
@@ -196,6 +197,12 @@ class Panel extends Component {
return prevState[v] !== this.state[v];
})
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();
}
}
@@ -205,13 +212,19 @@ class Panel extends Component {
}
executeQuery = ()=> {
- // TODO: Abort existing queries.
- if (this.state.expr === "") {
- return;
+ if (this.abortInFlightFetch) {
+ this.abortInFlightFetch();
+ this.abortInFlightFetch = null;
}
+ const abortController = new AbortController();
+ this.abortInFlightFetch = () => abortController.abort();
this.setState({loading: true});
+ if (this.state.expr === '') {
+ return;
+ }
+
let endTime = this.getEndTime() / 1000;
let startTime = endTime - this.state.range;
let resolution = this.state.resolution || Math.max(Math.floor(this.state.range / 250), 1);
@@ -238,11 +251,11 @@ class Panel extends Component {
})
break;
default:
- // TODO
+ throw new Error('Invalid panel type "' + this.state.type + '"');
}
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(json => {
if (json.status !== 'success') {
@@ -258,9 +271,14 @@ class Panel extends Component {
resolution: resolution,
},
loading: false,
- })
+ });
+ this.abortInFlightFetch = null;
})
.catch(error => {
+ if (error.name === 'AbortError') {
+ // Aborts are expected, don't show an error for them.
+ return
+ }
this.setState({
error: 'Error executing query: ' + error.message,
loading: false,
@@ -289,7 +307,11 @@ class Panel extends Component {
}
handleChangeResolution = (resolution) => {
- this.setState({resolution: 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});
+ }
}
// getEndDate = () => {
@@ -344,14 +366,6 @@ class Panel extends Component {
loading={this.state.loading}
metrics={this.props.metrics}
/>
- {/*
- {this.props.metrics.map(m => )}
- */}
-
-
-
-
- {/* {this.state.loading && "Loading..."} */}
@@ -426,38 +440,52 @@ class ExpressionInput extends Component {
stateReducer = (state, changes) => {
return changes;
- // 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.
- switch (changes.type) {
- case Downshift.stateChangeTypes.keyDownEnter:
- case Downshift.stateChangeTypes.clickItem:
- case Downshift.stateChangeTypes.changeInput:
- return {
- ...changes,
- selectedItem: changes.inputValue,
- };
- default:
- return changes;
- }
+ // // 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.
+ // switch (changes.type) {
+ // case Downshift.stateChangeTypes.keyDownEnter:
+ // case Downshift.stateChangeTypes.clickItem:
+ // case Downshift.stateChangeTypes.changeInput:
+ // return {
+ // ...changes,
+ // selectedItem: changes.inputValue,
+ // };
+ // default:
+ // return changes;
+ // }
}
renderAutosuggest = (downshift) => {
- let matches = this.props.metrics.filter(item => !downshift.inputValue || item.includes(downshift.inputValue));
- if (matches.length === 0 || !downshift.isOpen) {
+ if (this.prevNoMatchValue && downshift.inputValue.includes(this.prevNoMatchValue)) {
+ // TODO: Is this still correct with fuzzy?
return null;
}
+ let matches = fuzzy.filter(downshift.inputValue.replace(/ /g, ''), this.props.metrics, {
+ pre: "",
+ post: "",
+ });
+
+ if (matches.length === 0) {
+ this.prevNoMatchValue = downshift.inputValue;
+ return null;
+ }
+
+ if (!downshift.isOpen) {
+ return null; // TODO CHECK NEED FOR THIS
+ }
+
return (
{
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) => (
- {item}
+ {/* TODO: Find better way than setting inner HTML dangerously. We just want the to not be escaped.
+ This will be a problem when we save history and the user enters HTML into a query. q*/}
+
))
}
@@ -536,61 +566,80 @@ class ExpressionInput extends Component {
}
function TabPaneAlert(props) {
- const { color, message } = props;
-
return (
<>
{/* Without the following
hack, giving the any top margin
- will make the entire tab pane look detached. */}
+ makes the entire tab pane look detached by that margin. */}
- {props.children}
+ {props.children}
>
);
}
-function DataTable(props) {
- const data = props.data;
+class DataTable extends PureComponent {
+ limitSeries(series) {
+ const maxSeries = 10000;
- if (data === null) {
- return No data queried yet;
- }
-
- if (data.result === null || data.result.length === 0) {
- return Empty query result;
- }
-
- let rows = [];
- if (props.data) {
- switch(data.resultType) {
- case 'vector':
- rows = props.data.result.map((s, index) => {
- return