vmui: make the step input field global across all the tabs and views (#3644)

* feat: make the step input field global

* fix: correct get step from url

* fix: set minimumSignificantDigits to 1

* app/vmselect/vmui: `make vmui-update`

Co-authored-by: Aliaksandr Valialkin <valyala@victoriametrics.com>
This commit is contained in:
Yury Molodov 2023-01-15 22:47:08 +01:00 committed by GitHub
parent cdb6d651e9
commit 04c408e986
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 220 additions and 126 deletions

View file

@ -1,12 +1,12 @@
{
"files": {
"main.css": "./static/css/main.7672c15c.css",
"main.js": "./static/js/main.84759f8d.js",
"main.css": "./static/css/main.01144815.css",
"main.js": "./static/js/main.f9c0c050.js",
"static/js/27.c1ccfd29.chunk.js": "./static/js/27.c1ccfd29.chunk.js",
"index.html": "./index.html"
},
"entrypoints": [
"static/css/main.7672c15c.css",
"static/js/main.84759f8d.js"
"static/css/main.01144815.css",
"static/js/main.f9c0c050.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.84759f8d.js"></script><link href="./static/css/main.7672c15c.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.f9c0c050.js"></script><link href="./static/css/main.01144815.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

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,17 @@
* @license MIT
*/
/**
* React Router DOM v6.4.5
*
* Copyright (c) Remix Software Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE.md file in the root directory of this source tree.
*
* @license MIT
*/
/**
* React Router v6.4.5
*

View file

@ -1,20 +1,13 @@
import React, { FC, useEffect } from "preact/compat";
import StepConfigurator from "../StepConfigurator/StepConfigurator";
import { useGraphDispatch, useGraphState } from "../../../state/graph/GraphStateContext";
import React, { FC } from "preact/compat";
import { getAppModeParams } from "../../../utils/app-mode";
import TenantsConfiguration from "../TenantsConfiguration/TenantsConfiguration";
import { useCustomPanelDispatch, useCustomPanelState } from "../../../state/customPanel/CustomPanelStateContext";
import { useTimeState } from "../../../state/time/TimeStateContext";
import { useQueryDispatch, useQueryState } from "../../../state/query/QueryStateContext";
import "./style.scss";
import Switch from "../../Main/Switch/Switch";
import usePrevious from "../../../hooks/usePrevious";
const AdditionalSettings: FC = () => {
const { customStep } = useGraphState();
const graphDispatch = useGraphDispatch();
const { inputTenantID } = getAppModeParams();
const { autocomplete } = useQueryState();
@ -23,9 +16,6 @@ const AdditionalSettings: FC = () => {
const { nocache, isTracingEnabled } = useCustomPanelState();
const customPanelDispatch = useCustomPanelDispatch();
const { period: { step }, duration } = useTimeState();
const prevDuration = usePrevious(duration);
const onChangeCache = () => {
customPanelDispatch({ type: "TOGGLE_NO_CACHE" });
};
@ -38,19 +28,6 @@ const AdditionalSettings: FC = () => {
queryDispatch({ type: "TOGGLE_AUTOCOMPLETE" });
};
const onChangeStep = (value: string) => {
graphDispatch({ type: "SET_CUSTOM_STEP", payload: value });
};
useEffect(() => {
if (!customStep && step) onChangeStep(step);
}, [step]);
useEffect(() => {
if (duration === prevDuration || !prevDuration) return;
if (step) onChangeStep(step);
}, [duration, prevDuration]);
return <div className="vm-additional-settings">
<Switch
label={"Autocomplete"}
@ -67,13 +44,6 @@ const AdditionalSettings: FC = () => {
value={isTracingEnabled}
onChange={onChangeQueryTracing}
/>
<div className="vm-additional-settings__input">
<StepConfigurator
defaultStep={step}
setStep={onChangeStep}
value={customStep}
/>
</div>
{!!inputTenantID && (
<div className="vm-additional-settings__input">
<TenantsConfiguration/>

View file

@ -1,26 +1,59 @@
import React, { FC, useEffect, useState } from "preact/compat";
import { RestartIcon } from "../../Main/Icons";
import React, { FC, useEffect, useRef, useState } from "preact/compat";
import { RestartIcon, TimelineIcon } from "../../Main/Icons";
import TextField from "../../Main/TextField/TextField";
import Button from "../../Main/Button/Button";
import Tooltip from "../../Main/Tooltip/Tooltip";
import { ErrorTypes } from "../../../types";
import { supportedDurations } from "../../../utils/time";
import { useTimeState } from "../../../state/time/TimeStateContext";
import { useGraphDispatch, useGraphState } from "../../../state/graph/GraphStateContext";
import usePrevious from "../../../hooks/usePrevious";
import "./style.scss";
import { getAppModeEnable } from "../../../utils/app-mode";
import Popper from "../../Main/Popper/Popper";
interface StepConfiguratorProps {
defaultStep?: string,
value?: string,
setStep: (step: string) => void,
}
const StepConfigurator: FC = () => {
const appModeEnable = getAppModeEnable();
const StepConfigurator: FC<StepConfiguratorProps> = ({ value, defaultStep, setStep }) => {
const { customStep: value } = useGraphState();
const { period: { step: defaultStep } } = useTimeState();
const graphDispatch = useGraphDispatch();
const { period: duration } = useTimeState();
const prevDuration = usePrevious(duration.end - duration.start);
const [openOptions, setOpenOptions] = useState(false);
const [customStep, setCustomStep] = useState(value || defaultStep);
const [error, setError] = useState("");
const buttonRef = useRef<HTMLDivElement>(null);
const toggleOpenOptions = () => {
setOpenOptions(prev => !prev);
};
const handleCloseOptions = () => {
setOpenOptions(false);
};
const handleApply = (value?: string) => {
const step = value || customStep || defaultStep || "1s";
const durations = step.match(/[a-zA-Z]+/g) || [];
setStep(!durations.length ? `${step}s` : step);
const stepDur = !durations.length ? `${step}s` : step;
graphDispatch({ type: "SET_CUSTOM_STEP", payload: stepDur });
setCustomStep(stepDur);
setError("");
};
const handleFocus = () => {
if (document.activeElement instanceof HTMLInputElement) {
document.activeElement.select();
}
};
const handleEnter = () => {
handleApply();
handleCloseOptions();
};
const handleChangeStep = (value: string) => {
@ -46,28 +79,94 @@ const StepConfigurator: FC<StepConfiguratorProps> = ({ value, defaultStep, setSt
};
useEffect(() => {
if (value) handleChangeStep(value);
if (value) {
handleApply(value);
}
}, [value]);
useEffect(() => {
if (!value && defaultStep) {
handleApply(defaultStep);
}
}, [defaultStep]);
useEffect(() => {
const dur = duration.end - duration.start;
if (dur === prevDuration || !prevDuration) return;
if (defaultStep) {
handleApply(defaultStep);
}
}, [duration, prevDuration, defaultStep]);
return (
<TextField
label="Step value"
value={customStep}
error={error}
onChange={handleChangeStep}
onEnter={handleApply}
onBlur={handleApply}
endIcon={(
<Tooltip title="Reset step to default">
<Button
variant={"text"}
size={"small"}
startIcon={<RestartIcon/>}
onClick={handleReset}
<div
className="vm-step-control"
ref={buttonRef}
>
<Tooltip title="Query resolution step width">
<Button
className={appModeEnable ? "" : "vm-header-button"}
variant="contained"
color="primary"
startIcon={<TimelineIcon/>}
onClick={toggleOpenOptions}
>
STEP {customStep}
</Button>
</Tooltip>
<Popper
open={openOptions}
placement="bottom-right"
onClose={handleCloseOptions}
buttonRef={buttonRef}
>
<div className="vm-step-control-popper">
<TextField
autofocus
label="Step value"
value={customStep}
error={error}
onChange={handleChangeStep}
onEnter={handleEnter}
onFocus={handleFocus}
onBlur={handleApply}
endIcon={(
<Tooltip title={`Set default step value: ${defaultStep}`}>
<Button
size="small"
variant="text"
color="primary"
startIcon={<RestartIcon/>}
onClick={handleReset}
/>
</Tooltip>
)}
/>
</Tooltip>
)}
/>
<div className="vm-step-control-popper-info">
<code>step</code> - the <a
className="vm-link vm-link_colored"
href="https://prometheus.io/docs/prometheus/latest/querying/basics/#time-durations"
target="_blank"
rel="noreferrer"
>
interval
</a>
between datapoints, which must be returned from the range query.
The <code>query</code> is executed at
<code>start</code>, <code>start+step</code>, <code>start+2*step</code>, , <code>end</code> timestamps.
<a
className="vm-link vm-link_colored"
href="https://docs.victoriametrics.com/keyConcepts.html#range-query"
target="_blank"
rel="noreferrer"
>
Read more about Range query
</a>
</div>
</div>
</Popper>
</div>
);
};

View file

@ -0,0 +1,37 @@
@use "src/styles/variables" as *;
@use "src/components/Main/Button/style" as *;
.vm-step-control {
display: inline-flex;
button {
text-transform: none;
}
&-popper {
display: grid;
gap: $padding-small;
max-width: 300px;
max-height: 208px;
overflow: auto;
padding: $padding-global;
font-size: $font-size;
&-info {
font-size: $font-size-small;
line-height: 1.6;
a {
margin: 0 0.2em;
}
code {
padding: 0.2em 0.4em;
margin: 0 0.2em;
font-size: 85%;
background-color: rgba($color-black, 0.05);
border-radius: 6px;
}
}
}
}

View file

@ -1,10 +1,6 @@
import React, { FC, useEffect, useMemo } from "preact/compat";
import React, { FC, useMemo } from "preact/compat";
import Select from "../../Main/Select/Select";
import StepConfigurator from "../../Configurators/StepConfigurator/StepConfigurator";
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 {
@ -36,28 +32,9 @@ const ExploreMetricsHeader: FC<ExploreMetricsHeaderProps> = ({
onToggleMetric,
onChangeSize
}) => {
const { period: { step }, duration } = useTimeState();
const { customStep } = useGraphState();
const graphDispatch = useGraphDispatch();
const prevDuration = usePrevious(duration);
const noInstanceText = useMemo(() => job ? "" : "No instances. Please select job", [job]);
const noMetricsText = useMemo(() => job ? "" : "No metric names. Please select job", [job]);
const handleChangeStep = (value: string) => {
graphDispatch({ type: "SET_CUSTOM_STEP", payload: value });
};
useEffect(() => {
if (duration === prevDuration || !prevDuration) return;
if (customStep) handleChangeStep(step || "1s");
}, [duration, prevDuration]);
useEffect(() => {
if (!customStep && step) handleChangeStep(step);
}, [step]);
return (
<div className="vm-explore-metrics-header vm-block">
<div className="vm-explore-metrics-header__job">
@ -81,13 +58,6 @@ const ExploreMetricsHeader: FC<ExploreMetricsHeaderProps> = ({
clearable
/>
</div>
<div className="vm-explore-metrics-header__step">
<StepConfigurator
defaultStep={step}
setStep={handleChangeStep}
value={customStep}
/>
</div>
<div className="vm-explore-metrics-header__size">
<Select
label="Size graphs"

View file

@ -8,7 +8,7 @@ const Footer: FC = () => {
return <footer className="vm-footer">
<a
className="vm__link vm-footer__website"
className="vm-link vm-footer__website"
target="_blank"
href="https://victoriametrics.com/"
rel="noreferrer"
@ -17,7 +17,7 @@ const Footer: FC = () => {
victoriametrics.com
</a>
<a
className="vm__link"
className="vm-link"
target="_blank"
href="https://github.com/VictoriaMetrics/VictoriaMetrics/issues/new/choose"
rel="noreferrer"

View file

@ -15,6 +15,7 @@ import Tabs from "../../Main/Tabs/Tabs";
import "./style.scss";
import classNames from "classnames";
import { useDashboardsState } from "../../../state/dashboards/DashboardsStateContext";
import StepConfigurator from "../../Configurators/StepConfigurator/StepConfigurator";
const Header: FC = () => {
const primaryColor = getCssVariable("color-primary");
@ -101,6 +102,7 @@ const Header: FC = () => {
/>
</div>
<div className="vm-header__settings">
{headerSetup?.stepControl && <StepConfigurator/>}
{headerSetup?.timeSelector && <TimeSelector/>}
{headerSetup?.cardinalityDatePicker && <CardinalityDatePicker/>}
{headerSetup?.executionControls && <ExecutionControls/>}

View file

@ -333,3 +333,14 @@ export const ResizeIcon = () => (
<path d="M21 11V3h-8l3.29 3.29-10 10L3 13v8h8l-3.29-3.29 10-10z"></path>
</svg>
);
export const TimelineIcon = () => (
<svg
viewBox="0 0 24 24"
fill="currentColor"
>
<path
d="M23 8c0 1.1-.9 2-2 2-.18 0-.35-.02-.51-.07l-3.56 3.55c.05.16.07.34.07.52 0 1.1-.9 2-2 2s-2-.9-2-2c0-.18.02-.36.07-.52l-2.55-2.55c-.16.05-.34.07-.52.07s-.36-.02-.52-.07l-4.55 4.56c.05.16.07.33.07.51 0 1.1-.9 2-2 2s-2-.9-2-2 .9-2 2-2c.18 0 .35.02.51.07l4.56-4.55C8.02 9.36 8 9.18 8 9c0-1.1.9-2 2-2s2 .9 2 2c0 .18-.02.36-.07.52l2.55 2.55c.16-.05.34-.07.52-.07s.36.02.52.07l3.55-3.56C19.02 8.35 19 8.18 19 8c0-1.1.9-2 2-2s2 .9 2 2z"
></path>
</svg>
);

View file

@ -2,6 +2,7 @@ import { useEffect } from "react";
import { compactObject } from "../../../utils/object";
import { useTimeState } from "../../../state/time/TimeStateContext";
import { setQueryStringWithoutPageReload } from "../../../utils/query-string";
import { useGraphState } from "../../../state/graph/GraphStateContext";
interface queryProps {
job: string
@ -11,13 +12,14 @@ interface queryProps {
}
export const useSetQueryParams = ({ job, instance, metrics, size }: queryProps) => {
const { duration, relativeTime, period: { date, step } } = useTimeState();
const { duration, relativeTime, period: { date } } = useTimeState();
const { customStep } = useGraphState();
const setSearchParamsFromState = () => {
const params = compactObject({
["g0.range_input"]: duration,
["g0.end_input"]: date,
["g0.step_input"]: step,
["g0.step_input"]: customStep,
["g0.relative_time"]: relativeTime,
size,
job,
@ -28,6 +30,6 @@ export const useSetQueryParams = ({ job, instance, metrics, size }: queryProps)
setQueryStringWithoutPageReload(params);
};
useEffect(setSearchParamsFromState, [duration, relativeTime, date, step, job, instance, metrics, size]);
useEffect(setSearchParamsFromState, [duration, relativeTime, date, customStep, job, instance, metrics, size]);
useEffect(setSearchParamsFromState, []);
};

View file

@ -4,7 +4,6 @@ import { AxisRange, YaxisState } from "../../../state/graph/reducer";
import GraphView from "../../../components/Views/GraphView/GraphView";
import { useFetchQuery } from "../../../hooks/useFetchQuery";
import Spinner from "../../../components/Main/Spinner/Spinner";
import StepConfigurator from "../../../components/Configurators/StepConfigurator/StepConfigurator";
import GraphSettings from "../../../components/Configurators/GraphSettings/GraphSettings";
import { marked } from "marked";
import { useTimeDispatch, useTimeState } from "../../../state/time/TimeStateContext";
@ -12,7 +11,7 @@ import { InfoIcon } from "../../../components/Main/Icons";
import "./style.scss";
import Alert from "../../../components/Main/Alert/Alert";
import Tooltip from "../../../components/Main/Tooltip/Tooltip";
import usePrevious from "../../../hooks/usePrevious";
import { useGraphState } from "../../../state/graph/GraphStateContext";
export interface PredefinedPanelsProps extends PanelSettings {
filename: string;
@ -28,13 +27,12 @@ const PredefinedPanel: FC<PredefinedPanelsProps> = ({
alias
}) => {
const { period, duration } = useTimeState();
const { period } = useTimeState();
const { customStep } = useGraphState();
const dispatch = useTimeDispatch();
const prevDuration = usePrevious(duration);
const containerRef = useRef<HTMLDivElement>(null);
const [visible, setVisible] = useState(true);
const [customStep, setCustomStep] = useState(period.step || "1s");
const [yaxis, setYaxis] = useState<YaxisState>({
limits: {
enable: false,
@ -77,11 +75,6 @@ const PredefinedPanel: FC<PredefinedPanelsProps> = ({
};
}, []);
useEffect(() => {
if (duration === prevDuration || !prevDuration) return;
if (customStep) setCustomStep(period.step || "1s");
}, [duration, prevDuration]);
if (!validExpr) return (
<Alert variant="error">
<code>&quot;expr&quot;</code> not found. Check the configuration file <b>{filename}</b>.
@ -123,13 +116,6 @@ const PredefinedPanel: FC<PredefinedPanelsProps> = ({
<h3 className="vm-predefined-panel-header__title">
{title || ""}
</h3>
<div className="vm-predefined-panel-header__step">
<StepConfigurator
defaultStep={period.step}
value={customStep}
setStep={setCustomStep}
/>
</div>
<GraphSettings
yaxis={yaxis}
setYaxisLimits={setYaxisLimits}

View file

@ -4,7 +4,7 @@
&-header {
display: grid;
grid-template-columns: auto 1fr 160px auto;
grid-template-columns: auto 1fr auto;
align-items: center;
justify-content: flex-start;
gap: $padding-small;

View file

@ -2,21 +2,23 @@ import { useEffect } from "react";
import { compactObject } from "../../../utils/object";
import { useTimeState } from "../../../state/time/TimeStateContext";
import { setQueryStringWithoutPageReload } from "../../../utils/query-string";
import { useGraphState } from "../../../state/graph/GraphStateContext";
export const useSetQueryParams = () => {
const { duration, relativeTime, period: { date, step } } = useTimeState();
const { duration, relativeTime, period: { date } } = useTimeState();
const { customStep } = useGraphState();
const setSearchParamsFromState = () => {
const params = compactObject({
["g0.range_input"]: duration,
["g0.end_input"]: date,
["g0.step_input"]: step,
["g0.step_input"]: customStep,
["g0.relative_time"]: relativeTime
});
setQueryStringWithoutPageReload(params);
};
useEffect(setSearchParamsFromState, [duration, relativeTime, date, step]);
useEffect(setSearchParamsFromState, [duration, relativeTime, date, customStep]);
useEffect(setSearchParamsFromState, []);
};

View file

@ -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, 0, value);
return value || key;
};

View file

@ -148,7 +148,7 @@ const TracePage: FC = () => {
{"\n"}
In order to use tracing please refer to the doc:&nbsp;
<a
className="vm__link vm__link_colored"
className="vm-link vm-link_colored"
href="https://docs.victoriametrics.com/#query-tracing"
target="_blank"
rel="noreferrer"

View file

@ -11,6 +11,7 @@ const router = {
export interface RouterOptions {
title?: string,
header: {
stepControl?: boolean,
timeSelector?: boolean,
executionControls?: boolean,
globalSettings?: boolean,
@ -20,6 +21,7 @@ export interface RouterOptions {
const routerOptionsDefault = {
header: {
stepControl: true,
timeSelector: true,
executionControls: true,
}
@ -33,6 +35,7 @@ export const routerOptions: {[key: string]: RouterOptions} = {
[router.metrics]: {
title: "Explore metrics",
header: {
stepControl: true,
timeSelector: true,
}
},

View file

@ -1,6 +1,6 @@
@use "src/styles/variables" as *;
.vm__link {
.vm-link {
transition: color 200ms ease;
cursor: pointer;

View file

@ -16,6 +16,7 @@ The following tip changes can be tested by building VictoriaMetrics components f
## tip
* FEATURE: [vmui](https://docs.victoriametrics.com/#vmui): add ability to show custom dashboards at vmui by specifying a path to a directory with dashboard config files via `-vmui.customDashboardsPath` command-line flag. See [this feature request](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/3322) and [these docs](https://github.com/VictoriaMetrics/VictoriaMetrics/tree/master/app/vmui/packages/vmui/public/dashboards).
* FEATURE: [vmui](https://docs.victoriametrics.com/#vmui): apply the `step` globally to all the displayed graphs. See [this feature request](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/3574).
* BUGFIX: reduce the increased CPU usage at `vmselect` to v1.85.3 level when processing heavy queries. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/3641).
* BUGFIX: [vmagent](https://docs.victoriametrics.com/vmagent.html): [dockerswarm_sd_configs](https://docs.victoriametrics.com/sd_configs.html#dockerswarm_sd_configs): apply `filters` only to objects of the specified `role`. Previously filters were applied to all the objects, which could cause errors when different types of objects were used with filters that were not compatible with them. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/3579).