mirror of
https://github.com/VictoriaMetrics/VictoriaMetrics.git
synced 2024-11-21 14:44:00 +00:00
vmui: graph fixes (#1982)
* fix: remove disabling custom step when zooming * feat: add a dynamic calc of the width of the graph * fix: add validate y-axis limits * fix: correct axis limits for value 0 * fix: change logic create time series * fix: change types for tooltip * fix: correct points on the line * fix: change the logic for set graph width * fix: stop checking the period when auto-refresh is enabled * app/vmselect/vmui: `make vmui-update` Co-authored-by: Aliaksandr Valialkin <valyala@victoriametrics.com>
This commit is contained in:
parent
871528fedb
commit
718c352946
18 changed files with 76 additions and 43 deletions
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"files": {
|
||||
"main.css": "./static/css/main.a33903a8.css",
|
||||
"main.js": "./static/js/main.4305bd17.js",
|
||||
"main.js": "./static/js/main.23f635e5.js",
|
||||
"static/js/27.85f0e2b0.chunk.js": "./static/js/27.85f0e2b0.chunk.js",
|
||||
"index.html": "./index.html"
|
||||
},
|
||||
"entrypoints": [
|
||||
"static/css/main.a33903a8.css",
|
||||
"static/js/main.4305bd17.js"
|
||||
"static/js/main.23f635e5.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"/><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.4305bd17.js"></script><link href="./static/css/main.a33903a8.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.23f635e5.js"></script><link href="./static/css/main.a33903a8.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>
|
2
app/vmselect/vmui/static/js/main.23f635e5.js
Normal file
2
app/vmselect/vmui/static/js/main.23f635e5.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -15,6 +15,7 @@ const AxesLimitsConfigurator: FC = () => {
|
|||
const onChangeLimit = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>, axis: string, index: number) => {
|
||||
const newLimits = yaxis.limits.range;
|
||||
newLimits[axis][index] = +e.target.value;
|
||||
if (newLimits[axis][0] === newLimits[axis][1] || newLimits[axis][0] > newLimits[axis][1]) return;
|
||||
graphDispatch({type: "SET_YAXIS_LIMITS", payload: newLimits});
|
||||
};
|
||||
const debouncedOnChangeLimit = useCallback(debounce(onChangeLimit, 500), [yaxis.limits.range]);
|
||||
|
|
|
@ -9,7 +9,7 @@ const StepConfigurator: FC = () => {
|
|||
const {customStep} = useGraphState();
|
||||
const graphDispatch = useGraphDispatch();
|
||||
const [error, setError] = useState(false);
|
||||
const {time: {period: {step}, duration}} = useAppState();
|
||||
const {time: {period: {step}}} = useAppState();
|
||||
|
||||
const onChangeStep = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
||||
const value = +e.target.value;
|
||||
|
@ -28,10 +28,6 @@ const StepConfigurator: FC = () => {
|
|||
graphDispatch({type: "TOGGLE_CUSTOM_STEP"});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (customStep.enable) onChangeEnableStep();
|
||||
}, [duration]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!customStep.enable) graphDispatch({type: "SET_CUSTOM_STEP", payload: step || 1});
|
||||
}, [step]);
|
||||
|
|
|
@ -18,7 +18,7 @@ export const useFetchQuery = (): {
|
|||
liveData?: InstantMetricResult[],
|
||||
error?: ErrorTypes | string,
|
||||
} => {
|
||||
const {query, displayType, serverUrl, time: {period}, queryControls: {nocache}} = useAppState();
|
||||
const {query, displayType, serverUrl, time: {period}, queryControls: {nocache, autoRefresh}} = useAppState();
|
||||
|
||||
const {basicData, bearerData, authMethod} = useAuthState();
|
||||
const {customStep} = useGraphState();
|
||||
|
@ -37,7 +37,7 @@ export const useFetchQuery = (): {
|
|||
}, [error]);
|
||||
|
||||
const needUpdateData = useMemo(() => {
|
||||
if (!prevPeriod) return true;
|
||||
if (!prevPeriod || autoRefresh) return true;
|
||||
const duration = (prevPeriod.end - prevPeriod.start) / 3;
|
||||
const factorLimit = duration / (period.end - period.start) >= 0.7;
|
||||
const maxLimit = period.end > (prevPeriod.end + duration);
|
||||
|
|
|
@ -16,7 +16,7 @@ const HomeLayout: FC = () => {
|
|||
const {isLoading, liveData, graphData, error} = useFetchQuery();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box id="homeLayout">
|
||||
<AppBar position="static">
|
||||
<Toolbar>
|
||||
<Box display="flex">
|
||||
|
@ -78,7 +78,7 @@ const HomeLayout: FC = () => {
|
|||
</Box>}
|
||||
</Box>
|
||||
</Box>
|
||||
</>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
import React, {FC, useEffect, useState} from "react";
|
||||
import React, {FC, useEffect, useMemo, useState} from "react";
|
||||
import {MetricResult} from "../../../api/types";
|
||||
import LineChart from "../../LineChart/LineChart";
|
||||
import {AlignedData as uPlotData, Series as uPlotSeries} from "uplot";
|
||||
import Legend from "../../Legend/Legend";
|
||||
import {useGraphDispatch} from "../../../state/graph/GraphStateContext";
|
||||
import {useGraphDispatch, useGraphState} from "../../../state/graph/GraphStateContext";
|
||||
import {getHideSeries, getLegendItem, getSeriesItem} from "../../../utils/uplot/series";
|
||||
import {getLimitsYAxis, getTimeSeries} from "../../../utils/uplot/axes";
|
||||
import {LegendItem} from "../../../utils/uplot/types";
|
||||
import {AxisRange} from "../../../state/graph/reducer";
|
||||
import GraphSettings from "../Configurator/Graph/GraphSettings";
|
||||
import {useAppState} from "../../../state/common/StateContext";
|
||||
|
||||
export interface GraphViewProps {
|
||||
data?: MetricResult[];
|
||||
|
@ -16,16 +16,17 @@ export interface GraphViewProps {
|
|||
|
||||
const GraphView: FC<GraphViewProps> = ({data = []}) => {
|
||||
const graphDispatch = useGraphDispatch();
|
||||
const {time: {period}} = useAppState();
|
||||
const { customStep } = useGraphState();
|
||||
const currentStep = useMemo(() => customStep.enable ? customStep.value : period.step || 1, [period.step, customStep]);
|
||||
|
||||
const [dataChart, setDataChart] = useState<uPlotData>([[]]);
|
||||
const [series, setSeries] = useState<uPlotSeries[]>([]);
|
||||
const [legend, setLegend] = useState<LegendItem[]>([]);
|
||||
const [hideSeries, setHideSeries] = useState<string[]>([]);
|
||||
const [valuesLimit, setValuesLimit] = useState<AxisRange>({"1": [0, 1]});
|
||||
|
||||
const setLimitsYaxis = (values: {[key: string]: number[]}) => {
|
||||
const limits = getLimitsYAxis(values);
|
||||
setValuesLimit(limits);
|
||||
graphDispatch({type: "SET_YAXIS_LIMITS", payload: limits});
|
||||
};
|
||||
|
||||
|
@ -50,9 +51,12 @@ const GraphView: FC<GraphViewProps> = ({data = []}) => {
|
|||
});
|
||||
});
|
||||
|
||||
const timeSeries = getTimeSeries(tempTimes);
|
||||
const timeSeries = getTimeSeries(tempTimes, currentStep, period);
|
||||
setDataChart([timeSeries, ...data.map(d => {
|
||||
return new Array(timeSeries.length).fill(1).map((v, i) => d.values[i] ? +d.values[i][1] : null);
|
||||
return timeSeries.map(t => {
|
||||
const value = d.values.find(v => v[0] === t);
|
||||
return value ? +value[1] : null;
|
||||
});
|
||||
})] as uPlotData);
|
||||
setLimitsYaxis(tempValues);
|
||||
|
||||
|
@ -79,7 +83,7 @@ const GraphView: FC<GraphViewProps> = ({data = []}) => {
|
|||
{(data.length > 0)
|
||||
? <div>
|
||||
<GraphSettings/>
|
||||
<LineChart data={dataChart} series={series} metrics={data} limits={valuesLimit}/>
|
||||
<LineChart data={dataChart} series={series} metrics={data}/>
|
||||
<Legend labels={legend} onChange={onChangeLegend}/>
|
||||
</div>
|
||||
: <div style={{textAlign: "center"}}>No data to show</div>}
|
||||
|
|
|
@ -11,29 +11,28 @@ import {limitsDurations} from "../../utils/time";
|
|||
import throttle from "lodash.throttle";
|
||||
import "uplot/dist/uPlot.min.css";
|
||||
import "./tooltip.css";
|
||||
import {AxisRange} from "../../state/graph/reducer";
|
||||
import useResize from "../../hooks/useResize";
|
||||
|
||||
export interface LineChartProps {
|
||||
metrics: MetricResult[];
|
||||
data: uPlotData;
|
||||
series: uPlotSeries[];
|
||||
limits: AxisRange;
|
||||
}
|
||||
enum typeChartUpdate {xRange = "xRange", yRange = "yRange", data = "data"}
|
||||
|
||||
const LineChart: FC<LineChartProps> = ({data, series, metrics = [], limits}) => {
|
||||
const LineChart: FC<LineChartProps> = ({data, series, metrics = []}) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const {time: {period}} = useAppState();
|
||||
const {yaxis} = useGraphState();
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const uPlotRef = useRef<HTMLDivElement>(null);
|
||||
const [isPanning, setPanning] = useState(false);
|
||||
const [xRange, setXRange] = useState({min: period.start, max: period.end});
|
||||
const [uPlotInst, setUPlotInst] = useState<uPlot>();
|
||||
const layoutSize = useResize(document.getElementById("homeLayout"));
|
||||
|
||||
const tooltip = document.createElement("div");
|
||||
tooltip.className = "u-tooltip";
|
||||
const tooltipIdx = {seriesIdx: 1, dataIdx: 0};
|
||||
const tooltipIdx: {seriesIdx: number | null, dataIdx: number | undefined} = {seriesIdx: null, dataIdx: undefined};
|
||||
const tooltipOffset = {left: 0, top: 0};
|
||||
|
||||
const setScale = ({min, max}: { min: number, max: number }): void => {
|
||||
|
@ -73,22 +72,22 @@ const LineChart: FC<LineChartProps> = ({data, series, metrics = [], limits}) =>
|
|||
const setCursor = (u: uPlot) => {
|
||||
if (tooltipIdx.dataIdx === u.cursor.idx) return;
|
||||
tooltipIdx.dataIdx = u.cursor.idx || 0;
|
||||
if (tooltipIdx.seriesIdx && tooltipIdx.dataIdx) {
|
||||
if (tooltipIdx.seriesIdx !== null && tooltipIdx.dataIdx !== undefined) {
|
||||
setTooltip({u, tooltipIdx, metrics, series, tooltip, tooltipOffset});
|
||||
}
|
||||
};
|
||||
|
||||
const seriesFocus = (u: uPlot, sidx: (number | null)) => {
|
||||
if (tooltipIdx.seriesIdx === sidx) return;
|
||||
tooltipIdx.seriesIdx = sidx || 0;
|
||||
sidx && tooltipIdx.dataIdx
|
||||
tooltipIdx.seriesIdx = sidx;
|
||||
sidx && tooltipIdx.dataIdx !== undefined
|
||||
? setTooltip({u, tooltipIdx, metrics, series, tooltip, tooltipOffset})
|
||||
: tooltip.style.display = "none";
|
||||
};
|
||||
const getRangeX = (): Range.MinMax => [xRange.min, xRange.max];
|
||||
const getRangeY = (u: uPlot, min = 0, max = 1, axis: string): Range.MinMax => {
|
||||
if (yaxis.limits.enable) return yaxis.limits.range[axis];
|
||||
return min && max ? [min - (min * 0.05), max + (max * 0.05)] : limits[axis];
|
||||
return min && max ? [min - (min * 0.25), max + (max * 0.25)] : [-1, 1];
|
||||
};
|
||||
|
||||
const getScales = (): Scales => {
|
||||
|
@ -104,7 +103,7 @@ const LineChart: FC<LineChartProps> = ({data, series, metrics = [], limits}) =>
|
|||
series,
|
||||
axes: getAxes(series),
|
||||
scales: {...getScales()},
|
||||
width: containerRef.current ? containerRef.current.offsetWidth : 400,
|
||||
width: layoutSize.width ? layoutSize.width - 64 : 400,
|
||||
plugins: [{hooks: {ready: onReadyChart, setCursor, setSeries: seriesFocus}}],
|
||||
};
|
||||
|
||||
|
@ -135,13 +134,13 @@ const LineChart: FC<LineChartProps> = ({data, series, metrics = [], limits}) =>
|
|||
setUPlotInst(u);
|
||||
setXRange({min: period.start, max: period.end});
|
||||
return u.destroy;
|
||||
}, [uPlotRef.current, series]);
|
||||
}, [uPlotRef.current, series, layoutSize]);
|
||||
|
||||
useEffect(() => updateChart(typeChartUpdate.data), [data]);
|
||||
useEffect(() => updateChart(typeChartUpdate.xRange), [xRange]);
|
||||
useEffect(() => updateChart(typeChartUpdate.yRange), [yaxis]);
|
||||
|
||||
return <div ref={containerRef} style={{pointerEvents: isPanning ? "none" : "auto", height: "500px"}}>
|
||||
return <div style={{pointerEvents: isPanning ? "none" : "auto", height: "500px"}}>
|
||||
<div ref={uPlotRef}/>
|
||||
</div>;
|
||||
};
|
||||
|
|
23
app/vmui/packages/vmui/src/hooks/useResize.ts
Normal file
23
app/vmui/packages/vmui/src/hooks/useResize.ts
Normal file
|
@ -0,0 +1,23 @@
|
|||
import { useState, useEffect } from "react";
|
||||
|
||||
const useResize = (node: HTMLElement | null): {width: number, height: number} => {
|
||||
const [windowSize, setWindowSize] = useState({
|
||||
width: 0,
|
||||
height: 0,
|
||||
});
|
||||
useEffect(() => {
|
||||
if (!node) return;
|
||||
const handleResize = () => {
|
||||
setWindowSize({
|
||||
width: node.offsetWidth,
|
||||
height: node.offsetHeight,
|
||||
});
|
||||
};
|
||||
window.addEventListener("resize", handleResize);
|
||||
handleResize();
|
||||
return () => window.removeEventListener("resize", handleResize);
|
||||
}, []);
|
||||
return windowSize;
|
||||
};
|
||||
|
||||
export default useResize;
|
|
@ -21,7 +21,7 @@ export interface GraphState {
|
|||
|
||||
export type GraphAction =
|
||||
| { type: "TOGGLE_ENABLE_YAXIS_LIMITS" }
|
||||
| { type: "SET_YAXIS_LIMITS", payload: { [key: string]: [number, number] } }
|
||||
| { type: "SET_YAXIS_LIMITS", payload: AxisRange }
|
||||
| { type: "TOGGLE_CUSTOM_STEP" }
|
||||
| { type: "SET_CUSTOM_STEP", payload: number}
|
||||
|
||||
|
|
|
@ -3,6 +3,7 @@ import {getMaxFromArray, getMinFromArray} from "../math";
|
|||
import {roundTimeSeconds} from "../time";
|
||||
import {AxisRange} from "../../state/graph/reducer";
|
||||
import {formatTicks} from "./helpers";
|
||||
import {TimeParams} from "../../types";
|
||||
|
||||
export const getAxes = (series: Series[]): Axis[] => Array.from(new Set(series.map(s => s.scale))).map(a => {
|
||||
const axis = {scale: a, show: true, font: "10px Arial", values: formatTicks};
|
||||
|
@ -11,11 +12,11 @@ export const getAxes = (series: Series[]): Axis[] => Array.from(new Set(series.m
|
|||
return axis;
|
||||
});
|
||||
|
||||
export const getTimeSeries = (times: number[]): number[] => {
|
||||
export const getTimeSeries = (times: number[], defaultStep: number, period: TimeParams): number[] => {
|
||||
const allTimes = Array.from(new Set(times)).sort((a, b) => a - b);
|
||||
const step = getMinFromArray(allTimes.map((t, i) => allTimes[i + 1] - t));
|
||||
const length = Math.ceil((period.end - period.start)/defaultStep);
|
||||
const startTime = allTimes[0] || 0;
|
||||
return new Array(allTimes.length).fill(startTime).map((d, i) => roundTimeSeconds(d + (step * i)));
|
||||
return new Array(length*2).fill(startTime).map((d, i) => roundTimeSeconds(d + (defaultStep * i)));
|
||||
};
|
||||
|
||||
export const getLimitsYAxis = (values: { [key: string]: number[] }): AxisRange => {
|
||||
|
@ -24,7 +25,7 @@ export const getLimitsYAxis = (values: { [key: string]: number[] }): AxisRange =
|
|||
const numbers = values[key];
|
||||
const min = getMinFromArray(numbers);
|
||||
const max = getMaxFromArray(numbers);
|
||||
result[key] = [min - (min * 0.05), max + (max * 0.05)];
|
||||
result[key] = min && max ? [min - (min * 0.25), max + (max * 0.25)] : [-1, 1];
|
||||
}
|
||||
return result;
|
||||
};
|
|
@ -15,6 +15,10 @@ export const defaultOptions = {
|
|||
focus: {
|
||||
prox: 30
|
||||
},
|
||||
points: {
|
||||
size: 5.6,
|
||||
width: 1.4
|
||||
},
|
||||
bind: {
|
||||
mouseup: (): null => null,
|
||||
mousedown: (): null => null,
|
||||
|
|
|
@ -10,10 +10,14 @@ export const getSeriesItem = (d: MetricResult, hideSeries: string[]): Series =>
|
|||
return {
|
||||
label,
|
||||
dash: getDashLine(d.group),
|
||||
width: 1.5,
|
||||
width: 1.4,
|
||||
stroke: getColorLine(d.group, label),
|
||||
show: !includesHideSeries(label, d.group, hideSeries),
|
||||
scale: String(d.group)
|
||||
scale: String(d.group),
|
||||
points: {
|
||||
size: 4.2,
|
||||
width: 1.4
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@ import {getColorLine} from "./helpers";
|
|||
|
||||
export const setTooltip = ({u, tooltipIdx, metrics, series, tooltip, tooltipOffset}: SetupTooltip): void => {
|
||||
const {seriesIdx, dataIdx} = tooltipIdx;
|
||||
if (seriesIdx === null || dataIdx === undefined) return;
|
||||
const dataSeries = u.data[seriesIdx][dataIdx];
|
||||
const dataTime = u.data[0][dataIdx];
|
||||
const metric = metrics[seriesIdx - 1]?.metric || {};
|
||||
|
|
|
@ -11,8 +11,8 @@ export interface SetupTooltip {
|
|||
top: number
|
||||
},
|
||||
tooltipIdx: {
|
||||
seriesIdx: number,
|
||||
dataIdx: number
|
||||
seriesIdx: number | null,
|
||||
dataIdx: number | undefined
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue