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": { "files": {
"main.css": "./static/css/main.098d452b.css", "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", "static/js/27.939f971b.chunk.js": "./static/js/27.939f971b.chunk.js",
"index.html": "./index.html" "index.html": "./index.html"
}, },
"entrypoints": [ "entrypoints": [
"static/css/main.098d452b.css", "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. * A better abstraction over CSS.
* *

View file

@ -22,7 +22,6 @@
"@types/lodash.get": "^4.4.6", "@types/lodash.get": "^4.4.6",
"@types/lodash.throttle": "^4.1.6", "@types/lodash.throttle": "^4.1.6",
"@types/node": "^17.0.17", "@types/node": "^17.0.17",
"@types/numeral": "^2.0.2",
"@types/qs": "^6.9.7", "@types/qs": "^6.9.7",
"@types/react": "^17.0.39", "@types/react": "^17.0.39",
"@types/react-dom": "^17.0.11", "@types/react-dom": "^17.0.11",
@ -31,7 +30,6 @@
"lodash.debounce": "^4.0.8", "lodash.debounce": "^4.0.8",
"lodash.get": "^4.4.2", "lodash.get": "^4.4.2",
"lodash.throttle": "^4.1.1", "lodash.throttle": "^4.1.1",
"numeral": "^2.0.6",
"preact": "^10.6.5", "preact": "^10.6.5",
"qs": "^6.10.3", "qs": "^6.10.3",
"typescript": "~4.5.5", "typescript": "~4.5.5",
@ -4389,11 +4387,6 @@
"resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.17.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.17.tgz",
"integrity": "sha512-e8PUNQy1HgJGV3iU/Bp2+D/DXh3PYeyli8LgIwsQcs1Ar1LoaWHSIT6Rw+H2rNJmiq6SNWiDytfx8+gYj7wDHw==" "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": { "node_modules/@types/parse-json": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz", "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" "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": { "node_modules/nwsapi": {
"version": "2.2.0", "version": "2.2.0",
"resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.0.tgz", "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", "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.17.tgz",
"integrity": "sha512-e8PUNQy1HgJGV3iU/Bp2+D/DXh3PYeyli8LgIwsQcs1Ar1LoaWHSIT6Rw+H2rNJmiq6SNWiDytfx8+gYj7wDHw==" "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": { "@types/parse-json": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz", "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz",
@ -29754,11 +29734,6 @@
"boolbase": "^1.0.0" "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": { "nwsapi": {
"version": "2.2.0", "version": "2.2.0",
"resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.0.tgz", "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.get": "^4.4.6",
"@types/lodash.throttle": "^4.1.6", "@types/lodash.throttle": "^4.1.6",
"@types/node": "^17.0.17", "@types/node": "^17.0.17",
"@types/numeral": "^2.0.2",
"@types/qs": "^6.9.7", "@types/qs": "^6.9.7",
"@types/react": "^17.0.39", "@types/react": "^17.0.39",
"@types/react-dom": "^17.0.11", "@types/react-dom": "^17.0.11",
@ -27,7 +26,6 @@
"lodash.debounce": "^4.0.8", "lodash.debounce": "^4.0.8",
"lodash.get": "^4.4.2", "lodash.get": "^4.4.2",
"lodash.throttle": "^4.1.1", "lodash.throttle": "^4.1.1",
"numeral": "^2.0.6",
"preact": "^10.6.5", "preact": "^10.6.5",
"qs": "^6.10.3", "qs": "^6.10.3",
"typescript": "~4.5.5", "typescript": "~4.5.5",

View file

@ -13,6 +13,21 @@ export interface GraphViewProps {
data?: MetricResult[]; 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 GraphView: FC<GraphViewProps> = ({data = []}) => {
const graphDispatch = useGraphDispatch(); const graphDispatch = useGraphDispatch();
const {time: {period}} = useAppState(); const {time: {period}} = useAppState();
@ -43,19 +58,36 @@ const GraphView: FC<GraphViewProps> = ({data = []}) => {
const seriesItem = getSeriesItem(d, hideSeries); const seriesItem = getSeriesItem(d, hideSeries);
tempSeries.push(seriesItem); tempSeries.push(seriesItem);
tempLegend.push(getLegendItem(seriesItem, d.group)); tempLegend.push(getLegendItem(seriesItem, d.group));
let tmpValues = tempValues[d.group];
d.values.forEach(v => { if (!tmpValues) {
tmpValues = [];
}
for (const v of d.values) {
tempTimes.push(v[0]); 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); const timeSeries = getTimeSeries(tempTimes, currentStep, period);
setDataChart([timeSeries, ...data.map(d => { setDataChart([timeSeries, ...data.map(d => {
return timeSeries.map(t => { const results = [];
const value = d.values.find(v => v[0] === t); const values = d.values;
return value ? +value[1] : null; 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); })] as uPlotData);
setLimitsYaxis(tempValues); setLimitsYaxis(tempValues);
@ -88,4 +120,4 @@ const GraphView: FC<GraphViewProps> = ({data = []}) => {
</>; </>;
}; };
export default GraphView; export default GraphView;

View file

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

View file

@ -1,17 +1,23 @@
export const getMaxFromArray = (arr: number[]): number => { export const getMaxFromArray = (a: number[]) => {
let len = arr.length; let len = a.length;
let max = -Infinity; let max = -Infinity;
while (len--) { 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 => { export const getMinFromArray = (a: number[]) => {
let len = arr.length; let len = a.length;
let min = Infinity; let min = Infinity;
while (len--) { 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 dayjs, {UnitTypeShort} from "dayjs";
import duration from "dayjs/plugin/duration"; import duration from "dayjs/plugin/duration";
import utc from "dayjs/plugin/utc"; import utc from "dayjs/plugin/utc";
import numeral from "numeral";
dayjs.extend(duration); dayjs.extend(duration);
dayjs.extend(utc); 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 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); 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 => { 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 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 { return {
start: n - delta, start: n - delta,

View file

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

View file

@ -1,5 +1,4 @@
import uPlot from "uplot"; import uPlot from "uplot";
import numeral from "numeral";
import {getColorFromString} from "../color"; import {getColorFromString} from "../color";
export const defaultOptions = { export const defaultOptions = {
@ -29,8 +28,14 @@ export const defaultOptions = {
}, },
}; };
export const formatTicks = (u: uPlot, ticks: number[]): (string | number)[] => { export const formatTicks = (u: uPlot, ticks: number[]): string[] => {
return ticks.map(n => n > 1000 ? numeral(n).format("0.0a") : n); 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}`); export const getColorLine = (scale: number, label: string): string => getColorFromString(`${scale}${label}`);