Repository: VictoriaMetrics/VictoriaMetrics
Pull request #5427 (2023-12-05 13:03:26)

Expose Oauth2 Endpoint Parameters to cli
The user may which to control the endpoint parameters for instance to set the audience when requesting an access token. Exposing the parameters as a map allows for additional use cases without requiring modification.

mhill-holoplot: Fixed the lint issue now.

Patch:
diff --git a/README.md b/README.md
index b387501ab..e81401aee 100644
--- a/README.md
+++ b/README.md
@@ -2709,7 +2709,7 @@ Pass `-help` to VictoriaMetrics in order to see the list of supported command-li
   -loggerWarnsPerSecondLimit int
      Per-second limit on the number of WARN messages. If more than the given number of warns are emitted per second, then the remaining warns are suppressed. Zero values disable the rate limit
   -maxConcurrentInserts int
-     The maximum number of concurrent insert requests. Default value should work for most cases, since it minimizes the memory usage. The default value can be increased when clients send data over slow networks. See also -insert.maxQueueDuration (default 32)
+     The maximum number of concurrent insert requests. Default value should work for most cases, since it minimizes the memory usage. The default value can be increased when clients send data over slow networks. See also -insert.maxQueueDuration.
   -maxInsertRequestSize size
      The maximum size in bytes of a single Prometheus remote_write API request
      Supports the following optional suffixes for size values: KB, MB, GB, TB, KiB, MiB, GiB, TiB (default 33554432)
diff --git a/app/vmagent/remotewrite/client.go b/app/vmagent/remotewrite/client.go
index 94e6ad3ff..af7fe4d39 100644
--- a/app/vmagent/remotewrite/client.go
+++ b/app/vmagent/remotewrite/client.go
@@ -58,6 +58,7 @@ var (
 	oauth2ClientID         = flagutil.NewArrayString("remoteWrite.oauth2.clientID", "Optional OAuth2 clientID to use for the corresponding -remoteWrite.url")
 	oauth2ClientSecret     = flagutil.NewArrayString("remoteWrite.oauth2.clientSecret", "Optional OAuth2 clientSecret to use for the corresponding -remoteWrite.url")
 	oauth2ClientSecretFile = flagutil.NewArrayString("remoteWrite.oauth2.clientSecretFile", "Optional OAuth2 clientSecretFile to use for the corresponding -remoteWrite.url")
+	oauth2EndpointParams   = flagutil.NewMapString("remoteWrite.oauth2.endpointParams", "Optional OAuth2 endpoint parameters to use for the corresponding -remoteWrite.url")
 	oauth2TokenURL         = flagutil.NewArrayString("remoteWrite.oauth2.tokenUrl", "Optional OAuth2 tokenURL to use for the corresponding -remoteWrite.url")
 	oauth2Scopes           = flagutil.NewArrayString("remoteWrite.oauth2.scopes", "Optional OAuth2 scopes to use for the corresponding -remoteWrite.url. Scopes must be delimited by ';'")
 
@@ -238,6 +239,7 @@ func getAuthConfig(argIdx int) (*promauth.Config, error) {
 			ClientID:         oauth2ClientID.GetOptionalArg(argIdx),
 			ClientSecret:     promauth.NewSecret(clientSecret),
 			ClientSecretFile: clientSecretFile,
+			EndpointParams:   *oauth2EndpointParams,
 			TokenURL:         oauth2TokenURL.GetOptionalArg(argIdx),
 			Scopes:           strings.Split(oauth2Scopes.GetOptionalArg(argIdx), ";"),
 		}
diff --git a/app/vmagent/remotewrite/remotewrite.go b/app/vmagent/remotewrite/remotewrite.go
index 04d38a4aa..678193cc5 100644
--- a/app/vmagent/remotewrite/remotewrite.go
+++ b/app/vmagent/remotewrite/remotewrite.go
@@ -276,7 +276,7 @@ func reloadRelabelConfigs() {
 var (
 	relabelConfigReloads      = metrics.NewCounter(`vmagent_relabel_config_reloads_total`)
 	relabelConfigReloadErrors = metrics.NewCounter(`vmagent_relabel_config_reloads_errors_total`)
-	relabelConfigSuccess      = metrics.NewCounter(`vmagent_relabel_config_last_reload_successful`)
+	relabelConfigSuccess      = metrics.NewGauge(`vmagent_relabel_config_last_reload_successful`, nil)
 	relabelConfigTimestamp    = metrics.NewCounter(`vmagent_relabel_config_last_reload_success_timestamp_seconds`)
 )
 
diff --git a/app/vmalert/main.go b/app/vmalert/main.go
index 1c7a78408..a87897204 100644
--- a/app/vmalert/main.go
+++ b/app/vmalert/main.go
@@ -194,7 +194,7 @@ func main() {
 var (
 	configReloads      = metrics.NewCounter(`vmalert_config_last_reload_total`)
 	configReloadErrors = metrics.NewCounter(`vmalert_config_last_reload_errors_total`)
-	configSuccess      = metrics.NewCounter(`vmalert_config_last_reload_successful`)
+	configSuccess      = metrics.NewGauge(`vmalert_config_last_reload_successful`, nil)
 	configTimestamp    = metrics.NewCounter(`vmalert_config_last_reload_success_timestamp_seconds`)
 )
 
diff --git a/app/vmauth/auth_config.go b/app/vmauth/auth_config.go
index fa05a0aaf..59e004997 100644
--- a/app/vmauth/auth_config.go
+++ b/app/vmauth/auth_config.go
@@ -386,7 +386,7 @@ func (r *Regex) MarshalYAML() (interface{}, error) {
 var (
 	configReloads      = metrics.NewCounter(`vmauth_config_last_reload_total`)
 	configReloadErrors = metrics.NewCounter(`vmauth_config_last_reload_errors_total`)
-	configSuccess      = metrics.NewCounter(`vmauth_config_last_reload_successful`)
+	configSuccess      = metrics.NewGauge(`vmauth_config_last_reload_successful`, nil)
 	configTimestamp    = metrics.NewCounter(`vmauth_config_last_reload_success_timestamp_seconds`)
 )
 
diff --git a/app/vminsert/common/streamaggr.go b/app/vminsert/common/streamaggr.go
index 03624377a..6bee3dc0e 100644
--- a/app/vminsert/common/streamaggr.go
+++ b/app/vminsert/common/streamaggr.go
@@ -38,7 +38,7 @@ var (
 
 	saCfgReloads   = metrics.NewCounter(`vminsert_streamagg_config_reloads_total`)
 	saCfgReloadErr = metrics.NewCounter(`vminsert_streamagg_config_reloads_errors_total`)
-	saCfgSuccess   = metrics.NewCounter(`vminsert_streamagg_config_last_reload_successful`)
+	saCfgSuccess   = metrics.NewGauge(`vminsert_streamagg_config_last_reload_successful`, nil)
 	saCfgTimestamp = metrics.NewCounter(`vminsert_streamagg_config_last_reload_success_timestamp_seconds`)
 
 	sasGlobal atomic.Pointer[streamaggr.Aggregators]
diff --git a/app/vminsert/relabel/relabel.go b/app/vminsert/relabel/relabel.go
index 3409fb4c0..3508f154b 100644
--- a/app/vminsert/relabel/relabel.go
+++ b/app/vminsert/relabel/relabel.go
@@ -65,7 +65,7 @@ func Init() {
 var (
 	configReloads      = metrics.NewCounter(`vm_relabel_config_reloads_total`)
 	configReloadErrors = metrics.NewCounter(`vm_relabel_config_reloads_errors_total`)
-	configSuccess      = metrics.NewCounter(`vm_relabel_config_last_reload_successful`)
+	configSuccess      = metrics.NewGauge(`vm_relabel_config_last_reload_successful`, nil)
 	configTimestamp    = metrics.NewCounter(`vm_relabel_config_last_reload_success_timestamp_seconds`)
 )
 
diff --git a/app/vmui/Makefile b/app/vmui/Makefile
index eb3354044..7d4297a2b 100644
--- a/app/vmui/Makefile
+++ b/app/vmui/Makefile
@@ -22,6 +22,14 @@ vmui-logs-build: vmui-package-base-image
 		--entrypoint=/bin/bash \
 		vmui-builder-image -c "npm install && npm run build:logs"
 
+vmui-anomaly-build: vmui-package-base-image
+	docker run --rm \
+		--user $(shell id -u):$(shell id -g) \
+		--mount type=bind,src="$(shell pwd)/app/vmui",dst=/build \
+		-w /build/packages/vmui \
+		--entrypoint=/bin/bash \
+		vmui-builder-image -c "npm install && npm run build:anomaly"
+
 vmui-release: vmui-build
 	docker build -t ${DOCKER_NAMESPACE}/vmui:latest -f app/vmui/Dockerfile-web ./app/vmui/packages/vmui
 	docker tag ${DOCKER_NAMESPACE}/vmui:latest ${DOCKER_NAMESPACE}/vmui:${PKG_TAG}
diff --git a/app/vmui/packages/vmui/config-overrides.js b/app/vmui/packages/vmui/config-overrides.js
index 4b7a1e1c3..663e569e3 100644
--- a/app/vmui/packages/vmui/config-overrides.js
+++ b/app/vmui/packages/vmui/config-overrides.js
@@ -14,10 +14,12 @@ module.exports = override(
     new webpack.NormalModuleReplacementPlugin(
       /\.\/App/,
       function (resource) {
-        // eslint-disable-next-line no-undef
-        if (process.env.REACT_APP_LOGS === "true") {
+        if (process.env.REACT_APP_TYPE === "logs") {
           resource.request = "./AppLogs";
         }
+        if (process.env.REACT_APP_TYPE === "anomaly") {
+          resource.request = "./AppAnomaly";
+        }
       }
     )
   )
diff --git a/app/vmui/packages/vmui/package.json b/app/vmui/packages/vmui/package.json
index 7e9e58c06..ecf5e53c8 100644
--- a/app/vmui/packages/vmui/package.json
+++ b/app/vmui/packages/vmui/package.json
@@ -32,9 +32,11 @@
   "scripts": {
     "prestart": "npm run copy-metricsql-docs",
     "start": "react-app-rewired start",
-    "start:logs": "cross-env REACT_APP_LOGS=true npm run start",
+    "start:logs": "cross-env REACT_APP_TYPE=logs npm run start",
+    "start:anomaly": "cross-env REACT_APP_TYPE=anomaly npm run start",
     "build": "GENERATE_SOURCEMAP=false react-app-rewired build",
-    "build:logs": "cross-env REACT_APP_LOGS=true npm run build",
+    "build:logs": "cross-env REACT_APP_TYPE=logs npm run build",
+    "build:anomaly": "cross-env REACT_APP_TYPE=anomaly npm run build",
     "lint": "eslint src --ext tsx,ts",
     "lint:fix": "eslint src --ext tsx,ts --fix",
     "analyze": "source-map-explorer 'build/static/js/*.js'",
diff --git a/app/vmui/packages/vmui/src/AppAnomaly.tsx b/app/vmui/packages/vmui/src/AppAnomaly.tsx
new file mode 100644
index 000000000..de139cd98
--- /dev/null
+++ b/app/vmui/packages/vmui/src/AppAnomaly.tsx
@@ -0,0 +1,41 @@
+import React, { FC, useState } from "preact/compat";
+import { HashRouter, Route, Routes } from "react-router-dom";
+import AppContextProvider from "./contexts/AppContextProvider";
+import ThemeProvider from "./components/Main/ThemeProvider/ThemeProvider";
+import AnomalyLayout from "./layouts/AnomalyLayout/AnomalyLayout";
+import ExploreAnomaly from "./pages/ExploreAnomaly/ExploreAnomaly";
+import router from "./router";
+import CustomPanel from "./pages/CustomPanel";
+
+const AppLogs: FC = () => {
+  const [loadedTheme, setLoadedTheme] = useState(false);
+
+  return <>
+    <HashRouter>
+      <AppContextProvider>
+        <>
+          <ThemeProvider onLoaded={setLoadedTheme}/>
+          {loadedTheme && (
+            <Routes>
+              <Route
+                path={"/"}
+                element={<AnomalyLayout/>}
+              >
+                <Route
+                  path={"/"}
+                  element={<ExploreAnomaly/>}
+                />
+                <Route
+                  path={router.query}
+                  element={<CustomPanel/>}
+                />
+              </Route>
+            </Routes>
+          )}
+        </>
+      </AppContextProvider>
+    </HashRouter>
+  </>;
+};
+
+export default AppLogs;
diff --git a/app/vmui/packages/vmui/src/components/Chart/Line/LegendAnomaly/LegendAnomaly.tsx b/app/vmui/packages/vmui/src/components/Chart/Line/LegendAnomaly/LegendAnomaly.tsx
new file mode 100644
index 000000000..51d17fa73
--- /dev/null
+++ b/app/vmui/packages/vmui/src/components/Chart/Line/LegendAnomaly/LegendAnomaly.tsx
@@ -0,0 +1,86 @@
+import React, { FC, useMemo } from "preact/compat";
+import { ForecastType, SeriesItem } from "../../../../types";
+import { anomalyColors } from "../../../../utils/color";
+import "./style.scss";
+
+type Props = {
+  series: SeriesItem[];
+};
+
+const titles: Record<ForecastType, string> = {
+  [ForecastType.yhat]: "yhat",
+  [ForecastType.yhatLower]: "yhat_lower/_upper",
+  [ForecastType.yhatUpper]: "yhat_lower/_upper",
+  [ForecastType.anomaly]: "anomalies",
+  [ForecastType.training]: "training data",
+  [ForecastType.actual]: "y"
+};
+
+const LegendAnomaly: FC<Props> = ({ series }) => {
+
+  const uniqSeriesStyles = useMemo(() => {
+    const uniqSeries = series.reduce((accumulator, currentSeries) => {
+      const hasForecast = Object.prototype.hasOwnProperty.call(currentSeries, "forecast");
+      const isNotUpper = currentSeries.forecast !== ForecastType.yhatUpper;
+      const isUniqForecast = !accumulator.find(s => s.forecast === currentSeries.forecast);
+      if (hasForecast && isUniqForecast && isNotUpper) {
+        accumulator.push(currentSeries);
+      }
+      return accumulator;
+    }, [] as SeriesItem[]);
+
+    const trainingSeries = {
+      ...uniqSeries[0],
+      forecast: ForecastType.training,
+      color: anomalyColors[ForecastType.training],
+    };
+    uniqSeries.splice(1, 0, trainingSeries);
+
+    return uniqSeries.map(s => ({
+      ...s,
+      color: typeof s.stroke === "string" ? s.stroke : anomalyColors[s.forecast || ForecastType.actual],
+      forecast: titles[s.forecast || ForecastType.actual],
+    }));
+  }, [series]);
+
+  const container = document.getElementById("legendAnomaly");
+  if (!container) return null;
+
+  return <>
+    <div className="vm-legend-anomaly">
+      {/* TODO: remove .filter() after the correct training data has been added */}
+      {uniqSeriesStyles.filter(f => f.forecast !== titles[ForecastType.training]).map((s, i) => (
+        <div
+          key={`${i}_${s.forecast}`}
+          className="vm-legend-anomaly-item"
+        >
+          <svg>
+            {s.forecast === ForecastType.anomaly ? (
+              <circle
+                cx="15"
+                cy="7"
+                r="4"
+                fill={s.color}
+                stroke={s.color}
+                strokeWidth="1.4"
+              />
+            ) : (
+              <line
+                x1="0"
+                y1="7"
+                x2="30"
+                y2="7"
+                stroke={s.color}
+                strokeWidth={s.width || 1}
+                strokeDasharray={s.dash?.join(",")}
+              />
+            )}
+          </svg>
+          <div className="vm-legend-anomaly-item__title">{s.forecast || "y"}</div>
+        </div>
+      ))}
+    </div>
+  </>;
+};
+
+export default LegendAnomaly;
diff --git a/app/vmui/packages/vmui/src/components/Chart/Line/LegendAnomaly/style.scss b/app/vmui/packages/vmui/src/components/Chart/Line/LegendAnomaly/style.scss
new file mode 100644
index 000000000..38e473072
--- /dev/null
+++ b/app/vmui/packages/vmui/src/components/Chart/Line/LegendAnomaly/style.scss
@@ -0,0 +1,23 @@
+@use "src/styles/variables" as *;
+
+.vm-legend-anomaly {
+  position: relative;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  flex-wrap: wrap;
+  gap: calc($padding-large * 2);
+  cursor: default;
+
+  &-item {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    gap: $padding-small;
+
+    svg {
+      width: 30px;
+      height: 14px;
+    }
+  }
+}
diff --git a/app/vmui/packages/vmui/src/components/Chart/Line/LineChart/LineChart.tsx b/app/vmui/packages/vmui/src/components/Chart/Line/LineChart/LineChart.tsx
index 967a719d9..2ccff1f0a 100644
--- a/app/vmui/packages/vmui/src/components/Chart/Line/LineChart/LineChart.tsx
+++ b/app/vmui/packages/vmui/src/components/Chart/Line/LineChart/LineChart.tsx
@@ -5,14 +5,15 @@ import uPlot, {
   Series as uPlotSeries,
 } from "uplot";
 import {
-  getDefaultOptions,
   addSeries,
   delSeries,
+  getAxes,
+  getDefaultOptions,
   getRangeX,
   getRangeY,
   getScales,
   handleDestroy,
-  getAxes,
+  setBand,
   setSelect
 } from "../../../../utils/uplot";
 import { MetricResult } from "../../../../api/types";
@@ -39,6 +40,7 @@ export interface LineChartProps {
   setPeriod: ({ from, to }: { from: Date, to: Date }) => void;
   layoutSize: ElementSize;
   height?: number;
+  anomalyView?: boolean;
 }
 
 const LineChart: FC<LineChartProps> = ({
@@ -50,7 +52,8 @@ const LineChart: FC<LineChartProps> = ({
   unit,
   setPeriod,
   layoutSize,
-  height
+  height,
+  anomalyView
 }) => {
   const { isDarkTheme } = useAppState();
 
@@ -68,7 +71,7 @@ const LineChart: FC<LineChartProps> = ({
     seriesFocus,
     setCursor,
     resetTooltips
-  } = useLineTooltip({ u: uPlotInst, metrics, series, unit });
+  } = useLineTooltip({ u: uPlotInst, metrics, series, unit, anomalyView });
 
   const options: uPlotOptions = {
     ...getDefaultOptions({ width: layoutSize.width, height }),
@@ -82,6 +85,7 @@ const LineChart: FC<LineChartProps> = ({
       setSelect: [setSelect(setPlotScale)],
       destroy: [handleDestroy],
     },
+    bands: []
   };
 
   useEffect(() => {
@@ -103,6 +107,7 @@ const LineChart: FC<LineChartProps> = ({
     if (!uPlotInst) return;
     delSeries(uPlotInst);
     addSeries(uPlotInst, series);
+    setBand(uPlotInst, series);
     uPlotInst.redraw();
   }, [series]);
 
diff --git a/app/vmui/packages/vmui/src/components/Configurators/GlobalSettings/GlobalSettings.tsx b/app/vmui/packages/vmui/src/components/Configurators/GlobalSettings/GlobalSettings.tsx
index fce3dac62..f23d58b39 100644
--- a/app/vmui/packages/vmui/src/components/Configurators/GlobalSettings/GlobalSettings.tsx
+++ b/app/vmui/packages/vmui/src/components/Configurators/GlobalSettings/GlobalSettings.tsx
@@ -17,11 +17,14 @@ import ThemeControl from "../ThemeControl/ThemeControl";
 import useDeviceDetect from "../../../hooks/useDeviceDetect";
 import useBoolean from "../../../hooks/useBoolean";
 import { getTenantIdFromUrl } from "../../../utils/tenants";
+import { AppType } from "../../../types/appType";
 
 const title = "Settings";
 
+const { REACT_APP_TYPE } = process.env;
+const isLogsApp = REACT_APP_TYPE === AppType.logs;
+
 const GlobalSettings: FC = () => {
-  const { REACT_APP_LOGS } = process.env;
   const { isMobile } = useDeviceDetect();
 
   const appModeEnable = getAppModeEnable();
@@ -77,7 +80,7 @@ const GlobalSettings: FC = () => {
 
   const controls = [
     {
-      show: !appModeEnable && !REACT_APP_LOGS,
+      show: !appModeEnable && !isLogsApp,
       component: <ServerConfigurator
         stateServerUrl={stateServerUrl}
         serverUrl={serverUrl}
@@ -86,7 +89,7 @@ const GlobalSettings: FC = () => {
       />
     },
     {
-      show: !REACT_APP_LOGS,
+      show: !isLogsApp,
       component: <LimitsConfigurator
         limits={limits}
         onChange={setLimits}
diff --git a/app/vmui/packages/vmui/src/components/Configurators/GlobalSettings/LimitsConfigurator/LimitsConfigurator.tsx b/app/vmui/packages/vmui/src/components/Configurators/GlobalSettings/LimitsConfigurator/LimitsConfigurator.tsx
index 0e898e42c..4581c7cb3 100644
--- a/app/vmui/packages/vmui/src/components/Configurators/GlobalSettings/LimitsConfigurator/LimitsConfigurator.tsx
+++ b/app/vmui/packages/vmui/src/components/Configurators/GlobalSettings/LimitsConfigurator/LimitsConfigurator.tsx
@@ -16,9 +16,9 @@ export interface ServerConfiguratorProps {
 }
 
 const fields: {label: string, type: DisplayType}[] = [
-  { label: "Graph", type: "chart" },
-  { label: "JSON", type: "code" },
-  { label: "Table", type: "table" }
+  { label: "Graph", type: DisplayType.chart },
+  { label: "JSON", type: DisplayType.code },
+  { label: "Table", type: DisplayType.table }
 ];
 
 const LimitsConfigurator: FC<ServerConfiguratorProps> = ({ limits, onChange , onEnter }) => {
diff --git a/app/vmui/packages/vmui/src/components/Configurators/GlobalSettings/ServerConfigurator/ServerConfigurator.tsx b/app/vmui/packages/vmui/src/components/Configurators/GlobalSettings/ServerConfigurator/ServerConfigurator.tsx
index 6f0600aad..c3fa516f2 100644
--- a/app/vmui/packages/vmui/src/components/Configurators/GlobalSettings/ServerConfigurator/ServerConfigurator.tsx
+++ b/app/vmui/packages/vmui/src/components/Configurators/GlobalSettings/ServerConfigurator/ServerConfigurator.tsx
@@ -2,6 +2,11 @@ import React, { FC, useEffect, useState } from "preact/compat";
 import { ErrorTypes } from "../../../../types";
 import TextField from "../../../Main/TextField/TextField";
 import { isValidHttpUrl } from "../../../../utils/url";
+import Button from "../../../Main/Button/Button";
+import { StorageIcon } from "../../../Main/Icons";
+import Tooltip from "../../../Main/Tooltip/Tooltip";
+import { getFromStorage, removeFromStorage, saveToStorage } from "../../../../utils/storage";
+import useBoolean from "../../../../hooks/useBoolean";
 
 export interface ServerConfiguratorProps {
   serverUrl: string
@@ -10,13 +15,21 @@ export interface ServerConfiguratorProps {
   onEnter: () => void
 }
 
+const tooltipSave = {
+  enable: "Enable to save the modified server URL to local storage, preventing reset upon page refresh.",
+  disable: "Disable to stop saving the server URL to local storage, reverting to the default URL on page refresh."
+};
+
 const ServerConfigurator: FC<ServerConfiguratorProps> = ({
   serverUrl,
   stateServerUrl,
   onChange ,
   onEnter
 }) => {
-
+  const {
+    value: enabledStorage,
+    toggle: handleToggleStorage,
+  } = useBoolean(!!getFromStorage("SERVER_URL"));
   const [error, setError] = useState("");
 
   const onChangeServer = (val: string) => {
@@ -30,16 +43,39 @@ const ServerConfigurator: FC<ServerConfiguratorProps> = ({
     if (!isValidHttpUrl(stateServerUrl)) setError(ErrorTypes.validServer);
   }, [stateServerUrl]);
 
+  useEffect(() => {
+    if (enabledStorage) {
+      saveToStorage("SERVER_URL", serverUrl);
+    } else {
+      removeFromStorage(["SERVER_URL"]);
+    }
+  }, [enabledStorage]);
+
   return (
-    <TextField
-      autofocus
-      label="Server URL"
-      value={serverUrl}
-      error={error}
-      onChange={onChangeServer}
-      onEnter={onEnter}
-      inputmode="url"
-    />
+    <div>
+      <div className="vm-server-configurator__title">
+        Server URL
+      </div>
+      <div className="vm-server-configurator-url">
+        <TextField
+          autofocus
+          value={serverUrl}
+          error={error}
+          onChange={onChangeServer}
+          onEnter={onEnter}
+          inputmode="url"
+        />
+        <Tooltip title={enabledStorage ? tooltipSave.disable : tooltipSave.enable}>
+          <Button
+            className="vm-server-configurator-url__button"
+            variant="text"
+            color={enabledStorage ? "primary" : "gray"}
+            onClick={handleToggleStorage}
+            startIcon={<StorageIcon/>}
+          />
+        </Tooltip>
+      </div>
+    </div>
   );
 };
 
diff --git a/app/vmui/packages/vmui/src/components/Configurators/GlobalSettings/style.scss b/app/vmui/packages/vmui/src/components/Configurators/GlobalSettings/style.scss
index 9a1706615..0fc6b63b6 100644
--- a/app/vmui/packages/vmui/src/components/Configurators/GlobalSettings/style.scss
+++ b/app/vmui/packages/vmui/src/components/Configurators/GlobalSettings/style.scss
@@ -21,6 +21,12 @@
 
   &__input {
     width: 100%;
+
+    &_flex {
+      display: flex;
+      align-items: flex-start;
+      gap: $padding-global;
+    }
   }
 
   &__title {
@@ -33,6 +39,16 @@
     margin-bottom: $padding-global;
   }
 
+  &-url {
+    display: flex;
+    align-items: flex-start;
+    gap: $padding-small;
+
+    &__button {
+      margin-top: $padding-small;
+    }
+  }
+
   &-footer {
     display: flex;
     align-items: center;
diff --git a/app/vmui/packages/vmui/src/components/ExploreMetrics/ExploreMetricGraph/ExploreMetricItemGraph.tsx b/app/vmui/packages/vmui/src/components/ExploreMetrics/ExploreMetricGraph/ExploreMetricItemGraph.tsx
index 75aa6d472..4bf51ec96 100644
--- a/app/vmui/packages/vmui/src/components/ExploreMetrics/ExploreMetricGraph/ExploreMetricItemGraph.tsx
+++ b/app/vmui/packages/vmui/src/components/ExploreMetrics/ExploreMetricGraph/ExploreMetricItemGraph.tsx
@@ -6,12 +6,11 @@ import { useTimeDispatch, useTimeState } from "../../../state/time/TimeStateCont
 import { AxisRange } from "../../../state/graph/reducer";
 import Spinner from "../../Main/Spinner/Spinner";
 import Alert from "../../Main/Alert/Alert";
-import Button from "../../Main/Button/Button";
 import "./style.scss";
 import classNames from "classnames";
 import useDeviceDetect from "../../../hooks/useDeviceDetect";
 import { getDurationFromMilliseconds, getSecondsFromDuration, getStepFromDuration } from "../../../utils/time";
-import useBoolean from "../../../hooks/useBoolean";
+import WarningLimitSeries from "../../../pages/CustomPanel/WarningLimitSeries/WarningLimitSeries";
 
 interface ExploreMetricItemGraphProps {
   name: string,
@@ -40,12 +39,9 @@ const ExploreMetricItem: FC<ExploreMetricItemGraphProps> = ({
   const stepSeconds = getSecondsFromDuration(customStep);
   const heatmapStep = getDurationFromMilliseconds(stepSeconds * 10 * 1000);
   const [isHeatmap, setIsHeatmap] = useState(false);
+  const [showAllSeries, setShowAllSeries] = useState(false);
   const step = isHeatmap && customStep === defaultStep ? heatmapStep : customStep;
 
-  const {
-    value: showAllSeries,
-    setTrue: handleShowAll,
-  } = useBoolean(false);
 
   const query = useMemo(() => {
     const params = Object.entries({ job, instance })
@@ -99,18 +95,13 @@ with (q = ${queryBase}) (
       {isLoading && <Spinner />}
       {error && <Alert variant="error">{error}</Alert>}
       {queryErrors[0] && <Alert variant="error">{queryErrors[0]}</Alert>}
-      {warning && <Alert variant="warning">
-        <div className="vm-explore-metrics-graph__warning">
-          <p>{warning}</p>
-          <Button
-            color="warning"
-            variant="outlined"
-            onClick={handleShowAll}
-          >
-              Show all
-          </Button>
-        </div>
-      </Alert>}
+      {warning && (
+        <WarningLimitSeries
+          warning={warning}
+          query={[query]}
+          onChange={setShowAllSeries}
+        />
+      )}
       {graphData && period && (
         <GraphView
           data={graphData}
diff --git a/app/vmui/packages/vmui/src/components/Main/Icons/index.tsx b/app/vmui/packages/vmui/src/components/Main/Icons/index.tsx
index a3af70c69..aca59dfb7 100644
--- a/app/vmui/packages/vmui/src/components/Main/Icons/index.tsx
+++ b/app/vmui/packages/vmui/src/components/Main/Icons/index.tsx
@@ -24,7 +24,17 @@ export const LogoLogsIcon = () => (
       <path d="M0 0h85v38H0z"/>
     </defs>
   </svg>
+);
 
+export const LogoAnomalyIcon = () => (
+  <svg
+    viewBox="0 0 85 38"
+    fill="currentColor"
+  >
+    <path
+      d="M11.118 10.476c.36.28.801.433 1.257.436h.052c.48-.007.961-.192 1.25-.444 1.509-1.279 5.88-5.287 5.88-5.287 1.168-1.087-2.093-2.174-7.13-2.181h-.06c-5.036.007-8.298 1.094-7.13 2.181 0 0 4.372 4.008 5.88 5.295zm2.559 2.166c-.359.283-.801.439-1.258.444h-.044a2.071 2.071 0 0 1-1.257-.444C10.082 11.755 6.384 8.42 5 7.148v1.93c0 .215.081.496.222.629l.07.064c1.045.955 4.546 4.154 5.825 5.245.358.283.8.438 1.257.444h.044c.489-.015.962-.2 1.258-.444 1.309-1.11 4.948-4.444 5.887-5.31.148-.132.222-.413.222-.628v-1.93a455.127 455.127 0 0 1-6.11 5.494zm-1.258 4.984a2.071 2.071 0 0 0 1.258-.436c2.053-1.815 4.09-3.65 6.11-5.502v1.938c0 .207-.075.488-.223.621-.94.873-4.578 4.2-5.887 5.31-.296.25-.77.436-1.258.443h-.044a2.071 2.071 0 0 1-1.257-.436c-1.204-1.027-4.376-3.928-5.616-5.062l-.28-.255c-.14-.133-.221-.414-.221-.621v-1.938c1.383 1.265 5.081 4.607 6.117 5.495.358.282.8.438 1.257.443h.044zM40 5l-5.84 14.46h-2.43L25.89 5h2.16c.233 0 .423.057.57.17.146.113.256.26.33.44l3.41 8.82c.113.287.22.603.32.95.106.34.206.697.3 1.07.08-.373.166-.73.26-1.07a8.84 8.84 0 0 1 .31-.95l3.39-8.82a.959.959 0 0 1 .31-.42.906.906 0 0 1 .58-.19H40zm17.176 0v14.46h-2.37v-9.34c0-.373.02-.777.06-1.21l-4.37 8.21c-.206.393-.523.59-.95.59h-.38c-.426 0-.743-.197-.95-.59l-4.42-8.24c.02.22.037.437.05.65.014.213.02.41.02.59v9.34h-2.37V5h2.03c.12 0 .224.003.31.01a.778.778 0 0 1 .23.05c.074.027.137.07.19.13.06.06.117.14.17.24l4.33 8.03c.114.213.217.433.31.66.1.227.197.46.29.7.094-.247.19-.483.29-.71.1-.233.207-.457.32-.67l4.27-8.01c.054-.1.11-.18.17-.24a.57.57 0 0 1 .19-.13.903.903 0 0 1 .24-.05c.087-.007.19-.01.31-.01h2.03zm8.887 13.73c.68 0 1.286-.117 1.82-.35.54-.24.996-.57 1.37-.99a4.28 4.28 0 0 0 .85-1.48c.2-.573.3-1.19.3-1.85V5.31h1.02v8.75c0 .78-.124 1.51-.37 2.19a5.248 5.248 0 0 1-1.07 1.77c-.46.5-1.024.893-1.69 1.18-.66.287-1.404.43-2.23.43-.827 0-1.574-.143-2.24-.43a5.012 5.012 0 0 1-1.69-1.18 5.33 5.33 0 0 1-1.06-1.77 6.373 6.373 0 0 1-.37-2.19V5.31h1.03v8.74c0 .66.096 1.277.29 1.85.2.567.483 1.06.85 1.48.373.42.826.75 1.36.99.54.24 1.15.36 1.83.36zm10.38.73h-1.03V5.31h1.03v14.15zM4.242 35v-5.166l-.672-.078a.595.595 0 0 1-.21-.09.23.23 0 0 1-.078-.186v-.438h.96v-.588c0-.348.048-.656.144-.924.1-.272.24-.5.42-.684a1.79 1.79 0 0 1 .66-.426c.256-.096.544-.144.864-.144.272 0 .522.04.75.12l-.024.534c-.008.096-.062.148-.162.156a4.947 4.947 0 0 1-.39.012c-.184 0-.352.024-.504.072a.949.949 0 0 0-.384.234c-.108.108-.192.25-.252.426a2.184 2.184 0 0 0-.084.654v.558h1.752v.774H5.316V35H4.242zM10.416 28.826a3.1 3.1 0 0 1 1.2.222c.356.148.66.358.912.63s.444.602.576.99c.136.384.204.814.204 1.29 0 .48-.068.912-.204 1.296a2.735 2.735 0 0 1-.576.984 2.572 2.572 0 0 1-.912.63 3.175 3.175 0 0 1-1.2.216c-.448 0-.852-.072-1.212-.216a2.572 2.572 0 0 1-.912-.63 2.805 2.805 0 0 1-.582-.984 3.972 3.972 0 0 1-.198-1.296c0-.476.066-.906.198-1.29.136-.388.33-.718.582-.99.252-.272.556-.482.912-.63.36-.148.764-.222 1.212-.222zm0 5.424c.6 0 1.048-.2 1.344-.6.296-.404.444-.966.444-1.686 0-.724-.148-1.288-.444-1.692-.296-.404-.744-.606-1.344-.606-.304 0-.57.052-.798.156a1.507 1.507 0 0 0-.564.45c-.148.196-.26.438-.336.726a3.941 3.941 0 0 0-.108.966c0 .72.148 1.282.444 1.686.3.4.754.6 1.362.6zM15.677 30.14c.192-.416.428-.74.708-.972.28-.236.622-.354 1.026-.354.128 0 .25.014.366.042.12.028.226.072.318.132l-.078.798c-.024.1-.084.15-.18.15-.056 0-.138-.012-.246-.036a1.694 1.694 0 0 0-.366-.036c-.192 0-.364.028-.516.084-.148.056-.282.14-.402.252a1.782 1.782 0 0 0-.318.408c-.092.16-.176.344-.252.552V35h-1.074v-6.078h.612c.116 0 .196.022.24.066.044.044.074.12.09.228l.072.924zM26.761 28.922 24.283 35h-.96l-2.478-6.078h.87a.33.33 0 0 1 .33.222l1.542 3.912c.048.148.09.292.126.432.036.14.07.28.102.42.032-.14.066-.28.102-.42.036-.14.08-.284.132-.432l1.56-3.912a.33.33 0 0 1 .12-.156.311.311 0 0 1 .198-.066h.834zM27.74 35v-6.078h.643c.152 0 .246.074.282.222l.078.624c.224-.276.476-.502.756-.678.28-.176.604-.264.972-.264.408 0 .738.114.99.342.256.228.44.536.552.924.088-.22.2-.41.336-.57a1.987 1.987 0 0 1 1.014-.624c.196-.048.394-.072.594-.072.32 0 .604.052.852.156.252.1.464.248.636.444.176.196.31.438.402.726.092.284.138.61.138.978V35H34.91v-3.87c0-.476-.104-.836-.312-1.08-.208-.248-.508-.372-.9-.372-.176 0-.344.032-.504.096-.156.06-.294.15-.414.27-.12.12-.216.272-.288.456-.068.18-.102.39-.102.63V35h-1.074v-3.87c0-.488-.098-.852-.294-1.092-.196-.24-.482-.36-.858-.36-.264 0-.508.072-.732.216a2.38 2.38 0 0 0-.618.576V35H27.74zM40.746 32.372c-.428.02-.788.058-1.08.114-.292.052-.526.12-.702.204a.923.923 0 0 0-.378.294.639.639 0 0 0-.114.366c0 .26.076.446.228.558.156.112.358.168.606.168.304 0 .566-.054.786-.162.224-.112.442-.28.654-.504v-1.038zm-3.396-2.67c.708-.648 1.56-.972 2.556-.972.36 0 .682.06.966.18.284.116.524.28.72.492.196.208.344.458.444.75.104.292.156.612.156.96V35h-.672a.708.708 0 0 1-.324-.06c-.076-.044-.136-.13-.18-.258l-.132-.444c-.156.14-.308.264-.456.372a2.804 2.804 0 0 1-.462.264c-.16.072-.332.126-.516.162-.18.04-.38.06-.6.06-.26 0-.5-.034-.72-.102a1.618 1.618 0 0 1-.57-.318 1.414 1.414 0 0 1-.372-.522 1.852 1.852 0 0 1-.132-.726 1.419 1.419 0 0 1 .33-.906c.12-.14.274-.272.462-.396s.418-.232.69-.324c.276-.092.596-.166.96-.222.364-.06.78-.096 1.248-.108v-.36c0-.412-.088-.716-.264-.912-.176-.2-.43-.3-.762-.3-.24 0-.44.028-.6.084-.156.056-.294.12-.414.192l-.33.186a.631.631 0 0 1-.324.084.439.439 0 0 1-.264-.078.716.716 0 0 1-.174-.192l-.264-.474zM44.974 29.6c.124-.124.254-.238.39-.342a2.395 2.395 0 0 1 .936-.444c.176-.044.368-.066.576-.066.336 0 .634.058.894.174.26.112.476.272.648.48.176.204.308.45.396.738.092.284.138.598.138.942V35H47.47v-3.918c0-.376-.086-.666-.258-.87-.172-.208-.434-.312-.786-.312-.256 0-.496.058-.72.174a2.58 2.58 0 0 0-.636.474V35h-1.482v-6.156h.906c.192 0 .318.09.378.27l.102.486zM53.085 28.748c.456 0 .87.074 1.242.222a2.692 2.692 0 0 1 1.578 1.626c.144.392.216.83.216 1.314 0 .488-.072.928-.216 1.32-.144.392-.35.726-.618 1.002a2.653 2.653 0 0 1-.96.636 3.333 3.333 0 0 1-1.242.222c-.46 0-.878-.074-1.254-.222a2.712 2.712 0 0 1-.966-.636 2.922 2.922 0 0 1-.618-1.002 3.807 3.807 0 0 1-.216-1.32c0-.484.072-.922.216-1.314.148-.392.354-.724.618-.996.268-.272.59-.482.966-.63a3.397 3.397 0 0 1 1.254-.222zm0 5.202c.512 0 .89-.172 1.134-.516.248-.344.372-.848.372-1.512s-.124-1.17-.372-1.518c-.244-.348-.622-.522-1.134-.522-.52 0-.906.176-1.158.528-.248.348-.372.852-.372 1.512s.124 1.164.372 1.512c.252.344.638.516 1.158.516zM57.252 35v-6.156h.906c.192 0 .318.09.378.27l.096.456c.108-.12.22-.23.336-.33a2.017 2.017 0 0 1 1.32-.492c.388 0 .706.106.954.318.252.208.44.486.564.834a1.93 1.93 0 0 1 .834-.882c.172-.092.354-.16.546-.204.196-.044.392-.066.588-.066.34 0 .642.052.906.156.264.104.486.256.666.456.18.2.316.444.408.732.096.288.144.618.144.99V35h-1.482v-3.918c0-.392-.086-.686-.258-.882-.172-.2-.424-.3-.756-.3-.152 0-.294.026-.426.078a1.026 1.026 0 0 0-.342.228 1.019 1.019 0 0 0-.228.366 1.435 1.435 0 0 0-.084.51V35h-1.488v-3.918c0-.412-.084-.712-.252-.9-.164-.188-.406-.282-.726-.282-.216 0-.418.054-.606.162a1.979 1.979 0 0 0-.516.432V35h-1.482zM70.558 32.372c-.428.02-.788.058-1.08.114-.292.052-.526.12-.702.204a.923.923 0 0 0-.378.294.639.639 0 0 0-.114.366c0 .26.076.446.228.558.156.112.358.168.606.168.304 0 .566-.054.786-.162.224-.112.442-.28.654-.504v-1.038zm-3.396-2.67c.708-.648 1.56-.972 2.556-.972.36 0 .682.06.966.18.284.116.524.28.72.492.196.208.344.458.444.75.104.292.156.612.156.96V35h-.672a.708.708 0 0 1-.324-.06c-.076-.044-.136-.13-.18-.258l-.132-.444c-.156.14-.308.264-.456.372a2.804 2.804 0 0 1-.462.264c-.16.072-.332.126-.516.162-.18.04-.38.06-.6.06-.26 0-.5-.034-.72-.102a1.618 1.618 0 0 1-.57-.318 1.414 1.414 0 0 1-.372-.522 1.852 1.852 0 0 1-.132-.726 1.419 1.419 0 0 1 .33-.906c.12-.14.274-.272.462-.396s.418-.232.69-.324c.276-.092.596-.166.96-.222.364-.06.78-.096 1.248-.108v-.36c0-.412-.088-.716-.264-.912-.176-.2-.43-.3-.762-.3-.24 0-.44.028-.6.084-.156.056-.294.12-.414.192l-.33.186a.631.631 0 0 1-.324.084.439.439 0 0 1-.264-.078.716.716 0 0 1-.174-.192l-.264-.474zM74.9 26.084V35h-1.482v-8.916H74.9zM81.969 28.844l-3.354 7.848a.538.538 0 0 1-.174.234c-.068.056-.174.084-.318.084h-1.104l1.152-2.472-2.49-5.694h1.302c.116 0 .206.028.27.084.068.056.118.12.15.192l1.308 3.192c.044.108.08.216.108.324.032.108.062.218.09.33a32.3 32.3 0 0 1 .108-.33c.036-.112.076-.222.12-.33l1.236-3.186a.437.437 0 0 1 .408-.276h1.188z"
+    />
+  </svg>
 );
 
 export const LogoShortIcon = () => (
diff --git a/app/vmui/packages/vmui/src/components/Main/Select/Select.tsx b/app/vmui/packages/vmui/src/components/Main/Select/Select.tsx
index 4d93eddaa..186991412 100644
--- a/app/vmui/packages/vmui/src/components/Main/Select/Select.tsx
+++ b/app/vmui/packages/vmui/src/components/Main/Select/Select.tsx
@@ -18,6 +18,7 @@ interface SelectProps {
   clearable?: boolean
   searchable?: boolean
   autofocus?: boolean
+  disabled?: boolean
   onChange: (value: string) => void
 }
 
@@ -30,6 +31,7 @@ const Select: FC<SelectProps> = ({
   clearable = false,
   searchable = false,
   autofocus,
+  disabled,
   onChange
 }) => {
   const { isDarkTheme } = useAppState();
@@ -64,11 +66,12 @@ const Select: FC<SelectProps> = ({
   };
 
   const handleFocus = () => {
+    if (disabled) return;
     setOpenList(true);
   };
 
   const handleToggleList = (e: MouseEvent<HTMLDivElement>) => {
-    if (e.target instanceof HTMLInputElement) return;
+    if (e.target instanceof HTMLInputElement || disabled) return;
     setOpenList(prev => !prev);
   };
 
@@ -112,7 +115,8 @@ const Select: FC<SelectProps> = ({
     <div
       className={classNames({
         "vm-select": true,
-        "vm-select_dark": isDarkTheme
+        "vm-select_dark": isDarkTheme,
+        "vm-select_disabled": disabled
       })}
     >
       <div
diff --git a/app/vmui/packages/vmui/src/components/Main/Select/style.scss b/app/vmui/packages/vmui/src/components/Main/Select/style.scss
index 845600775..e079fb3cb 100644
--- a/app/vmui/packages/vmui/src/components/Main/Select/style.scss
+++ b/app/vmui/packages/vmui/src/components/Main/Select/style.scss
@@ -126,4 +126,18 @@
       max-height: calc(($vh * 100) - 70px);
     }
   }
+
+  &_disabled {
+    * {
+      cursor: not-allowed;
+    }
+
+    .vm-select-input {
+      &-content {
+        input {
+          color: $color-text-disabled;
+        }
+      }
+    }
+  }
 }
diff --git a/app/vmui/packages/vmui/src/components/Views/GraphView/GraphView.tsx b/app/vmui/packages/vmui/src/components/Views/GraphView/GraphView.tsx
index e53e88ddb..622fb54f3 100644
--- a/app/vmui/packages/vmui/src/components/Views/GraphView/GraphView.tsx
+++ b/app/vmui/packages/vmui/src/components/Views/GraphView/GraphView.tsx
@@ -24,6 +24,7 @@ import { promValueToNumber } from "../../../utils/metric";
 import useDeviceDetect from "../../../hooks/useDeviceDetect";
 import useElementSize from "../../../hooks/useElementSize";
 import { ChartTooltipProps } from "../../Chart/ChartTooltip/ChartTooltip";
+import LegendAnomaly from "../../Chart/Line/LegendAnomaly/LegendAnomaly";
 
 export interface GraphViewProps {
   data?: MetricResult[];
@@ -34,11 +35,12 @@ export interface GraphViewProps {
   yaxis: YaxisState;
   unit?: string;
   showLegend?: boolean;
-  setYaxisLimits: (val: AxisRange) => void
-  setPeriod: ({ from, to }: { from: Date, to: Date }) => void
-  fullWidth?: boolean
-  height?: number
-  isHistogram?: boolean
+  setYaxisLimits: (val: AxisRange) => void;
+  setPeriod: ({ from, to }: { from: Date, to: Date }) => void;
+  fullWidth?: boolean;
+  height?: number;
+  isHistogram?: boolean;
+  anomalyView?: boolean;
 }
 
 const GraphView: FC<GraphViewProps> = ({
@@ -54,7 +56,8 @@ const GraphView: FC<GraphViewProps> = ({
   alias = [],
   fullWidth = true,
   height,
-  isHistogram
+  isHistogram,
+  anomalyView,
 }) => {
   const { isMobile } = useDeviceDetect();
   const { timezone } = useTimeState();
@@ -69,8 +72,8 @@ const GraphView: FC<GraphViewProps> = ({
   const [legendValue, setLegendValue] = useState<ChartTooltipProps | null>(null);
 
   const getSeriesItem = useMemo(() => {
-    return getSeriesItemContext(data, hideSeries, alias);
-  }, [data, hideSeries, alias]);
+    return getSeriesItemContext(data, hideSeries, alias, anomalyView);
+  }, [data, hideSeries, alias, anomalyView]);
 
   const setLimitsYaxis = (values: { [key: string]: number[] }) => {
     const limits = getLimitsYAxis(values, !isHistogram);
@@ -148,7 +151,7 @@ const GraphView: FC<GraphViewProps> = ({
       const range = getMinMaxBuffer(getMinFromArray(resultAsNumber), getMaxFromArray(resultAsNumber));
       const rangeStep = Math.abs(range[1] - range[0]);
 
-      return (avg > rangeStep * 1e10) ? results.map(() => avg) : results;
+      return (avg > rangeStep * 1e10) && !anomalyView ? results.map(() => avg) : results;
     });
     timeDataSeries.unshift(timeSeries);
     setLimitsYaxis(tempValues);
@@ -192,6 +195,7 @@ const GraphView: FC<GraphViewProps> = ({
           setPeriod={setPeriod}
           layoutSize={containerSize}
           height={height}
+          anomalyView={anomalyView}
         />
       )}
       {isHistogram && (
@@ -206,7 +210,7 @@ const GraphView: FC<GraphViewProps> = ({
           onChangeLegend={setLegendValue}
         />
       )}
-      {!isHistogram && showLegend && (
+      {!isHistogram && !anomalyView && showLegend && (
         <Legend
           labels={legend}
           query={query}
@@ -221,6 +225,11 @@ const GraphView: FC<GraphViewProps> = ({
           legendValue={legendValue}
         />
       )}
+      {anomalyView && showLegend && (
+        <LegendAnomaly
+          series={series as SeriesItem[]}
+        />
+      )}
     </div>
   );
 };
diff --git a/app/vmui/packages/vmui/src/constants/navigation.ts b/app/vmui/packages/vmui/src/constants/navigation.ts
index 5974e91d6..f565c1964 100644
--- a/app/vmui/packages/vmui/src/constants/navigation.ts
+++ b/app/vmui/packages/vmui/src/constants/navigation.ts
@@ -7,6 +7,46 @@ export interface NavigationItem {
   submenu?: NavigationItem[],
 }
 
+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.withTemplate].title,
+      value: router.withTemplate,
+    },
+    {
+      label: routerOptions[router.relabel].title,
+      value: router.relabel,
+    },
+  ]
+};
+
 export const logsNavigation: NavigationItem[] = [
   {
     label: routerOptions[router.logs].title,
@@ -14,47 +54,22 @@ export const logsNavigation: NavigationItem[] = [
   },
 ];
 
-export const defaultNavigation: NavigationItem[] = [
+export const anomalyNavigation: NavigationItem[] = [
   {
-    label: routerOptions[router.home].title,
+    label: routerOptions[router.anomaly].title,
     value: router.home,
   },
   {
-    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,
-      },
-    ]
-  },
-  {
-    label: "Tools",
-    submenu: [
-      {
-        label: routerOptions[router.trace].title,
-        value: router.trace,
-      },
-      {
-        label: routerOptions[router.withTemplate].title,
-        value: router.withTemplate,
-      },
-      {
-        label: routerOptions[router.relabel].title,
-        value: router.relabel,
-      },
-    ]
+    label: routerOptions[router.home].title,
+    value: router.query,
   }
 ];
+
+export const defaultNavigation: NavigationItem[] = [
+  {
+    label: routerOptions[router.home].title,
+    value: router.home,
+  },
+  explore,
+  tools,
+];
diff --git a/app/vmui/packages/vmui/src/hooks/uplot/useLineTooltip.ts b/app/vmui/packages/vmui/src/hooks/uplot/useLineTooltip.ts
index a8fe0bee6..bc6012019 100644
--- a/app/vmui/packages/vmui/src/hooks/uplot/useLineTooltip.ts
+++ b/app/vmui/packages/vmui/src/hooks/uplot/useLineTooltip.ts
@@ -14,9 +14,10 @@ interface LineTooltipHook {
   metrics: MetricResult[];
   series: uPlotSeries[];
   unit?: string;
+  anomalyView?: boolean;
 }
 
-const useLineTooltip = ({ u, metrics, series, unit }: LineTooltipHook) => {
+const useLineTooltip = ({ u, metrics, series, unit, anomalyView }: LineTooltipHook) => {
   const [showTooltip, setShowTooltip] = useState(false);
   const [tooltipIdx, setTooltipIdx] = useState({ seriesIdx: -1, dataIdx: -1 });
   const [stickyTooltips, setStickyToolTips] = useState<ChartTooltipProps[]>([]);
@@ -60,14 +61,14 @@ const useLineTooltip = ({ u, metrics, series, unit }: LineTooltipHook) => {
       point,
       u: u,
       id: `${seriesIdx}_${dataIdx}`,
-      title: groups.size > 1 ? `Query ${group}` : "",
+      title: groups.size > 1 && !anomalyView ? `Query ${group}` : "",
       dates: [date ? dayjs(date * 1000).tz().format(DATE_FULL_TIMEZONE_FORMAT) : "-"],
       value: formatPrettyNumber(value, min, max),
       info: getMetricName(metricItem),
       statsFormatted: seriesItem?.statsFormatted,
       marker: `${seriesItem?.stroke}`,
     };
-  }, [u, tooltipIdx, metrics, series, unit]);
+  }, [u, tooltipIdx, metrics, series, unit, anomalyView]);
 
   const handleClick = useCallback(() => {
     if (!showTooltip) return;
diff --git a/app/vmui/packages/vmui/src/hooks/useFetchQuery.ts b/app/vmui/packages/vmui/src/hooks/useFetchQuery.ts
index 980527713..638eb375b 100644
--- a/app/vmui/packages/vmui/src/hooks/useFetchQuery.ts
+++ b/app/vmui/packages/vmui/src/hooks/useFetchQuery.ts
@@ -4,9 +4,8 @@ import { getQueryRangeUrl, getQueryUrl } from "../api/query-range";
 import { useAppState } from "../state/common/StateContext";
 import { InstantMetricResult, MetricBase, MetricResult, QueryStats } from "../api/types";
 import { isValidHttpUrl } from "../utils/url";
-import { ErrorTypes, SeriesLimits } from "../types";
+import { DisplayType, ErrorTypes, SeriesLimits } from "../types";
 import debounce from "lodash.debounce";
-import { DisplayType } from "../pages/CustomPanel/DisplayTypeSwitch";
 import Trace from "../components/TraceQuery/Trace";
 import { useQueryState } from "../state/query/QueryStateContext";
 import { useTimeState } from "../state/time/TimeStateContext";
@@ -90,7 +89,7 @@ export const useFetchQuery = ({
     const controller = new AbortController();
     setFetchQueue([...fetchQueue, controller]);
     try {
-      const isDisplayChart = displayType === "chart";
+      const isDisplayChart = displayType === DisplayType.chart;
       const defaultLimit = showAllSeries ? Infinity : (+stateSeriesLimits[displayType] || Infinity);
       let seriesLimit = defaultLimit;
       const tempData: MetricBase[] = [];
@@ -165,7 +164,7 @@ export const useFetchQuery = ({
     setQueryErrors([]);
     setQueryStats([]);
     const expr = predefinedQuery ?? query;
-    const displayChart = (display || displayType) === "chart";
+    const displayChart = (display || displayType) === DisplayType.chart;
     if (!period) return;
     if (!serverUrl) {
       setError(ErrorTypes.emptyServer);
diff --git a/app/vmui/packages/vmui/src/layouts/AnomalyLayout/AnomalyLayout.tsx b/app/vmui/packages/vmui/src/layouts/AnomalyLayout/AnomalyLayout.tsx
new file mode 100644
index 000000000..686e6e889
--- /dev/null
+++ b/app/vmui/packages/vmui/src/layouts/AnomalyLayout/AnomalyLayout.tsx
@@ -0,0 +1,59 @@
+import Header from "../Header/Header";
+import React, { FC, useEffect } from "preact/compat";
+import { Outlet, useLocation, useSearchParams } from "react-router-dom";
+import qs from "qs";
+import "../MainLayout/style.scss";
+import { getAppModeEnable } from "../../utils/app-mode";
+import classNames from "classnames";
+import Footer from "../Footer/Footer";
+import { routerOptions } from "../../router";
+import { useFetchDashboards } from "../../pages/PredefinedPanels/hooks/useFetchDashboards";
+import useDeviceDetect from "../../hooks/useDeviceDetect";
+import ControlsAnomalyLayout from "./ControlsAnomalyLayout";
+
+const AnomalyLayout: FC = () => {
+  const appModeEnable = getAppModeEnable();
+  const { isMobile } = useDeviceDetect();
+  const { pathname } = useLocation();
+  const [searchParams, setSearchParams] = useSearchParams();
+
+  useFetchDashboards();
+
+  const setDocumentTitle = () => {
+    const defaultTitle = "vmui for vmanomaly";
+    const routeTitle = routerOptions[pathname]?.title;
+    document.title = routeTitle ? `${routeTitle} - ${defaultTitle}` : defaultTitle;
+  };
+
+  // for support old links with search params
+  const redirectSearchToHashParams = () => {
+    const { search, href } = window.location;
+    if (search) {
+      const query = qs.parse(search, { ignoreQueryPrefix: true });
+      Object.entries(query).forEach(([key, value]) => searchParams.set(key, value as string));
+      setSearchParams(searchParams);
+      window.location.search = "";
+    }
+    const newHref = href.replace(/\/\?#\//, "/#/");
+    if (newHref !== href) window.location.replace(newHref);
+  };
+
+  useEffect(setDocumentTitle, [pathname]);
+  useEffect(redirectSearchToHashParams, []);
+
+  return <section className="vm-container">
+    <Header controlsComponent={ControlsAnomalyLayout}/>
+    <div
+      className={classNames({
+        "vm-container-body": true,
+        "vm-container-body_mobile": isMobile,
+        "vm-container-body_app": appModeEnable
+      })}
+    >
+      <Outlet/>
+    </div>
+    {!appModeEnable && <Footer/>}
+  </section>;
+};
+
+export default AnomalyLayout;
diff --git a/app/vmui/packages/vmui/src/layouts/AnomalyLayout/ControlsAnomalyLayout.tsx b/app/vmui/packages/vmui/src/layouts/AnomalyLayout/ControlsAnomalyLayout.tsx
new file mode 100644
index 000000000..495ded5cd
--- /dev/null
+++ b/app/vmui/packages/vmui/src/layouts/AnomalyLayout/ControlsAnomalyLayout.tsx
@@ -0,0 +1,38 @@
+import React, { FC } from "preact/compat";
+import classNames from "classnames";
+import TenantsConfiguration
+  from "../../components/Configurators/GlobalSettings/TenantsConfiguration/TenantsConfiguration";
+import StepConfigurator from "../../components/Configurators/StepConfigurator/StepConfigurator";
+import { TimeSelector } from "../../components/Configurators/TimeRangeSettings/TimeSelector/TimeSelector";
+import CardinalityDatePicker from "../../components/Configurators/CardinalityDatePicker/CardinalityDatePicker";
+import { ExecutionControls } from "../../components/Configurators/TimeRangeSettings/ExecutionControls/ExecutionControls";
+import GlobalSettings from "../../components/Configurators/GlobalSettings/GlobalSettings";
+import ShortcutKeys from "../../components/Main/ShortcutKeys/ShortcutKeys";
+import { ControlsProps } from "../Header/HeaderControls/HeaderControls";
+
+const ControlsAnomalyLayout: FC<ControlsProps> = ({
+  displaySidebar,
+  isMobile,
+  headerSetup,
+  accountIds
+}) => {
+
+  return (
+    <div
+      className={classNames({
+        "vm-header-controls": true,
+        "vm-header-controls_mobile": isMobile,
+      })}
+    >
+      {headerSetup?.tenant && <TenantsConfiguration accountIds={accountIds || []}/>}
+      {headerSetup?.stepControl && <StepConfigurator/>}
+      {headerSetup?.timeSelector && <TimeSelector/>}
+      {headerSetup?.cardinalityDatePicker && <CardinalityDatePicker/>}
+      {headerSetup?.executionControls && <ExecutionControls/>}
+      <GlobalSettings/>
+      {!displaySidebar && <ShortcutKeys/>}
+    </div>
+  );
+};
+
+export default ControlsAnomalyLayout;
diff --git a/app/vmui/packages/vmui/src/layouts/Header/Header.tsx b/app/vmui/packages/vmui/src/layouts/Header/Header.tsx
index 278352164..535750d7f 100644
--- a/app/vmui/packages/vmui/src/layouts/Header/Header.tsx
+++ b/app/vmui/packages/vmui/src/layouts/Header/Header.tsx
@@ -2,7 +2,7 @@ import React, { FC, useMemo } from "preact/compat";
 import { useNavigate } from "react-router-dom";
 import router from "../../router";
 import { getAppModeEnable, getAppModeParams } from "../../utils/app-mode";
-import { LogoIcon, LogoLogsIcon } from "../../components/Main/Icons";
+import { LogoAnomalyIcon, LogoIcon, LogoLogsIcon } from "../../components/Main/Icons";
 import { getCssVariable } from "../../utils/theme";
 import "./style.scss";
 import classNames from "classnames";
@@ -13,13 +13,26 @@ import HeaderControls, { ControlsProps } from "./HeaderControls/HeaderControls";
 import useDeviceDetect from "../../hooks/useDeviceDetect";
 import useWindowSize from "../../hooks/useWindowSize";
 import { ComponentType } from "react";
+import { AppType } from "../../types/appType";
 
 export interface HeaderProps {
   controlsComponent: ComponentType<ControlsProps>
 }
+const { REACT_APP_TYPE } = process.env;
+const isCustomApp = REACT_APP_TYPE === AppType.logs || REACT_APP_TYPE === AppType.anomaly;
+
+const Logo = () => {
+  switch (REACT_APP_TYPE) {
+    case AppType.logs:
+      return <LogoLogsIcon/>;
+    case AppType.anomaly:
+      return <LogoAnomalyIcon/>;
+    default:
+      return <LogoIcon/>;
+  }
+};
 
 const Header: FC<HeaderProps> = ({ controlsComponent }) => {
-  const { REACT_APP_LOGS } = process.env;
   const { isMobile } = useDeviceDetect();
 
   const windowSize = useWindowSize();
@@ -70,12 +83,12 @@ const Header: FC<HeaderProps> = ({ controlsComponent }) => {
           <div
             className={classNames({
               "vm-header-logo": true,
-              "vm-header-logo_logs": REACT_APP_LOGS
+              "vm-header-logo_logs": isCustomApp
             })}
             onClick={onClickLogo}
             style={{ color }}
           >
-            {REACT_APP_LOGS ? <LogoLogsIcon/> : <LogoIcon/>}
+            {<Logo/>}
           </div>
         )}
         <HeaderNav
@@ -89,12 +102,12 @@ const Header: FC<HeaderProps> = ({ controlsComponent }) => {
         className={classNames({
           "vm-header-logo": true,
           "vm-header-logo_mobile": true,
-          "vm-header-logo_logs": REACT_APP_LOGS
+          "vm-header-logo_logs": isCustomApp
         })}
         onClick={onClickLogo}
         style={{ color }}
       >
-        {REACT_APP_LOGS ? <LogoLogsIcon/> : <LogoIcon/>}
+        {<Logo/>}
       </div>
     )}
     <HeaderControls
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 0aab8e8b4..46bebc018 100644
--- a/app/vmui/packages/vmui/src/layouts/Header/HeaderNav/HeaderNav.tsx
+++ b/app/vmui/packages/vmui/src/layouts/Header/HeaderNav/HeaderNav.tsx
@@ -8,7 +8,8 @@ import "./style.scss";
 import NavItem from "./NavItem";
 import NavSubItem from "./NavSubItem";
 import classNames from "classnames";
-import { defaultNavigation, logsNavigation } from "../../../constants/navigation";
+import { anomalyNavigation, defaultNavigation, logsNavigation } from "../../../constants/navigation";
+import { AppType } from "../../../types/appType";
 
 interface HeaderNavProps {
   color: string
@@ -17,21 +18,29 @@ interface HeaderNavProps {
 }
 
 const HeaderNav: FC<HeaderNavProps> = ({ color, background, direction }) => {
-  const { REACT_APP_LOGS } = process.env;
   const appModeEnable = getAppModeEnable();
   const { dashboardsSettings } = useDashboardsState();
   const { pathname } = useLocation();
 
   const [activeMenu, setActiveMenu] = useState(pathname);
 
-  const menu = useMemo(() => REACT_APP_LOGS ? logsNavigation : ([
-    ...defaultNavigation,
-    {
-      label: routerOptions[router.dashboards].title,
-      value: router.dashboards,
-      hide: appModeEnable || !dashboardsSettings.length,
+  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,
+          }
+        ].filter(r => !r.hide));
     }
-  ].filter(r => !r.hide)), [appModeEnable, dashboardsSettings]);
+  }, [appModeEnable, dashboardsSettings]);
 
   useEffect(() => {
     setActiveMenu(pathname);
diff --git a/app/vmui/packages/vmui/src/layouts/Header/SidebarNav/SidebarHeader.tsx b/app/vmui/packages/vmui/src/layouts/Header/SidebarNav/SidebarHeader.tsx
index 104c32e46..321175405 100644
--- a/app/vmui/packages/vmui/src/layouts/Header/SidebarNav/SidebarHeader.tsx
+++ b/app/vmui/packages/vmui/src/layouts/Header/SidebarNav/SidebarHeader.tsx
@@ -8,17 +8,20 @@ import MenuBurger from "../../../components/Main/MenuBurger/MenuBurger";
 import useDeviceDetect from "../../../hooks/useDeviceDetect";
 import "./style.scss";
 import useBoolean from "../../../hooks/useBoolean";
+import { AppType } from "../../../types/appType";
 
 interface SidebarHeaderProps {
   background: string
   color: string
 }
 
+const { REACT_APP_TYPE } = process.env;
+const isLogsApp = REACT_APP_TYPE === AppType.logs;
+
 const SidebarHeader: FC<SidebarHeaderProps> = ({
   background,
   color,
 }) => {
-  const { REACT_APP_LOGS } = process.env;
   const { pathname } = useLocation();
   const { isMobile } = useDeviceDetect();
 
@@ -61,7 +64,7 @@ const SidebarHeader: FC<SidebarHeaderProps> = ({
         />
       </div>
       <div className="vm-header-sidebar-menu-settings">
-        {!isMobile && !REACT_APP_LOGS && <ShortcutKeys showTitle={true}/>}
+        {!isMobile && !isLogsApp && <ShortcutKeys showTitle={true}/>}
       </div>
     </div>
   </div>;
diff --git a/app/vmui/packages/vmui/src/layouts/LogsLayout/LogsLayout.tsx b/app/vmui/packages/vmui/src/layouts/LogsLayout/LogsLayout.tsx
index 3e3962e52..4d8a26eb1 100644
--- a/app/vmui/packages/vmui/src/layouts/LogsLayout/LogsLayout.tsx
+++ b/app/vmui/packages/vmui/src/layouts/LogsLayout/LogsLayout.tsx
@@ -1,7 +1,7 @@
 import Header from "../Header/Header";
 import React, { FC, useEffect } from "preact/compat";
 import { Outlet, useLocation } from "react-router-dom";
-import "./style.scss";
+import "../MainLayout/style.scss";
 import { getAppModeEnable } from "../../utils/app-mode";
 import classNames from "classnames";
 import Footer from "../Footer/Footer";
diff --git a/app/vmui/packages/vmui/src/layouts/LogsLayout/style.scss b/app/vmui/packages/vmui/src/layouts/LogsLayout/style.scss
deleted file mode 100644
index 32e8ccf90..000000000
--- a/app/vmui/packages/vmui/src/layouts/LogsLayout/style.scss
+++ /dev/null
@@ -1,27 +0,0 @@
-@use "src/styles/variables" as *;
-
-.vm-container {
-  display: flex;
-  flex-direction: column;
-  min-height: calc(($vh * 100) - var(--scrollbar-height));
-
-  &-body {
-    flex-grow: 1;
-    min-height: 100%;
-    padding: $padding-medium;
-    background-color: $color-background-body;
-
-    &_mobile {
-      padding: $padding-small 0 0;
-    }
-
-    @media (max-width: 768px) {
-      padding: $padding-small 0 0;
-    }
-
-    &_app {
-      padding: $padding-small 0;
-      background-color: transparent;
-    }
-  }
-}
diff --git a/app/vmui/packages/vmui/src/layouts/MainLayout/MainLayout.tsx b/app/vmui/packages/vmui/src/layouts/MainLayout/MainLayout.tsx
index 6dd457991..6d5f5fdc7 100644
--- a/app/vmui/packages/vmui/src/layouts/MainLayout/MainLayout.tsx
+++ b/app/vmui/packages/vmui/src/layouts/MainLayout/MainLayout.tsx
@@ -6,13 +6,12 @@ import "./style.scss";
 import { getAppModeEnable } from "../../utils/app-mode";
 import classNames from "classnames";
 import Footer from "../Footer/Footer";
-import router, { routerOptions } from "../../router";
+import { routerOptions } from "../../router";
 import { useFetchDashboards } from "../../pages/PredefinedPanels/hooks/useFetchDashboards";
 import useDeviceDetect from "../../hooks/useDeviceDetect";
 import ControlsMainLayout from "./ControlsMainLayout";
 
 const MainLayout: FC = () => {
-  const { REACT_APP_LOGS } = process.env;
   const appModeEnable = getAppModeEnable();
   const { isMobile } = useDeviceDetect();
   const { pathname } = useLocation();
@@ -22,7 +21,7 @@ const MainLayout: FC = () => {
 
   const setDocumentTitle = () => {
     const defaultTitle = "vmui";
-    const routeTitle = REACT_APP_LOGS ? routerOptions[router.logs]?.title : routerOptions[pathname]?.title;
+    const routeTitle = routerOptions[pathname]?.title;
     document.title = routeTitle ? `${routeTitle} - ${defaultTitle}` : defaultTitle;
   };
 
diff --git a/app/vmui/packages/vmui/src/pages/CustomPanel/CustomPanelTabs/GraphTab.tsx b/app/vmui/packages/vmui/src/pages/CustomPanel/CustomPanelTabs/GraphTab.tsx
new file mode 100644
index 000000000..ba87299cb
--- /dev/null
+++ b/app/vmui/packages/vmui/src/pages/CustomPanel/CustomPanelTabs/GraphTab.tsx
@@ -0,0 +1,72 @@
+import React, { FC } from "react";
+import GraphView from "../../../components/Views/GraphView/GraphView";
+import GraphTips from "../../../components/Chart/GraphTips/GraphTips";
+import GraphSettings from "../../../components/Configurators/GraphSettings/GraphSettings";
+import { AxisRange } from "../../../state/graph/reducer";
+import { useTimeDispatch, useTimeState } from "../../../state/time/TimeStateContext";
+import { useGraphDispatch, useGraphState } from "../../../state/graph/GraphStateContext";
+import useDeviceDetect from "../../../hooks/useDeviceDetect";
+import { useQueryState } from "../../../state/query/QueryStateContext";
+import { MetricResult } from "../../../api/types";
+import { createPortal } from "preact/compat";
+
+type Props = {
+  isHistogram: boolean;
+  graphData: MetricResult[];
+  controlsRef: React.RefObject<HTMLDivElement>;
+  anomalyView?: boolean;
+}
+
+const GraphTab: FC<Props> = ({ isHistogram, graphData, controlsRef, anomalyView }) => {
+  const { isMobile } = useDeviceDetect();
+
+  const { customStep, yaxis } = useGraphState();
+  const { period } = useTimeState();
+  const { query } = useQueryState();
+
+  const timeDispatch = useTimeDispatch();
+  const graphDispatch = useGraphDispatch();
+
+  const setYaxisLimits = (limits: AxisRange) => {
+    graphDispatch({ type: "SET_YAXIS_LIMITS", payload: limits });
+  };
+
+  const toggleEnableLimits = () => {
+    graphDispatch({ type: "TOGGLE_ENABLE_YAXIS_LIMITS" });
+  };
+
+  const setPeriod = ({ from, to }: {from: Date, to: Date}) => {
+    timeDispatch({ type: "SET_PERIOD", payload: { from, to } });
+  };
+
+  const controls = (
+    <div className="vm-custom-panel-body-header__graph-controls">
+      <GraphTips/>
+      <GraphSettings
+        yaxis={yaxis}
+        setYaxisLimits={setYaxisLimits}
+        toggleEnableLimits={toggleEnableLimits}
+      />
+    </div>
+  );
+
+  return (
+    <>
+      {controlsRef.current && createPortal(controls, controlsRef.current)}
+      <GraphView
+        data={graphData}
+        period={period}
+        customStep={customStep}
+        query={query}
+        yaxis={yaxis}
+        setYaxisLimits={setYaxisLimits}
+        setPeriod={setPeriod}
+        height={isMobile ? window.innerHeight * 0.5 : 500}
+        isHistogram={isHistogram}
+        anomalyView={anomalyView}
+      />
+    </>
+  );
+};
+
+export default GraphTab;
diff --git a/app/vmui/packages/vmui/src/pages/CustomPanel/CustomPanelTabs/TableTab.tsx b/app/vmui/packages/vmui/src/pages/CustomPanel/CustomPanelTabs/TableTab.tsx
new file mode 100644
index 000000000..afa42c1b1
--- /dev/null
+++ b/app/vmui/packages/vmui/src/pages/CustomPanel/CustomPanelTabs/TableTab.tsx
@@ -0,0 +1,47 @@
+import React, { FC } from "react";
+import { InstantMetricResult } from "../../../api/types";
+import { createPortal, useMemo, useState } from "preact/compat";
+import TableView from "../../../components/Views/TableView/TableView";
+import TableSettings from "../../../components/Table/TableSettings/TableSettings";
+import { getColumns } from "../../../hooks/useSortedCategories";
+import { useCustomPanelDispatch, useCustomPanelState } from "../../../state/customPanel/CustomPanelStateContext";
+
+type Props = {
+  liveData: InstantMetricResult[];
+  controlsRef: React.RefObject<HTMLDivElement>;
+}
+
+const TableTab: FC<Props> = ({ liveData, controlsRef }) => {
+  const { tableCompact } = useCustomPanelState();
+  const customPanelDispatch = useCustomPanelDispatch();
+
+  const [displayColumns, setDisplayColumns] = useState<string[]>();
+
+  const columns = useMemo(() => getColumns(liveData || []).map(c => c.key), [liveData]);
+
+  const toggleTableCompact = () => {
+    customPanelDispatch({ type: "TOGGLE_TABLE_COMPACT" });
+  };
+
+  const controls = (
+    <TableSettings
+      columns={columns}
+      defaultColumns={displayColumns}
+      onChangeColumns={setDisplayColumns}
+      tableCompact={tableCompact}
+      toggleTableCompact={toggleTableCompact}
+    />
+  );
+
+  return (
+    <>
+      {controlsRef.current && createPortal(controls, controlsRef.current)}
+      <TableView
+        data={liveData}
+        displayColumns={displayColumns}
+      />
+    </>
+  );
+};
+
+export default TableTab;
diff --git a/app/vmui/packages/vmui/src/pages/CustomPanel/CustomPanelTabs/index.tsx b/app/vmui/packages/vmui/src/pages/CustomPanel/CustomPanelTabs/index.tsx
new file mode 100644
index 000000000..741b7c03c
--- /dev/null
+++ b/app/vmui/packages/vmui/src/pages/CustomPanel/CustomPanelTabs/index.tsx
@@ -0,0 +1,45 @@
+import React, { FC, RefObject } from "react";
+import GraphTab from "./GraphTab";
+import JsonView from "../../../components/Views/JsonView/JsonView";
+import TableTab from "./TableTab";
+import { InstantMetricResult, MetricResult } from "../../../api/types";
+import { DisplayType } from "../../../types";
+
+type Props = {
+  graphData?: MetricResult[];
+  liveData?: InstantMetricResult[];
+  isHistogram: boolean;
+  displayType: DisplayType;
+  controlsRef: RefObject<HTMLDivElement>;
+}
+
+const CustomPanelTabs: FC<Props> = ({
+  graphData,
+  liveData,
+  isHistogram,
+  displayType,
+  controlsRef
+}) => {
+  if (displayType === DisplayType.code && liveData) {
+    return <JsonView data={liveData} />;
+  }
+
+  if (displayType === DisplayType.table && liveData) {
+    return <TableTab
+      liveData={liveData}
+      controlsRef={controlsRef}
+    />;
+  }
+
+  if (displayType === DisplayType.chart && graphData) {
+    return <GraphTab
+      graphData={graphData}
+      isHistogram={isHistogram}
+      controlsRef={controlsRef}
+    />;
+  }
+
+  return null;
+};
+
+export default CustomPanelTabs;
diff --git a/app/vmui/packages/vmui/src/pages/CustomPanel/CustomPanelTraces/CustomPanelTraces.tsx b/app/vmui/packages/vmui/src/pages/CustomPanel/CustomPanelTraces/CustomPanelTraces.tsx
new file mode 100644
index 000000000..73b9727f6
--- /dev/null
+++ b/app/vmui/packages/vmui/src/pages/CustomPanel/CustomPanelTraces/CustomPanelTraces.tsx
@@ -0,0 +1,43 @@
+import { useCustomPanelState } from "../../../state/customPanel/CustomPanelStateContext";
+import TracingsView from "../../../components/TraceQuery/TracingsView";
+import React, { FC, useEffect, useState } from "preact/compat";
+import Trace from "../../../components/TraceQuery/Trace";
+import { DisplayType } from "../../../types";
+
+type Props = {
+  traces?: Trace[];
+  displayType: DisplayType;
+}
+
+const CustomPanelTraces: FC<Props> = ({ traces, displayType }) => {
+  const { isTracingEnabled } = useCustomPanelState();
+  const [tracesState, setTracesState] = useState<Trace[]>([]);
+
+  const handleTraceDelete = (trace: Trace) => {
+    const updatedTraces = tracesState.filter((data) => data.idValue !== trace.idValue);
+    setTracesState([...updatedTraces]);
+  };
+
+  useEffect(() => {
+    if (traces) {
+      setTracesState([...tracesState, ...traces]);
+    }
+  }, [traces]);
+
+  useEffect(() => {
+    setTracesState([]);
+  }, [displayType]);
+
+  return <>
+    {isTracingEnabled && (
+      <div className="vm-custom-panel__trace">
+        <TracingsView
+          traces={tracesState}
+          onDeleteClick={handleTraceDelete}
+        />
+      </div>
+    )}
+  </>;
+};
+
+export default CustomPanelTraces;
diff --git a/app/vmui/packages/vmui/src/pages/CustomPanel/DisplayTypeSwitch.tsx b/app/vmui/packages/vmui/src/pages/CustomPanel/DisplayTypeSwitch.tsx
index 958044758..3e81640e9 100644
--- a/app/vmui/packages/vmui/src/pages/CustomPanel/DisplayTypeSwitch.tsx
+++ b/app/vmui/packages/vmui/src/pages/CustomPanel/DisplayTypeSwitch.tsx
@@ -2,8 +2,7 @@ import React, { FC } from "preact/compat";
 import { useCustomPanelDispatch, useCustomPanelState } from "../../state/customPanel/CustomPanelStateContext";
 import { ChartIcon, CodeIcon, TableIcon } from "../../components/Main/Icons";
 import Tabs from "../../components/Main/Tabs/Tabs";
-
-export type DisplayType = "table" | "chart" | "code";
+import { DisplayType } from "../../types";
 
 type DisplayTab = {
   value: DisplayType
@@ -13,9 +12,9 @@ type DisplayTab = {
 }
 
 export const displayTypeTabs: DisplayTab[] = [
-  { value: "chart", icon: <ChartIcon/>, label: "Graph", prometheusCode: 0 },
-  { value: "code", icon: <CodeIcon/>, label: "JSON", prometheusCode: 3 },
-  { value: "table", icon: <TableIcon/>, label: "Table", prometheusCode: 1 }
+  { value: DisplayType.chart, icon: <ChartIcon/>, label: "Graph", prometheusCode: 0 },
+  { value: DisplayType.code, icon: <CodeIcon/>, label: "JSON", prometheusCode: 3 },
+  { value: DisplayType.table, icon: <TableIcon/>, label: "Table", prometheusCode: 1 }
 ];
 
 export const DisplayTypeSwitch: FC = () => {
diff --git a/app/vmui/packages/vmui/src/pages/CustomPanel/WarningLimitSeries/WarningLimitSeries.tsx b/app/vmui/packages/vmui/src/pages/CustomPanel/WarningLimitSeries/WarningLimitSeries.tsx
new file mode 100644
index 000000000..fc94bcf60
--- /dev/null
+++ b/app/vmui/packages/vmui/src/pages/CustomPanel/WarningLimitSeries/WarningLimitSeries.tsx
@@ -0,0 +1,50 @@
+import classNames from "classnames";
+import Button from "../../../components/Main/Button/Button";
+import React, { FC, useEffect } from "preact/compat";
+import useBoolean from "../../../hooks/useBoolean";
+import useDeviceDetect from "../../../hooks/useDeviceDetect";
+import Alert from "../../../components/Main/Alert/Alert";
+
+type Props = {
+  warning: string;
+  query: string[];
+  onChange: (show: boolean) => void
+}
+
+const WarningLimitSeries: FC<Props> = ({ warning, query, onChange }) => {
+  const { isMobile } = useDeviceDetect();
+
+  const {
+    value: showAllSeries,
+    setTrue: handleShowAll,
+    setFalse: resetShowAll,
+  } = useBoolean(false);
+
+  useEffect(resetShowAll, [query]);
+
+  useEffect(() => {
+    onChange(showAllSeries);
+  }, [showAllSeries]);
+
+  return (
+    <Alert variant="warning">
+      <div
+        className={classNames({
+          "vm-custom-panel__warning": true,
+          "vm-custom-panel__warning_mobile": isMobile
+        })}
+      >
+        <p>{warning}</p>
+        <Button
+          color="warning"
+          variant="outlined"
+          onClick={handleShowAll}
+        >
+        Show all
+        </Button>
+      </div>
+    </Alert>
+  );
+};
+
+export default WarningLimitSeries;
diff --git a/app/vmui/packages/vmui/src/pages/CustomPanel/index.tsx b/app/vmui/packages/vmui/src/pages/CustomPanel/index.tsx
index 795ef78e6..4bb9fdc2f 100644
--- a/app/vmui/packages/vmui/src/pages/CustomPanel/index.tsx
+++ b/app/vmui/packages/vmui/src/pages/CustomPanel/index.tsx
@@ -1,53 +1,38 @@
-import React, { FC, useState, useEffect, useMemo } from "preact/compat";
-import GraphView from "../../components/Views/GraphView/GraphView";
+import React, { FC, useEffect, useState } from "preact/compat";
 import QueryConfigurator from "./QueryConfigurator/QueryConfigurator";
 import { useFetchQuery } from "../../hooks/useFetchQuery";
-import JsonView from "../../components/Views/JsonView/JsonView";
 import { DisplayTypeSwitch } from "./DisplayTypeSwitch";
-import GraphSettings from "../../components/Configurators/GraphSettings/GraphSettings";
 import { useGraphDispatch, useGraphState } from "../../state/graph/GraphStateContext";
-import { AxisRange } from "../../state/graph/reducer";
 import Spinner from "../../components/Main/Spinner/Spinner";
-import TracingsView from "../../components/TraceQuery/TracingsView";
-import Trace from "../../components/TraceQuery/Trace";
-import TableSettings from "../../components/Table/TableSettings/TableSettings";
-import { useCustomPanelState, useCustomPanelDispatch } from "../../state/customPanel/CustomPanelStateContext";
+import { useCustomPanelState } from "../../state/customPanel/CustomPanelStateContext";
 import { useQueryState } from "../../state/query/QueryStateContext";
-import { useTimeDispatch, useTimeState } from "../../state/time/TimeStateContext";
 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";
 import classNames from "classnames";
 import useDeviceDetect from "../../hooks/useDeviceDetect";
-import GraphTips from "../../components/Chart/GraphTips/GraphTips";
 import InstantQueryTip from "./InstantQueryTip/InstantQueryTip";
-import useBoolean from "../../hooks/useBoolean";
-import { getColumns } from "../../hooks/useSortedCategories";
 import useEventListener from "../../hooks/useEventListener";
+import { useRef } from "react";
+import CustomPanelTraces from "./CustomPanelTraces/CustomPanelTraces";
+import WarningLimitSeries from "./WarningLimitSeries/WarningLimitSeries";
+import CustomPanelTabs from "./CustomPanelTabs";
+import { DisplayType } from "../../types";
 
 const CustomPanel: FC = () => {
-  const { displayType, isTracingEnabled } = useCustomPanelState();
-  const { query } = useQueryState();
-  const { period } = useTimeState();
-  const timeDispatch = useTimeDispatch();
-  const { isMobile } = useDeviceDetect();
   useSetQueryParams();
+  const { isMobile } = useDeviceDetect();
+
+  const { displayType } = useCustomPanelState();
+  const { query } = useQueryState();
+  const { customStep } = useGraphState();
+  const graphDispatch = useGraphDispatch();
 
-  const [displayColumns, setDisplayColumns] = useState<string[]>();
-  const [tracesState, setTracesState] = useState<Trace[]>([]);
   const [hideQuery, setHideQuery] = useState<number[]>([]);
   const [hideError, setHideError] = useState(!query[0]);
+  const [showAllSeries, setShowAllSeries] = useState(false);
 
-  const {
-    value: showAllSeries,
-    setTrue: handleShowAll,
-    setFalse: handleHideSeries,
-  } = useBoolean(false);
-
-  const { customStep, yaxis } = useGraphState();
-  const graphDispatch = useGraphDispatch();
+  const controlsRef = useRef<HTMLDivElement>(null);
 
   const {
     isLoading,
@@ -67,22 +52,8 @@ const CustomPanel: FC = () => {
     showAllSeries
   });
 
-  const setYaxisLimits = (limits: AxisRange) => {
-    graphDispatch({ type: "SET_YAXIS_LIMITS", payload: limits });
-  };
-
-  const toggleEnableLimits = () => {
-    graphDispatch({ type: "TOGGLE_ENABLE_YAXIS_LIMITS" });
-  };
-
-  const setPeriod = ({ from, to }: {from: Date, to: Date}) => {
-    timeDispatch({ type: "SET_PERIOD", payload: { from, to } });
-  };
-
-  const handleTraceDelete = (trace: Trace) => {
-    const updatedTraces = tracesState.filter((data) => data.idValue !== trace.idValue);
-    setTracesState([...updatedTraces]);
-  };
+  const showInstantQueryTip = !liveData?.length && (displayType !== DisplayType.chart);
+  const showError = !hideError && error;
 
   const handleHideQuery = (queries: number[]) => {
     setHideQuery(queries);
@@ -92,29 +63,9 @@ const CustomPanel: FC = () => {
     setHideError(false);
   };
 
-  const columns = useMemo(() => getColumns(liveData || []).map(c => c.key), [liveData]);
-  const { tableCompact } = useCustomPanelState();
-  const customPanelDispatch = useCustomPanelDispatch();
-
-  const toggleTableCompact = () => {
-    customPanelDispatch({ type: "TOGGLE_TABLE_COMPACT" });
-  };
-
   const handleChangePopstate = () => window.location.reload();
   useEventListener("popstate", handleChangePopstate);
 
-  useEffect(() => {
-    if (traces) {
-      setTracesState([...tracesState, ...traces]);
-    }
-  }, [traces]);
-
-  useEffect(() => {
-    setTracesState([]);
-  }, [displayType]);
-
-  useEffect(handleHideSeries, [query]);
-
   useEffect(() => {
     graphDispatch({ type: "SET_IS_HISTOGRAM", payload: isHistogram });
   }, [graphData]);
@@ -134,34 +85,20 @@ const CustomPanel: FC = () => {
         onHideQuery={handleHideQuery}
         onRunQuery={handleRunQuery}
       />
-      {isTracingEnabled && (
-        <div className="vm-custom-panel__trace">
-          <TracingsView
-            traces={tracesState}
-            onDeleteClick={handleTraceDelete}
-          />
-        </div>
-      )}
+      <CustomPanelTraces
+        traces={traces}
+        displayType={displayType}
+      />
       {isLoading && <Spinner />}
-      {!hideError && error && <Alert variant="error">{error}</Alert>}
-      {!liveData?.length && (displayType !== "chart") && <Alert variant="info"><InstantQueryTip/></Alert>}
-      {warning && <Alert variant="warning">
-        <div
-          className={classNames({
-            "vm-custom-panel__warning": true,
-            "vm-custom-panel__warning_mobile": isMobile
-          })}
-        >
-          <p>{warning}</p>
-          <Button
-            color="warning"
-            variant="outlined"
-            onClick={handleShowAll}
-          >
-            Show all
-          </Button>
-        </div>
-      </Alert>}
+      {showError && <Alert variant="error">{error}</Alert>}
+      {showInstantQueryTip && <Alert variant="info"><InstantQueryTip/></Alert>}
+      {warning && (
+        <WarningLimitSeries
+          warning={warning}
+          query={query}
+          onChange={setShowAllSeries}
+        />
+      )}
       <div
         className={classNames({
           "vm-custom-panel-body": true,
@@ -170,50 +107,19 @@ const CustomPanel: FC = () => {
           "vm-block_mobile": isMobile,
         })}
       >
-        <div className="vm-custom-panel-body-header">
-          <DisplayTypeSwitch/>
-          {displayType === "chart" && (
-            <div className="vm-custom-panel-body-header__left">
-              <GraphTips/>
-              <GraphSettings
-                yaxis={yaxis}
-                setYaxisLimits={setYaxisLimits}
-                toggleEnableLimits={toggleEnableLimits}
-              />
-            </div>
-          )}
-          {displayType === "table" && (
-            <TableSettings
-              columns={columns}
-              defaultColumns={displayColumns}
-              onChangeColumns={setDisplayColumns}
-              tableCompact={tableCompact}
-              toggleTableCompact={toggleTableCompact}
-            />
-          )}
+        <div
+          className="vm-custom-panel-body-header"
+          ref={controlsRef}
+        >
+          {<DisplayTypeSwitch/>}
         </div>
-        {graphData && period && (displayType === "chart") && (
-          <GraphView
-            data={graphData}
-            period={period}
-            customStep={customStep}
-            query={query}
-            yaxis={yaxis}
-            setYaxisLimits={setYaxisLimits}
-            setPeriod={setPeriod}
-            height={isMobile ? window.innerHeight * 0.5 : 500}
-            isHistogram={isHistogram}
-          />
-        )}
-        {liveData && (displayType === "code") && (
-          <JsonView data={liveData}/>
-        )}
-        {liveData && (displayType === "table") && (
-          <TableView
-            data={liveData}
-            displayColumns={displayColumns}
-          />
-        )}
+        <CustomPanelTabs
+          graphData={graphData}
+          liveData={liveData}
+          isHistogram={isHistogram}
+          displayType={displayType}
+          controlsRef={controlsRef}
+        />
       </div>
     </div>
   );
diff --git a/app/vmui/packages/vmui/src/pages/CustomPanel/style.scss b/app/vmui/packages/vmui/src/pages/CustomPanel/style.scss
index 9d8da5207..2e8fc3f50 100644
--- a/app/vmui/packages/vmui/src/pages/CustomPanel/style.scss
+++ b/app/vmui/packages/vmui/src/pages/CustomPanel/style.scss
@@ -40,10 +40,11 @@
       border-bottom: $border-divider;
       z-index: 1;
 
-      &__left {
+      &__graph-controls {
         display: flex;
         align-items: center;
         gap: $padding-small;
+        margin: 5px 10px;
       }
     }
 
diff --git a/app/vmui/packages/vmui/src/pages/ExploreAnomaly/ExploreAnomaly.tsx b/app/vmui/packages/vmui/src/pages/ExploreAnomaly/ExploreAnomaly.tsx
new file mode 100644
index 000000000..277f39129
--- /dev/null
+++ b/app/vmui/packages/vmui/src/pages/ExploreAnomaly/ExploreAnomaly.tsx
@@ -0,0 +1,118 @@
+import React, { FC, useMemo, useRef } from "preact/compat";
+import classNames from "classnames";
+import useDeviceDetect from "../../hooks/useDeviceDetect";
+import useEventListener from "../../hooks/useEventListener";
+import "../CustomPanel/style.scss";
+import ExploreAnomalyHeader from "./ExploreAnomalyHeader/ExploreAnomalyHeader";
+import Alert from "../../components/Main/Alert/Alert";
+import { extractFields } from "../../utils/uplot";
+import { useFetchQuery } from "../../hooks/useFetchQuery";
+import Spinner from "../../components/Main/Spinner/Spinner";
+import GraphTab from "../CustomPanel/CustomPanelTabs/GraphTab";
+import { useGraphState } from "../../state/graph/GraphStateContext";
+import { MetricResult } from "../../api/types";
+import { promValueToNumber } from "../../utils/metric";
+import { ForecastType } from "../../types";
+import { useFetchAnomalySeries } from "./hooks/useFetchAnomalySeries";
+import { useQueryDispatch } from "../../state/query/QueryStateContext";
+import { useTimeDispatch } from "../../state/time/TimeStateContext";
+
+const ExploreAnomaly: FC = () => {
+  const { isMobile } = useDeviceDetect();
+
+  const queryDispatch = useQueryDispatch();
+  const timeDispatch = useTimeDispatch();
+  const { series, error: errorSeries, isLoading: isAnomalySeriesLoading } = useFetchAnomalySeries();
+  const queries = useMemo(() => series ? Object.keys(series) : [], [series]);
+
+  const controlsRef = useRef<HTMLDivElement>(null);
+  const { customStep } = useGraphState();
+
+  const { graphData, error, queryErrors, isHistogram, isLoading: isGraphDataLoading } = useFetchQuery({
+    visible: true,
+    customStep,
+    showAllSeries: true,
+  });
+
+  const data = useMemo(() => {
+    if (!graphData) return;
+    const group = queries.length + 1;
+    const realData = graphData.filter(d => d.group === 1);
+    const upperData = graphData.filter(d => d.group === 3);
+    const lowerData = graphData.filter(d => d.group === 4);
+    const anomalyData: MetricResult[] = realData.map((d) => ({
+      group,
+      metric: { ...d.metric, __name__: ForecastType.anomaly },
+      values: d.values.filter(([t, v]) => {
+        const id = extractFields(d.metric);
+        const upperDataByLabels = upperData.find(du => extractFields(du.metric) === id);
+        const lowerDataByLabels = lowerData.find(du => extractFields(du.metric) === id);
+        if (!upperDataByLabels || !lowerDataByLabels) return false;
+        const max = upperDataByLabels.values.find(([tMax]) => tMax === t) as [number, string];
+        const min = lowerDataByLabels.values.find(([tMin]) => tMin === t) as [number, string];
+        const num = v && promValueToNumber(v);
+        const numMin = min && promValueToNumber(min[1]);
+        const numMax = max && promValueToNumber(max[1]);
+        return num < numMin || num > numMax;
+      })
+    }));
+    return graphData.concat(anomalyData);
+  }, [graphData]);
+
+  const onChangeFilter = (expr: Record<string, string>) => {
+    const { __name__ = "", ...labelValue } = expr;
+    let prefix = __name__.replace(/y|_y/, "");
+    if (prefix) prefix += "_";
+    const metrics = [__name__, ForecastType.yhat, ForecastType.yhatUpper, ForecastType.yhatLower];
+    const filters = Object.entries(labelValue).map(([key, value]) => `${key}="${value}"`).join(",");
+    const queries = metrics.map((m, i) => `${i ? prefix : ""}${m}{${filters}}`);
+    queryDispatch({ type: "SET_QUERY", payload: queries });
+    timeDispatch({ type: "RUN_QUERY" });
+  };
+
+  const handleChangePopstate = () => window.location.reload();
+  useEventListener("popstate", handleChangePopstate);
+
+  return (
+    <div
+      className={classNames({
+        "vm-custom-panel": true,
+        "vm-custom-panel_mobile": isMobile,
+      })}
+    >
+      <ExploreAnomalyHeader
+        queries={queries}
+        series={series}
+        onChange={onChangeFilter}
+      />
+      {(isGraphDataLoading || isAnomalySeriesLoading) && <Spinner />}
+      {(error || errorSeries) && <Alert variant="error">{error || errorSeries}</Alert>}
+      {!error && !errorSeries && queryErrors?.[0] && <Alert variant="error">{queryErrors[0]}</Alert>}
+      <div
+        className={classNames({
+          "vm-custom-panel-body": true,
+          "vm-custom-panel-body_mobile": isMobile,
+          "vm-block": true,
+          "vm-block_mobile": isMobile,
+        })}
+      >
+        <div
+          className="vm-custom-panel-body-header"
+          ref={controlsRef}
+        >
+          <div/>
+        </div>
+        {data && (
+          <GraphTab
+            graphData={data}
+            isHistogram={isHistogram}
+            controlsRef={controlsRef}
+            anomalyView={true}
+          />
+        )}
+      </div>
+    </div>
+  );
+};
+
+export default ExploreAnomaly;
diff --git a/app/vmui/packages/vmui/src/pages/ExploreAnomaly/ExploreAnomalyHeader/ExploreAnomalyHeader.tsx b/app/vmui/packages/vmui/src/pages/ExploreAnomaly/ExploreAnomalyHeader/ExploreAnomalyHeader.tsx
new file mode 100644
index 000000000..b7b432672
--- /dev/null
+++ b/app/vmui/packages/vmui/src/pages/ExploreAnomaly/ExploreAnomalyHeader/ExploreAnomalyHeader.tsx
@@ -0,0 +1,112 @@
+import React, { FC, useMemo, useState } from "preact/compat";
+import classNames from "classnames";
+import useDeviceDetect from "../../../hooks/useDeviceDetect";
+import Select from "../../../components/Main/Select/Select";
+import "./style.scss";
+import usePrevious from "../../../hooks/usePrevious";
+import { useEffect } from "react";
+import { arrayEquals } from "../../../utils/array";
+import { getQueryStringValue } from "../../../utils/query-string";
+import { useSetQueryParams } from "../hooks/useSetQueryParams";
+
+type Props = {
+  queries: string[];
+  series?: Record<string, {[p: string]: string}[]>
+  onChange: (expr: Record<string, string>) => void;
+}
+
+const ExploreAnomalyHeader: FC<Props> = ({ queries, series, onChange }) => {
+  const { isMobile } = useDeviceDetect();
+  const [alias, setAlias] = useState(queries[0]);
+  const [selectedValues, setSelectedValues] = useState<Record<string, string>>({});
+  useSetQueryParams({ alias: alias, ...selectedValues });
+
+  const uniqueKeysWithValues = useMemo(() => {
+    if (!series) return {};
+    return series[alias]?.reduce((accumulator, currentSeries) => {
+      const metric = Object.entries(currentSeries);
+      if (!metric.length) return accumulator;
+      const excludeMetrics = ["__name__", "for"];
+      for (const [key, value] of metric) {
+        if (excludeMetrics.includes(key) || accumulator[key]?.includes(value)) continue;
+
+        if (!accumulator[key]) {
+          accumulator[key] = [];
+        }
+
+        accumulator[key].push(value);
+      }
+      return accumulator;
+    }, {} as Record<string, string[]>) || {};
+  }, [alias, series]);
+  const prevUniqueKeysWithValues = usePrevious(uniqueKeysWithValues);
+
+  const createHandlerChangeSelect = (key: string) => (value: string) => {
+    setSelectedValues((prev) => ({ ...prev, [key]: value }));
+  };
+
+  useEffect(() => {
+    const nextValues = Object.values(uniqueKeysWithValues).flat();
+    const prevValues = Object.values(prevUniqueKeysWithValues || {}).flat();
+    if (arrayEquals(prevValues, nextValues)) return;
+    const newSelectedValues: Record<string, string> = {};
+    Object.keys(uniqueKeysWithValues).forEach((key) => {
+      const value = getQueryStringValue(key, "") as string;
+      newSelectedValues[key] = value || uniqueKeysWithValues[key]?.[0];
+    });
+    setSelectedValues(newSelectedValues);
+  }, [uniqueKeysWithValues, prevUniqueKeysWithValues]);
+
+  useEffect(() => {
+    if (!alias || !Object.keys(selectedValues).length) return;
+    const __name__ = series?.[alias]?.[0]?.__name__ || "";
+    onChange({ ...selectedValues, for: alias, __name__ });
+  }, [selectedValues, alias]);
+
+  useEffect(() => {
+    setAlias(getQueryStringValue("alias", queries[0]) as string);
+  }, [series]);
+
+  return (
+    <div
+      id="legendAnomaly"
+      className={classNames({
+        "vm-explore-anomaly-header": true,
+        "vm-explore-anomaly-header_mobile": isMobile,
+        "vm-block": true,
+        "vm-block_mobile": isMobile,
+      })}
+    >
+      <div className="vm-explore-anomaly-header-main">
+        <div className="vm-explore-anomaly-header__select">
+          <Select
+            value={alias}
+            list={queries}
+            label="Query"
+            placeholder="Please select query"
+            onChange={setAlias}
+            searchable
+          />
+        </div>
+      </div>
+      {Object.entries(uniqueKeysWithValues).map(([key, values]) => (
+        <div
+          className="vm-explore-anomaly-header__values"
+          key={key}
+        >
+          <Select
+            value={selectedValues[key] || ""}
+            list={values}
+            label={key}
+            placeholder={`Please select ${key}`}
+            onChange={createHandlerChangeSelect(key)}
+            searchable={values.length > 2}
+            disabled={values.length === 1}
+          />
+        </div>
+      ))}
+    </div>
+  );
+};
+
+export default ExploreAnomalyHeader;
diff --git a/app/vmui/packages/vmui/src/pages/ExploreAnomaly/ExploreAnomalyHeader/style.scss b/app/vmui/packages/vmui/src/pages/ExploreAnomaly/ExploreAnomalyHeader/style.scss
new file mode 100644
index 000000000..1f8e15bd8
--- /dev/null
+++ b/app/vmui/packages/vmui/src/pages/ExploreAnomaly/ExploreAnomalyHeader/style.scss
@@ -0,0 +1,37 @@
+@use "src/styles/variables" as *;
+
+.vm-explore-anomaly-header {
+  display: flex;
+  flex-wrap: wrap;
+  align-items: center;
+  justify-content: flex-start;
+  gap: $padding-global calc($padding-small + 10px);
+  max-width: calc(100vw - var(--scrollbar-width));
+
+  &_mobile {
+    flex-direction: column;
+    align-items: stretch;
+  }
+
+  &-main {
+    display: grid;
+    gap: $padding-large;
+    align-items: center;
+    justify-items: center;
+    flex-grow: 1;
+    width: 100%;
+
+    &__config {
+      text-transform: lowercase;
+    }
+  }
+
+  &__select {
+    flex-grow: 1;
+    min-width: 100%;
+  }
+
+  &__values {
+    flex-grow: 1;
+  }
+}
diff --git a/app/vmui/packages/vmui/src/pages/ExploreAnomaly/hooks/useFetchAnomalySeries.ts b/app/vmui/packages/vmui/src/pages/ExploreAnomaly/hooks/useFetchAnomalySeries.ts
new file mode 100644
index 000000000..dbbb0d34b
--- /dev/null
+++ b/app/vmui/packages/vmui/src/pages/ExploreAnomaly/hooks/useFetchAnomalySeries.ts
@@ -0,0 +1,66 @@
+import { useMemo, useState } from "preact/compat";
+import { useAppState } from "../../../state/common/StateContext";
+import { ErrorTypes } from "../../../types";
+import { useEffect } from "react";
+import { MetricBase } from "../../../api/types";
+
+// TODO: Change the method of retrieving aliases from the configuration after the API has been added
+const seriesQuery = `{
+  for!="",
+  __name__!~".*yhat.*|.*trend.*|.*anomaly_score.*|.*daily.*|.*additive_terms.*|.*multiplicative_terms.*|.*weekly.*"
+}`;
+
+export const useFetchAnomalySeries = () => {
+  const { serverUrl } = useAppState();
+
+  const [series, setSeries] = useState<Record<string, MetricBase["metric"][]>>();
+  const [isLoading, setIsLoading] = useState(false);
+  const [error, setError] = useState<ErrorTypes | string>();
+
+  const fetchUrl = useMemo(() => {
+    const params = new URLSearchParams({
+      "match[]": seriesQuery,
+    });
+
+    return `${serverUrl}/api/v1/series?${params}`;
+  }, [serverUrl]);
+
+  useEffect(() => {
+    const fetchSeries = async () => {
+      setError("");
+      setIsLoading(true);
+      try {
+        const response = await fetch(fetchUrl);
+        const resp = await response.json();
+        const data = (resp?.data || []) as MetricBase["metric"][];
+        const groupedByFor = data.reduce<{ [key: string]: MetricBase["metric"][] }>((acc, item) => {
+          const forKey = item["for"];
+          if (!acc[forKey]) acc[forKey] = [];
+          acc[forKey].push(item);
+          return acc;
+        }, {});
+        setSeries(groupedByFor);
+
+        if (!response.ok) {
+          const errorType = resp.errorType ? `${resp.errorType}\r\n` : "";
+          setError(`${errorType}${resp?.error || resp?.message}`);
+        }
+      } catch (e) {
+        if (e instanceof Error && e.name !== "AbortError") {
+          const message = e.name === "SyntaxError" ? ErrorTypes.unknownType : `${e.name}: ${e.message}`;
+          setError(`${message}`);
+        }
+      } finally {
+        setIsLoading(false);
+      }
+    };
+
+    fetchSeries();
+  }, [fetchUrl]);
+
+  return {
+    error,
+    series,
+    isLoading,
+  };
+};
diff --git a/app/vmui/packages/vmui/src/pages/ExploreAnomaly/hooks/useSetQueryParams.ts b/app/vmui/packages/vmui/src/pages/ExploreAnomaly/hooks/useSetQueryParams.ts
new file mode 100644
index 000000000..b2ad643e1
--- /dev/null
+++ b/app/vmui/packages/vmui/src/pages/ExploreAnomaly/hooks/useSetQueryParams.ts
@@ -0,0 +1,31 @@
+import { useEffect } from "react";
+import { compactObject } from "../../../utils/object";
+import { useTimeState } from "../../../state/time/TimeStateContext";
+import { useGraphState } from "../../../state/graph/GraphStateContext";
+import useSearchParamsFromObject from "../../../hooks/useSearchParamsFromObject";
+
+interface stateParams extends Record<string, string> {
+  alias: string;
+}
+
+export const useSetQueryParams = ({ alias, ...args }: stateParams) => {
+  const { duration, relativeTime, period: { date } } = useTimeState();
+  const { customStep } = useGraphState();
+  const { setSearchParamsFromKeys } = useSearchParamsFromObject();
+
+  const setSearchParamsFromState = () => {
+    const params = compactObject({
+      ["g0.range_input"]: duration,
+      ["g0.end_input"]: date,
+      ["g0.step_input"]: customStep,
+      ["g0.relative_time"]: relativeTime,
+      alias,
+      ...args,
+    });
+
+    setSearchParamsFromKeys(params);
+  };
+
+  useEffect(setSearchParamsFromState, [duration, relativeTime, date, customStep, alias, args]);
+  useEffect(setSearchParamsFromState, []);
+};
diff --git a/app/vmui/packages/vmui/src/pages/PredefinedPanels/PredefinedPanel/PredefinedPanel.tsx b/app/vmui/packages/vmui/src/pages/PredefinedPanels/PredefinedPanel/PredefinedPanel.tsx
index 8ad0d4f0d..e5ba735b8 100644
--- a/app/vmui/packages/vmui/src/pages/PredefinedPanels/PredefinedPanel/PredefinedPanel.tsx
+++ b/app/vmui/packages/vmui/src/pages/PredefinedPanels/PredefinedPanel/PredefinedPanel.tsx
@@ -1,5 +1,5 @@
 import React, { FC, useEffect, useMemo, useRef, useState } from "preact/compat";
-import { PanelSettings } from "../../../types";
+import { DisplayType, PanelSettings } from "../../../types";
 import { AxisRange, YaxisState } from "../../../state/graph/reducer";
 import GraphView from "../../../components/Views/GraphView/GraphView";
 import { useFetchQuery } from "../../../hooks/useFetchQuery";
@@ -45,7 +45,7 @@ const PredefinedPanel: FC<PredefinedPanelsProps> = ({
 
   const { isLoading, graphData, error, warning } = useFetchQuery({
     predefinedQuery: validExpr ? expr : [],
-    display: "chart",
+    display: DisplayType.chart,
     visible,
     customStep,
   });
diff --git a/app/vmui/packages/vmui/src/pages/PredefinedPanels/hooks/useFetchDashboards.ts b/app/vmui/packages/vmui/src/pages/PredefinedPanels/hooks/useFetchDashboards.ts
index bffcc6163..94b7dee6a 100755
--- a/app/vmui/packages/vmui/src/pages/PredefinedPanels/hooks/useFetchDashboards.ts
+++ b/app/vmui/packages/vmui/src/pages/PredefinedPanels/hooks/useFetchDashboards.ts
@@ -15,7 +15,6 @@ export const useFetchDashboards = (): {
   error?: ErrorTypes | string,
   dashboardsSettings: DashboardSettings[],
 } => {
-  const { REACT_APP_LOGS } = process.env;
   const appModeEnable = getAppModeEnable();
   const { serverUrl } = useAppState();
   const dispatch = useDashboardsDispatch();
@@ -35,7 +34,7 @@ export const useFetchDashboards = (): {
   };
 
   const fetchRemoteDashboards = async () => {
-    if (!serverUrl || REACT_APP_LOGS) return;
+    if (!serverUrl || process.env.REACT_APP_TYPE) return;
     setError("");
     setIsLoading(true);
 
diff --git a/app/vmui/packages/vmui/src/router/index.ts b/app/vmui/packages/vmui/src/router/index.ts
index 4af8accd7..517c45aa0 100644
--- a/app/vmui/packages/vmui/src/router/index.ts
+++ b/app/vmui/packages/vmui/src/router/index.ts
@@ -1,3 +1,5 @@
+import { AppType } from "../types/appType";
+
 const router = {
   home: "/",
   metrics: "/metrics",
@@ -9,7 +11,9 @@ const router = {
   relabel: "/relabeling",
   logs: "/logs",
   activeQueries: "/active-queries",
-  icons: "/icons"
+  icons: "/icons",
+  anomaly: "/anomaly",
+  query: "/query",
 };
 
 export interface RouterOptionsHeader {
@@ -26,14 +30,15 @@ export interface RouterOptions {
   header: RouterOptionsHeader
 }
 
-const { REACT_APP_LOGS } = process.env;
+const { REACT_APP_TYPE } = process.env;
+const isLogsApp = REACT_APP_TYPE === AppType.logs;
 
 const routerOptionsDefault = {
   header: {
     tenant: true,
-    stepControl: !REACT_APP_LOGS,
-    timeSelector: !REACT_APP_LOGS,
-    executionControls: !REACT_APP_LOGS,
+    stepControl: !isLogsApp,
+    timeSelector: !isLogsApp,
+    executionControls: !isLogsApp,
   }
 };
 
@@ -90,6 +95,14 @@ export const routerOptions: {[key: string]: RouterOptions} = {
   [router.icons]: {
     title: "Icons",
     header: {}
+  },
+  [router.anomaly]: {
+    title: "Anomaly exploration",
+    ...routerOptionsDefault
+  },
+  [router.query]: {
+    title: "Query",
+    ...routerOptionsDefault
   }
 };
 
diff --git a/app/vmui/packages/vmui/src/state/customPanel/reducer.ts b/app/vmui/packages/vmui/src/state/customPanel/reducer.ts
index b9854d626..b80aaeaa3 100644
--- a/app/vmui/packages/vmui/src/state/customPanel/reducer.ts
+++ b/app/vmui/packages/vmui/src/state/customPanel/reducer.ts
@@ -1,7 +1,7 @@
-import { DisplayType, displayTypeTabs } from "../../pages/CustomPanel/DisplayTypeSwitch";
+import { displayTypeTabs } from "../../pages/CustomPanel/DisplayTypeSwitch";
 import { getQueryStringValue } from "../../utils/query-string";
 import { getFromStorage, saveToStorage } from "../../utils/storage";
-import { SeriesLimits } from "../../types";
+import { DisplayType, SeriesLimits } from "../../types";
 import { DEFAULT_MAX_SERIES } from "../../constants/graph";
 
 export interface CustomPanelState {
@@ -24,7 +24,7 @@ const displayType = displayTypeTabs.find(t => t.prometheusCode === +queryTab ||
 const limitsStorage = getFromStorage("SERIES_LIMITS") as string;
 
 export const initialCustomPanelState: CustomPanelState = {
-  displayType: (displayType?.value || "chart") as DisplayType,
+  displayType: (displayType?.value || DisplayType.chart),
   nocache: false,
   isTracingEnabled: false,
   seriesLimits: limitsStorage ? JSON.parse(limitsStorage) : DEFAULT_MAX_SERIES,
diff --git a/app/vmui/packages/vmui/src/types/appType.ts b/app/vmui/packages/vmui/src/types/appType.ts
new file mode 100644
index 000000000..0679afd86
--- /dev/null
+++ b/app/vmui/packages/vmui/src/types/appType.ts
@@ -0,0 +1,4 @@
+export enum AppType {
+  logs = "logs",
+  anomaly = "anomaly",
+}
diff --git a/app/vmui/packages/vmui/src/types/index.ts b/app/vmui/packages/vmui/src/types/index.ts
index f4bd75873..6d71c6b20 100644
--- a/app/vmui/packages/vmui/src/types/index.ts
+++ b/app/vmui/packages/vmui/src/types/index.ts
@@ -7,7 +7,11 @@ declare global {
   }
 }
 
-export type DisplayType = "table" | "chart" | "code";
+export enum DisplayType {
+  table = "table",
+  chart = "chart",
+  code = "code",
+}
 
 export interface TimeParams {
   start: number; // timestamp in seconds
diff --git a/app/vmui/packages/vmui/src/types/uplot.ts b/app/vmui/packages/vmui/src/types/uplot.ts
index f027c4c51..e86417af0 100644
--- a/app/vmui/packages/vmui/src/types/uplot.ts
+++ b/app/vmui/packages/vmui/src/types/uplot.ts
@@ -1,5 +1,14 @@
 import { Axis, Series } from "uplot";
 
+export enum ForecastType {
+    yhat = "yhat",
+    yhatUpper = "yhat_upper",
+    yhatLower = "yhat_lower",
+    anomaly = "vmui_anomalies_points",
+    training = "vmui_training_data",
+    actual = "actual"
+}
+
 export interface SeriesItemStatsFormatted {
     min: string,
     max: string,
@@ -10,7 +19,9 @@ export interface SeriesItemStatsFormatted {
 export interface SeriesItem extends Series {
     freeFormFields: {[key: string]: string};
     statsFormatted: SeriesItemStatsFormatted;
-    median: number
+    median: number;
+    forecast?: ForecastType | null;
+    forecastGroup?: string;
 }
 
 export interface HideSeriesArgs {
diff --git a/app/vmui/packages/vmui/src/utils/color.ts b/app/vmui/packages/vmui/src/utils/color.ts
index 03776d7e0..9f351297b 100644
--- a/app/vmui/packages/vmui/src/utils/color.ts
+++ b/app/vmui/packages/vmui/src/utils/color.ts
@@ -1,4 +1,4 @@
-import { ArrayRGB } from "../types";
+import { ArrayRGB, ForecastType } from "../types";
 
 export const baseContrastColors = [
   "#e54040",
@@ -13,6 +13,23 @@ export const baseContrastColors = [
   "#a44e0c",
 ];
 
+export const hexToRGB = (hex: string): string => {
+  if (hex.length != 7) return "0, 0, 0";
+  const r = parseInt(hex.slice(1, 3), 16);
+  const g = parseInt(hex.slice(3, 5), 16);
+  const b = parseInt(hex.slice(5, 7), 16);
+  return `${r}, ${g}, ${b}`;
+};
+
+export const anomalyColors: Record<ForecastType, string> = {
+  [ForecastType.yhatUpper]: "#7126a1",
+  [ForecastType.yhatLower]: "#7126a1",
+  [ForecastType.yhat]: "#da42a6",
+  [ForecastType.anomaly]: "#da4242",
+  [ForecastType.actual]: "#203ea9",
+  [ForecastType.training]: `rgba(${hexToRGB("#203ea9")}, 0.2)`,
+};
+
 export const getColorFromString = (text: string): string => {
   const SEED = 16777215;
   const FACTOR = 49979693;
@@ -34,14 +51,6 @@ export const getColorFromString = (text: string): string => {
   return `#${hex}`;
 };
 
-export const hexToRGB = (hex: string): string => {
-  if (hex.length != 7) return "0, 0, 0";
-  const r = parseInt(hex.slice(1, 3), 16);
-  const g = parseInt(hex.slice(3, 5), 16);
-  const b = parseInt(hex.slice(5, 7), 16);
-  return `${r}, ${g}, ${b}`;
-};
-
 export const getContrastColor = (value: string) => {
   let hex = value.replace("#", "").trim();
 
@@ -55,7 +64,7 @@ export const getContrastColor = (value: string) => {
   const r = parseInt(hex.slice(0, 2), 16);
   const g = parseInt(hex.slice(2, 4), 16);
   const b = parseInt(hex.slice(4, 6), 16);
-  const yiq = ((r*299)+(g*587)+(b*114))/1000;
+  const yiq = ((r * 299) + (g * 587) + (b * 114)) / 1000;
   return yiq >= 128 ? "#000000" : "#FFFFFF";
 };
 
@@ -66,7 +75,7 @@ export const generateGradient = (start: ArrayRGB, end: ArrayRGB, steps: number)
     const r = start[0] + (end[0] - start[0]) * k;
     const g = start[1] + (end[1] - start[1]) * k;
     const b = start[2] + (end[2] - start[2]) * k;
-    gradient.push([r,g,b].map(n => Math.round(n)).join(", "));
+    gradient.push([r, g, b].map(n => Math.round(n)).join(", "));
   }
   return gradient.map(c => `rgb(${c})`);
 };
diff --git a/app/vmui/packages/vmui/src/utils/default-server-url.ts b/app/vmui/packages/vmui/src/utils/default-server-url.ts
index a62b07f4d..8fd036803 100644
--- a/app/vmui/packages/vmui/src/utils/default-server-url.ts
+++ b/app/vmui/packages/vmui/src/utils/default-server-url.ts
@@ -1,12 +1,16 @@
 import { getAppModeParams } from "./app-mode";
 import { replaceTenantId } from "./tenants";
-const { REACT_APP_LOGS } = process.env;
+import { AppType } from "../types/appType";
+import { getFromStorage } from "./storage";
+const { REACT_APP_TYPE } = process.env;
 
 export const getDefaultServer = (tenantId?: string): string => {
   const { serverURL } = getAppModeParams();
+  const storageURL = getFromStorage("SERVER_URL") as string;
   const logsURL = window.location.href.replace(/\/(select\/)?(vmui)\/.*/, "");
-  const url = serverURL || window.location.href.replace(/\/(?:prometheus\/)?(?:graph|vmui)\/.*/, "/prometheus");
-  if (REACT_APP_LOGS) return logsURL;
+  const defaultURL = window.location.href.replace(/\/(?:prometheus\/)?(?:graph|vmui)\/.*/, "/prometheus");
+  const url = serverURL || storageURL || defaultURL;
+  if (REACT_APP_TYPE === AppType.logs) return logsURL;
   if (tenantId) return replaceTenantId(url, tenantId);
   return url;
 };
diff --git a/app/vmui/packages/vmui/src/utils/storage.ts b/app/vmui/packages/vmui/src/utils/storage.ts
index 844ac5b75..17f9c991f 100644
--- a/app/vmui/packages/vmui/src/utils/storage.ts
+++ b/app/vmui/packages/vmui/src/utils/storage.ts
@@ -9,6 +9,7 @@ export type StorageKeys = "AUTOCOMPLETE"
     | "EXPLORE_METRICS_TIPS"
     | "QUERY_HISTORY"
     | "QUERY_FAVORITES"
+    | "SERVER_URL"
 
 export const saveToStorage = (key: StorageKeys, value: string | boolean | Record<string, unknown>): void => {
   if (value) {
diff --git a/app/vmui/packages/vmui/src/utils/uplot/bands.ts b/app/vmui/packages/vmui/src/utils/uplot/bands.ts
new file mode 100644
index 000000000..510582991
--- /dev/null
+++ b/app/vmui/packages/vmui/src/utils/uplot/bands.ts
@@ -0,0 +1,41 @@
+import uPlot, { Series as uPlotSeries } from "uplot";
+import { ForecastType, SeriesItem } from "../../types";
+import { anomalyColors, hexToRGB } from "../color";
+
+export const setBand = (plot: uPlot, series: uPlotSeries[]) => {
+  // First, remove any existing bands
+  plot.delBand();
+
+  // If there aren't at least two series, we can't create a band
+  if (series.length < 2) return;
+
+  // Cast and enrich each series item with its index
+  const seriesItems = (series as SeriesItem[]).map((s, index) => ({ ...s, index }));
+
+  const upperSeries = seriesItems.filter(s => s.forecast === ForecastType.yhatUpper);
+  const lowerSeries = seriesItems.filter(s => s.forecast === ForecastType.yhatLower);
+
+  // Create bands by matching upper and lower series based on their freeFormFields
+  const bands = upperSeries.map((upper) => {
+    const correspondingLower = lowerSeries.find(lower => lower.forecastGroup === upper.forecastGroup);
+    if (!correspondingLower) return null;
+    return {
+      series: [upper.index, correspondingLower.index] as [number, number],
+      fill: createBandFill(ForecastType.yhatUpper),
+    };
+  }).filter(band => band !== null) as uPlot.Band[]; // Filter out any nulls from failed matches
+
+  // If there are no bands to add, exit the function
+  if (!bands.length) return;
+
+  // Add each band to the plot
+  bands.forEach(band => {
+    plot.addBand(band);
+  });
+};
+
+// Helper function to create the fill color for a band
+function createBandFill(forecastType: ForecastType): string {
+  const rgb = hexToRGB(anomalyColors[forecastType]);
+  return `rgba(${rgb}, 0.05)`;
+}
diff --git a/app/vmui/packages/vmui/src/utils/uplot/index.ts b/app/vmui/packages/vmui/src/utils/uplot/index.ts
index 47f727870..6e26fe6b4 100644
--- a/app/vmui/packages/vmui/src/utils/uplot/index.ts
+++ b/app/vmui/packages/vmui/src/utils/uplot/index.ts
@@ -5,3 +5,4 @@ export * from "./hooks";
 export * from "./instnance";
 export * from "./scales";
 export * from "./series";
+export * from "./bands";
diff --git a/app/vmui/packages/vmui/src/utils/uplot/scales.ts b/app/vmui/packages/vmui/src/utils/uplot/scales.ts
index eb0d4c024..b3ab36992 100644
--- a/app/vmui/packages/vmui/src/utils/uplot/scales.ts
+++ b/app/vmui/packages/vmui/src/utils/uplot/scales.ts
@@ -1,7 +1,8 @@
 import uPlot, { Range, Scale, Scales } from "uplot";
 import { getMinMaxBuffer } from "./axes";
 import { YaxisState } from "../../state/graph/reducer";
-import { MinMax, SetMinMax } from "../../types";
+import { ForecastType, MinMax, SetMinMax } from "../../types";
+import { anomalyColors } from "../color";
 
 export const getRangeX = ({ min, max }: MinMax): Range.MinMax => [min, max];
 
@@ -24,3 +25,80 @@ export const setSelect = (setPlotScale: SetMinMax) => (u: uPlot) => {
   const max = u.posToVal(u.select.left + u.select.width, "x");
   setPlotScale({ min, max });
 };
+
+export const scaleGradient = (
+  scaleKey: string,
+  ori: number,
+  scaleStops: [number, string][],
+  discrete = false
+) => (u: uPlot): CanvasGradient | string => {
+  const can = document.createElement("canvas");
+  const ctx = can.getContext("2d");
+  if (!ctx) return "";
+
+  const scale = u.scales[scaleKey];
+
+  // we want the stop below or at the scaleMax
+  // and the stop below or at the scaleMin, else the stop above scaleMin
+  let minStopIdx = 0;
+  let maxStopIdx = 1;
+
+  for (let i = 0; i < scaleStops.length; i++) {
+    const stopVal = scaleStops[i][0];
+
+    if (stopVal <= (scale.min || 0) || minStopIdx == null)
+      minStopIdx = i;
+
+    maxStopIdx = i;
+
+    if (stopVal >= (scale.max || 1))
+      break;
+  }
+
+  if (minStopIdx == maxStopIdx)
+    return scaleStops[minStopIdx][1];
+
+  let minStopVal = scaleStops[minStopIdx][0];
+  let maxStopVal = scaleStops[maxStopIdx][0];
+
+  if (minStopVal == -Infinity)
+    minStopVal = scale.min || 0;
+
+  if (maxStopVal == Infinity)
+    maxStopVal = scale.max || 1;
+
+  const minStopPos = u.valToPos(minStopVal, scaleKey, true) || 0;
+  const maxStopPos = u.valToPos(maxStopVal, scaleKey, true) || 1;
+
+  const range = minStopPos - maxStopPos;
+
+  let x0, y0, x1, y1;
+
+  if (ori == 1) {
+    x0 = x1 = 0;
+    y0 = minStopPos;
+    y1 = maxStopPos;
+  } else {
+    y0 = y1 = 0;
+    x0 = minStopPos;
+    x1 = maxStopPos;
+  }
+
+  const grd = ctx.createLinearGradient(x0, y0, x1, y1);
+
+  let prevColor = anomalyColors[ForecastType.actual];
+
+  for (let i = minStopIdx; i <= maxStopIdx; i++) {
+    const s = scaleStops[i];
+
+    const stopPos = i == minStopIdx ? minStopPos : i == maxStopIdx ? maxStopPos : u.valToPos(s[0], scaleKey, true) | 1;
+    const pct = Math.min(1, Math.max(0, (minStopPos - stopPos) / range));
+    if (discrete && i > minStopIdx) {
+      grd.addColorStop(pct, prevColor);
+    }
+
+    grd.addColorStop(pct, prevColor = s[1]);
+  }
+
+  return grd;
+};
diff --git a/app/vmui/packages/vmui/src/utils/uplot/series.ts b/app/vmui/packages/vmui/src/utils/uplot/series.ts
index 0a04e1d3e..50a630e07 100644
--- a/app/vmui/packages/vmui/src/utils/uplot/series.ts
+++ b/app/vmui/packages/vmui/src/utils/uplot/series.ts
@@ -1,45 +1,103 @@
-import { MetricResult } from "../../api/types";
+import { MetricBase, MetricResult } from "../../api/types";
 import uPlot, { Series as uPlotSeries } from "uplot";
 import { getNameForMetric, promValueToNumber } from "../metric";
-import { HideSeriesArgs, BarSeriesItem, Disp, Fill, LegendItemType, Stroke, SeriesItem } from "../../types";
-import { baseContrastColors, getColorFromString } from "../color";
-import { getMedianFromArray, getMaxFromArray, getMinFromArray, getLastFromArray } from "../math";
+import { BarSeriesItem, Disp, Fill, ForecastType, HideSeriesArgs, LegendItemType, SeriesItem, Stroke } from "../../types";
+import { anomalyColors, baseContrastColors, getColorFromString } from "../color";
+import { getLastFromArray, getMaxFromArray, getMedianFromArray, getMinFromArray } from "../math";
 import { formatPrettyNumber } from "./helpers";
 
-export const getSeriesItemContext = (data: MetricResult[], hideSeries: string[], alias: string[]) => {
+// Helper function to extract freeFormFields values as a comma-separated string
+export const extractFields = (metric: MetricBase["metric"]): string => {
+  const excludeMetrics = ["__name__", "for"];
+  return Object.entries(metric)
+    .filter(([key]) => !excludeMetrics.includes(key))
+    .map(([key, value]) => `${key}: ${value}`).join(",");
+};
+
+const isForecast = (metric: MetricBase["metric"]) => {
+  const metricName = metric?.__name__ || "";
+  const forecastRegex = new RegExp(`(${Object.values(ForecastType).join("|")})$`);
+  const match = metricName.match(forecastRegex);
+  const value = match && match[0] as ForecastType;
+  return {
+    value,
+    isUpper: value === ForecastType.yhatUpper,
+    isLower: value === ForecastType.yhatLower,
+    isYhat: value === ForecastType.yhat,
+    isAnomaly: value === ForecastType.anomaly,
+    group: extractFields(metric)
+  };
+};
+
+export const getSeriesItemContext = (data: MetricResult[], hideSeries: string[], alias: string[], isAnomaly?: boolean) => {
   const colorState: {[key: string]: string} = {};
-  const stats = data.map(d => {
+  const maxColors = isAnomaly ? 0 : Math.min(data.length, baseContrastColors.length);
+
+  for (let i = 0; i < maxColors; i++) {
+    const label = getNameForMetric(data[i], alias[data[i].group - 1]);
+    colorState[label] = baseContrastColors[i];
+  }
+
+  return (d: MetricResult, i: number): SeriesItem => {
+    const forecast = isForecast(data[i].metric);
+    const label = isAnomaly ? forecast.group : getNameForMetric(d, alias[d.group - 1]);
+
     const values = d.values.map(v => promValueToNumber(v[1]));
-    return {
+    const { min, max, median, last } = {
       min: getMinFromArray(values),
       max: getMaxFromArray(values),
       median: getMedianFromArray(values),
       last: getLastFromArray(values),
     };
-  });
 
-  const maxColors = Math.min(data.length, baseContrastColors.length);
-  for (let i = 0; i < maxColors; i++) {
-    const label = getNameForMetric(data[i], alias[data[i].group - 1]);
-    colorState[label] = baseContrastColors[i];
-  }
+    let dash: number[] = [];
+    if (forecast.isLower || forecast.isUpper) {
+      dash = [10, 5];
+    } else if (forecast.isYhat) {
+      dash = [10, 2];
+    }
 
-  return (d: MetricResult, i: number): SeriesItem => {
-    const label = getNameForMetric(d, alias[d.group - 1]);
-    const color = colorState[label] || getColorFromString(label);
-    const { min, max, median, last } = stats[i];
+    let width = 1.4;
+    if (forecast.isUpper || forecast.isLower) {
+      width = 0.7;
+    } else if (forecast.isYhat) {
+      width = 1;
+    } else if (forecast.isAnomaly) {
+      width = 0;
+    }
+
+    let points: uPlotSeries.Points = { size: 4.2, width: 1.4 };
+    if (forecast.isAnomaly) {
+      points = { size: 8, width: 4, space: 0 };
+    }
+
+    let stroke: uPlotSeries.Stroke = colorState[label] || getColorFromString(label);
+    if (isAnomaly && forecast.isAnomaly) {
+      stroke = anomalyColors[ForecastType.anomaly];
+    } else if (isAnomaly && !forecast.isAnomaly && !forecast.value) {
+      // TODO add stroke for training data
+      // const hzGrad: [number, string][] = [
+      //   [time, anomalyColors[ForecastType.actual]],
+      //   [time, anomalyColors[ForecastType.training]],
+      //   [time, anomalyColors[ForecastType.actual]],
+      // ];
+      // stroke = scaleGradient("x", 0, hzGrad, true);
+      stroke = anomalyColors[ForecastType.actual];
+    } else if (forecast.value) {
+      stroke = forecast.value ? anomalyColors[forecast.value] : stroke;
+    }
 
     return {
       label,
+      dash,
+      width,
+      stroke,
+      points,
+      forecast: forecast.value,
+      forecastGroup: forecast.group,
       freeFormFields: d.metric,
-      width: 1.4,
-      stroke: color,
       show: !includesHideSeries(label, hideSeries),
       scale: "1",
-      points: {
-        size: 4.2,
-        width: 1.4
-      },
       statsFormatted: {
         min: formatPrettyNumber(min, min, max),
         max: formatPrettyNumber(max, min, max),
diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md
index 16ad0013f..ae4d4d648 100644
--- a/docs/CHANGELOG.md
+++ b/docs/CHANGELOG.md
@@ -28,16 +28,19 @@ The sandbox cluster installation is running under the constant load generated by
 
 ## tip
 
+* FEATURE: [vmagent](https://docs.victoriametrics.com/vmagent.html): expose ability to set additional endpoint parameters when requesting an OAuth2 token via a the flag `remoteWrite.oauth2.endpointParams`. See [these docs](https://docs.victoriametrics.com/vmagent.html#advanced-usage).
 * FEATURE: [vmauth](https://docs.victoriametrics.com/vmauth.html): add ability to proxy incoming requests to different backends based on the requested host via `src_hosts` option at `url_map`. See [these docs](https://docs.victoriametrics.com/vmauth.html#generic-http-proxy-for-different-backends).
 * FEATURE: [vmctl](https://docs.victoriametrics.com/vmctl.html): rename cmd-line flag `vm-native-disable-retries` to `vm-native-disable-per-metric-migration` to better reflect its meaning.
 * FEATURE: all VictoriaMetrics components: add ability to specify arbitrary HTTP headers to send with every request to `-pushmetrics.url`. See [`push metrics` docs](https://docs.victoriametrics.com/#push-metrics).
 * FEATURE: all VictoriaMetrics components: add `-metrics.exposeMetadata` command-line flag, which allows displaying `TYPE` and `HELP` metadata at `/metrics` page exposed at `-httpListenAddr`. This may be needed when the `/metrics` page is scraped by collector, which requires the `TYPE` and `HELP` metadata such as [Google Cloud Managed Prometheus](https://cloud.google.com/stackdriver/docs/managed-prometheus/troubleshooting#missing-metric-type).
 
+* BUGFIX: [VictoriaMetrics cluster](https://docs.victoriametrics.com/Cluster-VictoriaMetrics.html): properly return full results when `-search.skipSlowReplicas` command-line flag is passed to `vmselect` and when [vmstorage groups](https://docs.victoriametrics.com/Cluster-VictoriaMetrics.html#vmstorage-groups-at-vmselect) are in use. Previously partial results could be returned in this case.
 * BUGFIX: `vminsert`: properly accept samples via [OpenTelemetry data ingestion protocol](https://docs.victoriametrics.com/#sending-data-via-opentelemetry) when these samples have no [resource attributes](https://opentelemetry.io/docs/instrumentation/go/resources/). Previously such samples were silently skipped.
 * BUGFIX: `vmstorage`: added missing `-inmemoryDataFlushInterval` command-line flag, which was missing in [VictoriaMetrics cluster](https://docs.victoriametrics.com/Cluster-VictoriaMetrics.html) after implementing [this feature](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/3337) in [v1.85.0](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v1.85.0).
 * BUGFIX: [vmalert](https://docs.victoriametrics.com/vmalert.html): check `-external.url` schema when starting vmalert, must be `http` or `https`. Before, alertmanager could reject alert notifications if `-external.url` contained no or wrong schema.
 * BUGFIX: [MetricsQL](https://docs.victoriametrics.com/MetricsQL.html): properly handle queries, which wrap [rollup functions](https://docs.victoriametrics.com/MetricsQL.html#rollup-functions) with multiple arguments without explicitly specified lookbehind window in square brackets into [aggregate functions](https://docs.victoriametrics.com/MetricsQL.html#aggregate-functions). For example, `sum(quantile_over_time(0.5, process_resident_memory_bytes))` was resulting to `expecting at least 2 args to ...; got 1 args` error. Thanks to @atykhyy for [the pull request](https://github.com/VictoriaMetrics/VictoriaMetrics/pull/5414).
 * BUGFIX: [vmctl](https://docs.victoriametrics.com/vmctl.html): retry on import errors in `vm-native` mode. Before, retries happened only on writes into a network connection between source and destination. But errors returned by server after all the data was transmitted were logged, but not retried.
+* BUGFIX: [vmagent](https://docs.victoriametrics.com/vmagent.html): properly assume role with [AWS IRSA authorization](https://docs.aws.amazon.com/eks/latest/userguide/iam-roles-for-service-accounts.html). Previously role chaining was not supported. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/3822) for details.
 
 ## [v1.96.0](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v1.96.0)
 
diff --git a/docs/Cluster-VictoriaMetrics.md b/docs/Cluster-VictoriaMetrics.md
index a15552702..83fd78292 100644
--- a/docs/Cluster-VictoriaMetrics.md
+++ b/docs/Cluster-VictoriaMetrics.md
@@ -1072,7 +1072,7 @@ Below is the output for `/path/to/vminsert -help`:
   -loggerWarnsPerSecondLimit int
      Per-second limit on the number of WARN messages. If more than the given number of warns are emitted per second, then the remaining warns are suppressed. Zero values disable the rate limit
   -maxConcurrentInserts int
-     The maximum number of concurrent insert requests. Default value should work for most cases, since it minimizes the memory usage. The default value can be increased when clients send data over slow networks. See also -insert.maxQueueDuration (default 32)
+     The maximum number of concurrent insert requests. Default value should work for most cases, since it minimizes the memory usage. The default value can be increased when clients send data over slow networks. See also -insert.maxQueueDuration.
   -maxInsertRequestSize size
      The maximum size in bytes of a single Prometheus remote_write API request
      Supports the following optional suffixes for size values: KB, MB, GB, TB, KiB, MiB, GiB, TiB (default 33554432)
@@ -1552,7 +1552,7 @@ Below is the output for `/path/to/vmstorage -help`:
   -loggerWarnsPerSecondLimit int
      Per-second limit on the number of WARN messages. If more than the given number of warns are emitted per second, then the remaining warns are suppressed. Zero values disable the rate limit
   -maxConcurrentInserts int
-     The maximum number of concurrent insert requests. Default value should work for most cases, since it minimizes the memory usage. The default value can be increased when clients send data over slow networks. See also -insert.maxQueueDuration (default 32)
+     The maximum number of concurrent insert requests. Default value should work for most cases, since it minimizes the memory usage. The default value can be increased when clients send data over slow networks. See also -insert.maxQueueDuration.
   -memory.allowedBytes size
      Allowed size of system memory VictoriaMetrics caches may occupy. This option overrides -memory.allowedPercent if set to a non-zero value. Too low a value may increase the cache miss rate usually resulting in higher CPU and disk IO usage. Too high a value may evict too much data from the OS page cache resulting in higher disk IO usage
      Supports the following optional suffixes for size values: KB, MB, GB, TB, KiB, MiB, GiB, TiB (default 0)
diff --git a/docs/README.md b/docs/README.md
index 9fe49efe8..0a48307a5 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -2712,7 +2712,7 @@ Pass `-help` to VictoriaMetrics in order to see the list of supported command-li
   -loggerWarnsPerSecondLimit int
      Per-second limit on the number of WARN messages. If more than the given number of warns are emitted per second, then the remaining warns are suppressed. Zero values disable the rate limit
   -maxConcurrentInserts int
-     The maximum number of concurrent insert requests. Default value should work for most cases, since it minimizes the memory usage. The default value can be increased when clients send data over slow networks. See also -insert.maxQueueDuration (default 32)
+     The maximum number of concurrent insert requests. Default value should work for most cases, since it minimizes the memory usage. The default value can be increased when clients send data over slow networks. See also -insert.maxQueueDuration.
   -maxInsertRequestSize size
      The maximum size in bytes of a single Prometheus remote_write API request
      Supports the following optional suffixes for size values: KB, MB, GB, TB, KiB, MiB, GiB, TiB (default 33554432)
diff --git a/docs/Single-server-VictoriaMetrics.md b/docs/Single-server-VictoriaMetrics.md
index c27999349..fed6433f4 100644
--- a/docs/Single-server-VictoriaMetrics.md
+++ b/docs/Single-server-VictoriaMetrics.md
@@ -2720,7 +2720,7 @@ Pass `-help` to VictoriaMetrics in order to see the list of supported command-li
   -loggerWarnsPerSecondLimit int
      Per-second limit on the number of WARN messages. If more than the given number of warns are emitted per second, then the remaining warns are suppressed. Zero values disable the rate limit
   -maxConcurrentInserts int
-     The maximum number of concurrent insert requests. Default value should work for most cases, since it minimizes the memory usage. The default value can be increased when clients send data over slow networks. See also -insert.maxQueueDuration (default 32)
+     The maximum number of concurrent insert requests. Default value should work for most cases, since it minimizes the memory usage. The default value can be increased when clients send data over slow networks. See also -insert.maxQueueDuration.
   -maxInsertRequestSize size
      The maximum size in bytes of a single Prometheus remote_write API request
      Supports the following optional suffixes for size values: KB, MB, GB, TB, KiB, MiB, GiB, TiB (default 33554432)
diff --git a/docs/VictoriaLogs/README.md b/docs/VictoriaLogs/README.md
index a9d674941..ef7dea0cf 100644
--- a/docs/VictoriaLogs/README.md
+++ b/docs/VictoriaLogs/README.md
@@ -218,7 +218,7 @@ Pass `-help` to VictoriaLogs in order to see the list of supported command-line
   -loggerWarnsPerSecondLimit int
     	Per-second limit on the number of WARN messages. If more than the given number of warns are emitted per second, then the remaining warns are suppressed. Zero values disable the rate limit
   -maxConcurrentInserts int
-    	The maximum number of concurrent insert requests. The default value should work for most cases, since it minimizes memory usage. The default value can be increased when clients send data over slow networks. See also -insert.maxQueueDuration (default 12)
+    	The maximum number of concurrent insert requests. The default value should work for most cases, since it minimizes memory usage. The default value can be increased when clients send data over slow networks. See also -insert.maxQueueDuration.
   -memory.allowedBytes size
     	Allowed size of system memory VictoriaMetrics caches may occupy. This option overrides -memory.allowedPercent if set to a non-zero value. Too low a value may increase the cache miss rate usually resulting in higher CPU and disk IO usage. Too high a value may evict too much data from the OS page cache resulting in higher disk IO usage
     	Supports the following optional suffixes for size values: KB, MB, GB, TB, KiB, MiB, GiB, TiB (default 0)
diff --git a/docs/vmagent.md b/docs/vmagent.md
index 8cae9da03..d7f3cc531 100644
--- a/docs/vmagent.md
+++ b/docs/vmagent.md
@@ -1696,7 +1696,7 @@ See the docs at https://docs.victoriametrics.com/vmagent.html .
   -loggerWarnsPerSecondLimit int
      Per-second limit on the number of WARN messages. If more than the given number of warns are emitted per second, then the remaining warns are suppressed. Zero values disable the rate limit
   -maxConcurrentInserts int
-     The maximum number of concurrent insert requests. Default value should work for most cases, since it minimizes the memory usage. The default value can be increased when clients send data over slow networks. See also -insert.maxQueueDuration (default 32)
+     The maximum number of concurrent insert requests. Default value should work for most cases, since it minimizes the memory usage. The default value can be increased when clients send data over slow networks. See also -insert.maxQueueDuration.
   -maxInsertRequestSize size
      The maximum size in bytes of a single Prometheus remote_write API request
      Supports the following optional suffixes for size values: KB, MB, GB, TB, KiB, MiB, GiB, TiB (default 33554432)
@@ -1921,6 +1921,9 @@ See the docs at https://docs.victoriametrics.com/vmagent.html .
   -remoteWrite.oauth2.clientSecretFile array
      Optional OAuth2 clientSecretFile to use for the corresponding -remoteWrite.url
      Supports an array of values separated by comma or specified via multiple flags.
+  -remoteWrite.oauth2.endpointParams array
+     Optional OAuth2 endpointParams to use for the corresponding -remoteWrite.url. Keys and values must be seperated by ':'.
+     Supports and array of key:value pairs seperated by comma or specified via multiple flags.
   -remoteWrite.oauth2.scopes array
      Optional OAuth2 scopes to use for the corresponding -remoteWrite.url. Scopes must be delimited by ';'
      Supports an array of values separated by comma or specified via multiple flags.
diff --git a/go.mod b/go.mod
index 323fe3d4d..ccd3e7655 100644
--- a/go.mod
+++ b/go.mod
@@ -11,7 +11,7 @@ require (
 	// Do not use the original github.com/valyala/fasthttp because of issues
 	// like https://github.com/valyala/fasthttp/commit/996610f021ff45fdc98c2ce7884d5fa4e7f9199b
 	github.com/VictoriaMetrics/fasthttp v1.2.0
-	github.com/VictoriaMetrics/metrics v1.28.2
+	github.com/VictoriaMetrics/metrics v1.29.0
 	github.com/VictoriaMetrics/metricsql v0.70.0
 	github.com/aws/aws-sdk-go-v2 v1.24.0
 	github.com/aws/aws-sdk-go-v2/config v1.26.1
diff --git a/go.sum b/go.sum
index 5535b14f5..03a063261 100644
--- a/go.sum
+++ b/go.sum
@@ -63,8 +63,8 @@ github.com/VictoriaMetrics/fastcache v1.12.2/go.mod h1:AmC+Nzz1+3G2eCPapF6UcsnkT
 github.com/VictoriaMetrics/fasthttp v1.2.0 h1:nd9Wng4DlNtaI27WlYh5mGXCJOmee/2c2blTJwfyU9I=
 github.com/VictoriaMetrics/fasthttp v1.2.0/go.mod h1:zv5YSmasAoSyv8sBVexfArzFDIGGTN4TfCKAtAw7IfE=
 github.com/VictoriaMetrics/metrics v1.24.0/go.mod h1:eFT25kvsTidQFHb6U0oa0rTrDRdz4xTYjpL8+UPohys=
-github.com/VictoriaMetrics/metrics v1.28.2 h1:yWIq53N8G6hJI6vQ8I2NsiD4p+UsmQa/msLo3yqDfPQ=
-github.com/VictoriaMetrics/metrics v1.28.2/go.mod h1:r7hveu6xMdUACXvB8TYdAj8WEsKzWB0EkpJN+RDtOf8=
+github.com/VictoriaMetrics/metrics v1.29.0 h1:3qC+jcvymGJaQKt6wsXIlJieVFQwD/par9J1Bxul+Mc=
+github.com/VictoriaMetrics/metrics v1.29.0/go.mod h1:r7hveu6xMdUACXvB8TYdAj8WEsKzWB0EkpJN+RDtOf8=
 github.com/VictoriaMetrics/metricsql v0.70.0 h1:G0k/m1yAF6pmk0dM3VT9/XI5PZ8dL7EbcLhREf4bgeI=
 github.com/VictoriaMetrics/metricsql v0.70.0/go.mod h1:k4UaP/+CjuZslIjd+kCigNG9TQmUqh5v0TP/nMEy90I=
 github.com/VividCortex/ewma v1.2.0 h1:f58SaIzcDXrSy3kWaHNvuJgJ3Nmz59Zji6XoJR/q1ow=
diff --git a/lib/awsapi/config.go b/lib/awsapi/config.go
index 4f3d53514..862aabba9 100644
--- a/lib/awsapi/config.go
+++ b/lib/awsapi/config.go
@@ -17,9 +17,15 @@ import (
 
 // Config represent aws access configuration.
 type Config struct {
-	client       *http.Client
-	region       string
-	roleARN      string
+	client  *http.Client
+	region  string
+	roleARN string
+
+	// IRSA may use a different role for assume API call.
+	// It can only be set via AWS_ROLE_ARN env variable.
+	// See https://docs.aws.amazon.com/eks/latest/userguide/pod-configuration.html
+	irsaRoleARN string
+
 	webTokenPath string
 
 	ec2Endpoint string
@@ -49,6 +55,7 @@ func NewConfig(ec2Endpoint, stsEndpoint, region, roleARN, accessKey, secretKey,
 		client:           http.DefaultClient,
 		region:           region,
 		roleARN:          roleARN,
+		irsaRoleARN:      os.Getenv("AWS_ROLE_ARN"),
 		service:          service,
 		defaultAccessKey: os.Getenv("AWS_ACCESS_KEY_ID"),
 		defaultSecretKey: os.Getenv("AWS_SECRET_ACCESS_KEY"),
@@ -69,7 +76,7 @@ func NewConfig(ec2Endpoint, stsEndpoint, region, roleARN, accessKey, secretKey,
 		cfg.roleARN = os.Getenv("AWS_ROLE_ARN")
 	}
 	cfg.webTokenPath = os.Getenv("AWS_WEB_IDENTITY_TOKEN_FILE")
-	if cfg.webTokenPath != "" && cfg.roleARN == "" {
+	if cfg.webTokenPath != "" && cfg.irsaRoleARN == "" {
 		return nil, fmt.Errorf("roleARN is missing for AWS_WEB_IDENTITY_TOKEN_FILE=%q; set it via env var AWS_ROLE_ARN", cfg.webTokenPath)
 	}
 	// explicitly set credentials has priority over env variables
@@ -83,6 +90,7 @@ func NewConfig(ec2Endpoint, stsEndpoint, region, roleARN, accessKey, secretKey,
 		AccessKeyID:     cfg.defaultAccessKey,
 		SecretAccessKey: cfg.defaultSecretKey,
 	}
+
 	return cfg, nil
 }
 
@@ -201,7 +209,7 @@ func (cfg *Config) getAPICredentials() (*credentials, error) {
 		if err != nil {
 			return nil, fmt.Errorf("cannot read webToken from path: %q, err: %w", cfg.webTokenPath, err)
 		}
-		return cfg.getRoleWebIdentityCredentials(string(token))
+		return cfg.getRoleWebIdentityCredentials(string(token), cfg.irsaRoleARN)
 	}
 	if ecsMetaURI := os.Getenv("AWS_CONTAINER_CREDENTIALS_RELATIVE_URI"); len(ecsMetaURI) > 0 {
 		path := "http://169.254.170.2" + ecsMetaURI
@@ -223,7 +231,7 @@ func (cfg *Config) getAPICredentials() (*credentials, error) {
 
 	// read credentials from sts api, if role_arn is defined
 	if len(cfg.roleARN) > 0 {
-		ac, err := cfg.getRoleARNCredentials(acNew)
+		ac, err := cfg.getRoleARNCredentials(acNew, cfg.roleARN)
 		if err != nil {
 			return nil, fmt.Errorf("cannot get credentials for role_arn %q: %w", cfg.roleARN, err)
 		}
@@ -330,28 +338,44 @@ func getMetadataByPath(client *http.Client, apiPath string) ([]byte, error) {
 }
 
 // getRoleWebIdentityCredentials obtains credentials for the given roleARN with webToken.
+//
 // https://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRoleWithWebIdentity.html
 // aws IRSA for kubernetes.
 // https://aws.amazon.com/blogs/opensource/introducing-fine-grained-iam-roles-service-accounts/
-func (cfg *Config) getRoleWebIdentityCredentials(token string) (*credentials, error) {
-	data, err := cfg.getSTSAPIResponse("AssumeRoleWithWebIdentity", func(apiURL string) (*http.Request, error) {
+func (cfg *Config) getRoleWebIdentityCredentials(token, roleARN string) (*credentials, error) {
+	data, err := cfg.getSTSAPIResponse("AssumeRoleWithWebIdentity", roleARN, func(apiURL string) (*http.Request, error) {
 		apiURL += fmt.Sprintf("&WebIdentityToken=%s", url.QueryEscape(token))
 		return http.NewRequest(http.MethodGet, apiURL, nil)
 	})
 	if err != nil {
 		return nil, err
 	}
-	return parseARNCredentials(data, "AssumeRoleWithWebIdentity")
+	creds, err := parseARNCredentials(data, "AssumeRoleWithWebIdentity")
+	if err != nil {
+		return nil, err
+	}
+	if roleARN != cfg.roleARN {
+		// need to assume a different role
+		assumeCreds, err := cfg.getRoleARNCredentials(creds, cfg.roleARN)
+		if err != nil {
+			return nil, fmt.Errorf("cannot assume chained role=%q for roleARN=%q: %w", cfg.roleARN, roleARN, err)
+		}
+		if assumeCreds.Expiration.After(creds.Expiration) {
+			assumeCreds.Expiration = creds.Expiration
+		}
+		return assumeCreds, nil
+	}
+	return creds, nil
 }
 
 // getSTSAPIResponse makes request to aws sts api with the given cfg and returns temporary credentials with expiration time.
 //
 // See https://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRole.html
-func (cfg *Config) getSTSAPIResponse(action string, reqBuilder func(apiURL string) (*http.Request, error)) ([]byte, error) {
+func (cfg *Config) getSTSAPIResponse(action string, roleARN string, reqBuilder func(apiURL string) (*http.Request, error)) ([]byte, error) {
 	// See https://docs.aws.amazon.com/AWSEC2/latest/APIReference/Query-Requests.html
 	apiURL := fmt.Sprintf("%s?Action=%s", cfg.stsEndpoint, action)
 	apiURL += "&Version=2011-06-15"
-	apiURL += fmt.Sprintf("&RoleArn=%s", cfg.roleARN)
+	apiURL += fmt.Sprintf("&RoleArn=%s", roleARN)
 	// we have to provide unique session name for cloudtrail audit
 	apiURL += "&RoleSessionName=vmagent-ec2-discovery"
 	req, err := reqBuilder(apiURL)
@@ -366,8 +390,8 @@ func (cfg *Config) getSTSAPIResponse(action string, reqBuilder func(apiURL strin
 }
 
 // getRoleARNCredentials obtains credentials for the given roleARN.
-func (cfg *Config) getRoleARNCredentials(creds *credentials) (*credentials, error) {
-	data, err := cfg.getSTSAPIResponse("AssumeRole", func(apiURL string) (*http.Request, error) {
+func (cfg *Config) getRoleARNCredentials(creds *credentials, roleARN string) (*credentials, error) {
+	data, err := cfg.getSTSAPIResponse("AssumeRole", roleARN, func(apiURL string) (*http.Request, error) {
 		return newSignedGetRequest(apiURL, "sts", cfg.region, creds)
 	})
 	if err != nil {
diff --git a/lib/flagutil/map.go b/lib/flagutil/map.go
new file mode 100644
index 000000000..2ba97f88d
--- /dev/null
+++ b/lib/flagutil/map.go
@@ -0,0 +1,49 @@
+package flagutil
+
+import (
+	"flag"
+	"fmt"
+	"strings"
+)
+
+type MapString map[string]string
+
+// String returns a string representation of the map.
+func (m *MapString) String() string {
+	if m == nil {
+		return ""
+	}
+	return fmt.Sprintf("%v", *m)
+}
+
+// Set parses the given value into a map.
+func (m *MapString) Set(value string) error {
+	if *m == nil {
+		*m = make(map[string]string)
+	}
+	for _, pair := range parseArrayValues(value) {
+		key, value, err := parseMapValue(pair)
+		if err != nil {
+			return err
+		}
+		(*m)[key] = value
+	}
+	return nil
+}
+
+func parseMapValue(s string) (string, string, error) {
+	kv := strings.SplitN(s, ":", 2)
+	if len(kv) != 2 {
+		return "", "", fmt.Errorf("invalid map value '%s' values must be 'key:value'", s)
+	}
+
+	return kv[0], kv[1], nil
+}
+
+// NewMapString returns a new MapString with the given name and description.
+func NewMapString(name, description string) *MapString {
+	description += fmt.Sprintf("\nSupports multiple flags with the following syntax: -%s=key:value", name)
+	var m MapString
+	flag.Var(&m, name, description)
+	return &m
+}
diff --git a/lib/promscrape/scraper.go b/lib/promscrape/scraper.go
index 2ad7b2698..d5ec1cf8b 100644
--- a/lib/promscrape/scraper.go
+++ b/lib/promscrape/scraper.go
@@ -202,7 +202,7 @@ var (
 	configMetricsSet   = metrics.NewSet()
 	configReloads      = configMetricsSet.NewCounter(`vm_promscrape_config_reloads_total`)
 	configReloadErrors = configMetricsSet.NewCounter(`vm_promscrape_config_reloads_errors_total`)
-	configSuccess      = configMetricsSet.NewCounter(`vm_promscrape_config_last_reload_successful`)
+	configSuccess      = configMetricsSet.NewGauge(`vm_promscrape_config_last_reload_successful`, nil)
 	configTimestamp    = configMetricsSet.NewCounter(`vm_promscrape_config_last_reload_success_timestamp_seconds`)
 )
 
diff --git a/vendor/github.com/VictoriaMetrics/metrics/gauge.go b/vendor/github.com/VictoriaMetrics/metrics/gauge.go
index d40b73098..9f676f40e 100644
--- a/vendor/github.com/VictoriaMetrics/metrics/gauge.go
+++ b/vendor/github.com/VictoriaMetrics/metrics/gauge.go
@@ -3,10 +3,11 @@ package metrics
 import (
 	"fmt"
 	"io"
+	"math"
+	"sync/atomic"
 )
 
-// NewGauge registers and returns gauge with the given name, which calls f
-// to obtain gauge value.
+// NewGauge registers and returns gauge with the given name, which calls f to obtain gauge value.
 //
 // name must be valid Prometheus-compatible metric with possible labels.
 // For instance,
@@ -16,6 +17,7 @@ import (
 //   - foo{bar="baz",aaa="b"}
 //
 // f must be safe for concurrent calls.
+// if f is nil, then it is expected that the gauge value is changed via Gauge.Set() call.
 //
 // The returned gauge is safe to use from concurrent goroutines.
 //
@@ -25,19 +27,36 @@ func NewGauge(name string, f func() float64) *Gauge {
 }
 
 // Gauge is a float64 gauge.
-//
-// See also Counter, which could be used as a gauge with Set and Dec calls.
 type Gauge struct {
+	// f is a callback, which is called for returning the gauge value.
 	f func() float64
+
+	// valueBits contains uint64 representation of float64 passed to Gauge.Set.
+	valueBits uint64
 }
 
 // Get returns the current value for g.
 func (g *Gauge) Get() float64 {
-	return g.f()
+	if f := g.f; f != nil {
+		return f()
+	}
+	n := atomic.LoadUint64(&g.valueBits)
+	return math.Float64frombits(n)
+}
+
+// Set sets g value to v.
+//
+// The g must be created with nil callback in order to be able to call this function.
+func (g *Gauge) Set(v float64) {
+	if g.f != nil {
+		panic(fmt.Errorf("cannot call Set on gauge created with non-nil callback"))
+	}
+	n := math.Float64bits(v)
+	atomic.StoreUint64(&g.valueBits, n)
 }
 
 func (g *Gauge) marshalTo(prefix string, w io.Writer) {
-	v := g.f()
+	v := g.Get()
 	if float64(int64(v)) == v {
 		// Marshal integer values without scientific notation
 		fmt.Fprintf(w, "%s %d\n", prefix, int64(v))
diff --git a/vendor/github.com/VictoriaMetrics/metrics/set.go b/vendor/github.com/VictoriaMetrics/metrics/set.go
index 9949b7c1c..50a095b53 100644
--- a/vendor/github.com/VictoriaMetrics/metrics/set.go
+++ b/vendor/github.com/VictoriaMetrics/metrics/set.go
@@ -251,9 +251,6 @@ func (s *Set) GetOrCreateFloatCounter(name string) *FloatCounter {
 //
 // The returned gauge is safe to use from concurrent goroutines.
 func (s *Set) NewGauge(name string, f func() float64) *Gauge {
-	if f == nil {
-		panic(fmt.Errorf("BUG: f cannot be nil"))
-	}
 	g := &Gauge{
 		f: f,
 	}
@@ -280,9 +277,6 @@ func (s *Set) GetOrCreateGauge(name string, f func() float64) *Gauge {
 	s.mu.Unlock()
 	if nm == nil {
 		// Slow path - create and register missing gauge.
-		if f == nil {
-			panic(fmt.Errorf("BUG: f cannot be nil"))
-		}
 		if err := validateMetric(name); err != nil {
 			panic(fmt.Errorf("BUG: invalid metric name %q: %s", name, err))
 		}
diff --git a/vendor/modules.txt b/vendor/modules.txt
index 66e89e036..52ac9993b 100644
--- a/vendor/modules.txt
+++ b/vendor/modules.txt
@@ -97,7 +97,7 @@ github.com/VictoriaMetrics/fastcache
 github.com/VictoriaMetrics/fasthttp
 github.com/VictoriaMetrics/fasthttp/fasthttputil
 github.com/VictoriaMetrics/fasthttp/stackless
-# github.com/VictoriaMetrics/metrics v1.28.2
+# github.com/VictoriaMetrics/metrics v1.29.0
 ## explicit; go 1.17
 github.com/VictoriaMetrics/metrics
 # github.com/VictoriaMetrics/metricsql v0.70.0


Test patch:
diff --git a/app/vmalert/main_test.go b/app/vmalert/main_test.go
index 12531c66e..d6a289285 100644
--- a/app/vmalert/main_test.go
+++ b/app/vmalert/main_test.go
@@ -141,7 +141,7 @@ groups:
 				t.Fatalf("expected to have config error %s; got nil instead", cErr)
 			}
 			if cfgSuc != 0 {
-				t.Fatalf("expected to have metric configSuccess to be set to 0; got %d instead", cfgSuc)
+				t.Fatalf("expected to have metric configSuccess to be set to 0; got %v instead", cfgSuc)
 			}
 			return
 		}
@@ -150,7 +150,7 @@ groups:
 			t.Fatalf("unexpected config error: %s", cErr)
 		}
 		if cfgSuc != 1 {
-			t.Fatalf("expected to have metric configSuccess to be set to 1; got %d instead", cfgSuc)
+			t.Fatalf("expected to have metric configSuccess to be set to 1; got %v instead", cfgSuc)
 		}
 	}