diff --git a/app/vmui/packages/vmui/src/constants/navigation.ts b/app/vmui/packages/vmui/src/constants/navigation.ts index d198636748..0df6c64d72 100644 --- a/app/vmui/packages/vmui/src/constants/navigation.ts +++ b/app/vmui/packages/vmui/src/constants/navigation.ts @@ -1,10 +1,16 @@ 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 = { diff --git a/app/vmui/packages/vmui/src/hooks/useFetchFlags.ts b/app/vmui/packages/vmui/src/hooks/useFetchFlags.ts new file mode 100644 index 0000000000..7c695323af --- /dev/null +++ b/app/vmui/packages/vmui/src/hooks/useFetchFlags.ts @@ -0,0 +1,44 @@ +import { useAppDispatch, useAppState } from "../state/common/StateContext"; +import { useEffect, useState } from "preact/compat"; +import { ErrorTypes } from "../types"; +import { getUrlWithoutTenant } from "../utils/tenants"; + +const useFetchFlags = () => { + const { serverUrl } = useAppState(); + const dispatch = useAppDispatch(); + + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(""); + + useEffect(() => { + const fetchFlags = async () => { + if (!serverUrl || process.env.REACT_APP_TYPE) return; + setError(""); + setIsLoading(true); + + try { + const url = getUrlWithoutTenant(serverUrl); + const response = await fetch(`${url}/flags`); + const data = await response.text(); + const flags = data.split("\n").filter(flag => flag.trim() !== "") + .reduce((acc, flag) => { + const [keyRaw, valueRaw] = flag.split("="); + const key = keyRaw.trim().replace(/^-/, ""); + acc[key.trim()] = valueRaw ? valueRaw.trim().replace(/^"(.*)"$/, "$1") : null; + return acc; + }, {} as Record); + dispatch({ type: "SET_FLAGS", payload: flags }); + } catch (e) { + setIsLoading(false); + if (e instanceof Error) setError(`${e.name}: ${e.message}`); + } + }; + + fetchFlags(); + }, [serverUrl]); + + return { isLoading, error }; +}; + +export default useFetchFlags; + diff --git a/app/vmui/packages/vmui/src/layouts/Header/HeaderNav/HeaderNav.tsx b/app/vmui/packages/vmui/src/layouts/Header/HeaderNav/HeaderNav.tsx index 46bebc0187..88c8bc19df 100644 --- a/app/vmui/packages/vmui/src/layouts/Header/HeaderNav/HeaderNav.tsx +++ b/app/vmui/packages/vmui/src/layouts/Header/HeaderNav/HeaderNav.tsx @@ -8,8 +8,9 @@ import "./style.scss"; import NavItem from "./NavItem"; import NavSubItem from "./NavSubItem"; import classNames from "classnames"; -import { anomalyNavigation, defaultNavigation, logsNavigation } from "../../../constants/navigation"; +import { anomalyNavigation, defaultNavigation, logsNavigation, NavigationItemType } from "../../../constants/navigation"; import { AppType } from "../../../types/appType"; +import { useAppState } from "../../../state/common/StateContext"; interface HeaderNavProps { color: string @@ -21,6 +22,7 @@ const HeaderNav: FC = ({ color, background, direction }) => { const appModeEnable = getAppModeEnable(); const { dashboardsSettings } = useDashboardsState(); const { pathname } = useLocation(); + const { serverUrl, flags } = useAppState(); const [activeMenu, setActiveMenu] = useState(pathname); @@ -37,7 +39,14 @@ const HeaderNav: FC = ({ color, background, direction }) => { 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]); @@ -74,6 +83,7 @@ const HeaderNav: FC = ({ color, background, direction }) => { value={m.value || ""} label={m.label || ""} color={color} + type={m.type || NavigationItemType.internalLink} /> ) ))} diff --git a/app/vmui/packages/vmui/src/layouts/Header/HeaderNav/NavItem.tsx b/app/vmui/packages/vmui/src/layouts/Header/HeaderNav/NavItem.tsx index 193f5ac0ab..83a7e149b7 100644 --- a/app/vmui/packages/vmui/src/layouts/Header/HeaderNav/NavItem.tsx +++ b/app/vmui/packages/vmui/src/layouts/Header/HeaderNav/NavItem.tsx @@ -1,30 +1,49 @@ import React, { FC } from "preact/compat"; import { NavLink } from "react-router-dom"; import classNames from "classnames"; +import { NavigationItemType } from "../../../constants/navigation"; interface NavItemProps { activeMenu: string, label: string, value: string, - color?: string + type: NavigationItemType, + color?: string, } const NavItem: FC = ({ activeMenu, label, value, + type, color -}) => ( - m.value === activeMenu) - })} - style={{ color }} - to={value} - > - {label} - -); +}) => { + if (type === NavigationItemType.externalLink) return ( + + {label} + + ); + return ( + m.value === activeMenu) + })} + style={{ color }} + to={value} + > + {label} + + ); +}; export default NavItem; diff --git a/app/vmui/packages/vmui/src/layouts/Header/HeaderNav/NavSubItem.tsx b/app/vmui/packages/vmui/src/layouts/Header/HeaderNav/NavSubItem.tsx index aab22c0c8c..16b2fb3e68 100644 --- a/app/vmui/packages/vmui/src/layouts/Header/HeaderNav/NavSubItem.tsx +++ b/app/vmui/packages/vmui/src/layouts/Header/HeaderNav/NavSubItem.tsx @@ -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 } from "../../../constants/navigation"; +import { NavigationItem, NavigationItemType } from "../../../constants/navigation"; interface NavItemProps { activeMenu: string, @@ -64,6 +64,7 @@ const NavSubItem: FC = ({ activeMenu={activeMenu} value={sm.value || ""} label={sm.label || ""} + type={sm.type || NavigationItemType.internalLink} /> ))} @@ -106,6 +107,7 @@ const NavSubItem: FC = ({ value={sm.value || ""} label={sm.label || ""} color={color} + type={sm.type || NavigationItemType.internalLink} /> ))} diff --git a/app/vmui/packages/vmui/src/layouts/MainLayout/MainLayout.tsx b/app/vmui/packages/vmui/src/layouts/MainLayout/MainLayout.tsx index 552b0b0726..8ea80e7d60 100644 --- a/app/vmui/packages/vmui/src/layouts/MainLayout/MainLayout.tsx +++ b/app/vmui/packages/vmui/src/layouts/MainLayout/MainLayout.tsx @@ -11,6 +11,7 @@ import { useFetchDashboards } from "../../pages/PredefinedPanels/hooks/useFetchD import useDeviceDetect from "../../hooks/useDeviceDetect"; import ControlsMainLayout from "./ControlsMainLayout"; import useFetchDefaultTimezone from "../../hooks/useFetchDefaultTimezone"; +import useFetchFlags from "../../hooks/useFetchFlags"; const MainLayout: FC = () => { const appModeEnable = getAppModeEnable(); @@ -20,6 +21,7 @@ const MainLayout: FC = () => { useFetchDashboards(); useFetchDefaultTimezone(); + useFetchFlags(); const setDocumentTitle = () => { const defaultTitle = "vmui"; diff --git a/app/vmui/packages/vmui/src/state/common/reducer.ts b/app/vmui/packages/vmui/src/state/common/reducer.ts index f2347eaca2..5c5986fb30 100644 --- a/app/vmui/packages/vmui/src/state/common/reducer.ts +++ b/app/vmui/packages/vmui/src/state/common/reducer.ts @@ -10,12 +10,14 @@ export interface AppState { tenantId: string; theme: Theme; isDarkTheme: boolean | null; + flags: Record; } export type Action = | { type: "SET_SERVER", payload: string } | { type: "SET_THEME", payload: Theme } | { type: "SET_TENANT_ID", payload: string } + | { type: "SET_FLAGS", payload: Record } | { type: "SET_DARK_THEME" } const tenantId = getQueryStringValue("g0.tenantID", "") as string; @@ -24,7 +26,8 @@ export const initialState: AppState = { serverUrl: removeTrailingSlash(getDefaultServer(tenantId)), tenantId, theme: (getFromStorage("THEME") || Theme.system) as Theme, - isDarkTheme: null + isDarkTheme: null, + flags: {}, }; export function reducer(state: AppState, action: Action): AppState { @@ -50,6 +53,11 @@ export function reducer(state: AppState, action: Action): AppState { ...state, isDarkTheme: isDarkTheme(state.theme) }; + case "SET_FLAGS": + return { + ...state, + flags: action.payload + }; default: throw new Error(); } diff --git a/app/vmui/packages/vmui/src/utils/tenants.ts b/app/vmui/packages/vmui/src/utils/tenants.ts index 89133d1655..94662068ba 100644 --- a/app/vmui/packages/vmui/src/utils/tenants.ts +++ b/app/vmui/packages/vmui/src/utils/tenants.ts @@ -7,3 +7,7 @@ export const replaceTenantId = (serverUrl: string, tenantId: string) => { export const getTenantIdFromUrl = (url: string): string => { return url.match(regexp)?.[2] || ""; }; + +export const getUrlWithoutTenant = (url: string): string => { + return url.replace(regexp, ""); +}; diff --git a/docs/changelog/CHANGELOG.md b/docs/changelog/CHANGELOG.md index 667bbe1952..a47759ea83 100644 --- a/docs/changelog/CHANGELOG.md +++ b/docs/changelog/CHANGELOG.md @@ -26,6 +26,7 @@ See also [LTS releases](https://docs.victoriametrics.com/lts-releases/). * FEATURE: [vmgateway](https://docs.victoriametrics.com/vmgateway/): support parsing `vm_access` claims in string format. This is useful for cases when identity provider does not support mapping claims to JSON format. * FEATURE: [Single-node VictoriaMetrics](https://docs.victoriametrics.com/) and `vmstorage` in [VictoriaMetrics cluster](https://docs.victoriametrics.com/cluster-victoriametrics/): add new metrics for data ingestion: `vm_rows_received_by_storage_total`, `vm_rows_ignored_total{reason="nan_value"}`, `vm_rows_ignored_total{reason="invalid_raw_metric_name"}`, `vm_rows_ignored_total{reason="hourly_limit_exceeded"}`, `vm_rows_ignored_total{reason="daily_limit_exceeded"}`. See this [PR](https://github.com/VictoriaMetrics/VictoriaMetrics/pull/6663) for details. * FEATURE: [vmui](https://docs.victoriametrics.com/#vmui): change request method for `/query_range` and `/query` calls from `GET` to `POST`. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/6288). +* FEATURE: [vmui](https://docs.victoriametrics.com/#vmui): add link to vmalert when proxy is enabled. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/5924). * FEATURE: [vmui](https://docs.victoriametrics.com/#vmui): keep selected columns in table view on page reloads. Before, selected columns were reset on each update. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/7016). * FEATURE: [dashboards](https://github.com/VictoriaMetrics/VictoriaMetrics/blob/master/dashboards) for VM single-node, cluster, vmalert, vmagent, VictoriaLogs: add `Go scheduling latency` panel to show the 99th quantile of Go goroutines scheduling. This panel should help identifying insufficient CPU resources for the service. It is especially useful if CPU gets throttled, which now should be visible on this panel. * FEATURE: [alerts](https://github.com/VictoriaMetrics/VictoriaMetrics/blob/master/deployment/docker/alerts-health.yml): add alerting rule to track the Go scheduling latency for goroutines. It should notify users if VM component doesn't have enough CPU to run or gets throttled.