From 00d3821218e8dcd955de6a9d7862596c4cfb7575 Mon Sep 17 00:00:00 2001 From: Julius Volz Date: Sun, 10 Feb 2019 00:09:34 +0100 Subject: [PATCH] Autosuggest and graph improvements Signed-off-by: Julius Volz --- package-lock.json | 171 ++++++++++++++++++++++++++++++++-------- package.json | 3 +- src/App.css | 57 ++++++++++++++ src/App.js | 194 +++++++++++++++++++++++++++++++++++++++------- 4 files changed, 363 insertions(+), 62 deletions(-) diff --git a/package-lock.json b/package-lock.json index d9607f8a23..9a832ed0e7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3077,6 +3077,11 @@ } } }, + "compute-scroll-into-view": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-1.0.11.tgz", + "integrity": "sha512-uUnglJowSe0IPmWOdDtrlHXof5CTIJitfJEyITHBW6zDVOGu9Pjk5puaLM73SLcwak0L4hEjO7Td88/a6P5i7A==" + }, "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -4065,6 +4070,17 @@ "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-4.2.0.tgz", "integrity": "sha1-3vHxyl1gWdJKdm5YeULCEQbOEnU=" }, + "downshift": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/downshift/-/downshift-3.2.2.tgz", + "integrity": "sha512-z4rkPnfC/ax1LnZkipQOlEu8WSDKduPudRjurlU85Qxy39eeY1ArAi0xU24qXcxd27bMS81mBMgkH7X/pEH2GA==", + "requires": { + "@babel/runtime": "^7.1.2", + "compute-scroll-into-view": "^1.0.9", + "prop-types": "^15.6.0", + "react-is": "^16.5.2" + } + }, "duplexer": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.1.tgz", @@ -6304,6 +6320,46 @@ "requires": { "h2x-types": "^1.1.0", "jsdom": ">=11.0.0" + }, + "dependencies": { + "jsdom": { + "version": "13.2.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-13.2.0.tgz", + "integrity": "sha512-cG1NtMWO9hWpqRNRR3dSvEQa8bFI6iLlqU2x4kwX51FQjp0qus8T9aBaAO6iGp3DeBrhdwuKxckknohkmfvsFw==", + "requires": { + "abab": "^2.0.0", + "acorn": "^6.0.4", + "acorn-globals": "^4.3.0", + "array-equal": "^1.0.0", + "cssom": "^0.3.4", + "cssstyle": "^1.1.1", + "data-urls": "^1.1.0", + "domexception": "^1.0.1", + "escodegen": "^1.11.0", + "html-encoding-sniffer": "^1.0.2", + "nwsapi": "^2.0.9", + "parse5": "5.1.0", + "pn": "^1.1.0", + "request": "^2.88.0", + "request-promise-native": "^1.0.5", + "saxes": "^3.1.5", + "symbol-tree": "^3.2.2", + "tough-cookie": "^2.5.0", + "w3c-hr-time": "^1.0.1", + "w3c-xmlserializer": "^1.0.1", + "webidl-conversions": "^4.0.2", + "whatwg-encoding": "^1.0.5", + "whatwg-mimetype": "^2.3.0", + "whatwg-url": "^7.0.0", + "ws": "^6.1.2", + "xml-name-validator": "^3.0.0" + } + }, + "parse5": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-5.1.0.tgz", + "integrity": "sha512-fxNG2sQjHvlVAYmzBZS9YlDp6PTSSDwa98vkD4QgVDDCAo84z5X1t5XyJQ62ImdLXx5NdIIfihey6xpum9/gRQ==" + } } }, "h2x-plugin-jsx": { @@ -8401,36 +8457,79 @@ "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=" }, "jsdom": { - "version": "13.2.0", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-13.2.0.tgz", - "integrity": "sha512-cG1NtMWO9hWpqRNRR3dSvEQa8bFI6iLlqU2x4kwX51FQjp0qus8T9aBaAO6iGp3DeBrhdwuKxckknohkmfvsFw==", + "version": "9.6.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-9.6.0.tgz", + "integrity": "sha1-4OmxW6B+kLHZ7Ag/m+3uD2gApPs=", "requires": { - "abab": "^2.0.0", - "acorn": "^6.0.4", - "acorn-globals": "^4.3.0", + "abab": "^1.0.0", + "acorn": "^2.4.0", + "acorn-globals": "^1.0.4", "array-equal": "^1.0.0", - "cssom": "^0.3.4", - "cssstyle": "^1.1.1", - "data-urls": "^1.1.0", - "domexception": "^1.0.1", - "escodegen": "^1.11.0", - "html-encoding-sniffer": "^1.0.2", - "nwsapi": "^2.0.9", - "parse5": "5.1.0", - "pn": "^1.1.0", - "request": "^2.88.0", - "request-promise-native": "^1.0.5", - "saxes": "^3.1.5", - "symbol-tree": "^3.2.2", - "tough-cookie": "^2.5.0", - "w3c-hr-time": "^1.0.1", - "w3c-xmlserializer": "^1.0.1", - "webidl-conversions": "^4.0.2", - "whatwg-encoding": "^1.0.5", - "whatwg-mimetype": "^2.3.0", - "whatwg-url": "^7.0.0", - "ws": "^6.1.2", - "xml-name-validator": "^3.0.0" + "cssom": ">= 0.3.0 < 0.4.0", + "cssstyle": ">= 0.2.36 < 0.3.0", + "escodegen": "^1.6.1", + "iconv-lite": "^0.4.13", + "nwmatcher": ">= 1.3.7 < 2.0.0", + "parse5": "^1.5.1", + "request": "^2.55.0", + "sax": "^1.1.4", + "symbol-tree": ">= 3.1.0 < 4.0.0", + "tough-cookie": "^2.3.1", + "webidl-conversions": "^3.0.1", + "whatwg-url": "^3.0.0", + "xml-name-validator": ">= 2.0.1 < 3.0.0" + }, + "dependencies": { + "abab": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/abab/-/abab-1.0.4.tgz", + "integrity": "sha1-X6rZwsB/YN12dw9xzwJbYqY8/U4=" + }, + "acorn": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-2.7.0.tgz", + "integrity": "sha1-q259nYhqrKiwhbwzEreaGYQz8Oc=" + }, + "acorn-globals": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-1.0.9.tgz", + "integrity": "sha1-VbtemGkVB7dFedBRNBMhfDgMVM8=", + "requires": { + "acorn": "^2.1.0" + } + }, + "cssstyle": { + "version": "0.2.37", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-0.2.37.tgz", + "integrity": "sha1-VBCXI0yyUTyDzu06zdwn/yeYfVQ=", + "requires": { + "cssom": "0.3.x" + } + }, + "tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=" + }, + "webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=" + }, + "whatwg-url": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-3.1.0.tgz", + "integrity": "sha1-e9yuSQ+SGu9kUftnOexrvY6Qe/Y=", + "requires": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "xml-name-validator": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-2.0.1.tgz", + "integrity": "sha1-TYuPHszTQZqjYgYb7O9RXh5VljU=" + } } }, "jsesc": { @@ -9286,6 +9385,11 @@ "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=" }, + "nwmatcher": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/nwmatcher/-/nwmatcher-1.4.4.tgz", + "integrity": "sha512-3iuY4N5dhgMpCUrOVnuAdGrgxVqV2cJpM+XNccjR2DKOB1RUP0aA+wGXEiNziG/UKboFyGBIoKOaNlJxx8bciQ==" + }, "nwsapi": { "version": "2.0.9", "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.0.9.tgz", @@ -9608,9 +9712,9 @@ "integrity": "sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY=" }, "parse5": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-5.1.0.tgz", - "integrity": "sha512-fxNG2sQjHvlVAYmzBZS9YlDp6PTSSDwa98vkD4QgVDDCAo84z5X1t5XyJQ62ImdLXx5NdIIfihey6xpum9/gRQ==" + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-1.5.1.tgz", + "integrity": "sha1-m387DeMr543CQBsXVzzK8Pb1nZQ=" }, "parseurl": { "version": "1.3.2", @@ -13678,6 +13782,11 @@ "jquery": "^3.1.1" } }, + "react-is": { + "version": "16.8.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.8.1.tgz", + "integrity": "sha512-ioMCzVDWvCvKD8eeT+iukyWrBGrA3DiFYkXfBsVYIRdaREZuBjENG+KjrikavCLasozqRWTwFUagU/O4vPpRMA==" + }, "react-lifecycles-compat": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", diff --git a/package.json b/package.json index 1c1fa7bdf7..4d1d78c504 100644 --- a/package.json +++ b/package.json @@ -4,8 +4,9 @@ "private": true, "dependencies": { "bootstrap": "^4.2.1", + "downshift": "^3.2.2", "jquery": "^3.3.1", - "jsdom": "^13.2.0", + "jsdom": "^9.6.0", "react": "^16.7.0", "react-dom": "^16.7.0", "react-flot": "^1.3.0", diff --git a/src/App.css b/src/App.css index 6fa5d36c2f..9cf3542e67 100644 --- a/src/App.css +++ b/src/App.css @@ -28,3 +28,60 @@ body { .data-table > tbody > tr > td { padding: 6px 16px 6px 16px; } + +.autosuggest-dropdown { + position: absolute; + border: 1px solid #ced4da; + border-radius: .25rem; + background-color: #fff; + color: #495057; + font-size: 1rem; + z-index: 1000; + min-width: 10rem; + top: 100%; + float: left; + padding: .5rem 1px .5rem 1px; + margin: .125rem 0 0 0; + top: 40px; + list-style: none; +} + +.autosuggest-dropdown li { + width: 100%; + padding: .25rem 1.5rem; + clear: both; + white-space: nowrap; + background-color: transparent; + border: 0; + display: block; +} + +.graph-legend { + margin: 15px 0 15px 25px; +} + +.graph .flot-overlay { + cursor: crosshair; +} + +.graph-tooltip { + background: rgba(0,0,0,.8); + color: #fff; + font-family: Arial, Helvetica, sans-serif; + font-size: 12px; + white-space: nowrap; + padding: 8px; + border-radius: 3px; +} + +.graph-tooltip .labels { + font-size: 11px; + line-height: 11px; + } + +.graph-tooltip .detail-swatch { + display: inline-block; + width: 10px; + height: 10px; + margin: 0 5px 0 0; +} diff --git a/src/App.js b/src/App.js index 98927cdb06..c3d5978aaf 100755 --- a/src/App.js +++ b/src/App.js @@ -17,7 +17,10 @@ import { } from 'reactstrap'; import ReactFlot from 'react-flot'; import '../node_modules/react-flot/flot/jquery.flot.time.min'; +import '../node_modules/react-flot/flot/jquery.flot.crosshair.min'; +import '../node_modules/react-flot/flot/jquery.flot.tooltip.min'; import './App.css'; +import Downshift from 'downshift'; class App extends Component { render() { @@ -55,9 +58,7 @@ class PanelList extends Component { } }) .then(json => - this.setState({ - metrics: json.data, - }) + this.setState({ metrics: json.data }) ) .catch(error => { this.setState({error}) @@ -100,7 +101,7 @@ class Panel extends Component { this.state = { expr: 'rate(node_cpu_seconds_total[1m])', - type: 'table', // TODO enum? + type: 'graph', // TODO enum? range: 3600, endTime: null, step: null, @@ -161,7 +162,6 @@ class Panel extends Component { if (resp.ok) { return resp.json(); } else { - console.log(resp); throw new Error('Unexpected response status: ' + resp.statusText); } }) @@ -180,8 +180,9 @@ class Panel extends Component { }); } - handleExpressionChange(event) { - this.setState({expr: event.target.value}); + handleExpressionChange(expr) { + //this.setState({expr: event.target.value}); + this.setState({expr: expr}); } render() { @@ -189,7 +190,12 @@ class Panel extends Component { <> - + {/* {this.props.metrics.map(m => )} */} @@ -275,21 +281,97 @@ class ExpressionInput extends Component { return this.props.value.split(/\r\n|\r|\n/).length; } + stateReducer = (state, changes) => { + switch (changes.type) { + case Downshift.stateChangeTypes.keyDownEnter: + case Downshift.stateChangeTypes.clickItem: + case Downshift.stateChangeTypes.changeInput: + return { + ...changes, + selectedItem: changes.inputValue, + }; + default: + return changes; + } + } + render() { return ( - - - - - - + + {downshift => ( +
+ + + alert(`You selected ${selection}`)} + {...downshift.getInputProps({ + onKeyDown: event => { + switch (event.key) { + case 'Home': + case 'End': + // We want to be able to jump to the beginning/end of the input field. + // By default, Downshift otherwise jumps to the first/last suggestion item instead. + event.nativeEvent.preventDownshiftDefault = true; + break; + case 'Enter': + downshift.closeMenu(); + break; + default: + } + } + })} + /> + + + + + {downshift.isOpen && +
    + { + this.props.metrics + .filter(item => !downshift.inputValue || item.includes(downshift.inputValue)) + .slice(0, 100) // Limit DOM rendering to 100 results, as DOM rendering is sloooow. + .map((item, index) => ( +
  • + {item} +
  • + )) + } +
+ } +
+ )} +
+ + // + ); } } @@ -347,23 +429,74 @@ function getGraphID() { } class Graph extends Component { - componentDidMount() { - this.chart = null; + escapeHTML(string) { + var entityMap = { + "&": "&", + "<": "<", + ">": ">", + '"': '"', + "'": ''', + "/": '/' + }; + + return String(string).replace(/[&<>"'/]/g, function (s) { + return entityMap[s]; + }); } + renderLabels(labels) { + let labelStrings = []; + for (let label in labels) { + if (label !== '__name__') { + labelStrings.push('' + label + ': ' + this.escapeHTML(labels[label])); + } + } + return labels = '
' + labelStrings.join('
') + '
'; + }; + getOptions() { return { grid: { hoverable: true, clickable: true, + autoHighlight: true, + mouseActiveRadius: 100, }, legend: { container: this.legend, + labelFormatter: (s) => {return '  ' + s} }, xaxis: { - mode: "time", - //timeformat: "%Y/%m/%d", + mode: 'time', + showTicks: true, + showMinorTicks: true, + // min: (new Date()).getTime(), + // max: (new Date(2000, 1, 1)).getTime(), }, + crosshair: { + mode: 'xy', + color: '#bbb', + }, + tooltip: { + show: true, + cssClass: 'graph-tooltip', + content: (label, xval, yval, flotItem) => { + const series = flotItem.series; + var date = '' + new Date(xval).toUTCString() + ''; + var swatch = ''; + var content = swatch + (series.labels.__name__ || 'value') + ": " + yval + ''; + return date + '
' + content + '
' + this.renderLabels(series.labels); + }, + defaultTheme: false, + lines: true, + }, + series: { + lines: { + lineWidth: 2, + steps: false, + }, + shadowSize: 0, + } }; } @@ -376,6 +509,7 @@ class Graph extends Component { return this.props.data.result.map(ts => { return { label: metricToSeriesName(ts.metric), + labels: ts.metric, data: ts.values.map(v => [v[0] * 1000, this.parseValue(v[1])]), }; }) @@ -396,15 +530,15 @@ class Graph extends Component { return null; } return ( -
+
-
{ this.legend = ref; }}>
+
{ this.legend = ref; }}>df
); } @@ -415,10 +549,10 @@ function metricToSeriesName(labels) { var labelStrings = []; for (var label in labels) { if (label !== "__name__") { - labelStrings.push(label + "=\"" + labels[label] + "\""); + labelStrings.push('' + label + "=\"" + labels[label] + "\""); } } - tsName += labelStrings.join(",") + "}"; + tsName += labelStrings.join(", ") + "}"; return tsName; };