mirror of
https://github.com/VictoriaMetrics/VictoriaMetrics.git
synced 2024-11-21 14:44:00 +00:00
vmui: add retention and downsampling filters debug pages (#7238)
### Describe Your Changes - add VMUI pages for filters debug - add `config.json` file to the root of the application. The file structure is as follows: ``` { "license": { "type": "enterprise" or "opensource" } } ``` - refactor navigation configuration files. This refactor enables more flexible customization of menu elements. UI: <details> <summary>Renention filters debug</summary> Empty page: ![1723474670](https://github.com/user-attachments/assets/3824bf64-dd22-410a-beb5-9599b8769acd) Results: ![1723474597](https://github.com/user-attachments/assets/1bc074ba-b6a7-4127-8638-65cb32e04db8) Example config: ![1723541836](https://github.com/user-attachments/assets/ccdb7f75-4e77-42c4-98be-4bfa7809a3b0) </details> <details> <summary>Downsampling filters debug</summary> Empty page: ![1723474663](https://github.com/user-attachments/assets/7bbd07bd-adce-440f-ba43-f4218e237280) Results: ![1723474589](https://github.com/user-attachments/assets/b793ae08-b685-427d-81f1-1c7c532a244a) Example config: ![1723541828](https://github.com/user-attachments/assets/d2ee4e37-8945-4c0f-a4ca-cff5fe3cfcd2) </details> ### Checklist The following checks are **mandatory**: - [ ] My change adheres [VictoriaMetrics contributing guidelines](https://docs.victoriametrics.com/contributing/).
This commit is contained in:
parent
6c9772b101
commit
0ff17c3ec4
22 changed files with 733 additions and 121 deletions
5
app/vmui/packages/vmui/public/config.json
Normal file
5
app/vmui/packages/vmui/public/config.json
Normal file
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"license": {
|
||||
"type": "opensource"
|
||||
}
|
||||
}
|
|
@ -15,6 +15,8 @@ import WithTemplate from "./pages/WithTemplate";
|
|||
import Relabel from "./pages/Relabel";
|
||||
import ActiveQueries from "./pages/ActiveQueries";
|
||||
import QueryAnalyzer from "./pages/QueryAnalyzer";
|
||||
import DownsamplingFilters from "./pages/DownsamplingFilters";
|
||||
import RetentionFilters from "./pages/RetentionFilters";
|
||||
|
||||
const App: FC = () => {
|
||||
const [loadedTheme, setLoadedTheme] = useState(false);
|
||||
|
@ -74,6 +76,14 @@ const App: FC = () => {
|
|||
path={router.icons}
|
||||
element={<PreviewIcons/>}
|
||||
/>
|
||||
<Route
|
||||
path={router.downsamplingDebug}
|
||||
element={<DownsamplingFilters/>}
|
||||
/>
|
||||
<Route
|
||||
path={router.retentionDebug}
|
||||
element={<RetentionFilters/>}
|
||||
/>
|
||||
</Route>
|
||||
</Routes>
|
||||
)}
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
export const getDownsamplingFiltersDebug = (server: string, flags: string, metrics: string): string => {
|
||||
const params = [
|
||||
`flags=${encodeURIComponent(flags)}`,
|
||||
`metrics=${encodeURIComponent(metrics)}`
|
||||
];
|
||||
return `${server}/downsampling-filters-debug?${params.join("&")}`;
|
||||
};
|
|
@ -0,0 +1,7 @@
|
|||
export const getRetentionFiltersDebug = (server: string, flags: string, metrics: string): string => {
|
||||
const params = [
|
||||
`flags=${encodeURIComponent(flags)}`,
|
||||
`metrics=${encodeURIComponent(metrics)}`
|
||||
];
|
||||
return `${server}/retention-filters-debug?${params.join("&")}`;
|
||||
};
|
|
@ -1,81 +0,0 @@
|
|||
import router, { routerOptions } from "../router";
|
||||
|
||||
export enum NavigationItemType {
|
||||
internalLink,
|
||||
externalLink,
|
||||
}
|
||||
|
||||
export interface NavigationItem {
|
||||
label?: string,
|
||||
value?: string,
|
||||
hide?: boolean
|
||||
submenu?: NavigationItem[],
|
||||
type?: NavigationItemType,
|
||||
}
|
||||
|
||||
const explore = {
|
||||
label: "Explore",
|
||||
submenu: [
|
||||
{
|
||||
label: routerOptions[router.metrics].title,
|
||||
value: router.metrics,
|
||||
},
|
||||
{
|
||||
label: routerOptions[router.cardinality].title,
|
||||
value: router.cardinality,
|
||||
},
|
||||
{
|
||||
label: routerOptions[router.topQueries].title,
|
||||
value: router.topQueries,
|
||||
},
|
||||
{
|
||||
label: routerOptions[router.activeQueries].title,
|
||||
value: router.activeQueries,
|
||||
},
|
||||
]
|
||||
};
|
||||
|
||||
const tools = {
|
||||
label: "Tools",
|
||||
submenu: [
|
||||
{
|
||||
label: routerOptions[router.trace].title,
|
||||
value: router.trace,
|
||||
},
|
||||
{
|
||||
label: routerOptions[router.queryAnalyzer].title,
|
||||
value: router.queryAnalyzer,
|
||||
},
|
||||
{
|
||||
label: routerOptions[router.withTemplate].title,
|
||||
value: router.withTemplate,
|
||||
},
|
||||
{
|
||||
label: routerOptions[router.relabel].title,
|
||||
value: router.relabel,
|
||||
},
|
||||
]
|
||||
};
|
||||
|
||||
export const logsNavigation: NavigationItem[] = [
|
||||
{
|
||||
label: routerOptions[router.logs].title,
|
||||
value: router.home,
|
||||
},
|
||||
];
|
||||
|
||||
export const anomalyNavigation: NavigationItem[] = [
|
||||
{
|
||||
label: routerOptions[router.anomaly].title,
|
||||
value: router.home,
|
||||
}
|
||||
];
|
||||
|
||||
export const defaultNavigation: NavigationItem[] = [
|
||||
{
|
||||
label: routerOptions[router.home].title,
|
||||
value: router.home,
|
||||
},
|
||||
explore,
|
||||
tools,
|
||||
];
|
34
app/vmui/packages/vmui/src/hooks/useFetchAppConfig.ts
Normal file
34
app/vmui/packages/vmui/src/hooks/useFetchAppConfig.ts
Normal file
|
@ -0,0 +1,34 @@
|
|||
import { useAppDispatch } from "../state/common/StateContext";
|
||||
import { useEffect, useState } from "preact/compat";
|
||||
import { ErrorTypes } from "../types";
|
||||
|
||||
const useFetchFlags = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<ErrorTypes | string>("");
|
||||
|
||||
useEffect(() => {
|
||||
const fetchAppConfig = async () => {
|
||||
if (process.env.REACT_APP_TYPE) return;
|
||||
setError("");
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
const data = await fetch("./config.json");
|
||||
const config = await data.json();
|
||||
dispatch({ type: "SET_APP_CONFIG", payload: config || {} });
|
||||
} catch (e) {
|
||||
setIsLoading(false);
|
||||
if (e instanceof Error) setError(`${e.name}: ${e.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
fetchAppConfig();
|
||||
}, []);
|
||||
|
||||
return { isLoading, error };
|
||||
};
|
||||
|
||||
export default useFetchFlags;
|
||||
|
|
@ -1,16 +1,12 @@
|
|||
import React, { FC, useMemo, useState } from "preact/compat";
|
||||
import router, { routerOptions } from "../../../router";
|
||||
import { getAppModeEnable } from "../../../utils/app-mode";
|
||||
import React, { FC, useState } from "preact/compat";
|
||||
import { useLocation } from "react-router-dom";
|
||||
import { useDashboardsState } from "../../../state/dashboards/DashboardsStateContext";
|
||||
import { useEffect } from "react";
|
||||
import "./style.scss";
|
||||
import NavItem from "./NavItem";
|
||||
import NavSubItem from "./NavSubItem";
|
||||
import classNames from "classnames";
|
||||
import { anomalyNavigation, defaultNavigation, logsNavigation, NavigationItemType } from "../../../constants/navigation";
|
||||
import { AppType } from "../../../types/appType";
|
||||
import { useAppState } from "../../../state/common/StateContext";
|
||||
import useNavigationMenu from "../../../router/useNavigationMenu";
|
||||
import { NavigationItemType } from "../../../router/navigation";
|
||||
|
||||
interface HeaderNavProps {
|
||||
color: string
|
||||
|
@ -19,43 +15,14 @@ interface HeaderNavProps {
|
|||
}
|
||||
|
||||
const HeaderNav: FC<HeaderNavProps> = ({ color, background, direction }) => {
|
||||
const appModeEnable = getAppModeEnable();
|
||||
const { dashboardsSettings } = useDashboardsState();
|
||||
const { pathname } = useLocation();
|
||||
const { serverUrl, flags } = useAppState();
|
||||
|
||||
const [activeMenu, setActiveMenu] = useState(pathname);
|
||||
|
||||
const menu = useMemo(() => {
|
||||
switch (process.env.REACT_APP_TYPE) {
|
||||
case AppType.logs:
|
||||
return logsNavigation;
|
||||
case AppType.anomaly:
|
||||
return anomalyNavigation;
|
||||
default:
|
||||
return ([
|
||||
...defaultNavigation,
|
||||
{
|
||||
label: routerOptions[router.dashboards].title,
|
||||
value: router.dashboards,
|
||||
hide: appModeEnable || !dashboardsSettings.length,
|
||||
},
|
||||
{
|
||||
// see more https://docs.victoriametrics.com/cluster-victoriametrics/?highlight=vmalertproxyurl#vmalert
|
||||
label: "Alerts",
|
||||
value: `${serverUrl}/vmalert`,
|
||||
type: NavigationItemType.externalLink,
|
||||
hide: !Object.keys(flags).includes("vmalert.proxyURL"),
|
||||
},
|
||||
].filter(r => !r.hide));
|
||||
}
|
||||
}, [appModeEnable, dashboardsSettings]);
|
||||
const menu = useNavigationMenu();
|
||||
|
||||
useEffect(() => {
|
||||
setActiveMenu(pathname);
|
||||
}, [pathname]);
|
||||
|
||||
|
||||
return (
|
||||
<nav
|
||||
className={classNames({
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import React, { FC } from "preact/compat";
|
||||
import { NavLink } from "react-router-dom";
|
||||
import classNames from "classnames";
|
||||
import { NavigationItemType } from "../../../constants/navigation";
|
||||
import { NavigationItemType } from "../../../router/navigation";
|
||||
|
||||
interface NavItemProps {
|
||||
activeMenu: string,
|
||||
|
|
|
@ -6,7 +6,7 @@ import Popper from "../../../components/Main/Popper/Popper";
|
|||
import NavItem from "./NavItem";
|
||||
import { useEffect } from "react";
|
||||
import useBoolean from "../../../hooks/useBoolean";
|
||||
import { NavigationItem, NavigationItemType } from "../../../constants/navigation";
|
||||
import { NavigationItem, NavigationItemType } from "../../../router/navigation";
|
||||
|
||||
interface NavItemProps {
|
||||
activeMenu: string,
|
||||
|
|
|
@ -12,6 +12,7 @@ import useDeviceDetect from "../../hooks/useDeviceDetect";
|
|||
import ControlsMainLayout from "./ControlsMainLayout";
|
||||
import useFetchDefaultTimezone from "../../hooks/useFetchDefaultTimezone";
|
||||
import useFetchFlags from "../../hooks/useFetchFlags";
|
||||
import useFetchAppConfig from "../../hooks/useFetchAppConfig";
|
||||
|
||||
const MainLayout: FC = () => {
|
||||
const appModeEnable = getAppModeEnable();
|
||||
|
@ -21,6 +22,7 @@ const MainLayout: FC = () => {
|
|||
|
||||
useFetchDashboards();
|
||||
useFetchDefaultTimezone();
|
||||
useFetchAppConfig();
|
||||
useFetchFlags();
|
||||
|
||||
const setDocumentTitle = () => {
|
||||
|
|
|
@ -0,0 +1,53 @@
|
|||
import { useAppState } from "../../../state/common/StateContext";
|
||||
import { useState } from "react";
|
||||
import { ErrorTypes } from "../../../types";
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
import { getDownsamplingFiltersDebug } from "../../../api/downsampling-filters-debug";
|
||||
import { useCallback } from "preact/compat";
|
||||
|
||||
export const useDebugDownsamplingFilters = () => {
|
||||
const { serverUrl } = useAppState();
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
|
||||
const [data, setData] = useState<Map<string, string[]>>(new Map());
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [metricsError, setMetricsError] = useState<ErrorTypes | string>();
|
||||
const [flagsError, setFlagsError] = useState<ErrorTypes | string>();
|
||||
const [error, setError] = useState<ErrorTypes | string>();
|
||||
|
||||
const fetchData = useCallback(async (flags: string, metrics: string) => {
|
||||
metrics ? setMetricsError("") : setMetricsError("metrics are required");
|
||||
flags ? setFlagsError("") : setFlagsError("flags are required");
|
||||
if (!metrics || !flags) return;
|
||||
|
||||
searchParams.set("flags", flags);
|
||||
searchParams.set("metrics", metrics);
|
||||
setSearchParams(searchParams);
|
||||
const fetchUrl = getDownsamplingFiltersDebug(serverUrl, flags, metrics);
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await fetch(fetchUrl);
|
||||
|
||||
const resp = await response.json();
|
||||
setData(new Map(Object.entries(resp.result || {})));
|
||||
setMetricsError(resp.error?.metrics || "");
|
||||
setFlagsError(resp.error?.flags || "");
|
||||
setError("");
|
||||
|
||||
} catch (e) {
|
||||
if (e instanceof Error && e.name !== "AbortError") {
|
||||
setError(`${e.name}: ${e.message}`);
|
||||
}
|
||||
}
|
||||
setLoading(false);
|
||||
}, [serverUrl]);
|
||||
|
||||
return {
|
||||
data,
|
||||
error: error,
|
||||
metricsError: metricsError,
|
||||
flagsError: flagsError,
|
||||
loading,
|
||||
applyFilters: fetchData
|
||||
};
|
||||
};
|
137
app/vmui/packages/vmui/src/pages/DownsamplingFilters/index.tsx
Normal file
137
app/vmui/packages/vmui/src/pages/DownsamplingFilters/index.tsx
Normal file
|
@ -0,0 +1,137 @@
|
|||
import React, { FC, useEffect } from "preact/compat";
|
||||
import "./style.scss";
|
||||
import TextField from "../../components/Main/TextField/TextField";
|
||||
import { useCallback, useState } from "react";
|
||||
import Button from "../../components/Main/Button/Button";
|
||||
import { PlayIcon, WikiIcon } from "../../components/Main/Icons";
|
||||
import { useDebugDownsamplingFilters } from "./hooks/useDebugDownsamplingFilters";
|
||||
import Spinner from "../../components/Main/Spinner/Spinner";
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
|
||||
const example = {
|
||||
flags: `-downsampling.period={env="dev"}:7d:5m,{env="dev"}:30d:30m
|
||||
-downsampling.period=30d:1m
|
||||
-downsampling.period=60d:5m
|
||||
`,
|
||||
metrics: `up
|
||||
up{env="dev"}
|
||||
up{env="prod"}`,
|
||||
};
|
||||
|
||||
const DownsamplingFilters: FC = () => {
|
||||
const [searchParams] = useSearchParams();
|
||||
|
||||
const { data, loading, error, metricsError, flagsError, applyFilters } = useDebugDownsamplingFilters();
|
||||
const [metrics, setMetrics] = useState(searchParams.get("metrics") || "");
|
||||
const [flags, setFlags] = useState(searchParams.get("flags") || "");
|
||||
|
||||
const handleMetricsChangeInput = useCallback((val: string) => {
|
||||
setMetrics(val);
|
||||
}, [setMetrics]);
|
||||
|
||||
const handleFlagsChangeInput = useCallback((val: string) => {
|
||||
setFlags(val);
|
||||
}, [setFlags]);
|
||||
|
||||
const handleApplyFilters = useCallback(() => {
|
||||
applyFilters(flags, metrics);
|
||||
}, [applyFilters, flags, metrics]);
|
||||
|
||||
const handleRunExample = useCallback(() => {
|
||||
const { flags, metrics } = example;
|
||||
setFlags(flags);
|
||||
setMetrics(metrics);
|
||||
applyFilters(flags, metrics);
|
||||
searchParams.set("flags", flags);
|
||||
searchParams.set("metrics", metrics);
|
||||
}, [example, setFlags, setMetrics, searchParams]);
|
||||
|
||||
useEffect(() => {
|
||||
if (flags && metrics) handleApplyFilters();
|
||||
}, []);
|
||||
|
||||
const rows = [];
|
||||
for (const [key, value] of data) {
|
||||
rows.push(<tr className="vm-table__row">
|
||||
<td className="vm-table-cell">{key}</td>
|
||||
<td className="vm-table-cell">{value.join(" ")}</td>
|
||||
</tr>);
|
||||
}
|
||||
return (
|
||||
<section className="vm-downsampling-filters">
|
||||
{loading && <Spinner/>}
|
||||
|
||||
<div className="vm-downsampling-filters-body vm-block">
|
||||
<div className="vm-downsampling-filters-body__expr">
|
||||
<div className="vm-retention-filters-body__title">
|
||||
<p>Provide a list of flags for downsampling configuration. Note that
|
||||
only <code>-downsampling.period</code> and <code>-dedup.minScrapeInterval</code> flags are supported</p>
|
||||
</div>
|
||||
<TextField
|
||||
type="textarea"
|
||||
label="Flags"
|
||||
value={flags}
|
||||
error={error || flagsError}
|
||||
autofocus
|
||||
onEnter={handleApplyFilters}
|
||||
onChange={handleFlagsChangeInput}
|
||||
placeholder={"-downsampling.period=30d:1m -downsampling.period=7d:5m -dedup.minScrapeInterval=30s"}
|
||||
/>
|
||||
</div>
|
||||
<div className="vm-downsampling-filters-body__expr">
|
||||
<div className="vm-retention-filters-body__title">
|
||||
<p>Provide a list of metrics to check downsampling configuration.</p>
|
||||
</div>
|
||||
<TextField
|
||||
type="textarea"
|
||||
label="Metrics"
|
||||
value={metrics}
|
||||
error={error || metricsError}
|
||||
onEnter={handleApplyFilters}
|
||||
onChange={handleMetricsChangeInput}
|
||||
placeholder={"up{env=\"dev\"}\nup{env=\"prod\"}\n"}
|
||||
/>
|
||||
</div>
|
||||
<div className="vm-downsampling-filters-body__result">
|
||||
<table className="vm-table">
|
||||
<thead className="vm-table-header">
|
||||
<tr>
|
||||
<th className="vm-table-cell vm-table-cell_header">Metric</th>
|
||||
<th className="vm-table-cell vm-table-cell_header">Applied downsampling rules</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="vm-table-body">
|
||||
{rows}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div className="vm-downsampling-filters-body-top">
|
||||
<a
|
||||
className="vm-link vm-link_with-icon"
|
||||
target="_blank"
|
||||
href="https://docs.victoriametrics.com/#downsampling"
|
||||
rel="help noreferrer"
|
||||
>
|
||||
<WikiIcon/>
|
||||
Documentation
|
||||
</a>
|
||||
<Button
|
||||
variant="text"
|
||||
onClick={handleRunExample}
|
||||
>
|
||||
Try example
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={handleApplyFilters}
|
||||
startIcon={<PlayIcon/>}
|
||||
>
|
||||
Apply
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default DownsamplingFilters;
|
|
@ -0,0 +1,46 @@
|
|||
@use "src/styles/variables" as *;
|
||||
|
||||
.vm-downsampling-filters {
|
||||
display: grid;
|
||||
gap: $padding-medium;
|
||||
|
||||
&-body {
|
||||
display: grid;
|
||||
gap: $padding-global;
|
||||
align-items: flex-start;
|
||||
width: 100%;
|
||||
|
||||
&__title {
|
||||
margin-bottom: $padding-medium;
|
||||
}
|
||||
|
||||
&-top {
|
||||
display: flex;
|
||||
gap: $padding-small;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
&__expr textarea {
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
&__result textarea {
|
||||
min-height: 60px;
|
||||
}
|
||||
|
||||
code {
|
||||
background-color: var(--color-hover-black);
|
||||
border-radius: 6px;
|
||||
font-size: 85%;
|
||||
padding: .2em .4em;
|
||||
}
|
||||
|
||||
textarea {
|
||||
font-family: $font-family-monospace;
|
||||
overflow: auto;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
import { useAppState } from "../../../state/common/StateContext";
|
||||
import { useState } from "react";
|
||||
import { ErrorTypes } from "../../../types";
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
import { getRetentionFiltersDebug } from "../../../api/retention-filters-debug";
|
||||
import { useCallback } from "preact/compat";
|
||||
|
||||
export const useDebugRetentionFilters = () => {
|
||||
const { serverUrl } = useAppState();
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
|
||||
const [data, setData] = useState<Map<string, string>>(new Map());
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [metricsError, setMetricsError] = useState<ErrorTypes | string>();
|
||||
const [flagsError, setFlagsError] = useState<ErrorTypes | string>();
|
||||
const [error, setError] = useState<ErrorTypes | string>();
|
||||
|
||||
const fetchData = useCallback(async (flags: string, metrics: string) => {
|
||||
metrics ? setMetricsError("") : setMetricsError("metrics are required");
|
||||
flags ? setFlagsError("") : setFlagsError("flags are required");
|
||||
if (!metrics || !flags) return;
|
||||
|
||||
searchParams.set("flags", flags);
|
||||
searchParams.set("metrics", metrics);
|
||||
setSearchParams(searchParams);
|
||||
const fetchUrl = getRetentionFiltersDebug(serverUrl, flags, metrics);
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await fetch(fetchUrl);
|
||||
|
||||
const resp = await response.json();
|
||||
setData(new Map(Object.entries(resp.result || {})));
|
||||
setMetricsError(resp.error?.metrics || "");
|
||||
setFlagsError(resp.error?.flags || "");
|
||||
setError("");
|
||||
|
||||
} catch (e) {
|
||||
if (e instanceof Error && e.name !== "AbortError") {
|
||||
setError(`${e.name}: ${e.message}`);
|
||||
}
|
||||
}
|
||||
setLoading(false);
|
||||
}, [serverUrl]);
|
||||
|
||||
return {
|
||||
data,
|
||||
error: error,
|
||||
metricsError: metricsError,
|
||||
flagsError: flagsError,
|
||||
loading,
|
||||
applyFilters: fetchData
|
||||
};
|
||||
};
|
137
app/vmui/packages/vmui/src/pages/RetentionFilters/index.tsx
Normal file
137
app/vmui/packages/vmui/src/pages/RetentionFilters/index.tsx
Normal file
|
@ -0,0 +1,137 @@
|
|||
import React, { FC, useEffect } from "preact/compat";
|
||||
import "./style.scss";
|
||||
import TextField from "../../components/Main/TextField/TextField";
|
||||
import { useCallback, useState } from "react";
|
||||
import Button from "../../components/Main/Button/Button";
|
||||
import { PlayIcon, WikiIcon } from "../../components/Main/Icons";
|
||||
import { useDebugRetentionFilters } from "./hooks/useDebugRetentionFilters";
|
||||
import Spinner from "../../components/Main/Spinner/Spinner";
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
|
||||
const example = {
|
||||
flags: `-retentionPeriod=1y
|
||||
-retentionFilters={env!="prod"}:2w
|
||||
`,
|
||||
metrics: `up
|
||||
up{env="dev"}
|
||||
up{env="prod"}`,
|
||||
};
|
||||
|
||||
const RetentionFilters: FC = () => {
|
||||
const [searchParams] = useSearchParams();
|
||||
|
||||
const { data, loading, error, metricsError, flagsError, applyFilters } = useDebugRetentionFilters();
|
||||
const [metrics, setMetrics] = useState(searchParams.get("metrics") || "");
|
||||
const [flags, setFlags] = useState(searchParams.get("flags") || "");
|
||||
|
||||
const handleMetricsChangeInput = useCallback((val: string) => {
|
||||
setMetrics(val);
|
||||
}, [setMetrics]);
|
||||
|
||||
const handleFlagsChangeInput = useCallback((val: string) => {
|
||||
setFlags(val);
|
||||
}, [setFlags]);
|
||||
|
||||
const handleApplyFilters = useCallback(() => {
|
||||
applyFilters(flags, metrics);
|
||||
}, [applyFilters, flags, metrics]);
|
||||
|
||||
const handleRunExample = useCallback(() => {
|
||||
const { flags, metrics } = example;
|
||||
setFlags(flags);
|
||||
setMetrics(metrics);
|
||||
applyFilters(flags, metrics);
|
||||
searchParams.set("flags", flags);
|
||||
searchParams.set("metrics", metrics);
|
||||
}, [example, setFlags, setMetrics, searchParams]);
|
||||
|
||||
useEffect(() => {
|
||||
if (flags && metrics) handleApplyFilters();
|
||||
}, []);
|
||||
|
||||
const rows = [];
|
||||
for (const [key, value] of data) {
|
||||
rows.push(<tr className="vm-table__row">
|
||||
<td className="vm-table-cell">{key}</td>
|
||||
<td className="vm-table-cell">{value}</td>
|
||||
</tr>);
|
||||
}
|
||||
return (
|
||||
<section className="vm-retention-filters">
|
||||
{loading && <Spinner/>}
|
||||
|
||||
<div className="vm-retention-filters-body vm-block">
|
||||
<div className="vm-retention-filters-body__expr">
|
||||
<div className="vm-retention-filters-body__title">
|
||||
<p>Provide a list of flags for retention configuration. Note that
|
||||
only <code>-retentionPeriod</code> and <code>-retentionFilters</code> flags are
|
||||
supported.</p>
|
||||
</div>
|
||||
<TextField
|
||||
type="textarea"
|
||||
label="Flags"
|
||||
value={flags}
|
||||
error={error || flagsError}
|
||||
autofocus
|
||||
onEnter={handleApplyFilters}
|
||||
onChange={handleFlagsChangeInput}
|
||||
placeholder={"-retentionPeriod=4w -retentionFilters=up{env=\"dev\"}:2w"}
|
||||
/>
|
||||
</div>
|
||||
<div className="vm-retention-filters-body__expr">
|
||||
<div className="vm-retention-filters-body__title">
|
||||
<p>Provide a list of metrics to check retention configuration.</p>
|
||||
</div>
|
||||
<TextField
|
||||
type="textarea"
|
||||
label="Metrics"
|
||||
value={metrics}
|
||||
error={error || metricsError}
|
||||
onEnter={handleApplyFilters}
|
||||
onChange={handleMetricsChangeInput}
|
||||
placeholder={"up{env=\"dev\"}\nup{env=\"prod\"}\n"}
|
||||
/>
|
||||
</div>
|
||||
<div className="vm-retention-filters-body__result">
|
||||
<table className="vm-table">
|
||||
<thead className="vm-table-header">
|
||||
<tr>
|
||||
<th className="vm-table-cell vm-table-cell_header">Metric</th>
|
||||
<th className="vm-table-cell vm-table-cell_header">Applied retention</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="vm-table-body">
|
||||
{rows}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div className="vm-retention-filters-body-top">
|
||||
<a
|
||||
className="vm-link vm-link_with-icon"
|
||||
target="_blank"
|
||||
href="https://docs.victoriametrics.com/#retention-filters"
|
||||
rel="help noreferrer"
|
||||
>
|
||||
<WikiIcon/>
|
||||
Documentation
|
||||
</a>
|
||||
<Button
|
||||
variant="text"
|
||||
onClick={handleRunExample}
|
||||
>
|
||||
Try example
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={handleApplyFilters}
|
||||
startIcon={<PlayIcon/>}
|
||||
>
|
||||
Apply
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default RetentionFilters;
|
46
app/vmui/packages/vmui/src/pages/RetentionFilters/style.scss
Normal file
46
app/vmui/packages/vmui/src/pages/RetentionFilters/style.scss
Normal file
|
@ -0,0 +1,46 @@
|
|||
@use "src/styles/variables" as *;
|
||||
|
||||
.vm-retention-filters {
|
||||
display: grid;
|
||||
gap: $padding-medium;
|
||||
|
||||
&-body {
|
||||
display: grid;
|
||||
gap: $padding-global;
|
||||
align-items: flex-start;
|
||||
width: 100%;
|
||||
|
||||
&__title {
|
||||
margin-bottom: $padding-medium;
|
||||
}
|
||||
|
||||
&-top {
|
||||
display: flex;
|
||||
gap: $padding-small;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
&__expr textarea {
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
&__result textarea {
|
||||
min-height: 60px;
|
||||
}
|
||||
|
||||
code {
|
||||
background-color: var(--color-hover-black);
|
||||
border-radius: 6px;
|
||||
font-size: 85%;
|
||||
padding: .2em .4em;
|
||||
}
|
||||
|
||||
textarea {
|
||||
font-family: $font-family-monospace;
|
||||
overflow: auto;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -15,6 +15,8 @@ const router = {
|
|||
icons: "/icons",
|
||||
anomaly: "/anomaly",
|
||||
query: "/query",
|
||||
downsamplingDebug: "/downsampling-filters-debug",
|
||||
retentionDebug: "/retention-filters-debug",
|
||||
};
|
||||
|
||||
export interface RouterOptionsHeader {
|
||||
|
@ -108,6 +110,14 @@ export const routerOptions: {[key: string]: RouterOptions} = {
|
|||
[router.query]: {
|
||||
title: "Query",
|
||||
...routerOptionsDefault
|
||||
},
|
||||
[router.downsamplingDebug]: {
|
||||
title: "Downsampling filters debug",
|
||||
header: {}
|
||||
},
|
||||
[router.retentionDebug]: {
|
||||
title: "Retention filters debug",
|
||||
header: {}
|
||||
}
|
||||
};
|
||||
|
||||
|
|
92
app/vmui/packages/vmui/src/router/navigation.ts
Normal file
92
app/vmui/packages/vmui/src/router/navigation.ts
Normal file
|
@ -0,0 +1,92 @@
|
|||
import router, { routerOptions } from "./index";
|
||||
|
||||
export enum NavigationItemType {
|
||||
internalLink,
|
||||
externalLink,
|
||||
}
|
||||
|
||||
export interface NavigationItem {
|
||||
label?: string,
|
||||
value?: string,
|
||||
hide?: boolean
|
||||
submenu?: NavigationItem[],
|
||||
type?: NavigationItemType,
|
||||
}
|
||||
|
||||
interface NavigationConfig {
|
||||
serverUrl: string,
|
||||
isEnterpriseLicense: boolean,
|
||||
showPredefinedDashboards: boolean,
|
||||
showAlertLink: boolean,
|
||||
}
|
||||
|
||||
/**
|
||||
* Special case for alert link
|
||||
*/
|
||||
const getAlertLink = (url: string, showAlertLink: boolean) => {
|
||||
// see more https://docs.victoriametrics.com/cluster-victoriametrics/?highlight=vmalertproxyurl#vmalert
|
||||
return {
|
||||
label: "Alerts",
|
||||
value: `${url}/vmalert`,
|
||||
type: NavigationItemType.externalLink,
|
||||
hide: !showAlertLink,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Submenu for Tools tab
|
||||
*/
|
||||
const getToolsNav = (isEnterpriseLicense: boolean) => [
|
||||
{ value: router.trace },
|
||||
{ value: router.queryAnalyzer },
|
||||
{ value: router.withTemplate },
|
||||
{ value: router.relabel },
|
||||
{ value: router.downsamplingDebug, hide: !isEnterpriseLicense },
|
||||
{ value: router.retentionDebug, hide: !isEnterpriseLicense },
|
||||
];
|
||||
|
||||
/**
|
||||
* Submenu for Explore tab
|
||||
*/
|
||||
const getExploreNav = () => [
|
||||
{ value: router.metrics },
|
||||
{ value: router.cardinality },
|
||||
{ value: router.topQueries },
|
||||
{ value: router.activeQueries },
|
||||
];
|
||||
|
||||
/**
|
||||
* Default navigation menu
|
||||
*/
|
||||
export const getDefaultNavigation = ({
|
||||
serverUrl,
|
||||
isEnterpriseLicense,
|
||||
showPredefinedDashboards,
|
||||
showAlertLink,
|
||||
}: NavigationConfig): NavigationItem[] => [
|
||||
{ value: router.home },
|
||||
{ label: "Explore", submenu: getExploreNav() },
|
||||
{ label: "Tools", submenu: getToolsNav(isEnterpriseLicense) },
|
||||
{ value: router.dashboards, hide: !showPredefinedDashboards },
|
||||
getAlertLink(serverUrl, showAlertLink),
|
||||
];
|
||||
|
||||
/**
|
||||
* VictoriaLogs navigation menu
|
||||
*/
|
||||
export const getLogsNavigation = (): NavigationItem[] => [
|
||||
{
|
||||
label: routerOptions[router.logs].title,
|
||||
value: router.home,
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* vmanomaly navigation menu
|
||||
*/
|
||||
export const getAnomalyNavigation = (): NavigationItem[] => [
|
||||
{
|
||||
label: routerOptions[router.anomaly].title,
|
||||
value: router.home,
|
||||
},
|
||||
];
|
43
app/vmui/packages/vmui/src/router/useNavigationMenu.ts
Normal file
43
app/vmui/packages/vmui/src/router/useNavigationMenu.ts
Normal file
|
@ -0,0 +1,43 @@
|
|||
import { getAppModeEnable } from "../utils/app-mode";
|
||||
import { useDashboardsState } from "../state/dashboards/DashboardsStateContext";
|
||||
import { useAppState } from "../state/common/StateContext";
|
||||
import { useMemo } from "preact/compat";
|
||||
import { AppType } from "../types/appType";
|
||||
import { processNavigationItems } from "./utils";
|
||||
import { getAnomalyNavigation, getDefaultNavigation, getLogsNavigation } from "./navigation";
|
||||
|
||||
const appType = process.env.REACT_APP_TYPE;
|
||||
|
||||
const useNavigationMenu = () => {
|
||||
const appModeEnable = getAppModeEnable();
|
||||
const { dashboardsSettings } = useDashboardsState();
|
||||
const { serverUrl, flags, appConfig } = useAppState();
|
||||
const isEnterpriseLicense = appConfig.license?.type === "enterprise";
|
||||
const showAlertLink = Boolean(flags["vmalert.proxyURL"]);
|
||||
const showPredefinedDashboards = Boolean(!appModeEnable && dashboardsSettings.length);
|
||||
|
||||
const navigationConfig = useMemo(() => ({
|
||||
serverUrl,
|
||||
isEnterpriseLicense,
|
||||
showAlertLink,
|
||||
showPredefinedDashboards
|
||||
}), [serverUrl, isEnterpriseLicense, showAlertLink, showPredefinedDashboards]);
|
||||
|
||||
|
||||
const menu = useMemo(() => {
|
||||
switch (appType) {
|
||||
case AppType.logs:
|
||||
return getLogsNavigation();
|
||||
case AppType.anomaly:
|
||||
return getAnomalyNavigation();
|
||||
default:
|
||||
return getDefaultNavigation(navigationConfig);
|
||||
}
|
||||
}, [navigationConfig]);
|
||||
|
||||
return processNavigationItems(menu);
|
||||
};
|
||||
|
||||
export default useNavigationMenu;
|
||||
|
||||
|
30
app/vmui/packages/vmui/src/router/utils.ts
Normal file
30
app/vmui/packages/vmui/src/router/utils.ts
Normal file
|
@ -0,0 +1,30 @@
|
|||
import { routerOptions } from "./index";
|
||||
import { NavigationItem } from "./navigation";
|
||||
|
||||
const routePathToTitle = (path: string): string => {
|
||||
try {
|
||||
return path
|
||||
.replace(/^\/+/, "") // Remove leading slashes
|
||||
.replace(/-/g, " ") // Replace hyphens with spaces
|
||||
.trim() // Trim whitespace from both ends
|
||||
.replace(/^\w/, (c) => c.toUpperCase()); // Capitalize the first character
|
||||
} catch (e) {
|
||||
return path;
|
||||
}
|
||||
};
|
||||
|
||||
export const processNavigationItems = (items: NavigationItem[]): NavigationItem[] => {
|
||||
return items.filter((item) => !item.hide).map((item) => {
|
||||
const newItem: NavigationItem = { ...item };
|
||||
|
||||
if (newItem.value && !newItem.label) {
|
||||
newItem.label = routerOptions[newItem.value]?.title || routePathToTitle(newItem.value);
|
||||
}
|
||||
|
||||
if (newItem.submenu && newItem.submenu.length > 0) {
|
||||
newItem.submenu = processNavigationItems(newItem.submenu);
|
||||
}
|
||||
|
||||
return newItem;
|
||||
});
|
||||
};
|
|
@ -1,7 +1,7 @@
|
|||
import { getDefaultServer } from "../../utils/default-server-url";
|
||||
import { getQueryStringValue } from "../../utils/query-string";
|
||||
import { getFromStorage, saveToStorage } from "../../utils/storage";
|
||||
import { Theme } from "../../types";
|
||||
import { AppConfig, Theme } from "../../types";
|
||||
import { isDarkTheme } from "../../utils/theme";
|
||||
import { removeTrailingSlash } from "../../utils/url";
|
||||
|
||||
|
@ -11,6 +11,7 @@ export interface AppState {
|
|||
theme: Theme;
|
||||
isDarkTheme: boolean | null;
|
||||
flags: Record<string, string | null>;
|
||||
appConfig: AppConfig
|
||||
}
|
||||
|
||||
export type Action =
|
||||
|
@ -18,6 +19,7 @@ export type Action =
|
|||
| { type: "SET_THEME", payload: Theme }
|
||||
| { type: "SET_TENANT_ID", payload: string }
|
||||
| { type: "SET_FLAGS", payload: Record<string, string | null> }
|
||||
| { type: "SET_APP_CONFIG", payload: AppConfig }
|
||||
| { type: "SET_DARK_THEME" }
|
||||
|
||||
const tenantId = getQueryStringValue("g0.tenantID", "") as string;
|
||||
|
@ -28,6 +30,7 @@ export const initialState: AppState = {
|
|||
theme: (getFromStorage("THEME") || Theme.system) as Theme,
|
||||
isDarkTheme: null,
|
||||
flags: {},
|
||||
appConfig: {}
|
||||
};
|
||||
|
||||
export function reducer(state: AppState, action: Action): AppState {
|
||||
|
@ -58,6 +61,11 @@ export function reducer(state: AppState, action: Action): AppState {
|
|||
...state,
|
||||
flags: action.payload
|
||||
};
|
||||
case "SET_APP_CONFIG":
|
||||
return {
|
||||
...state,
|
||||
appConfig: action.payload
|
||||
};
|
||||
default:
|
||||
throw new Error();
|
||||
}
|
||||
|
|
|
@ -165,3 +165,9 @@ export enum QueryContextType {
|
|||
label = "label",
|
||||
labelValue = "labelValue",
|
||||
}
|
||||
|
||||
export interface AppConfig {
|
||||
license?: {
|
||||
type?: "enterprise" | "opensource";
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue