app/vmui: small fixes

* Remove unneeded dependency on `numeral` package
* Properly parse numbers obtained from /api/v1/query_range according to
  https://prometheus.io/docs/prometheus/latest/querying/api/#expression-query-result-formats
* Optimize updating processing the received data from /api/v1/query_range
* Make smoother zoom on `ctrl+scroll`
* Reduce the number of points received from /api/v1/query_range by 2x in order to reduce load on backend
This commit is contained in:
Aliaksandr Valialkin 2022-02-14 16:25:43 +02:00
parent 93c2db5546
commit 1d7c877b7b
No known key found for this signature in database
GPG key ID: A72BEC6CD3D0DED1
13 changed files with 102 additions and 77 deletions

View file

@ -1,12 +1,12 @@
{
"files": {
"main.css": "./static/css/main.098d452b.css",
"main.js": "./static/js/main.c31c0e34.js",
"main.js": "./static/js/main.c945b173.js",
"static/js/27.939f971b.chunk.js": "./static/js/27.939f971b.chunk.js",
"index.html": "./index.html"
},
"entrypoints": [
"static/css/main.098d452b.css",
"static/js/main.c31c0e34.js"
"static/js/main.c945b173.js"
]
}

View file

@ -1 +1 @@
<!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="icon" href="./favicon.ico"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#000000"/><meta name="description" content="VM-UI is a metric explorer for Victoria Metrics"/><link rel="apple-touch-icon" href="./apple-touch-icon.png"/><link rel="icon" type="image/png" sizes="32x32" href="./favicon-32x32.png"><link rel="manifest" href="./manifest.json"/><title>VM UI</title><link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap"/><script defer="defer" src="./static/js/main.c31c0e34.js"></script><link href="./static/css/main.098d452b.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>
<!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="icon" href="./favicon.ico"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#000000"/><meta name="description" content="VM-UI is a metric explorer for Victoria Metrics"/><link rel="apple-touch-icon" href="./apple-touch-icon.png"/><link rel="icon" type="image/png" sizes="32x32" href="./favicon-32x32.png"><link rel="manifest" href="./manifest.json"/><title>VM UI</title><link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap"/><script defer="defer" src="./static/js/main.c945b173.js"></script><link href="./static/css/main.098d452b.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1,11 +1,3 @@
/*! @preserve
* numeral.js
* version : 2.0.6
* author : Adam Draper
* license : MIT
* http://adamwdraper.github.com/Numeral-js/
*/
/**
* A better abstraction over CSS.
*

View file

@ -22,7 +22,6 @@
"@types/lodash.get": "^4.4.6",
"@types/lodash.throttle": "^4.1.6",
"@types/node": "^17.0.17",
"@types/numeral": "^2.0.2",
"@types/qs": "^6.9.7",
"@types/react": "^17.0.39",
"@types/react-dom": "^17.0.11",
@ -31,7 +30,6 @@
"lodash.debounce": "^4.0.8",
"lodash.get": "^4.4.2",
"lodash.throttle": "^4.1.1",
"numeral": "^2.0.6",
"preact": "^10.6.5",
"qs": "^6.10.3",
"typescript": "~4.5.5",
@ -4389,11 +4387,6 @@
"resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.17.tgz",
"integrity": "sha512-e8PUNQy1HgJGV3iU/Bp2+D/DXh3PYeyli8LgIwsQcs1Ar1LoaWHSIT6Rw+H2rNJmiq6SNWiDytfx8+gYj7wDHw=="
},
"node_modules/@types/numeral": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/@types/numeral/-/numeral-2.0.2.tgz",
"integrity": "sha512-A8F30k2gYJ/6e07spSCPpkuZu79LCnkPTvqmIWQzNGcrzwFKpVOydG41lNt5wZXjSI149qjyzC2L1+F2PD/NUA=="
},
"node_modules/@types/parse-json": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz",
@ -13751,14 +13744,6 @@
"url": "https://github.com/fb55/nth-check?sponsor=1"
}
},
"node_modules/numeral": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/numeral/-/numeral-2.0.6.tgz",
"integrity": "sha1-StCAk21EPCVhrtnyGX7//iX05QY=",
"engines": {
"node": "*"
}
},
"node_modules/nwsapi": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.0.tgz",
@ -22571,11 +22556,6 @@
"resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.17.tgz",
"integrity": "sha512-e8PUNQy1HgJGV3iU/Bp2+D/DXh3PYeyli8LgIwsQcs1Ar1LoaWHSIT6Rw+H2rNJmiq6SNWiDytfx8+gYj7wDHw=="
},
"@types/numeral": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/@types/numeral/-/numeral-2.0.2.tgz",
"integrity": "sha512-A8F30k2gYJ/6e07spSCPpkuZu79LCnkPTvqmIWQzNGcrzwFKpVOydG41lNt5wZXjSI149qjyzC2L1+F2PD/NUA=="
},
"@types/parse-json": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz",
@ -29754,11 +29734,6 @@
"boolbase": "^1.0.0"
}
},
"numeral": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/numeral/-/numeral-2.0.6.tgz",
"integrity": "sha1-StCAk21EPCVhrtnyGX7//iX05QY="
},
"nwsapi": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.0.tgz",

View file

@ -18,7 +18,6 @@
"@types/lodash.get": "^4.4.6",
"@types/lodash.throttle": "^4.1.6",
"@types/node": "^17.0.17",
"@types/numeral": "^2.0.2",
"@types/qs": "^6.9.7",
"@types/react": "^17.0.39",
"@types/react-dom": "^17.0.11",
@ -27,7 +26,6 @@
"lodash.debounce": "^4.0.8",
"lodash.get": "^4.4.2",
"lodash.throttle": "^4.1.1",
"numeral": "^2.0.6",
"preact": "^10.6.5",
"qs": "^6.10.3",
"typescript": "~4.5.5",

View file

@ -13,6 +13,21 @@ export interface GraphViewProps {
data?: MetricResult[];
}
const promValueToNumber = (s: string): number => {
// See https://prometheus.io/docs/prometheus/latest/querying/api/#expression-query-result-formats
switch (s) {
case "NaN":
return NaN;
case "Inf":
case "+Inf":
return Infinity;
case "-Inf":
return -Infinity;
default:
return parseFloat(s);
}
};
const GraphView: FC<GraphViewProps> = ({data = []}) => {
const graphDispatch = useGraphDispatch();
const {time: {period}} = useAppState();
@ -43,19 +58,36 @@ const GraphView: FC<GraphViewProps> = ({data = []}) => {
const seriesItem = getSeriesItem(d, hideSeries);
tempSeries.push(seriesItem);
tempLegend.push(getLegendItem(seriesItem, d.group));
d.values.forEach(v => {
let tmpValues = tempValues[d.group];
if (!tmpValues) {
tmpValues = [];
}
for (const v of d.values) {
tempTimes.push(v[0]);
tempValues[d.group] ? tempValues[d.group].push(+v[1]) : tempValues[d.group] = [+v[1]];
});
tmpValues.push(promValueToNumber(v[1]));
}
tempValues[d.group] = tmpValues;
});
const timeSeries = getTimeSeries(tempTimes, currentStep, period);
setDataChart([timeSeries, ...data.map(d => {
return timeSeries.map(t => {
const value = d.values.find(v => v[0] === t);
return value ? +value[1] : null;
});
const results = [];
const values = d.values;
let j = 0;
for (const t of timeSeries) {
while (j < values.length && values[j][0] < t) j++;
let v = null;
if (j < values.length && values[j][0] == t) {
v = promValueToNumber(values[j][1]);
if (!Number.isFinite(v)) {
// Treat special values as nulls in order to satisfy uPlot.
// Otherwise it may draw unexpected graphs.
v = null;
}
}
results.push(v);
}
return results;
})] as uPlotData);
setLimitsYaxis(tempValues);

View file

@ -48,7 +48,7 @@ const LineChart: FC<LineChartProps> = ({data, series, metrics = []}) => {
};
const onReadyChart = (u: uPlot) => {
const factor = 0.85;
const factor = 0.9;
tooltipOffset.left = parseFloat(u.over.style.left);
tooltipOffset.top = parseFloat(u.over.style.top);
u.root.querySelector(".u-wrap")?.appendChild(tooltip);

View file

@ -1,17 +1,23 @@
export const getMaxFromArray = (arr: number[]): number => {
let len = arr.length;
export const getMaxFromArray = (a: number[]) => {
let len = a.length;
let max = -Infinity;
while (len--) {
if (arr[len] > max) max = arr[len];
const v = a[len];
if (Number.isFinite(v) && v > max) {
max = v;
}
return max;
}
return Number.isFinite(max) ? max : null;
};
export const getMinFromArray = (arr: number[]): number => {
let len = arr.length;
export const getMinFromArray = (a: number[]) => {
let len = a.length;
let min = Infinity;
while (len--) {
if (arr[len] < min) min = arr[len];
const v = a[len];
if (Number.isFinite(v) && v < min) {
min = v;
}
return min;
}
return Number.isFinite(min) ? min : null;
};

View file

@ -2,12 +2,11 @@ import {TimeParams, TimePeriod} from "../types";
import dayjs, {UnitTypeShort} from "dayjs";
import duration from "dayjs/plugin/duration";
import utc from "dayjs/plugin/utc";
import numeral from "numeral";
dayjs.extend(duration);
dayjs.extend(utc);
const MAX_ITEMS_PER_CHART = window.innerWidth / 2;
const MAX_ITEMS_PER_CHART = window.innerWidth / 4;
export const limitsDurations = {min: 1, max: 1.578e+11}; // min: 1 ms, max: 5 years
@ -26,7 +25,7 @@ export const supportedDurations = [
const shortDurations = supportedDurations.map(d => d.short);
export const roundTimeSeconds = (num: number): number => +(numeral(num).format("0.000"));
export const roundToMilliseconds = (num: number): number => Math.round(num*1000)/1000;
export const isSupportedDuration = (str: string): Partial<Record<UnitTypeShort, string>> | undefined => {
@ -59,7 +58,7 @@ export const getTimeperiodForDuration = (dur: string, date?: Date): TimeParams =
}, {});
const delta = dayjs.duration(durObject).asSeconds();
const step = roundTimeSeconds(delta / MAX_ITEMS_PER_CHART) || 0.001;
const step = roundToMilliseconds(delta / MAX_ITEMS_PER_CHART) || 0.001;
return {
start: n - delta,

View file

@ -1,6 +1,6 @@
import {Axis, Series} from "uplot";
import {getMaxFromArray, getMinFromArray} from "../math";
import {roundTimeSeconds} from "../time";
import {roundToMilliseconds} from "../time";
import {AxisRange} from "../../state/graph/reducer";
import {formatTicks} from "./helpers";
import {TimeParams} from "../../types";
@ -12,19 +12,37 @@ export const getAxes = (series: Series[]): Axis[] => Array.from(new Set(series.m
return axis;
});
export const getTimeSeries = (times: number[], defaultStep: number, period: TimeParams): number[] => {
export const getTimeSeries = (times: number[], step: number, period: TimeParams): number[] => {
const allTimes = Array.from(new Set(times)).sort((a, b) => a - b);
const length = Math.ceil((period.end - period.start)/defaultStep);
const startTime = allTimes[0] || 0;
return new Array(length*2).fill(startTime).map((d, i) => roundTimeSeconds(d + (defaultStep * i)));
let t = period.start;
const tEnd = roundToMilliseconds(period.end + step);
let j = 0;
const results: number[] = [];
while (t <= tEnd) {
while (j < allTimes.length && allTimes[j] <= t) {
t = allTimes[j];
j++;
results.push(t);
}
t = roundToMilliseconds(t + step);
if (j >= allTimes.length || allTimes[j] > t) {
results.push(t);
}
}
while (results.length < 2) {
results.push(t);
t = roundToMilliseconds(t + step);
}
return results;
};
export const getMinMaxBuffer = (min: number, max: number): [number, number] => {
const minCorrect = isNaN(min) ? -1 : min;
const maxCorrect = isNaN(max) ? 1 : max;
const valueRange = Math.abs(maxCorrect - minCorrect) || Math.abs(minCorrect) || 1;
export const getMinMaxBuffer = (min: number | null, max: number | null): [number, number] => {
if (min == null || max == null) {
return [-1, 1];
}
const valueRange = Math.abs(max - min) || Math.abs(min) || 1;
const padding = 0.02*valueRange;
return [minCorrect - padding, maxCorrect + padding];
return [min - padding, max + padding];
};
export const getLimitsYAxis = (values: { [key: string]: number[] }): AxisRange => {

View file

@ -1,5 +1,4 @@
import uPlot from "uplot";
import numeral from "numeral";
import {getColorFromString} from "../color";
export const defaultOptions = {
@ -29,8 +28,14 @@ export const defaultOptions = {
},
};
export const formatTicks = (u: uPlot, ticks: number[]): (string | number)[] => {
return ticks.map(n => n > 1000 ? numeral(n).format("0.0a") : n);
export const formatTicks = (u: uPlot, ticks: number[]): string[] => {
return ticks.map(v => {
const n = Math.abs(v);
if (n > 1e-3 && n < 1e4) {
return v.toString();
}
return v.toExponential(1);
});
};
export const getColorLine = (scale: number, label: string): string => getColorFromString(`${scale}${label}`);