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 Relabel from "./pages/Relabel";
|
||||||
import ActiveQueries from "./pages/ActiveQueries";
|
import ActiveQueries from "./pages/ActiveQueries";
|
||||||
import QueryAnalyzer from "./pages/QueryAnalyzer";
|
import QueryAnalyzer from "./pages/QueryAnalyzer";
|
||||||
|
import DownsamplingFilters from "./pages/DownsamplingFilters";
|
||||||
|
import RetentionFilters from "./pages/RetentionFilters";
|
||||||
|
|
||||||
const App: FC = () => {
|
const App: FC = () => {
|
||||||
const [loadedTheme, setLoadedTheme] = useState(false);
|
const [loadedTheme, setLoadedTheme] = useState(false);
|
||||||
|
@ -74,6 +76,14 @@ const App: FC = () => {
|
||||||
path={router.icons}
|
path={router.icons}
|
||||||
element={<PreviewIcons/>}
|
element={<PreviewIcons/>}
|
||||||
/>
|
/>
|
||||||
|
<Route
|
||||||
|
path={router.downsamplingDebug}
|
||||||
|
element={<DownsamplingFilters/>}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path={router.retentionDebug}
|
||||||
|
element={<RetentionFilters/>}
|
||||||
|
/>
|
||||||
</Route>
|
</Route>
|
||||||
</Routes>
|
</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 React, { FC, useState } from "preact/compat";
|
||||||
import router, { routerOptions } from "../../../router";
|
|
||||||
import { getAppModeEnable } from "../../../utils/app-mode";
|
|
||||||
import { useLocation } from "react-router-dom";
|
import { useLocation } from "react-router-dom";
|
||||||
import { useDashboardsState } from "../../../state/dashboards/DashboardsStateContext";
|
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import "./style.scss";
|
import "./style.scss";
|
||||||
import NavItem from "./NavItem";
|
import NavItem from "./NavItem";
|
||||||
import NavSubItem from "./NavSubItem";
|
import NavSubItem from "./NavSubItem";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { anomalyNavigation, defaultNavigation, logsNavigation, NavigationItemType } from "../../../constants/navigation";
|
import useNavigationMenu from "../../../router/useNavigationMenu";
|
||||||
import { AppType } from "../../../types/appType";
|
import { NavigationItemType } from "../../../router/navigation";
|
||||||
import { useAppState } from "../../../state/common/StateContext";
|
|
||||||
|
|
||||||
interface HeaderNavProps {
|
interface HeaderNavProps {
|
||||||
color: string
|
color: string
|
||||||
|
@ -19,43 +15,14 @@ interface HeaderNavProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
const HeaderNav: FC<HeaderNavProps> = ({ color, background, direction }) => {
|
const HeaderNav: FC<HeaderNavProps> = ({ color, background, direction }) => {
|
||||||
const appModeEnable = getAppModeEnable();
|
|
||||||
const { dashboardsSettings } = useDashboardsState();
|
|
||||||
const { pathname } = useLocation();
|
const { pathname } = useLocation();
|
||||||
const { serverUrl, flags } = useAppState();
|
|
||||||
|
|
||||||
const [activeMenu, setActiveMenu] = useState(pathname);
|
const [activeMenu, setActiveMenu] = useState(pathname);
|
||||||
|
const menu = useNavigationMenu();
|
||||||
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]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setActiveMenu(pathname);
|
setActiveMenu(pathname);
|
||||||
}, [pathname]);
|
}, [pathname]);
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<nav
|
<nav
|
||||||
className={classNames({
|
className={classNames({
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import React, { FC } from "preact/compat";
|
import React, { FC } from "preact/compat";
|
||||||
import { NavLink } from "react-router-dom";
|
import { NavLink } from "react-router-dom";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { NavigationItemType } from "../../../constants/navigation";
|
import { NavigationItemType } from "../../../router/navigation";
|
||||||
|
|
||||||
interface NavItemProps {
|
interface NavItemProps {
|
||||||
activeMenu: string,
|
activeMenu: string,
|
||||||
|
|
|
@ -6,7 +6,7 @@ import Popper from "../../../components/Main/Popper/Popper";
|
||||||
import NavItem from "./NavItem";
|
import NavItem from "./NavItem";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import useBoolean from "../../../hooks/useBoolean";
|
import useBoolean from "../../../hooks/useBoolean";
|
||||||
import { NavigationItem, NavigationItemType } from "../../../constants/navigation";
|
import { NavigationItem, NavigationItemType } from "../../../router/navigation";
|
||||||
|
|
||||||
interface NavItemProps {
|
interface NavItemProps {
|
||||||
activeMenu: string,
|
activeMenu: string,
|
||||||
|
|
|
@ -12,6 +12,7 @@ import useDeviceDetect from "../../hooks/useDeviceDetect";
|
||||||
import ControlsMainLayout from "./ControlsMainLayout";
|
import ControlsMainLayout from "./ControlsMainLayout";
|
||||||
import useFetchDefaultTimezone from "../../hooks/useFetchDefaultTimezone";
|
import useFetchDefaultTimezone from "../../hooks/useFetchDefaultTimezone";
|
||||||
import useFetchFlags from "../../hooks/useFetchFlags";
|
import useFetchFlags from "../../hooks/useFetchFlags";
|
||||||
|
import useFetchAppConfig from "../../hooks/useFetchAppConfig";
|
||||||
|
|
||||||
const MainLayout: FC = () => {
|
const MainLayout: FC = () => {
|
||||||
const appModeEnable = getAppModeEnable();
|
const appModeEnable = getAppModeEnable();
|
||||||
|
@ -21,6 +22,7 @@ const MainLayout: FC = () => {
|
||||||
|
|
||||||
useFetchDashboards();
|
useFetchDashboards();
|
||||||
useFetchDefaultTimezone();
|
useFetchDefaultTimezone();
|
||||||
|
useFetchAppConfig();
|
||||||
useFetchFlags();
|
useFetchFlags();
|
||||||
|
|
||||||
const setDocumentTitle = () => {
|
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",
|
icons: "/icons",
|
||||||
anomaly: "/anomaly",
|
anomaly: "/anomaly",
|
||||||
query: "/query",
|
query: "/query",
|
||||||
|
downsamplingDebug: "/downsampling-filters-debug",
|
||||||
|
retentionDebug: "/retention-filters-debug",
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface RouterOptionsHeader {
|
export interface RouterOptionsHeader {
|
||||||
|
@ -108,6 +110,14 @@ export const routerOptions: {[key: string]: RouterOptions} = {
|
||||||
[router.query]: {
|
[router.query]: {
|
||||||
title: "Query",
|
title: "Query",
|
||||||
...routerOptionsDefault
|
...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 { getDefaultServer } from "../../utils/default-server-url";
|
||||||
import { getQueryStringValue } from "../../utils/query-string";
|
import { getQueryStringValue } from "../../utils/query-string";
|
||||||
import { getFromStorage, saveToStorage } from "../../utils/storage";
|
import { getFromStorage, saveToStorage } from "../../utils/storage";
|
||||||
import { Theme } from "../../types";
|
import { AppConfig, Theme } from "../../types";
|
||||||
import { isDarkTheme } from "../../utils/theme";
|
import { isDarkTheme } from "../../utils/theme";
|
||||||
import { removeTrailingSlash } from "../../utils/url";
|
import { removeTrailingSlash } from "../../utils/url";
|
||||||
|
|
||||||
|
@ -11,6 +11,7 @@ export interface AppState {
|
||||||
theme: Theme;
|
theme: Theme;
|
||||||
isDarkTheme: boolean | null;
|
isDarkTheme: boolean | null;
|
||||||
flags: Record<string, string | null>;
|
flags: Record<string, string | null>;
|
||||||
|
appConfig: AppConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Action =
|
export type Action =
|
||||||
|
@ -18,6 +19,7 @@ export type Action =
|
||||||
| { type: "SET_THEME", payload: Theme }
|
| { type: "SET_THEME", payload: Theme }
|
||||||
| { type: "SET_TENANT_ID", payload: string }
|
| { type: "SET_TENANT_ID", payload: string }
|
||||||
| { type: "SET_FLAGS", payload: Record<string, string | null> }
|
| { type: "SET_FLAGS", payload: Record<string, string | null> }
|
||||||
|
| { type: "SET_APP_CONFIG", payload: AppConfig }
|
||||||
| { type: "SET_DARK_THEME" }
|
| { type: "SET_DARK_THEME" }
|
||||||
|
|
||||||
const tenantId = getQueryStringValue("g0.tenantID", "") as string;
|
const tenantId = getQueryStringValue("g0.tenantID", "") as string;
|
||||||
|
@ -28,6 +30,7 @@ export const initialState: AppState = {
|
||||||
theme: (getFromStorage("THEME") || Theme.system) as Theme,
|
theme: (getFromStorage("THEME") || Theme.system) as Theme,
|
||||||
isDarkTheme: null,
|
isDarkTheme: null,
|
||||||
flags: {},
|
flags: {},
|
||||||
|
appConfig: {}
|
||||||
};
|
};
|
||||||
|
|
||||||
export function reducer(state: AppState, action: Action): AppState {
|
export function reducer(state: AppState, action: Action): AppState {
|
||||||
|
@ -58,6 +61,11 @@ export function reducer(state: AppState, action: Action): AppState {
|
||||||
...state,
|
...state,
|
||||||
flags: action.payload
|
flags: action.payload
|
||||||
};
|
};
|
||||||
|
case "SET_APP_CONFIG":
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
appConfig: action.payload
|
||||||
|
};
|
||||||
default:
|
default:
|
||||||
throw new Error();
|
throw new Error();
|
||||||
}
|
}
|
||||||
|
|
|
@ -165,3 +165,9 @@ export enum QueryContextType {
|
||||||
label = "label",
|
label = "label",
|
||||||
labelValue = "labelValue",
|
labelValue = "labelValue",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AppConfig {
|
||||||
|
license?: {
|
||||||
|
type?: "enterprise" | "opensource";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue