mirror of
https://github.com/VictoriaMetrics/VictoriaMetrics.git
synced 2024-11-21 14:44:00 +00:00
vmui: add last/max/avg values (#3789)
* feat: add last/max/avg values (#3706) * fix: change filter exclude values * app/vmui: wip - improve the visualization for avg/max/last values - make getAvgFromArray() function resilient against inf/undefined/nil - export getLastFromArray() function, which is resilient against inf/undefined/nil - run `make vmui-update` --------- Co-authored-by: Aliaksandr Valialkin <valyala@victoriametrics.com>
This commit is contained in:
parent
114c14febf
commit
8afc0aef8d
19 changed files with 111 additions and 48 deletions
|
@ -1,14 +1,14 @@
|
|||
{
|
||||
"files": {
|
||||
"main.css": "./static/css/main.a6dcf95f.css",
|
||||
"main.js": "./static/js/main.83e96a22.js",
|
||||
"main.css": "./static/css/main.f22be84b.css",
|
||||
"main.js": "./static/js/main.eca4a392.js",
|
||||
"static/js/27.c1ccfd29.chunk.js": "./static/js/27.c1ccfd29.chunk.js",
|
||||
"static/media/Lato-Regular.ttf": "./static/media/Lato-Regular.d714fec1633b69a9c2e9.ttf",
|
||||
"static/media/Lato-Bold.ttf": "./static/media/Lato-Bold.32360ba4b57802daa4d6.ttf",
|
||||
"index.html": "./index.html"
|
||||
},
|
||||
"entrypoints": [
|
||||
"static/css/main.a6dcf95f.css",
|
||||
"static/js/main.83e96a22.js"
|
||||
"static/css/main.f22be84b.css",
|
||||
"static/js/main.eca4a392.js"
|
||||
]
|
||||
}
|
|
@ -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,maximum-scale=1,user-scalable=no"/><meta name="theme-color" content="#000000"/><meta name="description" content="UI for VictoriaMetrics"/><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><script src="./dashboards/index.js" type="module"></script><meta name="twitter:card" content="summary_large_image"><meta name="twitter:image" content="./preview.jpg"><meta name="twitter:title" content="UI for VictoriaMetrics"><meta name="twitter:description" content="Explore and troubleshoot your VictoriaMetrics data"><meta name="twitter:site" content="@VictoriaMetrics"><meta property="og:title" content="Metric explorer for VictoriaMetrics"><meta property="og:description" content="Explore and troubleshoot your VictoriaMetrics data"><meta property="og:image" content="./preview.jpg"><meta property="og:type" content="website"><script defer="defer" src="./static/js/main.83e96a22.js"></script><link href="./static/css/main.a6dcf95f.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,maximum-scale=1,user-scalable=no"/><meta name="theme-color" content="#000000"/><meta name="description" content="UI for VictoriaMetrics"/><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><script src="./dashboards/index.js" type="module"></script><meta name="twitter:card" content="summary_large_image"><meta name="twitter:image" content="./preview.jpg"><meta name="twitter:title" content="UI for VictoriaMetrics"><meta name="twitter:description" content="Explore and troubleshoot your VictoriaMetrics data"><meta name="twitter:site" content="@VictoriaMetrics"><meta property="og:title" content="Metric explorer for VictoriaMetrics"><meta property="og:description" content="Explore and troubleshoot your VictoriaMetrics data"><meta property="og:image" content="./preview.jpg"><meta property="og:type" content="website"><script defer="defer" src="./static/js/main.eca4a392.js"></script><link href="./static/css/main.f22be84b.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
|
@ -1,5 +1,5 @@
|
|||
import React, { FC, useEffect, useMemo, useRef, useState } from "preact/compat";
|
||||
import uPlot, { Series } from "uplot";
|
||||
import uPlot from "uplot";
|
||||
import { MetricResult } from "../../../api/types";
|
||||
import { formatPrettyNumber } from "../../../utils/uplot/helpers";
|
||||
import dayjs from "dayjs";
|
||||
|
@ -11,12 +11,13 @@ import { CloseIcon, DragIcon } from "../../Main/Icons";
|
|||
import classNames from "classnames";
|
||||
import { MouseEvent as ReactMouseEvent } from "react";
|
||||
import "./style.scss";
|
||||
import { SeriesItem } from "../../../utils/uplot/series";
|
||||
|
||||
export interface ChartTooltipProps {
|
||||
id: string,
|
||||
u: uPlot,
|
||||
metrics: MetricResult[],
|
||||
series: Series[],
|
||||
series: SeriesItem[],
|
||||
yRange: number[];
|
||||
unit?: string,
|
||||
isSticky?: boolean,
|
||||
|
@ -55,6 +56,8 @@ const ChartTooltip: FC<ChartTooltipProps> = ({
|
|||
|
||||
const color = series[seriesIdx]?.stroke+"";
|
||||
|
||||
const calculations = series[seriesIdx]?.calculations || {};
|
||||
|
||||
const groups = new Set(metrics.map(m => m.group));
|
||||
const showQueryNum = groups.size > 1;
|
||||
const group = metrics[seriesIdx-1]?.group || 0;
|
||||
|
@ -175,9 +178,14 @@ const ChartTooltip: FC<ChartTooltipProps> = ({
|
|||
style={{ background: color }}
|
||||
/>
|
||||
<p>
|
||||
{metricName}:
|
||||
<b className="vm-chart-tooltip-data__value">{valueFormat}</b>
|
||||
{unit}
|
||||
{calculations.last !== undefined && (
|
||||
<div>
|
||||
avg:<b>{calculations.avg}</b>,
|
||||
max:<b>{calculations.max}</b>,
|
||||
last:<b>{calculations.last}</b>
|
||||
</div>
|
||||
)}
|
||||
{metricName}:<b>{valueFormat}{unit}</b>
|
||||
</p>
|
||||
</div>
|
||||
{!!fields.length && (
|
||||
|
|
|
@ -61,20 +61,9 @@ $chart-tooltip-y: -1 * ($padding-small + $chart-tooltip-half-icon);
|
|||
word-break: break-all;
|
||||
line-height: 12px;
|
||||
|
||||
&__value {
|
||||
padding: 4px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
&__marker {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
&-info {
|
||||
display: grid;
|
||||
grid-gap: 4px;
|
||||
word-break: break-all;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,6 +14,7 @@ interface LegendItemProps {
|
|||
const LegendItem: FC<LegendItemProps> = ({ legend, onChange }) => {
|
||||
const [copiedValue, setCopiedValue] = useState("");
|
||||
const freeFormFields = useMemo(() => getFreeFields(legend), [legend]);
|
||||
const calculations = legend.calculations;
|
||||
|
||||
const handleClickFreeField = async (val: string, id: string) => {
|
||||
await navigator.clipboard.writeText(val);
|
||||
|
@ -30,11 +31,11 @@ const LegendItem: FC<LegendItemProps> = ({ legend, onChange }) => {
|
|||
handleClickFreeField(freeField, id);
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames({
|
||||
"vm-legend-item": true,
|
||||
"vm-legend-row": true,
|
||||
"vm-legend-item_hide": !legend.checked,
|
||||
})}
|
||||
onClick={createHandlerClick(legend)}
|
||||
|
@ -70,6 +71,11 @@ const LegendItem: FC<LegendItemProps> = ({ legend, onChange }) => {
|
|||
</span>
|
||||
}
|
||||
</div>
|
||||
{calculations.last !== undefined && (
|
||||
<div className="vm-legend-item-values">
|
||||
avg:{calculations.avg}, max:{calculations.max}, last:{calculations.last}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
background-color: $color-background-block;
|
||||
cursor: pointer;
|
||||
transition: 0.2s ease;
|
||||
margin-bottom: $padding-small;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
|
@ -33,10 +34,11 @@
|
|||
word-break: break-all;
|
||||
|
||||
&__label {
|
||||
margin-right: 2px;
|
||||
}
|
||||
|
||||
&__free-fields {
|
||||
padding: 3px;
|
||||
padding: 2px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
|
@ -48,4 +50,11 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-values {
|
||||
grid-column: 2;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $padding-small;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,12 +9,13 @@
|
|||
|
||||
&-group {
|
||||
min-width: 23%;
|
||||
width: 100%;
|
||||
margin: 0 $padding-global $padding-global 0;
|
||||
|
||||
&-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 $padding-small $padding-small;
|
||||
padding: $padding-small;
|
||||
margin-bottom: 1px;
|
||||
border-bottom: $border-divider;
|
||||
|
||||
|
|
|
@ -22,6 +22,7 @@ import classNames from "classnames";
|
|||
import ChartTooltip, { ChartTooltipProps } from "../ChartTooltip/ChartTooltip";
|
||||
import dayjs from "dayjs";
|
||||
import { useAppState } from "../../../state/common/StateContext";
|
||||
import { SeriesItem } from "../../../utils/uplot/series";
|
||||
|
||||
export interface LineChartProps {
|
||||
metrics: MetricResult[];
|
||||
|
@ -316,7 +317,7 @@ const LineChart: FC<LineChartProps> = ({
|
|||
<ChartTooltip
|
||||
unit={unit}
|
||||
u={uPlotInst}
|
||||
series={series}
|
||||
series={series as SeriesItem[]}
|
||||
metrics={metrics}
|
||||
yRange={yRange}
|
||||
tooltipIdx={tooltipIdx}
|
||||
|
|
|
@ -12,6 +12,7 @@ import { getAvgFromArray, getMaxFromArray, getMinFromArray } from "../../../util
|
|||
import classNames from "classnames";
|
||||
import { useTimeState } from "../../../state/time/TimeStateContext";
|
||||
import "./style.scss";
|
||||
import { promValueToNumber } from "../../../utils/metric";
|
||||
|
||||
export interface GraphViewProps {
|
||||
data?: MetricResult[];
|
||||
|
@ -28,21 +29,6 @@ export interface GraphViewProps {
|
|||
height?: number
|
||||
}
|
||||
|
||||
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 = [],
|
||||
period,
|
||||
|
|
|
@ -40,7 +40,7 @@ const Index: FC = () => {
|
|||
const getQueryStatsTitle = (key: keyof TopQueryStats) => {
|
||||
if (!data) return key;
|
||||
const value = data[key];
|
||||
if (typeof value === "number") return formatPrettyNumber(value);
|
||||
if (typeof value === "number") return formatPrettyNumber(value, value, value);
|
||||
return value || key;
|
||||
};
|
||||
|
||||
|
|
|
@ -22,4 +22,25 @@ export const getMinFromArray = (a: number[]) => {
|
|||
return Number.isFinite(min) ? min : null;
|
||||
};
|
||||
|
||||
export const getAvgFromArray = (a: number[]) => a.reduce((a,b) => a+b) / a.length;
|
||||
export const getAvgFromArray = (a: number[]) => {
|
||||
let mean = a[0];
|
||||
let n = 1;
|
||||
for (let i = 1; i < a.length; i++) {
|
||||
const v = a[i];
|
||||
if (Number.isFinite(v)) {
|
||||
mean = mean * (n-1)/n + v / n;
|
||||
n++;
|
||||
}
|
||||
}
|
||||
return mean;
|
||||
};
|
||||
|
||||
export const getLastFromArray = (a: number[]) => {
|
||||
let len = a.length;
|
||||
while (len--) {
|
||||
const v = a[len];
|
||||
if (Number.isFinite(v)) {
|
||||
return v;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
@ -13,3 +13,18 @@ export const getNameForMetric = (result: MetricBase, alias?: string, showQueryNu
|
|||
`${e[0]}=${JSON.stringify(e[1])}`
|
||||
).join(", ")}}`;
|
||||
};
|
||||
|
||||
export 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);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -32,10 +32,12 @@ export const formatTicks = (u: uPlot, ticks: number[], unit = ""): string[] => {
|
|||
return ticks.map(v => `${formatPrettyNumber(v, min, max)} ${unit}`);
|
||||
};
|
||||
|
||||
export const formatPrettyNumber = (n: number | null | undefined, min = 0, max = 0): string => {
|
||||
export const formatPrettyNumber = (n: number | null | undefined, min: number | null | undefined, max: number | null | undefined): string => {
|
||||
if (n === undefined || n === null) {
|
||||
return "";
|
||||
}
|
||||
max = max || 0;
|
||||
min = min || 0;
|
||||
const range = Math.abs(max - min);
|
||||
if (isNaN(range) || range == 0) {
|
||||
// Return the constant number as is if the range isn't set of it is too small.
|
||||
|
|
|
@ -1,12 +1,19 @@
|
|||
import { MetricResult } from "../../api/types";
|
||||
import { Series } from "uplot";
|
||||
import { getNameForMetric } from "../metric";
|
||||
import { getNameForMetric, promValueToNumber } from "../metric";
|
||||
import { BarSeriesItem, Disp, Fill, LegendItemType, Stroke } from "./types";
|
||||
import { HideSeriesArgs } from "./types";
|
||||
import { baseContrastColors, getColorFromString } from "../color";
|
||||
import { getAvgFromArray, getMaxFromArray, getMinFromArray, getLastFromArray } from "../math";
|
||||
import { formatPrettyNumber } from "./helpers";
|
||||
|
||||
interface SeriesItem extends Series {
|
||||
export interface SeriesItem extends Series {
|
||||
freeFormFields: {[key: string]: string};
|
||||
calculations: {
|
||||
max: string,
|
||||
avg: string,
|
||||
last: string
|
||||
}
|
||||
}
|
||||
|
||||
export const getSeriesItemContext = () => {
|
||||
|
@ -18,6 +25,12 @@ export const getSeriesItemContext = () => {
|
|||
const hasBasicColors = countSavedColors < baseContrastColors.length;
|
||||
if (hasBasicColors) colorState[label] = colorState[label] || baseContrastColors[countSavedColors];
|
||||
|
||||
const values = d.values.map(v => promValueToNumber(v[1]));
|
||||
const min = getMinFromArray(values);
|
||||
const max = getMaxFromArray(values);
|
||||
const avg = getAvgFromArray(values);
|
||||
const last = getLastFromArray(values);
|
||||
|
||||
return {
|
||||
label,
|
||||
freeFormFields: d.metric,
|
||||
|
@ -28,6 +41,11 @@ export const getSeriesItemContext = () => {
|
|||
points: {
|
||||
size: 4.2,
|
||||
width: 1.4
|
||||
},
|
||||
calculations: {
|
||||
max: formatPrettyNumber(max, min, max),
|
||||
avg: formatPrettyNumber(avg, min, max),
|
||||
last: formatPrettyNumber(last, min, max),
|
||||
}
|
||||
};
|
||||
};
|
||||
|
@ -39,6 +57,7 @@ export const getLegendItem = (s: SeriesItem, group: number): LegendItemType => (
|
|||
color: s.stroke as string,
|
||||
checked: s.show || false,
|
||||
freeFormFields: s.freeFormFields,
|
||||
calculations: s.calculations,
|
||||
});
|
||||
|
||||
export const getHideSeries = ({ hideSeries, legend, metaKey, series }: HideSeriesArgs): string[] => {
|
||||
|
|
|
@ -21,6 +21,11 @@ export interface LegendItemType {
|
|||
color: string;
|
||||
checked: boolean;
|
||||
freeFormFields: {[key: string]: string};
|
||||
calculations: {
|
||||
max: string;
|
||||
avg: string;
|
||||
last: string;
|
||||
}
|
||||
}
|
||||
|
||||
export interface BarSeriesItem {
|
||||
|
|
|
@ -19,6 +19,7 @@ The following tip changes can be tested by building VictoriaMetrics components f
|
|||
This also means that `-remoteRead.ignoreRestoreErrors` command-line flag becomes deprecated now and will have no effect if configured.
|
||||
While previously state restore attempt was made for all the loaded alerting rules, now it is called only for alerts which became active after the first evaluation. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/2608).
|
||||
* FEATURE: [vmui](https://docs.victoriametrics.com/#vmui): optimize VMUI for use from smarthones and tablets. See [this feature request](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/3707).
|
||||
* FEATURE: [vmui](https://docs.victoriametrics.com/#vmui): add avg/max/last values to line legends and tooltips for graphs. See [this feature request](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/3706).
|
||||
* FEATURE: [vmui](https://docs.victoriametrics.com/#vmui): hide the default `per-job resource usage` dashboard if there is a custom dashboard exists at the directory specified via `-vmui.customDashboardsPath` command-line flag. See [this feature request](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/3740).
|
||||
|
||||
* BUGFIX: [vmagent](https://docs.victoriametrics.com/vmagent.html): fix panic in [HashiCorp Nomad service discovery](https://docs.victoriametrics.com/sd_configs.html#nomad_sd_configs). Thanks to @mr-karan for the [pull request](https://github.com/VictoriaMetrics/VictoriaMetrics/pull/3784).
|
||||
|
|
Loading…
Reference in a new issue