vmui: small changes on explore metrics page (#3634)

* fix: change issue link

* fix: remove legend toggle

* fix: move select graph size

* feat: save url params on explore metrics page

* wip

Co-authored-by: Aliaksandr Valialkin <valyala@victoriametrics.com>
This commit is contained in:
Yury Molodov 2023-01-12 07:16:10 +01:00 committed by Aliaksandr Valialkin
parent efec0f150f
commit 4295ca2ce2
No known key found for this signature in database
GPG key ID: A72BEC6CD3D0DED1
24 changed files with 147 additions and 180 deletions

View file

@ -1,12 +1,12 @@
{
"files": {
"main.css": "./static/css/main.e9e7cdb7.css",
"main.js": "./static/js/main.d34bbb5e.js",
"main.css": "./static/css/main.8692abc6.css",
"main.js": "./static/js/main.9c17bdf0.js",
"static/js/27.c1ccfd29.chunk.js": "./static/js/27.c1ccfd29.chunk.js",
"index.html": "./index.html"
},
"entrypoints": [
"static/css/main.e9e7cdb7.css",
"static/js/main.d34bbb5e.js"
"static/css/main.8692abc6.css",
"static/js/main.9c17bdf0.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="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin><link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono&family=Lato:wght@300;400;700&display=swap" rel="stylesheet"><script src="./dashboards/index.js" type="module"></script><script defer="defer" src="./static/js/main.d34bbb5e.js"></script><link href="./static/css/main.e9e7cdb7.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="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin><link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono&family=Lato:wght@300;400;700&display=swap" rel="stylesheet"><script src="./dashboards/index.js" type="module"></script><script defer="defer" src="./static/js/main.9c17bdf0.js"></script><link href="./static/css/main.8692abc6.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

@ -17,6 +17,7 @@ export interface ChartTooltipProps {
u: uPlot,
metrics: MetricResult[],
series: Series[],
yRange: number[];
unit?: string,
isSticky?: boolean,
tooltipOffset: { left: number, top: number },
@ -30,6 +31,7 @@ const ChartTooltip: FC<ChartTooltipProps> = ({
unit = "",
metrics,
series,
yRange,
tooltipIdx,
tooltipOffset,
isSticky,
@ -46,22 +48,24 @@ const ChartTooltip: FC<ChartTooltipProps> = ({
const targetPortal = useMemo(() => u.root.querySelector(".u-wrap"), [u]);
const value = useMemo(() => get(u, ["data", seriesIdx, dataIdx], 0), [u, seriesIdx, dataIdx]);
const valueFormat = useMemo(() => formatPrettyNumber(value), [value]);
const dataTime = useMemo(() => u.data[0][dataIdx], [u, dataIdx]);
const date = useMemo(() => dayjs(dataTime * 1000).tz().format(DATE_FULL_TIMEZONE_FORMAT), [dataTime]);
const value = get(u, ["data", seriesIdx, dataIdx], 0);
const valueFormat = formatPrettyNumber(value, get(yRange, [0]), get(yRange, [1]));
const dataTime = u.data[0][dataIdx];
const date = dayjs(dataTime * 1000).tz().format(DATE_FULL_TIMEZONE_FORMAT);
const color = useMemo(() => series[seriesIdx]?.stroke+"", [series, seriesIdx]);
const color = series[seriesIdx]?.stroke+"";
const name = useMemo(() => {
const group = metrics[seriesIdx -1]?.group || 0;
return `Query ${group}`;
}, [series, seriesIdx]);
const groups = new Set();
metrics.forEach(m => groups.add(m.group));
const groupsSize = groups.size;
const group = metrics[seriesIdx-1]?.group || 0;
const metric = metrics[seriesIdx-1]?.metric || {};
const labelNames = Object.keys(metric).filter(x => x != "__name__");
const metricName = metric["__name__"] || "value";
const fields = useMemo(() => {
const metric = metrics[seriesIdx - 1]?.metric || {};
const fields = Object.keys(metric);
return fields.map(key => `${key}=${JSON.stringify(metric[key])}`);
return labelNames.map(key => `${key}=${JSON.stringify(metric[key])}`);
}, [metrics, seriesIdx]);
const handleClose = () => {
@ -136,7 +140,12 @@ const ChartTooltip: FC<ChartTooltipProps> = ({
style={position}
>
<div className="vm-chart-tooltip-header">
<div className="vm-chart-tooltip-header__date">{date}</div>
<div className="vm-chart-tooltip-header__date">
{groupsSize > 1 && (
<div>Query {group}</div>
)}
{date}
</div>
{isSticky && (
<>
<Button
@ -162,7 +171,7 @@ const ChartTooltip: FC<ChartTooltipProps> = ({
style={{ background: color }}
/>
<p>
{name}:
{metricName}:
<b className="vm-chart-tooltip-data__value">{valueFormat}</b>
{unit}
</p>

View file

@ -1,5 +1,5 @@
@use "src/styles/variables" as *;
$chart-tooltip-width: 300px;
$chart-tooltip-width: 325px;
$chart-tooltip-icon-width: 25px;
$chart-tooltip-half-icon: calc($chart-tooltip-icon-width/2);
$chart-tooltip-date-width: $chart-tooltip-width - (2*$chart-tooltip-icon-width) - (2*$padding-global) - $padding-small;

View file

@ -50,6 +50,7 @@ const LineChart: FC<LineChartProps> = ({
const uPlotRef = useRef<HTMLDivElement>(null);
const [isPanning, setPanning] = useState(false);
const [xRange, setXRange] = useState({ min: period.start, max: period.end });
const [yRange, setYRange] = useState([0, 1]);
const [uPlotInst, setUPlotInst] = useState<uPlot>();
const layoutSize = useResize(container);
@ -128,6 +129,7 @@ const LineChart: FC<LineChartProps> = ({
unit,
series,
metrics,
yRange,
tooltipIdx,
tooltipOffset,
};
@ -153,7 +155,11 @@ const LineChart: FC<LineChartProps> = ({
};
const getRangeX = (): Range.MinMax => [xRange.min, xRange.max];
const getRangeY = (u: uPlot, min = 0, max = 1, axis: string): Range.MinMax => {
if (axis == "1") {
setYRange([min, max]);
}
if (yaxis.limits.enable) return yaxis.limits.range[axis];
return getMinMaxBuffer(min, max);
};
@ -258,6 +264,7 @@ const LineChart: FC<LineChartProps> = ({
u={uPlotInst}
series={series}
metrics={metrics}
yRange={yRange}
tooltipIdx={tooltipIdx}
tooltipOffset={tooltipOffset}
id={tooltipId}

View file

@ -15,7 +15,6 @@ interface ExploreMetricItemGraphProps {
instance: string,
rateEnabled: boolean,
isBucket: boolean,
showLegend: boolean
height?: number
}
@ -25,7 +24,6 @@ const ExploreMetricItem: FC<ExploreMetricItemGraphProps> = ({
instance,
rateEnabled,
isBucket,
showLegend,
height
}) => {
const { customStep, yaxis } = useGraphState();
@ -118,7 +116,7 @@ with (q = ${queryBase}) (
yaxis={yaxis}
setYaxisLimits={setYaxisLimits}
setPeriod={setPeriod}
showLegend={showLegend}
showLegend={false}
height={height}
/>
)}

View file

@ -3,37 +3,24 @@ import ExploreMetricItemGraph from "../ExploreMetricGraph/ExploreMetricItemGraph
import ExploreMetricItemHeader from "../ExploreMetricItemHeader/ExploreMetricItemHeader";
import "./style.scss";
import useResize from "../../../hooks/useResize";
import { GraphSize } from "../../../types";
interface ExploreMetricItemProps {
name: string
job: string
instance: string
index: number
size: GraphSize
onRemoveItem: (name: string) => void
onChangeOrder: (name: string, oldIndex: number, newIndex: number) => void
}
export const sizeVariants = [
{
id: "small",
height: () => window.innerHeight * 0.2
},
{
id: "medium",
isDefault: true,
height: () => window.innerHeight * 0.4
},
{
id: "large",
height: () => window.innerHeight * 0.8
},
];
const ExploreMetricItem: FC<ExploreMetricItemProps> = ({
name,
job,
instance,
index,
size,
onRemoveItem,
onChangeOrder,
}) => {
@ -42,17 +29,10 @@ const ExploreMetricItem: FC<ExploreMetricItemProps> = ({
const isBucket = useMemo(() => /_bucket?/.test(name), [name]);
const [rateEnabled, setRateEnabled] = useState(isCounter);
const [showLegend, setShowLegend] = useState(false);
const [size, setSize] = useState(sizeVariants.find(v => v.isDefault) || sizeVariants[0]);
const windowSize = useResize(document.body);
const graphHeight = useMemo(size.height, [size, windowSize]);
const handleChangeSize = (id: string) => {
const target = sizeVariants.find(variant => variant.id === id);
if (target) setSize(target);
};
useEffect(() => {
setRateEnabled(isCounter);
}, [job]);
@ -64,13 +44,10 @@ const ExploreMetricItem: FC<ExploreMetricItemProps> = ({
index={index}
isBucket={isBucket}
rateEnabled={rateEnabled}
showLegend={showLegend}
size={size.id}
onChangeRate={setRateEnabled}
onChangeLegend={setShowLegend}
onRemoveItem={onRemoveItem}
onChangeOrder={onChangeOrder}
onChangeSize={handleChangeSize}
/>
<ExploreMetricItemGraph
key={`${name}_${job}_${instance}_${rateEnabled}`}
@ -79,7 +56,6 @@ const ExploreMetricItem: FC<ExploreMetricItemProps> = ({
instance={instance}
rateEnabled={rateEnabled}
isBucket={isBucket}
showLegend={showLegend}
height={graphHeight}
/>
</div>

View file

@ -1,24 +1,19 @@
import React, { FC, useRef, useState } from "preact/compat";
import React, { FC } from "preact/compat";
import "./style.scss";
import Switch from "../../Main/Switch/Switch";
import Tooltip from "../../Main/Tooltip/Tooltip";
import Button from "../../Main/Button/Button";
import { ArrowDownIcon, CloseIcon, ResizeIcon } from "../../Main/Icons";
import Popper from "../../Main/Popper/Popper";
import ExploreMetricLayouts from "../ExploreMetricLayouts/ExploreMetricLayouts";
import { ArrowDownIcon, CloseIcon } from "../../Main/Icons";
interface ExploreMetricItemControlsProps {
name: string
index: number
isBucket: boolean
rateEnabled: boolean
showLegend: boolean
size: string
onChangeRate: (val: boolean) => void
onChangeLegend: (val: boolean) => void
onRemoveItem: (name: string) => void
onChangeOrder: (name: string, oldIndex: number, newIndex: number) => void
onChangeSize: (id: string) => void
}
const ExploreMetricItemHeader: FC<ExploreMetricItemControlsProps> = ({
@ -26,17 +21,11 @@ const ExploreMetricItemHeader: FC<ExploreMetricItemControlsProps> = ({
index,
isBucket,
rateEnabled,
showLegend,
size,
onChangeRate,
onChangeLegend,
onRemoveItem,
onChangeOrder,
onChangeSize
}) => {
const layoutButtonRef = useRef<HTMLDivElement>(null);
const [openPopper, setOpenPopper] = useState(false);
const handleClickRemove = () => {
onRemoveItem(name);
};
@ -49,19 +38,6 @@ const ExploreMetricItemHeader: FC<ExploreMetricItemControlsProps> = ({
onChangeOrder(name, index, index - 1);
};
const handleTogglePopper = () => {
setOpenPopper(prev => !prev);
};
const handleClosePopper = () => {
setOpenPopper(false);
};
const handleChangeSize = (id: string) => {
onChangeSize(id);
handleClosePopper();
};
return (
<div className="vm-explore-metrics-item-header">
<div className="vm-explore-metrics-item-header-order">
@ -97,23 +73,7 @@ const ExploreMetricItemHeader: FC<ExploreMetricItemControlsProps> = ({
/>
</Tooltip>
)}
<Switch
label="show legend"
value={showLegend}
onChange={onChangeLegend}
/>
<div className="vm-explore-metrics-item-header__layout">
<Tooltip title="change size the graph">
<div ref={layoutButtonRef}>
<Button
startIcon={<ResizeIcon/>}
variant="text"
color="gray"
size="small"
onClick={handleTogglePopper}
/>
</div>
</Tooltip>
<Tooltip title="close graph">
<Button
startIcon={<CloseIcon/>}
@ -124,18 +84,6 @@ const ExploreMetricItemHeader: FC<ExploreMetricItemControlsProps> = ({
/>
</Tooltip>
</div>
<Popper
open={openPopper}
onClose={handleClosePopper}
placement="bottom-right"
buttonRef={layoutButtonRef}
>
<ExploreMetricLayouts
value={size}
onChange={handleChangeSize}
/>
</Popper>
</div>
);
};

View file

@ -33,7 +33,6 @@
&__layout {
display: grid;
grid-template-columns: auto auto;
align-items: center;
}

View file

@ -1,41 +0,0 @@
import React, { FC } from "preact/compat";
import "./style.scss";
import { sizeVariants } from "../ExploreMetricItem/ExploreMetricItem";
import classNames from "classnames";
import { DoneIcon } from "../../Main/Icons";
interface ExploreMetricLayoutsProps {
value: string
onChange: (id: string) => void
}
const ExploreMetricLayouts: FC<ExploreMetricLayoutsProps> = ({
value,
onChange
}) => {
const createHandlerClick = (id: string) => () => {
onChange(id);
};
return (
<div className="vm-explore-metrics-layouts">
{sizeVariants.map(variant => (
<div
className={classNames({
"vm-list-item": true,
"vm-list-item_multiselect": true,
"vm-list-item_multiselect_selected": variant.id === value
})}
key={variant.id}
onClick={createHandlerClick(variant.id)}
>
{variant.id === value && <DoneIcon/>}
<span>{variant.id}</span>
</div>
))}
</div>
);
};
export default ExploreMetricLayouts;

View file

@ -1,5 +0,0 @@
@use "src/styles/variables" as *;
.vm-explore-metrics-layouts {
display: grid;
}

View file

@ -5,6 +5,7 @@ import "./style.scss";
import { useTimeState } from "../../../state/time/TimeStateContext";
import { useGraphDispatch, useGraphState } from "../../../state/graph/GraphStateContext";
import usePrevious from "../../../hooks/usePrevious";
import { GRAPH_SIZES } from "../../../constants/graph";
interface ExploreMetricsHeaderProps {
jobs: string[]
@ -12,22 +13,28 @@ interface ExploreMetricsHeaderProps {
names: string[]
job: string
instance: string
size: string
selectedMetrics: string[]
onChangeJob: (job: string) => void
onChangeInstance: (instance: string) => void
onToggleMetric: (name: string) => void
onChangeSize: (sizeId: string) => void
}
const sizeOptions = GRAPH_SIZES.map(s => s.id);
const ExploreMetricsHeader: FC<ExploreMetricsHeaderProps> = ({
jobs,
instances,
names,
job,
instance,
size,
selectedMetrics,
onChangeJob,
onChangeInstance,
onToggleMetric
onToggleMetric,
onChangeSize
}) => {
const { period: { step }, duration } = useTimeState();
@ -60,7 +67,7 @@ const ExploreMetricsHeader: FC<ExploreMetricsHeaderProps> = ({
label="Job"
placeholder="Please select job"
onChange={onChangeJob}
autofocus
autofocus={!job}
/>
</div>
<div className="vm-explore-metrics-header__instance">
@ -81,6 +88,14 @@ const ExploreMetricsHeader: FC<ExploreMetricsHeaderProps> = ({
value={customStep}
/>
</div>
<div className="vm-explore-metrics-header__size">
<Select
label="Size graphs"
value={size}
list={sizeOptions}
onChange={onChangeSize}
/>
</div>
<div className="vm-explore-metrics-header-metrics">
<Select
value={selectedMetrics}

View file

@ -8,18 +8,13 @@
gap: $padding-small calc($padding-small + 10px);
&__job {
flex-grow: 0.5;
min-width: 200px;
max-width: 300px;
width: 100%;
}
&__instance {
min-width: 200px;
max-width: 500px;
width: 100%;
}
&__step {
flex-grow: 1;
min-width: 300px;
}
&-metrics {

View file

@ -19,7 +19,7 @@ const Footer: FC = () => {
<a
className="vm__link"
target="_blank"
href="https://github.com/VictoriaMetrics/VictoriaMetrics/issues/new"
href="https://github.com/VictoriaMetrics/VictoriaMetrics/issues/new/choose"
rel="noreferrer"
>
create an issue

View file

@ -32,7 +32,7 @@
max-width: calc(100% - $padding-global);
padding: 0 3px;
font-size: $font-size-small;
line-height: $font-size-small;
line-height: calc($font-size-small + 2px);
pointer-events: none;
user-select: none;
background-color: $color-background-block;
@ -46,12 +46,12 @@
}
&__label {
top: calc($font-size-small/-2);
top: calc(($font-size-small/-2) - 2px);
color: $color-text-secondary;
}
&__error {
top: calc(100% - ($font-size-small/2));
top: calc((100% - ($font-size-small/2)) - 2px);
color: $color-error;
}

View file

@ -1,6 +1,24 @@
import { GraphSize } from "../types";
export const MAX_QUERY_FIELDS = 4;
export const DEFAULT_MAX_SERIES = {
table: 100,
chart: 20,
code: 1000,
};
export const GRAPH_SIZES: GraphSize[] = [
{
id: "small",
height: () => window.innerHeight * 0.2
},
{
id: "medium",
isDefault: true,
height: () => window.innerHeight * 0.4
},
{
id: "large",
height: () => window.innerHeight * 0.8
},
];

View file

@ -3,7 +3,14 @@ import { compactObject } from "../../../utils/object";
import { useTimeState } from "../../../state/time/TimeStateContext";
import { setQueryStringWithoutPageReload } from "../../../utils/query-string";
export const useSetQueryParams = () => {
interface queryProps {
job: string
instance?: string
metrics: string
size: string
}
export const useSetQueryParams = ({ job, instance, metrics, size }: queryProps) => {
const { duration, relativeTime, period: { date, step } } = useTimeState();
const setSearchParamsFromState = () => {
@ -11,12 +18,16 @@ export const useSetQueryParams = () => {
["g0.range_input"]: duration,
["g0.end_input"]: date,
["g0.step_input"]: step,
["g0.relative_time"]: relativeTime
["g0.relative_time"]: relativeTime,
size,
job,
instance,
metrics
});
setQueryStringWithoutPageReload(params);
};
useEffect(setSearchParamsFromState, [duration, relativeTime, date, step]);
useEffect(setSearchParamsFromState, [duration, relativeTime, date, step, job, instance, metrics, size]);
useEffect(setSearchParamsFromState, []);
};

View file

@ -8,13 +8,22 @@ import { useFetchNames } from "./hooks/useFetchNames";
import "./style.scss";
import ExploreMetricItem from "../../components/ExploreMetrics/ExploreMetricItem/ExploreMetricItem";
import ExploreMetricsHeader from "../../components/ExploreMetrics/ExploreMetricsHeader/ExploreMetricsHeader";
import { GRAPH_SIZES } from "../../constants/graph";
import { getQueryStringValue } from "../../utils/query-string";
const defaultJob = getQueryStringValue("job", "") as string;
const defaultInstance = getQueryStringValue("instance", "") as string;
const defaultMetricsStr = getQueryStringValue("metrics", "") as string;
const defaultSizeId = getQueryStringValue("size", "") as string;
const defaultSize = GRAPH_SIZES.find(v => defaultSizeId ? v.id === defaultSizeId : v.isDefault) || GRAPH_SIZES[0];
const ExploreMetrics: FC = () => {
useSetQueryParams();
const [job, setJob] = useState(defaultJob);
const [instance, setInstance] = useState(defaultInstance);
const [metrics, setMetrics] = useState(defaultMetricsStr ? defaultMetricsStr.split("&") : []);
const [size, setSize] = useState(defaultSize);
const [job, setJob] = useState("");
const [instance, setInstance] = useState("");
const [metrics, setMetrics] = useState<string[]>([]);
useSetQueryParams({ job, instance, metrics: metrics.join("&"), size: size.id });
const { jobs, isLoading: loadingJobs, error: errorJobs } = useFetchJobs();
const { instances, isLoading: loadingInstances, error: errorInstances } = useFetchInstances(job);
@ -36,6 +45,11 @@ const ExploreMetrics: FC = () => {
}
};
const handleChangeSize = (sizeId: string) => {
const target = GRAPH_SIZES.find(variant => variant.id === sizeId);
if (target) setSize(target);
};
const handleChangeOrder = (name: string, oldIndex: number, newIndex: number) => {
const maxIndex = newIndex > (metrics.length - 1);
const minIndex = newIndex < 0;
@ -49,8 +63,10 @@ const ExploreMetrics: FC = () => {
};
useEffect(() => {
setInstance("");
}, [job]);
if (instance && instances.length && !instances.includes(instance)) {
setInstance("");
}
}, [instances, instance]);
return (
<div className="vm-explore-metrics">
@ -59,9 +75,11 @@ const ExploreMetrics: FC = () => {
instances={instances}
names={names}
job={job}
size={size.id}
instance={instance}
selectedMetrics={metrics}
onChangeJob={setJob}
onChangeSize={handleChangeSize}
onChangeInstance={setInstance}
onToggleMetric={handleToggleMetric}
/>
@ -78,6 +96,7 @@ const ExploreMetrics: FC = () => {
job={job}
instance={instance}
index={i}
size={size}
onRemoveItem={handleToggleMetric}
onChangeOrder={handleChangeOrder}
/>

View file

@ -112,3 +112,9 @@ export interface Timezone {
utc: string,
search?: string
}
export interface GraphSize {
id: string,
isDefault?: boolean,
height: () => number
}

View file

@ -25,14 +25,26 @@ export const defaultOptions = {
};
export const formatTicks = (u: uPlot, ticks: number[], unit = ""): string[] => {
return ticks.map(v => `${formatPrettyNumber(v)} ${unit}`);
const min = ticks[0];
const max = ticks[ticks.length-1];
if (!unit) {
return ticks.map(v => formatPrettyNumber(v, min, max));
}
return ticks.map(v => `${formatPrettyNumber(v, min, max)} ${unit}`);
};
export const formatPrettyNumber = (n: number | null | undefined): string => {
export const formatPrettyNumber = (n: number | null | undefined, min = 0, max = 0): string => {
if (n === undefined || n === null) {
return "";
}
return n.toLocaleString("en-US", { maximumSignificantDigits: 20 });
let digits = 3 + Math.floor(1 + Math.log10(Math.max(Math.abs(min), Math.abs(max))) - Math.log10(Math.abs(min - max)));
if (isNaN(digits) || digits > 20) {
digits = 20;
}
return n.toLocaleString("en-US", {
minimumSignificantDigits: digits,
maximumSignificantDigits: digits,
});
};
interface AxisExtend extends Axis {