Autosuggest and graph improvements

Signed-off-by: Julius Volz <julius.volz@gmail.com>
This commit is contained in:
Julius Volz 2019-02-10 00:09:34 +01:00
parent 9a910701fa
commit 00d3821218
4 changed files with 363 additions and 62 deletions

171
package-lock.json generated
View file

@ -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",

View file

@ -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",

View file

@ -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;
}

View file

@ -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 {
<>
<Row>
<Col>
<ExpressionInput value={this.state.expr} onChange={this.handleExpressionChange} execute={this.execute}/>
<ExpressionInput
value={this.state.expr}
onChange={this.handleExpressionChange}
execute={this.execute}
metrics={this.props.metrics}
/>
{/*<Input type="select" name="selectMetric">
{this.props.metrics.map(m => <option key={m}>{m}</option>)}
</Input>*/}
@ -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 (
<InputGroup className="expression-input">
<Input
autoFocus
type="textarea"
rows={this.numRows()}
value={this.props.value}
onChange={this.props.onChange}
onKeyPress={this.handleKeyPress}
placeholder="Expression (press Shift+Enter for newlines)" />
<InputGroupAddon addonType="append">
<Button color="primary" onClick={this.props.execute}>Execute</Button>
</InputGroupAddon>
</InputGroup>
<Downshift
inputValue={this.props.value}
onInputValueChange={this.props.onChange}
selectedItem={this.props.value}
>
{downshift => (
<div>
<InputGroup className="expression-input">
<Input
autoFocus
type="textarea"
rows={this.numRows()}
onKeyPress={this.handleKeyPress}
placeholder="Expression (press Shift+Enter for newlines)"
//onChange={selection => 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:
}
}
})}
/>
<InputGroupAddon addonType="append">
<Button color="primary" onClick={this.props.execute}>Execute</Button>
</InputGroupAddon>
</InputGroup>
{downshift.isOpen &&
<ul className="autosuggest-dropdown" {...downshift.getMenuProps()}>
{
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) => (
<li
{...downshift.getItemProps({
key: item,
index,
item,
style: {
backgroundColor:
downshift.highlightedIndex === index ? 'lightgray' : 'white',
fontWeight: downshift.selectedItem === item ? 'bold' : 'normal',
},
})}
>
{item}
</li>
))
}
</ul>
}
</div>
)}
</Downshift>
// <Input
// autoFocus
// type="textarea"
// rows={this.numRows()}
// value={this.props.value}
// onChange={this.props.onChange}
// onKeyPress={this.handleKeyPress}
// placeholder="Expression (press Shift+Enter for newlines)" />
);
}
}
@ -347,23 +429,74 @@ function getGraphID() {
}
class Graph extends Component {
componentDidMount() {
this.chart = null;
escapeHTML(string) {
var entityMap = {
"&": "&amp;",
"<": "&lt;",
">": "&gt;",
'"': '&quot;',
"'": '&#39;',
"/": '&#x2F;'
};
return String(string).replace(/[&<>"'/]/g, function (s) {
return entityMap[s];
});
}
renderLabels(labels) {
let labelStrings = [];
for (let label in labels) {
if (label !== '__name__') {
labelStrings.push('<strong>' + label + '</strong>: ' + this.escapeHTML(labels[label]));
}
}
return labels = '<div class="labels">' + labelStrings.join('<br>') + '</div>';
};
getOptions() {
return {
grid: {
hoverable: true,
clickable: true,
autoHighlight: true,
mouseActiveRadius: 100,
},
legend: {
container: this.legend,
labelFormatter: (s) => {return '&nbsp;&nbsp;' + 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 = '<span class="date">' + new Date(xval).toUTCString() + '</span>';
var swatch = '<span class="detail-swatch" style="background-color: ' + series.color + '"></span>';
var content = swatch + (series.labels.__name__ || 'value') + ": <strong>" + yval + '</strong>';
return date + '<br>' + content + '<br>' + 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 (
<div>
<div className="graph">
<ReactFlot
id={getGraphID().toString()}
data={this.getData()}
options={this.getOptions()}
width="1900px"
height="500px"
width="100%"
/>
<div ref={ref => { this.legend = ref; }}></div>
<div className="graph-legend" ref={ref => { this.legend = ref; }}>df</div>
</div>
);
}
@ -415,10 +549,10 @@ function metricToSeriesName(labels) {
var labelStrings = [];
for (var label in labels) {
if (label !== "__name__") {
labelStrings.push(label + "=\"" + labels[label] + "\"");
labelStrings.push('<b>' + label + "</b>=\"" + labels[label] + "\"");
}
}
tsName += labelStrings.join(",") + "}";
tsName += labelStrings.join(", ") + "}";
return tsName;
};