From 1a3df0b78c88adbdea94fc9474479f1c2da2a6cc Mon Sep 17 00:00:00 2001 From: Julius Volz Date: Tue, 12 Feb 2019 20:59:27 +0100 Subject: [PATCH] Fixed and built more features Signed-off-by: Julius Volz --- package-lock.json | 5 + package.json | 2 + src/App.css | 8 ++ src/App.js | 252 ++++++++++++++++++++++++++++------------------ 4 files changed, 171 insertions(+), 96 deletions(-) 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 {metricToSeriesName(s.metric)}{s.value[1]} - }); - break; - case 'matrix': - rows = props.data.result.map((s, index) => { - const valueText = s.values.map((v) => { - return [1] + ' @' + v[0]; - }).join('\n'); - return {metricToSeriesName(s.metric)}{valueText} - }); - break; - case 'scalar': - rows.push(scalar{data.result[1]}); - case 'string': - rows.push(scalar{data.result[1]}); - default: - return Unsupported result value type '{data.resultType}'; + if (series.length > maxSeries) { + return series.slice(0, maxSeries); } + return series; } - return ( - - - {rows} - -
    - ); + render() { + const data = this.props.data; + + if (data === null) { + return No data queried yet; + } + + if (data.result === null || data.result.length === 0) { + return Empty query result; + } + + let rows = []; + let limitedSeries = this.limitSeries(data.result); + if (data) { + switch(data.resultType) { + case 'vector': + rows = limitedSeries.map((s, index) => { + return {metricToSeriesName(s.metric)}{s.value[1]} + }); + break; + case 'matrix': + rows = limitedSeries.map((s, index) => { + const valueText = s.values.map((v) => { + return [1] + ' @' + v[0]; + }).join('\n'); + return {metricToSeriesName(s.metric)}{valueText} + }); + break; + case 'scalar': + rows.push(scalar{data.result[1]}); + break; + case 'string': + rows.push(scalar{data.result[1]}); + break; + default: + return Unsupported result value type '{data.resultType}'; + } + } + + return ( + <> + {data.result.length !== limitedSeries.length && + + Warning: Fetched {data.result.length} metrics, only displaying first {limitedSeries.length}. + + } + + + {rows} + +
    + + ); + } } class GraphControls extends Component { constructor(props) { @@ -602,6 +651,7 @@ class GraphControls extends Component { this.rangeRef = React.createRef(); this.endTimeRef = React.createRef(); + this.resolutionRef = React.createRef(); } rangeUnits = { @@ -638,7 +688,7 @@ class GraphControls extends Component { return range + 's'; } - onRangeInputChanged = (rangeText) => { + onChangeRangeInput = (rangeText) => { const range = this.parseRange(rangeText); if (range === null) { this.changeRangeInput(this.formatRange(this.props.range)); @@ -689,7 +739,6 @@ class GraphControls extends Component { this.$endTime.datetimepicker('date', endTime); } - // TODO: Handle manual textual changes to datetime input. componentDidMount() { this.$endTime = window.$(this.endTimeRef.current); @@ -707,10 +756,15 @@ class GraphControls extends Component { format: 'YYYY-MM-DD HH:mm:ss', locale: 'en', timeZone: 'UTC', + defaultDate: this.props.endTime, }); this.$endTime.on('change.datetimepicker', e => { - this.props.onChangeEndTime(e.date); + if (e.date) { + this.props.onChangeEndTime(e.date); + } else { + this.$endTime.datetimepicker('date', e.target.value); + } }); } @@ -723,7 +777,11 @@ class GraphControls extends Component { {/* this.changeRangeInput(e.target.value)}/> */} - this.onRangeInputChanged(this.rangeRef.current.value)}/> + this.onChangeRangeInput(this.rangeRef.current.value)} + /> @@ -752,7 +810,14 @@ class GraphControls extends Component { {/* TODO: validate resolution and only update when valid */} - this.props.onChangeResolution(e.target.value)} placeholder="Res. (s)" bsSize="sm"/> + this.props.onChangeResolution(parseInt(this.resolutionRef.current.value))} + bsSize="sm" + /> @@ -769,12 +834,13 @@ function getGraphID() { return graphID++; } -class Graph extends Component { +class Graph extends PureComponent { constructor(props) { super(props); this.state = { legendRef: null, }; + this.id = getGraphID(); } escapeHTML(string) { @@ -866,7 +932,6 @@ class Graph extends Component { } getOptions() { - console.log(this.props); return { // colors: [ // '#7EB26D', // 0: pale green @@ -976,11 +1041,6 @@ class Graph extends Component { } 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 => { // Insert nulls for all missing steps. let data = []; @@ -1031,7 +1091,7 @@ class Graph extends Component {
    {this.state.legendRef &&