mirror of
https://github.com/VictoriaMetrics/VictoriaMetrics.git
synced 2025-03-11 15:34:56 +00:00
vmui: implement heatmap improvements (#4078)
* fix: disabled limits for histogram * fix: add sorted buckets by upper bound * refactor: move line chart components to folder * feat: implement heatmap improvements (https://github.com/VictoriaMetrics/VictoriaMetrics/issues/3384#issuecomment-1484023162) * app/vmselect/vmui: `make vmui-update` --------- Co-authored-by: Aliaksandr Valialkin <valyala@victoriametrics.com>
This commit is contained in:
parent
593c151831
commit
74eea53dee
24 changed files with 202 additions and 149 deletions
|
@ -1,14 +1,14 @@
|
||||||
{
|
{
|
||||||
"files": {
|
"files": {
|
||||||
"main.css": "./static/css/main.ebde9e58.css",
|
"main.css": "./static/css/main.6fe27a94.css",
|
||||||
"main.js": "./static/js/main.ee50e2ce.js",
|
"main.js": "./static/js/main.22a4d00b.js",
|
||||||
"static/js/27.c1ccfd29.chunk.js": "./static/js/27.c1ccfd29.chunk.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-Regular.ttf": "./static/media/Lato-Regular.d714fec1633b69a9c2e9.ttf",
|
||||||
"static/media/Lato-Bold.ttf": "./static/media/Lato-Bold.32360ba4b57802daa4d6.ttf",
|
"static/media/Lato-Bold.ttf": "./static/media/Lato-Bold.32360ba4b57802daa4d6.ttf",
|
||||||
"index.html": "./index.html"
|
"index.html": "./index.html"
|
||||||
},
|
},
|
||||||
"entrypoints": [
|
"entrypoints": [
|
||||||
"static/css/main.ebde9e58.css",
|
"static/css/main.6fe27a94.css",
|
||||||
"static/js/main.ee50e2ce.js"
|
"static/js/main.22a4d00b.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.ee50e2ce.js"></script><link href="./static/css/main.ebde9e58.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.22a4d00b.js"></script><link href="./static/css/main.6fe27a94.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>
|
1
app/vmselect/vmui/static/css/main.6fe27a94.css
Normal file
1
app/vmselect/vmui/static/css/main.6fe27a94.css
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -1,18 +1,17 @@
|
||||||
import React, { FC, useEffect, useMemo, useRef, useState } from "preact/compat";
|
import React, { FC, useEffect, useMemo, useRef, useState } from "preact/compat";
|
||||||
import uPlot from "uplot";
|
import uPlot from "uplot";
|
||||||
import ReactDOM from "react-dom";
|
import ReactDOM from "react-dom";
|
||||||
import Button from "../../Main/Button/Button";
|
import Button from "../../../Main/Button/Button";
|
||||||
import { CloseIcon, DragIcon } from "../../Main/Icons";
|
import { CloseIcon, DragIcon } from "../../../Main/Icons";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { MouseEvent as ReactMouseEvent } from "react";
|
import { MouseEvent as ReactMouseEvent } from "react";
|
||||||
import "../ChartTooltip/style.scss";
|
import "../../Line/ChartTooltip/style.scss";
|
||||||
|
|
||||||
export interface TooltipHeatmapProps {
|
export interface TooltipHeatmapProps {
|
||||||
cursor: {left: number, top: number}
|
cursor: {left: number, top: number}
|
||||||
startDate: string,
|
startDate: string,
|
||||||
endDate: string,
|
endDate: string,
|
||||||
metricName: string,
|
bucket: string,
|
||||||
fields: string[],
|
|
||||||
value: number,
|
value: number,
|
||||||
valueFormat: string
|
valueFormat: string
|
||||||
}
|
}
|
||||||
|
@ -36,8 +35,7 @@ const ChartTooltipHeatmap: FC<ChartTooltipHeatmapProps> = ({
|
||||||
onClose,
|
onClose,
|
||||||
startDate,
|
startDate,
|
||||||
endDate,
|
endDate,
|
||||||
metricName,
|
bucket,
|
||||||
fields,
|
|
||||||
valueFormat,
|
valueFormat,
|
||||||
value
|
value
|
||||||
}) => {
|
}) => {
|
||||||
|
@ -141,18 +139,12 @@ const ChartTooltipHeatmap: FC<ChartTooltipHeatmapProps> = ({
|
||||||
</div>
|
</div>
|
||||||
<div className="vm-chart-tooltip-data">
|
<div className="vm-chart-tooltip-data">
|
||||||
<p>
|
<p>
|
||||||
{metricName}:
|
value: <b className="vm-chart-tooltip-data__value">{valueFormat}</b>{unit}
|
||||||
<b className="vm-chart-tooltip-data__value">{valueFormat}</b>
|
|
||||||
{unit}
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{!!fields.length && (
|
<div className="vm-chart-tooltip-info">
|
||||||
<div className="vm-chart-tooltip-info">
|
{bucket}
|
||||||
{fields.map((f, i) => (
|
</div>
|
||||||
<div key={`${f}_${i}`}>{f}</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
), targetPortal);
|
), targetPortal);
|
||||||
};
|
};
|
|
@ -4,21 +4,21 @@ import uPlot, {
|
||||||
Options as uPlotOptions,
|
Options as uPlotOptions,
|
||||||
Range
|
Range
|
||||||
} from "uplot";
|
} from "uplot";
|
||||||
import { defaultOptions, sizeAxis } from "../../../utils/uplot/helpers";
|
import { defaultOptions, sizeAxis } from "../../../../utils/uplot/helpers";
|
||||||
import { dragChart } from "../../../utils/uplot/events";
|
import { dragChart } from "../../../../utils/uplot/events";
|
||||||
import { getAxes } from "../../../utils/uplot/axes";
|
import { getAxes } from "../../../../utils/uplot/axes";
|
||||||
import { MetricResult } from "../../../api/types";
|
import { MetricResult } from "../../../../api/types";
|
||||||
import { dateFromSeconds, formatDateForNativeInput, limitsDurations } from "../../../utils/time";
|
import { dateFromSeconds, formatDateForNativeInput, limitsDurations } from "../../../../utils/time";
|
||||||
import throttle from "lodash.throttle";
|
import throttle from "lodash.throttle";
|
||||||
import useResize from "../../../hooks/useResize";
|
import useResize from "../../../../hooks/useResize";
|
||||||
import { TimeParams } from "../../../types";
|
import { TimeParams } from "../../../../types";
|
||||||
import { YaxisState } from "../../../state/graph/reducer";
|
import { YaxisState } from "../../../../state/graph/reducer";
|
||||||
import "uplot/dist/uPlot.min.css";
|
import "uplot/dist/uPlot.min.css";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import { useAppState } from "../../../state/common/StateContext";
|
import { useAppState } from "../../../../state/common/StateContext";
|
||||||
import { heatmapPaths } from "../../../utils/uplot/heatmap";
|
import { heatmapPaths } from "../../../../utils/uplot/heatmap";
|
||||||
import { DATE_FULL_TIMEZONE_FORMAT } from "../../../constants/date";
|
import { DATE_FULL_TIMEZONE_FORMAT } from "../../../../constants/date";
|
||||||
import ChartTooltipHeatmap, {
|
import ChartTooltipHeatmap, {
|
||||||
ChartTooltipHeatmapProps,
|
ChartTooltipHeatmapProps,
|
||||||
TooltipHeatmapProps
|
TooltipHeatmapProps
|
||||||
|
@ -33,7 +33,7 @@ export interface HeatmapChartProps {
|
||||||
setPeriod: ({ from, to }: {from: Date, to: Date}) => void;
|
setPeriod: ({ from, to }: {from: Date, to: Date}) => void;
|
||||||
container: HTMLDivElement | null;
|
container: HTMLDivElement | null;
|
||||||
height?: number;
|
height?: number;
|
||||||
onChangeLegend: (val: number) => void;
|
onChangeLegend: (val: TooltipHeatmapProps) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
enum typeChartUpdate {xRange = "xRange", yRange = "yRange"}
|
enum typeChartUpdate {xRange = "xRange", yRange = "yRange"}
|
||||||
|
@ -62,7 +62,7 @@ const HeatmapChart: FC<HeatmapChartProps> = ({
|
||||||
const [tooltipOffset, setTooltipOffset] = useState({ left: 0, top: 0 });
|
const [tooltipOffset, setTooltipOffset] = useState({ left: 0, top: 0 });
|
||||||
const [stickyTooltips, setStickyToolTips] = useState<ChartTooltipHeatmapProps[]>([]);
|
const [stickyTooltips, setStickyToolTips] = useState<ChartTooltipHeatmapProps[]>([]);
|
||||||
const tooltipId = useMemo(() => {
|
const tooltipId = useMemo(() => {
|
||||||
return `${tooltipProps?.fields.join(",")}_${tooltipProps?.startDate}`;
|
return `${tooltipProps?.bucket}_${tooltipProps?.startDate}`;
|
||||||
}, [tooltipProps]);
|
}, [tooltipProps]);
|
||||||
|
|
||||||
const setScale = ({ min, max }: { min: number, max: number }): void => {
|
const setScale = ({ min, max }: { min: number, max: number }): void => {
|
||||||
|
@ -135,7 +135,7 @@ const HeatmapChart: FC<HeatmapChartProps> = ({
|
||||||
|
|
||||||
const handleClick = () => {
|
const handleClick = () => {
|
||||||
if (!tooltipProps) return;
|
if (!tooltipProps) return;
|
||||||
const id = `${tooltipProps?.fields.join(",")}_${tooltipProps?.startDate}`;
|
const id = `${tooltipProps?.bucket}_${tooltipProps?.startDate}`;
|
||||||
const props = {
|
const props = {
|
||||||
id,
|
id,
|
||||||
unit,
|
unit,
|
||||||
|
@ -171,12 +171,6 @@ const HeatmapChart: FC<HeatmapChartProps> = ({
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const metric = result?.metric;
|
|
||||||
const metricName = metric["__name__"] || "value";
|
|
||||||
|
|
||||||
const labelNames = Object.keys(metric).filter(x => x != "__name__");
|
|
||||||
const fields = labelNames.map(key => `${key}=${JSON.stringify(metric[key])}`);
|
|
||||||
|
|
||||||
const [endTime = 0, value = ""] = result.values.find(v => v[0] === second) || [];
|
const [endTime = 0, value = ""] = result.values.find(v => v[0] === second) || [];
|
||||||
const valueFormat = `${+value}%`;
|
const valueFormat = `${+value}%`;
|
||||||
const startTime = xArr[xIdx];
|
const startTime = xArr[xIdx];
|
||||||
|
@ -187,8 +181,7 @@ const HeatmapChart: FC<HeatmapChartProps> = ({
|
||||||
cursor: { left, top },
|
cursor: { left, top },
|
||||||
startDate,
|
startDate,
|
||||||
endDate,
|
endDate,
|
||||||
metricName,
|
bucket: result?.metric?.vmrange || "",
|
||||||
fields,
|
|
||||||
value: +value,
|
value: +value,
|
||||||
valueFormat: valueFormat,
|
valueFormat: valueFormat,
|
||||||
});
|
});
|
||||||
|
@ -228,7 +221,7 @@ const HeatmapChart: FC<HeatmapChartProps> = ({
|
||||||
font: axes[0].font,
|
font: axes[0].font,
|
||||||
size: sizeAxis,
|
size: sizeAxis,
|
||||||
splits: metrics.map((m, i) => i),
|
splits: metrics.map((m, i) => i),
|
||||||
values: metrics.map(m => Object.entries(m.metric).map(e => `${e[0]}=${JSON.stringify(e[1])}`)[0]),
|
values: metrics.map(m => m.metric.vmrange),
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
scales: {
|
scales: {
|
||||||
|
@ -339,7 +332,7 @@ const HeatmapChart: FC<HeatmapChartProps> = ({
|
||||||
}, [tooltipProps, stickyTooltips]);
|
}, [tooltipProps, stickyTooltips]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
onChangeLegend(tooltipProps?.value || 0);
|
if (tooltipProps) onChangeLegend(tooltipProps);
|
||||||
}, [tooltipProps]);
|
}, [tooltipProps]);
|
||||||
|
|
||||||
return (
|
return (
|
|
@ -0,0 +1,68 @@
|
||||||
|
import React, { FC, useEffect, useState } from "preact/compat";
|
||||||
|
import { gradMetal16 } from "../../../../utils/uplot/heatmap";
|
||||||
|
import "./style.scss";
|
||||||
|
import { TooltipHeatmapProps } from "../ChartTooltipHeatmap/ChartTooltipHeatmap";
|
||||||
|
import { SeriesItem } from "../../../../utils/uplot/series";
|
||||||
|
import LegendItem from "../../Line/Legend/LegendItem/LegendItem";
|
||||||
|
import { LegendItemType } from "../../../../utils/uplot/types";
|
||||||
|
|
||||||
|
interface LegendHeatmapProps {
|
||||||
|
min: number
|
||||||
|
max: number
|
||||||
|
legendValue: TooltipHeatmapProps | null,
|
||||||
|
series: SeriesItem[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const LegendHeatmap: FC<LegendHeatmapProps> = (
|
||||||
|
{
|
||||||
|
min,
|
||||||
|
max,
|
||||||
|
legendValue,
|
||||||
|
series,
|
||||||
|
}
|
||||||
|
) => {
|
||||||
|
|
||||||
|
const [percent, setPercent] = useState(0);
|
||||||
|
const [valueFormat, setValueFormat] = useState("");
|
||||||
|
const [minFormat, setMinFormat] = useState("");
|
||||||
|
const [maxFormat, setMaxFormat] = useState("");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const value = legendValue?.value || 0;
|
||||||
|
setPercent(value ? (value - min) / (max - min) * 100 : 0);
|
||||||
|
setValueFormat(value ? `${value}%` : "");
|
||||||
|
setMinFormat(`${min}%`);
|
||||||
|
setMaxFormat(`${max}%`);
|
||||||
|
}, [legendValue, min, max]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="vm-legend-heatmap__wrapper">
|
||||||
|
<div className="vm-legend-heatmap">
|
||||||
|
<div
|
||||||
|
className="vm-legend-heatmap-gradient"
|
||||||
|
style={{ background: `linear-gradient(to right, ${gradMetal16.join(", ")})` }}
|
||||||
|
>
|
||||||
|
{!!legendValue?.value && (
|
||||||
|
<div
|
||||||
|
className="vm-legend-heatmap-gradient__value"
|
||||||
|
style={{ left: `${percent}%` }}
|
||||||
|
>
|
||||||
|
<span>{valueFormat}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="vm-legend-heatmap__value">{minFormat}</div>
|
||||||
|
<div className="vm-legend-heatmap__value">{maxFormat}</div>
|
||||||
|
</div>
|
||||||
|
{series[1] && (
|
||||||
|
<LegendItem
|
||||||
|
key={series[1]?.label}
|
||||||
|
legend={series[1] as LegendItemType}
|
||||||
|
isHeatmap
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LegendHeatmap;
|
|
@ -7,6 +7,14 @@
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
|
|
||||||
|
&__wrapper {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: $padding-global;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
&__value {
|
&__value {
|
||||||
color: $color-text;
|
color: $color-text;
|
||||||
font-size: $font-size-small;
|
font-size: $font-size-small;
|
||||||
|
@ -52,4 +60,8 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__labels {
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -1,46 +0,0 @@
|
||||||
import React, { FC, useEffect, useState } from "preact/compat";
|
|
||||||
import { gradMetal16 } from "../../../utils/uplot/heatmap";
|
|
||||||
import "./style.scss";
|
|
||||||
|
|
||||||
interface LegendHeatmapProps {
|
|
||||||
min: number
|
|
||||||
max: number
|
|
||||||
value?: number
|
|
||||||
}
|
|
||||||
|
|
||||||
const LegendHeatmap: FC<LegendHeatmapProps> = ({ min, max, value }) => {
|
|
||||||
|
|
||||||
const [percent, setPercent] = useState(0);
|
|
||||||
const [valueFormat, setValueFormat] = useState("");
|
|
||||||
const [minFormat, setMinFormat] = useState("");
|
|
||||||
const [maxFormat, setMaxFormat] = useState("");
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setPercent(value ? (value - min) / (max - min) * 100 : 0);
|
|
||||||
setValueFormat(value ? `${value}%` : "");
|
|
||||||
setMinFormat(`${min}%`);
|
|
||||||
setMaxFormat(`${max}%`);
|
|
||||||
}, [value, min, max]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="vm-legend-heatmap">
|
|
||||||
<div
|
|
||||||
className="vm-legend-heatmap-gradient"
|
|
||||||
style={{ background: `linear-gradient(to right, ${gradMetal16.join(", ")})` }}
|
|
||||||
>
|
|
||||||
{!!value && (
|
|
||||||
<div
|
|
||||||
className="vm-legend-heatmap-gradient__value"
|
|
||||||
style={{ left: `${percent}%` }}
|
|
||||||
>
|
|
||||||
<span>{valueFormat}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="vm-legend-heatmap__value">{minFormat}</div>
|
|
||||||
<div className="vm-legend-heatmap__value">{maxFormat}</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default LegendHeatmap;
|
|
|
@ -1,17 +1,17 @@
|
||||||
import React, { FC, useEffect, useMemo, useRef, useState } from "preact/compat";
|
import React, { FC, useEffect, useMemo, useRef, useState } from "preact/compat";
|
||||||
import uPlot from "uplot";
|
import uPlot from "uplot";
|
||||||
import { MetricResult } from "../../../api/types";
|
import { MetricResult } from "../../../../api/types";
|
||||||
import { formatPrettyNumber } from "../../../utils/uplot/helpers";
|
import { formatPrettyNumber } from "../../../../utils/uplot/helpers";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import { DATE_FULL_TIMEZONE_FORMAT } from "../../../constants/date";
|
import { DATE_FULL_TIMEZONE_FORMAT } from "../../../../constants/date";
|
||||||
import ReactDOM from "react-dom";
|
import ReactDOM from "react-dom";
|
||||||
import get from "lodash.get";
|
import get from "lodash.get";
|
||||||
import Button from "../../Main/Button/Button";
|
import Button from "../../../Main/Button/Button";
|
||||||
import { CloseIcon, DragIcon } from "../../Main/Icons";
|
import { CloseIcon, DragIcon } from "../../../Main/Icons";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { MouseEvent as ReactMouseEvent } from "react";
|
import { MouseEvent as ReactMouseEvent } from "react";
|
||||||
import "./style.scss";
|
import "./style.scss";
|
||||||
import { SeriesItem } from "../../../utils/uplot/series";
|
import { SeriesItem } from "../../../../utils/uplot/series";
|
||||||
|
|
||||||
export interface ChartTooltipProps {
|
export interface ChartTooltipProps {
|
||||||
id: string,
|
id: string,
|
|
@ -78,5 +78,6 @@ $chart-tooltip-y: -1 * ($padding-small + $chart-tooltip-half-icon);
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-gap: 4px;
|
grid-gap: 4px;
|
||||||
word-break: break-all;
|
word-break: break-all;
|
||||||
|
white-space: pre-wrap;
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,7 +1,7 @@
|
||||||
import React, { FC, useMemo } from "preact/compat";
|
import React, { FC, useMemo } from "preact/compat";
|
||||||
import { LegendItemType } from "../../../utils/uplot/types";
|
import { LegendItemType } from "../../../../utils/uplot/types";
|
||||||
import LegendItem from "./LegendItem/LegendItem";
|
import LegendItem from "./LegendItem/LegendItem";
|
||||||
import Accordion from "../../Main/Accordion/Accordion";
|
import Accordion from "../../../Main/Accordion/Accordion";
|
||||||
import "./style.scss";
|
import "./style.scss";
|
||||||
|
|
||||||
interface LegendProps {
|
interface LegendProps {
|
|
@ -1,19 +1,23 @@
|
||||||
import React, { FC, useState, useMemo } from "preact/compat";
|
import React, { FC, useState, useMemo } from "preact/compat";
|
||||||
import { MouseEvent } from "react";
|
import { MouseEvent } from "react";
|
||||||
import { LegendItemType } from "../../../../utils/uplot/types";
|
import { LegendItemType } from "../../../../../utils/uplot/types";
|
||||||
import "./style.scss";
|
import "./style.scss";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import Tooltip from "../../../Main/Tooltip/Tooltip";
|
import Tooltip from "../../../../Main/Tooltip/Tooltip";
|
||||||
import { getFreeFields } from "./helpers";
|
import { getFreeFields } from "./helpers";
|
||||||
|
|
||||||
interface LegendItemProps {
|
interface LegendItemProps {
|
||||||
legend: LegendItemType;
|
legend: LegendItemType;
|
||||||
onChange: (item: LegendItemType, metaKey: boolean) => void;
|
onChange?: (item: LegendItemType, metaKey: boolean) => void;
|
||||||
|
isHeatmap?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const LegendItem: FC<LegendItemProps> = ({ legend, onChange }) => {
|
const LegendItem: FC<LegendItemProps> = ({ legend, onChange, isHeatmap }) => {
|
||||||
const [copiedValue, setCopiedValue] = useState("");
|
const [copiedValue, setCopiedValue] = useState("");
|
||||||
const freeFormFields = useMemo(() => getFreeFields(legend), [legend]);
|
const freeFormFields = useMemo(() => {
|
||||||
|
const result = getFreeFields(legend);
|
||||||
|
return isHeatmap ? result.filter(f => f.key !== "vmrange") : result;
|
||||||
|
}, [legend, isHeatmap]);
|
||||||
const calculations = legend.calculations;
|
const calculations = legend.calculations;
|
||||||
const showCalculations = Object.values(calculations).some(v => v);
|
const showCalculations = Object.values(calculations).some(v => v);
|
||||||
|
|
||||||
|
@ -24,7 +28,7 @@ const LegendItem: FC<LegendItemProps> = ({ legend, onChange }) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const createHandlerClick = (legend: LegendItemType) => (e: MouseEvent<HTMLDivElement>) => {
|
const createHandlerClick = (legend: LegendItemType) => (e: MouseEvent<HTMLDivElement>) => {
|
||||||
onChange(legend, e.ctrlKey || e.metaKey);
|
onChange && onChange(legend, e.ctrlKey || e.metaKey);
|
||||||
};
|
};
|
||||||
|
|
||||||
const createHandlerCopy = (freeField: string, id: string) => (e: MouseEvent<HTMLDivElement>) => {
|
const createHandlerCopy = (freeField: string, id: string) => (e: MouseEvent<HTMLDivElement>) => {
|
||||||
|
@ -37,18 +41,21 @@ const LegendItem: FC<LegendItemProps> = ({ legend, onChange }) => {
|
||||||
className={classNames({
|
className={classNames({
|
||||||
"vm-legend-item": true,
|
"vm-legend-item": true,
|
||||||
"vm-legend-row": true,
|
"vm-legend-row": true,
|
||||||
"vm-legend-item_hide": !legend.checked,
|
"vm-legend-item_hide": !legend.checked && !isHeatmap,
|
||||||
|
"vm-legend-item_static": isHeatmap,
|
||||||
})}
|
})}
|
||||||
onClick={createHandlerClick(legend)}
|
onClick={createHandlerClick(legend)}
|
||||||
>
|
>
|
||||||
<div
|
{!isHeatmap && (
|
||||||
className="vm-legend-item__marker"
|
<div
|
||||||
style={{ backgroundColor: legend.color }}
|
className="vm-legend-item__marker"
|
||||||
/>
|
style={{ backgroundColor: legend.color }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<div className="vm-legend-item-info">
|
<div className="vm-legend-item-info">
|
||||||
<span className="vm-legend-item-info__label">
|
<span className="vm-legend-item-info__label">
|
||||||
{legend.freeFormFields["__name__"]}
|
{legend.freeFormFields["__name__"]}
|
||||||
{
|
{!!freeFormFields.length && <>{</>}
|
||||||
{freeFormFields.map((f, i) => (
|
{freeFormFields.map((f, i) => (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
key={f.id}
|
key={f.id}
|
||||||
|
@ -66,10 +73,10 @@ const LegendItem: FC<LegendItemProps> = ({ legend, onChange }) => {
|
||||||
</span>
|
</span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
))}
|
))}
|
||||||
}
|
{!!freeFormFields.length && <>}</>}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{showCalculations && (
|
{!isHeatmap && showCalculations && (
|
||||||
<div className="vm-legend-item-values">
|
<div className="vm-legend-item-values">
|
||||||
median:{calculations.median}, min:{calculations.min}, max:{calculations.max}, last:{calculations.last}
|
median:{calculations.median}, min:{calculations.min}, max:{calculations.max}, last:{calculations.last}
|
||||||
</div>
|
</div>
|
|
@ -1,4 +1,4 @@
|
||||||
import { LegendItemType } from "../../../../utils/uplot/types";
|
import { LegendItemType } from "../../../../../utils/uplot/types";
|
||||||
|
|
||||||
export const getFreeFields = (legend: LegendItemType) => {
|
export const getFreeFields = (legend: LegendItemType) => {
|
||||||
const keys = Object.keys(legend.freeFormFields).filter(f => f !== "__name__");
|
const keys = Object.keys(legend.freeFormFields).filter(f => f !== "__name__");
|
|
@ -21,6 +21,17 @@
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&_static {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
cursor: default;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: $color-background-block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&__marker {
|
&__marker {
|
||||||
width: 14px;
|
width: 14px;
|
||||||
height: 14px;
|
height: 14px;
|
|
@ -7,22 +7,22 @@ import uPlot, {
|
||||||
Scales,
|
Scales,
|
||||||
Scale,
|
Scale,
|
||||||
} from "uplot";
|
} from "uplot";
|
||||||
import { defaultOptions } from "../../../utils/uplot/helpers";
|
import { defaultOptions } from "../../../../utils/uplot/helpers";
|
||||||
import { dragChart } from "../../../utils/uplot/events";
|
import { dragChart } from "../../../../utils/uplot/events";
|
||||||
import { getAxes, getMinMaxBuffer } from "../../../utils/uplot/axes";
|
import { getAxes, getMinMaxBuffer } from "../../../../utils/uplot/axes";
|
||||||
import { MetricResult } from "../../../api/types";
|
import { MetricResult } from "../../../../api/types";
|
||||||
import { dateFromSeconds, formatDateForNativeInput, limitsDurations } from "../../../utils/time";
|
import { dateFromSeconds, formatDateForNativeInput, limitsDurations } from "../../../../utils/time";
|
||||||
import throttle from "lodash.throttle";
|
import throttle from "lodash.throttle";
|
||||||
import useResize from "../../../hooks/useResize";
|
import useResize from "../../../../hooks/useResize";
|
||||||
import { TimeParams } from "../../../types";
|
import { TimeParams } from "../../../../types";
|
||||||
import { YaxisState } from "../../../state/graph/reducer";
|
import { YaxisState } from "../../../../state/graph/reducer";
|
||||||
import "uplot/dist/uPlot.min.css";
|
import "uplot/dist/uPlot.min.css";
|
||||||
import "./style.scss";
|
import "./style.scss";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import ChartTooltip, { ChartTooltipProps } from "../ChartTooltip/ChartTooltip";
|
import ChartTooltip, { ChartTooltipProps } from "../ChartTooltip/ChartTooltip";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import { useAppState } from "../../../state/common/StateContext";
|
import { useAppState } from "../../../../state/common/StateContext";
|
||||||
import { SeriesItem } from "../../../utils/uplot/series";
|
import { SeriesItem } from "../../../../utils/uplot/series";
|
||||||
|
|
||||||
export interface LineChartProps {
|
export interface LineChartProps {
|
||||||
metrics: MetricResult[];
|
metrics: MetricResult[];
|
|
@ -1,10 +1,10 @@
|
||||||
import React, { FC, useCallback, useEffect, useMemo, useRef, useState } from "preact/compat";
|
import React, { FC, useCallback, useEffect, useMemo, useRef, useState } from "preact/compat";
|
||||||
import { MetricResult } from "../../../api/types";
|
import { MetricResult } from "../../../api/types";
|
||||||
import LineChart from "../../Chart/LineChart/LineChart";
|
import LineChart from "../../Chart/Line/LineChart/LineChart";
|
||||||
import { AlignedData as uPlotData, Series as uPlotSeries } from "uplot";
|
import { AlignedData as uPlotData, Series as uPlotSeries } from "uplot";
|
||||||
import Legend from "../../Chart/Legend/Legend";
|
import Legend from "../../Chart/Line/Legend/Legend";
|
||||||
import LegendHeatmap from "../../Chart/LegendHeatmap/LegendHeatmap";
|
import LegendHeatmap from "../../Chart/Heatmap/LegendHeatmap/LegendHeatmap";
|
||||||
import { getHideSeries, getLegendItem, getSeriesItemContext } from "../../../utils/uplot/series";
|
import { getHideSeries, getLegendItem, getSeriesItemContext, SeriesItem } from "../../../utils/uplot/series";
|
||||||
import { getLimitsYAxis, getMinMaxBuffer, getTimeSeries } from "../../../utils/uplot/axes";
|
import { getLimitsYAxis, getMinMaxBuffer, getTimeSeries } from "../../../utils/uplot/axes";
|
||||||
import { LegendItemType } from "../../../utils/uplot/types";
|
import { LegendItemType } from "../../../utils/uplot/types";
|
||||||
import { TimeParams } from "../../../types";
|
import { TimeParams } from "../../../types";
|
||||||
|
@ -12,11 +12,12 @@ import { AxisRange, YaxisState } from "../../../state/graph/reducer";
|
||||||
import { getAvgFromArray, getMaxFromArray, getMinFromArray } from "../../../utils/math";
|
import { getAvgFromArray, getMaxFromArray, getMinFromArray } from "../../../utils/math";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { useTimeState } from "../../../state/time/TimeStateContext";
|
import { useTimeState } from "../../../state/time/TimeStateContext";
|
||||||
import HeatmapChart from "../../Chart/HeatmapChart/HeatmapChart";
|
import HeatmapChart from "../../Chart/Heatmap/HeatmapChart/HeatmapChart";
|
||||||
import "./style.scss";
|
import "./style.scss";
|
||||||
import { promValueToNumber } from "../../../utils/metric";
|
import { promValueToNumber } from "../../../utils/metric";
|
||||||
import { normalizeData } from "../../../utils/uplot/heatmap";
|
import { normalizeData } from "../../../utils/uplot/heatmap";
|
||||||
import useDeviceDetect from "../../../hooks/useDeviceDetect";
|
import useDeviceDetect from "../../../hooks/useDeviceDetect";
|
||||||
|
import { TooltipHeatmapProps } from "../../Chart/Heatmap/ChartTooltipHeatmap/ChartTooltipHeatmap";
|
||||||
|
|
||||||
export interface GraphViewProps {
|
export interface GraphViewProps {
|
||||||
data?: MetricResult[];
|
data?: MetricResult[];
|
||||||
|
@ -60,7 +61,7 @@ const GraphView: FC<GraphViewProps> = ({
|
||||||
const [series, setSeries] = useState<uPlotSeries[]>([]);
|
const [series, setSeries] = useState<uPlotSeries[]>([]);
|
||||||
const [legend, setLegend] = useState<LegendItemType[]>([]);
|
const [legend, setLegend] = useState<LegendItemType[]>([]);
|
||||||
const [hideSeries, setHideSeries] = useState<string[]>([]);
|
const [hideSeries, setHideSeries] = useState<string[]>([]);
|
||||||
const [legendValue, setLegendValue] = useState(0);
|
const [legendValue, setLegendValue] = useState<TooltipHeatmapProps | null>(null);
|
||||||
|
|
||||||
const setLimitsYaxis = (values: {[key: string]: number[]}) => {
|
const setLimitsYaxis = (values: {[key: string]: number[]}) => {
|
||||||
const limits = getLimitsYAxis(values, !isHistogram);
|
const limits = getLimitsYAxis(values, !isHistogram);
|
||||||
|
@ -71,7 +72,7 @@ const GraphView: FC<GraphViewProps> = ({
|
||||||
setHideSeries(getHideSeries({ hideSeries, legend, metaKey, series }));
|
setHideSeries(getHideSeries({ hideSeries, legend, metaKey, series }));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleChangeLegend = (val: number) => {
|
const handleChangeLegend = (val: TooltipHeatmapProps) => {
|
||||||
setLegendValue(val);
|
setLegendValue(val);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -209,9 +210,10 @@ const GraphView: FC<GraphViewProps> = ({
|
||||||
)}
|
)}
|
||||||
{isHistogram && showLegend && (
|
{isHistogram && showLegend && (
|
||||||
<LegendHeatmap
|
<LegendHeatmap
|
||||||
|
series={series as SeriesItem[]}
|
||||||
min={yaxis.limits.range[1][0] || 0}
|
min={yaxis.limits.range[1][0] || 0}
|
||||||
max={yaxis.limits.range[1][1] || 0}
|
max={yaxis.limits.range[1][1] || 0}
|
||||||
value={legendValue}
|
legendValue={legendValue}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -79,7 +79,7 @@ export const useFetchQuery = ({
|
||||||
setFetchQueue([...fetchQueue, controller]);
|
setFetchQueue([...fetchQueue, controller]);
|
||||||
try {
|
try {
|
||||||
const isDisplayChart = displayType === "chart";
|
const isDisplayChart = displayType === "chart";
|
||||||
const seriesLimit = showAllSeries ? Infinity : (+stateSeriesLimits[displayType] || Infinity);
|
let seriesLimit = showAllSeries ? Infinity : (+stateSeriesLimits[displayType] || Infinity);
|
||||||
const tempData: MetricBase[] = [];
|
const tempData: MetricBase[] = [];
|
||||||
const tempTraces: Trace[] = [];
|
const tempTraces: Trace[] = [];
|
||||||
let counter = 1;
|
let counter = 1;
|
||||||
|
@ -104,6 +104,8 @@ export const useFetchQuery = ({
|
||||||
tempTraces.push(trace);
|
tempTraces.push(trace);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isHistogramResult = isDisplayChart && isHistogramData(resp.data.result);
|
||||||
|
if (isHistogramResult) seriesLimit = Infinity;
|
||||||
const freeTempSize = seriesLimit - tempData.length;
|
const freeTempSize = seriesLimit - tempData.length;
|
||||||
resp.data.result.slice(0, freeTempSize).forEach((d: MetricBase) => {
|
resp.data.result.slice(0, freeTempSize).forEach((d: MetricBase) => {
|
||||||
d.group = counter;
|
d.group = counter;
|
||||||
|
|
|
@ -31,14 +31,13 @@ export const promValueToNumber = (s: string): number => {
|
||||||
|
|
||||||
export const isHistogramData = (result: MetricBase[]) => {
|
export const isHistogramData = (result: MetricBase[]) => {
|
||||||
if (result.length < 2) return false;
|
if (result.length < 2) return false;
|
||||||
const histogramNames = ["le", "vmrange"];
|
const histogramLabels = ["le", "vmrange"];
|
||||||
|
|
||||||
return result.every(r => {
|
const firstLabels = Object.keys(result[0].metric).filter(n => !histogramLabels.includes(n));
|
||||||
const keys = Object.keys(r.metric);
|
const isHistogram = result.every(r => {
|
||||||
const labels = Object.keys(r.metric).filter(n => !histogramNames.includes(n));
|
const labels = Object.keys(r.metric).filter(n => !histogramLabels.includes(n));
|
||||||
const byName = keys.length > labels.length;
|
return firstLabels.length === labels.length && labels.every(l => r.metric[l] === result[0].metric[l]);
|
||||||
const byLabels = labels.every(l => r.metric[l] === result[0].metric[l]);
|
|
||||||
|
|
||||||
return byName && byLabels;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return isHistogram && result.every(r => histogramLabels.some(l => l in r.metric));
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import uPlot from "uplot";
|
import uPlot from "uplot";
|
||||||
import { generateGradient } from "../color";
|
import { generateGradient } from "../color";
|
||||||
import { MetricResult } from "../../api/types";
|
import { MetricResult } from "../../api/types";
|
||||||
|
import { promValueToNumber } from "../metric";
|
||||||
|
|
||||||
// 16-color gradient from "rgb(246, 226, 219)" to "rgb(127, 39, 4)"
|
// 16-color gradient from "rgb(246, 226, 219)" to "rgb(127, 39, 4)"
|
||||||
export const gradMetal16 = generateGradient([246, 226, 219], [127, 39, 4], 16);
|
export const gradMetal16 = generateGradient([246, 226, 219], [127, 39, 4], 16);
|
||||||
|
@ -115,11 +116,11 @@ export const convertPrometheusToVictoriaMetrics = (buckets: MetricResult[]): Met
|
||||||
|
|
||||||
const sortedBuckets = buckets.sort((a,b) => parseFloat(a.metric.le) - parseFloat(b.metric.le));
|
const sortedBuckets = buckets.sort((a,b) => parseFloat(a.metric.le) - parseFloat(b.metric.le));
|
||||||
const group = buckets[0]?.group || 1;
|
const group = buckets[0]?.group || 1;
|
||||||
let prevBucket: MetricResult = { metric: { le: "0" }, values: [], group };
|
let prevBucket: MetricResult = { metric: { le: "" }, values: [], group };
|
||||||
const result: MetricResult[] = [];
|
const result: MetricResult[] = [];
|
||||||
|
|
||||||
for (const bucket of sortedBuckets) {
|
for (const bucket of sortedBuckets) {
|
||||||
const vmrange = `${prevBucket.metric.le}..${bucket.metric.le}`;
|
const vmrange = [prevBucket.metric.le, bucket.metric.le].filter(n => n).join("...");
|
||||||
const values: [number, string][] = [];
|
const values: [number, string][] = [];
|
||||||
|
|
||||||
for (const [timestamp, value] of bucket.values) {
|
for (const [timestamp, value] of bucket.values) {
|
||||||
|
@ -135,14 +136,25 @@ export const convertPrometheusToVictoriaMetrics = (buckets: MetricResult[]): Met
|
||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getUpperBound = (bucket: MetricResult) => {
|
||||||
|
const values = (bucket.metric.vmrange || bucket.metric.le).split("...");
|
||||||
|
return promValueToNumber(values[values.length - 1]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const sortBucketsByValues = (a: MetricResult, b: MetricResult) => getUpperBound(a) - getUpperBound(b);
|
||||||
|
|
||||||
export const normalizeData = (buckets: MetricResult[], isHistogram?: boolean): MetricResult[] => {
|
export const normalizeData = (buckets: MetricResult[], isHistogram?: boolean): MetricResult[] => {
|
||||||
if (!isHistogram) return buckets;
|
if (!isHistogram) return buckets;
|
||||||
const vmBuckets = convertPrometheusToVictoriaMetrics(buckets);
|
const sortedBuckets = buckets.sort(sortBucketsByValues);
|
||||||
|
const vmBuckets = convertPrometheusToVictoriaMetrics(sortedBuckets);
|
||||||
const allValues = vmBuckets.map(b => b.values).flat();
|
const allValues = vmBuckets.map(b => b.values).flat();
|
||||||
|
|
||||||
return vmBuckets.map(bucket => {
|
return vmBuckets.map(bucket => {
|
||||||
const values = bucket.values.map((v) => {
|
const values = bucket.values.map((v) => {
|
||||||
const totalHits = allValues.filter(av => av[0] === v[0]).reduce((bucketSum, v) => bucketSum + +v[1], 0);
|
const totalHits = allValues
|
||||||
|
.filter(av => av[0] === v[0])
|
||||||
|
.reduce((bucketSum, v) => bucketSum + +v[1], 0);
|
||||||
|
|
||||||
return [v[0], `${Math.round((+v[1] / totalHits) * 100)}`];
|
return [v[0], `${Math.round((+v[1] / totalHits) * 100)}`];
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue