vmui: add set up series custom limits (#3368)

* feat: add set up series custom limits

* feat: add button for show series without limits

* fix: resolve merge conflicts
This commit is contained in:
Yury Molodov 2022-11-22 14:31:17 +01:00 committed by Aliaksandr Valialkin
parent e45022c344
commit a59c7a0dd7
No known key found for this signature in database
GPG Key ID: A72BEC6CD3D0DED1
19 changed files with 279 additions and 53 deletions

View File

@ -1,9 +1,10 @@
@use "src/styles/variables" as *;
$chart-tooltip-width: 300px;
$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;
$chart-tooltip-x: -1 * ($padding-small + $padding-global + $chart-tooltip-date-width + ($chart-tooltip-icon-width/2));
$chart-tooltip-y: -1 * ($padding-small + ($chart-tooltip-icon-width/2));
$chart-tooltip-x: -1 * ($padding-small + $padding-global + $chart-tooltip-date-width + $chart-tooltip-half-icon);
$chart-tooltip-y: -1 * ($padding-small + $chart-tooltip-half-icon);
.vm-chart-tooltip {
position: absolute;
@ -54,9 +55,12 @@ $chart-tooltip-y: -1 * ($padding-small + ($chart-tooltip-icon-width/2));
}
&-data {
display: flex;
flex-wrap: wrap;
align-items: center;
display: grid;
grid-template-columns: auto 1fr;
gap: $padding-small;
align-items: flex-start;
word-break: break-all;
line-height: 12px;
&__value {
padding: 4px;
@ -66,7 +70,6 @@ $chart-tooltip-y: -1 * ($padding-small + ($chart-tooltip-icon-width/2));
&__marker {
width: 12px;
height: 12px;
margin-right: $padding-small;
}
}

View File

@ -6,28 +6,33 @@ import Button from "../../Main/Button/Button";
import Modal from "../../Main/Modal/Modal";
import "./style.scss";
import Tooltip from "../../Main/Tooltip/Tooltip";
import LimitsConfigurator from "./LimitsConfigurator/LimitsConfigurator";
import { SeriesLimits } from "../../../types";
import { useCustomPanelDispatch, useCustomPanelState } from "../../../state/customPanel/CustomPanelStateContext";
const title = "Setting Server URL";
const title = "Settings";
const GlobalSettings: FC = () => {
const { serverUrl } = useAppState();
const { serverUrl: stateServerUrl } = useAppState();
const { seriesLimits } = useCustomPanelState();
const dispatch = useAppDispatch();
const [changedServerUrl, setChangedServerUrl] = useState(serverUrl);
const customPanelDispatch = useCustomPanelDispatch();
const setServer = (url?: string) => {
dispatch({ type: "SET_SERVER", payload: url || changedServerUrl });
handleClose();
};
const createSetServer = () => () => {
setServer();
};
const [serverUrl, setServerUrl] = useState(stateServerUrl);
const [limits, setLimits] = useState<SeriesLimits>(seriesLimits);
const [open, setOpen] = useState(false);
const handleOpen = () => setOpen(true);
const handleClose = () => setOpen(false);
const handlerApply = () => {
dispatch({ type: "SET_SERVER", payload: serverUrl });
customPanelDispatch({ type: "SET_SERIES_LIMITS", payload: limits });
handleClose();
};
return <>
<Tooltip title={title}>
<Button
@ -46,8 +51,16 @@ const GlobalSettings: FC = () => {
<div className="vm-server-configurator">
<div className="vm-server-configurator__input">
<ServerConfigurator
setServer={setChangedServerUrl}
onEnter={setServer}
serverUrl={serverUrl}
onChange={setServerUrl}
onEnter={handlerApply}
/>
</div>
<div className="vm-server-configurator__input">
<LimitsConfigurator
limits={limits}
onChange={setLimits}
onEnter={handlerApply}
/>
</div>
<div className="vm-server-configurator__footer">
@ -60,7 +73,7 @@ const GlobalSettings: FC = () => {
</Button>
<Button
variant="contained"
onClick={createSetServer()}
onClick={handlerApply}
>
apply
</Button>

View File

@ -0,0 +1,88 @@
import React, { FC, useState } from "preact/compat";
import { DisplayType, ErrorTypes, SeriesLimits } from "../../../../types";
import TextField from "../../../Main/TextField/TextField";
import Tooltip from "../../../Main/Tooltip/Tooltip";
import { InfoIcon, RestartIcon } from "../../../Main/Icons";
import Button from "../../../Main/Button/Button";
import { DEFAULT_MAX_SERIES } from "../../../../constants/graph";
import "./style.scss";
export interface ServerConfiguratorProps {
limits: SeriesLimits
onChange: (limits: SeriesLimits) => void
onEnter: () => void
}
const fields: {label: string, type: DisplayType}[] = [
{ label: "Graph", type: "chart" },
{ label: "JSON", type: "code" },
{ label: "Table", type: "table" }
];
const LimitsConfigurator: FC<ServerConfiguratorProps> = ({ limits, onChange , onEnter }) => {
const [error, setError] = useState({
table: "",
chart: "",
code: ""
});
const handleChange = (val: string, type: DisplayType) => {
const value = val || "";
setError(prev => ({ ...prev, [type]: +value < 0 ? ErrorTypes.positiveNumber : "" }));
onChange({
...limits,
[type]: !value ? Infinity : value
});
};
const handleReset = () => {
onChange(DEFAULT_MAX_SERIES);
};
const createChangeHandler = (type: DisplayType) => (val: string) => {
handleChange(val, type);
};
return (
<div className="vm-limits-configurator">
<div className="vm-limits-configurator-title">
Series limits by tabs
<Tooltip title="To disable limits set to 0">
<Button
variant="text"
color="primary"
size="small"
startIcon={<InfoIcon/>}
/>
</Tooltip>
<div className="vm-limits-configurator-title__reset">
<Button
variant="text"
color="primary"
size="small"
startIcon={<RestartIcon/>}
onClick={handleReset}
>
Reset
</Button>
</div>
</div>
<div className="vm-limits-configurator__inputs">
{fields.map(f => (
<TextField
key={f.type}
label={f.label}
value={limits[f.type]}
error={error[f.type]}
onChange={createChangeHandler(f.type)}
onEnter={onEnter}
type="number"
/>
))}
</div>
</div>
);
};
export default LimitsConfigurator;

View File

@ -0,0 +1,28 @@
@use "src/styles/variables" as *;
.vm-limits-configurator {
&-title {
display: flex;
align-items: center;
justify-content: flex-start;
font-size: $font-size;
font-weight: bold;
margin-bottom: $padding-global;
&__reset {
display: flex;
align-items: center;
justify-content: flex-end;
flex-grow: 1;
}
}
&__inputs {
display: grid;
grid-template-columns: repeat(3, 1fr);
align-items: center;
justify-content: space-between;
gap: $padding-global;
}
}

View File

@ -1,41 +1,34 @@
import React, { FC, useState } from "preact/compat";
import { useAppState } from "../../../../state/common/StateContext";
import { ErrorTypes } from "../../../../types";
import TextField from "../../../Main/TextField/TextField";
import { isValidHttpUrl } from "../../../../utils/url";
export interface ServerConfiguratorProps {
setServer: (url: string) => void
onEnter: (url: string) => void
serverUrl: string
onChange: (url: string) => void
onEnter: () => void
}
const ServerConfigurator: FC<ServerConfiguratorProps> = ({ setServer , onEnter }) => {
const ServerConfigurator: FC<ServerConfiguratorProps> = ({ serverUrl, onChange , onEnter }) => {
const { serverUrl } = useAppState();
const [error, setError] = useState("");
const [changedServerUrl, setChangedServerUrl] = useState(serverUrl);
const onChangeServer = (val: string) => {
const value = val || "";
setChangedServerUrl(value);
setServer(value);
onChange(value);
setError("");
if (!value) setError(ErrorTypes.emptyServer);
if (!isValidHttpUrl(value)) setError(ErrorTypes.validServer);
};
const handleEnter = () => {
onEnter(changedServerUrl);
};
return (
<TextField
autofocus
label="Server URL"
value={changedServerUrl}
value={serverUrl}
error={error}
onChange={onChangeServer}
onEnter={handleEnter}
onEnter={onEnter}
/>
);
};

View File

@ -3,7 +3,7 @@
.vm-server-configurator {
display: grid;
align-items: center;
gap: $padding-global;
gap: $padding-medium;
width: 600px;
&__input {

View File

@ -113,11 +113,11 @@ const QueryEditor: FC<QueryEditorProps> = ({
};
useEffect(() => {
if (!autocomplete || !foundOptions.length) return;
if (!autocomplete) return;
setFocusOption(-1);
const words = (value.match(/[a-zA-Z_:.][a-zA-Z0-9_:.]*/gm) || []).length;
setOpenAutocomplete(autocomplete && value.length > 2 && words <= 1);
}, [autocomplete, value, foundOptions]);
}, [autocomplete, value]);
useEffect(() => {
if (!wrapperEl.current) return;

View File

@ -1,8 +1,8 @@
import React, { FC } from "preact/compat";
import { ReactNode } from "react";
import classNames from "classnames";
import "./style.scss";
import { ErrorIcon, InfoIcon, SuccessIcon, WarningIcon } from "../Icons";
import "./style.scss";
interface AlertProps {
variant?: "success" | "error" | "info" | "warning"

View File

@ -4,7 +4,7 @@
position: relative;
display: grid;
grid-template-columns: 20px 1fr;
align-items: flex-start;
align-items: center;
gap: $padding-small;
padding: $padding-global;
background-color: $color-background-block;

View File

@ -5,7 +5,7 @@ import "./style.scss";
interface ButtonProps {
variant?: "contained" | "outlined" | "text"
color?: "primary" | "secondary" | "success" | "error" | "gray"
color?: "primary" | "secondary" | "success" | "error" | "gray" | "warning"
size?: "small" | "medium" | "large"
endIcon?: ReactNode
startIcon?: ReactNode

View File

@ -143,6 +143,15 @@ $button-radius: 6px;
}
}
&_contained_warning {
color: $color-warning;
&:before {
background-color: $color-warning;
opacity: 0.2;
}
}
/* variant TEXT */
&_text_primary {
@ -165,6 +174,10 @@ $button-radius: 6px;
color: $color-text-secondary;
}
&_text_warning {
color: $color-warning;
}
/* variant OUTLINED */
&_outlined_primary {
@ -191,4 +204,9 @@ $button-radius: 6px;
border: 1px solid $color-text-secondary;
color: $color-text-secondary;
}
&_outlined_warning {
border: 1px solid $color-warning;
color: $color-warning;
}
}

View File

@ -1,6 +1,6 @@
export const MAX_QUERY_FIELDS = 4;
export const MAX_SERIES = {
export const DEFAULT_MAX_SERIES = {
table: 100,
chart: 20,
code: Infinity,
code: 1000,
};

View File

@ -3,11 +3,10 @@ import { getQueryRangeUrl, getQueryUrl } from "../api/query-range";
import { useAppState } from "../state/common/StateContext";
import { InstantMetricResult, MetricBase, MetricResult } from "../api/types";
import { isValidHttpUrl } from "../utils/url";
import { ErrorTypes } from "../types";
import { ErrorTypes, SeriesLimits } from "../types";
import debounce from "lodash.debounce";
import { DisplayType } from "../pages/CustomPanel/DisplayTypeSwitch";
import Trace from "../components/TraceQuery/Trace";
import { MAX_SERIES } from "../constants/graph";
import { useQueryState } from "../state/query/QueryStateContext";
import { useTimeState } from "../state/time/TimeStateContext";
import { useCustomPanelState } from "../state/customPanel/CustomPanelStateContext";
@ -18,9 +17,10 @@ interface FetchQueryParams {
display?: DisplayType,
customStep: number,
hideQuery?: number[]
showAllSeries?: boolean
}
export const useFetchQuery = ({ predefinedQuery, visible, display, customStep, hideQuery }: FetchQueryParams): {
interface FetchQueryReturn {
fetchUrl?: string[],
isLoading: boolean,
graphData?: MetricResult[],
@ -28,10 +28,28 @@ export const useFetchQuery = ({ predefinedQuery, visible, display, customStep, h
error?: ErrorTypes | string,
warning?: string,
traces?: Trace[],
} => {
}
interface FetchDataParams {
fetchUrl: string[],
fetchQueue: AbortController[],
displayType: DisplayType,
query: string[],
stateSeriesLimits: SeriesLimits,
showAllSeries?: boolean,
}
export const useFetchQuery = ({
predefinedQuery,
visible,
display,
customStep,
hideQuery,
showAllSeries
}: FetchQueryParams): FetchQueryReturn => {
const { query } = useQueryState();
const { period } = useTimeState();
const { displayType, nocache, isTracingEnabled } = useCustomPanelState();
const { displayType, nocache, isTracingEnabled, seriesLimits: stateSeriesLimits } = useCustomPanelState();
const { serverUrl } = useAppState();
const [isLoading, setIsLoading] = useState(false);
@ -50,12 +68,19 @@ export const useFetchQuery = ({ predefinedQuery, visible, display, customStep, h
}
}, [error]);
const fetchData = async (fetchUrl: string[], fetchQueue: AbortController[], displayType: DisplayType, query: string[]) => {
const fetchData = async ({
fetchUrl,
fetchQueue,
displayType,
query,
stateSeriesLimits,
showAllSeries,
}: FetchDataParams) => {
const controller = new AbortController();
setFetchQueue([...fetchQueue, controller]);
try {
const isDisplayChart = displayType === "chart";
const seriesLimit = MAX_SERIES[displayType];
const seriesLimit = showAllSeries ? Infinity : stateSeriesLimits[displayType];
const tempData: MetricBase[] = [];
const tempTraces: Trace[] = [];
let counter = 1;
@ -131,8 +156,15 @@ export const useFetchQuery = ({ predefinedQuery, visible, display, customStep, h
if (!visible || !fetchUrl?.length) return;
setIsLoading(true);
const expr = predefinedQuery ?? query;
throttledFetchData(fetchUrl, fetchQueue, (display || displayType), expr);
}, [fetchUrl, visible]);
throttledFetchData({
fetchUrl,
fetchQueue,
displayType: display || displayType,
query: expr,
stateSeriesLimits,
showAllSeries,
});
}, [fetchUrl, visible, stateSeriesLimits, showAllSeries]);
useEffect(() => {
const fetchPast = fetchQueue.slice(0, -1);

View File

@ -19,6 +19,7 @@ import { useSetQueryParams } from "./hooks/useSetQueryParams";
import "./style.scss";
import Alert from "../../components/Main/Alert/Alert";
import TableView from "../../components/Views/TableView/TableView";
import Button from "../../components/Main/Button/Button";
const CustomPanel: FC = () => {
const { displayType, isTracingEnabled } = useCustomPanelState();
@ -30,6 +31,7 @@ const CustomPanel: FC = () => {
const [displayColumns, setDisplayColumns] = useState<string[]>();
const [tracesState, setTracesState] = useState<Trace[]>([]);
const [hideQuery, setHideQuery] = useState<number[]>([]);
const [showAllSeries, setShowAllSeries] = useState(false);
const { customStep, yaxis } = useGraphState();
const graphDispatch = useGraphDispatch();
@ -38,7 +40,8 @@ const CustomPanel: FC = () => {
const { isLoading, liveData, graphData, error, warning, traces } = useFetchQuery({
visible: true,
customStep,
hideQuery
hideQuery,
showAllSeries
});
const setYaxisLimits = (limits: AxisRange) => {
@ -53,6 +56,10 @@ const CustomPanel: FC = () => {
timeDispatch({ type: "SET_PERIOD", payload: { from, to } });
};
const handleShowAll = () => {
setShowAllSeries(true);
};
const handleTraceDelete = (trace: Trace) => {
const updatedTraces = tracesState.filter((data) => data.idValue !== trace.idValue);
setTracesState([...updatedTraces]);
@ -72,6 +79,10 @@ const CustomPanel: FC = () => {
setTracesState([]);
}, [displayType]);
useEffect(() => {
setShowAllSeries(false);
}, [query]);
return (
<div className="vm-custom-panel">
<QueryConfigurator
@ -89,7 +100,18 @@ const CustomPanel: FC = () => {
)}
{isLoading && <Spinner />}
{error && <Alert variant="error">{error}</Alert>}
{warning && <Alert variant="warning">{warning}</Alert>}
{warning && <Alert variant="warning">
<div className="vm-custom-panel__warning">
<p>{warning}</p>
<Button
color="warning"
variant="outlined"
onClick={handleShowAll}
>
Show all
</Button>
</div>
</Alert>}
<div className="vm-custom-panel-body vm-block">
<div className="vm-custom-panel-body-header">
<DisplayTypeSwitch/>

View File

@ -6,6 +6,13 @@
gap: $padding-medium;
height: 100%;
&__warning {
display: grid;
grid-template-columns: 1fr auto;
align-items: center;
justify-content: space-between;
}
&__trace {
}

View File

@ -1,24 +1,31 @@
import { DisplayType, displayTypeTabs } from "../../pages/CustomPanel/DisplayTypeSwitch";
import { getFromStorage, saveToStorage } from "../../utils/storage";
import { getQueryStringValue } from "../../utils/query-string";
import { SeriesLimits } from "../../types";
import { DEFAULT_MAX_SERIES } from "../../constants/graph";
export interface CustomPanelState {
displayType: DisplayType;
nocache: boolean;
isTracingEnabled: boolean;
seriesLimits: SeriesLimits
}
export type CustomPanelAction =
| { type: "SET_DISPLAY_TYPE", payload: DisplayType }
| { type: "SET_SERIES_LIMITS", payload: SeriesLimits }
| { type: "TOGGLE_NO_CACHE"}
| { type: "TOGGLE_QUERY_TRACING" }
const queryTab = getQueryStringValue("g0.tab", 0) as string;
const displayType = displayTypeTabs.find(t => t.prometheusCode === +queryTab || t.value === queryTab);
const limitsStorage = getFromStorage("SERIES_LIMITS") as string;
export const initialCustomPanelState: CustomPanelState = {
displayType: (displayType?.value || "chart") as DisplayType,
nocache: false,
isTracingEnabled: false,
seriesLimits: limitsStorage ? JSON.parse(getFromStorage("SERIES_LIMITS") as string) : DEFAULT_MAX_SERIES
};
export function reducer(state: CustomPanelState, action: CustomPanelAction): CustomPanelState {
@ -28,6 +35,12 @@ export function reducer(state: CustomPanelState, action: CustomPanelAction): Cus
...state,
displayType: action.payload
};
case "SET_SERIES_LIMITS":
saveToStorage("SERIES_LIMITS", JSON.stringify(action.payload));
return {
...state,
seriesLimits: action.payload
};
case "TOGGLE_QUERY_TRACING":
return {
...state,

View File

@ -44,6 +44,7 @@ export enum ErrorTypes {
validQuery = "Please enter a valid Query and execute it",
traceNotFound = "Not found the tracing information",
emptyTitle = "Please enter title",
positiveNumber = "Please enter positive number"
}
export interface PanelSettings {
@ -98,3 +99,9 @@ export interface TopQueriesData extends TopQueryStats{
topByCount: TopQuery[]
topBySumDuration: TopQuery[]
}
export interface SeriesLimits {
table: number,
chart: number,
code: number,
}

View File

@ -4,6 +4,7 @@ export type StorageKeys = "BASIC_AUTH_DATA"
| "AUTOCOMPLETE"
| "NO_CACHE"
| "QUERY_TRACING"
| "SERIES_LIMITS"
export const saveToStorage = (key: StorageKeys, value: string | boolean | Record<string, unknown>): void => {
if (value) {

View File

@ -26,6 +26,7 @@ The following tip changes can be tested by building VictoriaMetrics components f
* FEATURE: [vmui](https://docs.victoriametrics.com/#vmui): add the ability to hide results of a particular query by clicking the `eye` icon. See [this pull request](https://github.com/VictoriaMetrics/VictoriaMetrics/pull/3359).
* FEATURE: [vmui](https://docs.victoriametrics.com/#vmui): add copy button to row on Table view. The button copies row in MetricQL format. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/2815).
* FEATURE: [vmui](https://docs.victoriametrics.com/#vmui): add the ability to "stick" a tooltip on the chart by clicking on a data point. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/3321) and [this pull request](https://github.com/VictoriaMetrics/VictoriaMetrics/pull/3376)
* FEATURE: [vmui](https://docs.victoriametrics.com/#vmui): add the ability to set up series custom limits. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/3297).
* FEATURE: [vmalert](https://docs.victoriametrics.com/vmalert.html): add default alert list for vmalert's metrics. See [alerts-vmalert.yml](https://github.com/VictoriaMetrics/VictoriaMetrics/blob/master/deployment/docker/alerts-vmalert.yml).
* FEATURE: [vmagent](https://docs.victoriametrics.com/vmagent.html): expose `vmagent_relabel_config_*`, `vm_relabel_config_*` and `vm_promscrape_config_*` metrics for tracking relabel and scrape configuration hot-reloads. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/3345).