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 (