Repository: VictoriaMetrics/VictoriaMetrics
Pull request #4830 (2023-08-14 12:28:50)

vmui: chart refactoring
Changes Made:

- Extracted common functions for line charts and heatmap.
- Improved overall code structure.
- Optimized chart rendering when there are multiple queries, one of which is a heatmap.

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/VictoriaMetrics/VictoriaMetrics/pull/4830?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=VictoriaMetrics) Report
Patch coverage has no change and project coverage change: **`-0.02%`** :warning:
> Comparison is base [(`214be01`)](https://app.codecov.io/gh/VictoriaMetrics/VictoriaMetrics/commit/214be01dfaf7fd8010822aa3f0c212a8a26b507b?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=VictoriaMetrics) 60.57% compared to head [(`4dce6df`)](https://app.codecov.io/gh/VictoriaMetrics/VictoriaMetrics/pull/4830?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=VictoriaMetrics) 60.56%.

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##           master    #4830      +/-   ##
==========================================
- Coverage   60.57%   60.56%   -0.02%     
==========================================
  Files         384      384              
  Lines       72223    72223              
==========================================
- Hits        43750    43742       -8     
- Misses      26077    26081       +4     
- Partials     2396     2400       +4     
```


[see 11 files with indirect coverage changes](https://app.codecov.io/gh/VictoriaMetrics/VictoriaMetrics/pull/4830/indirect-changes?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=VictoriaMetrics)


</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/VictoriaMetrics/VictoriaMetrics/pull/4830?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=VictoriaMetrics).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=VictoriaMetrics).


Patch:
diff --git a/Makefile b/Makefile
index 45ecabafc..d800b352f 100644
--- a/Makefile
+++ b/Makefile
@@ -34,7 +34,6 @@ clean:
 
 publish: package-base \
 	publish-victoria-metrics \
-	publish-victoria-logs \
 	publish-vmagent \
 	publish-vmalert \
 	publish-vmauth \
@@ -181,7 +180,6 @@ publish-release:
 
 release: \
 	release-victoria-metrics \
-	release-victoria-logs \
 	release-vmutils
 
 release-victoria-metrics: \
diff --git a/README.md b/README.md
index f792d1473..aebd03b8f 100644
--- a/README.md
+++ b/README.md
@@ -173,7 +173,8 @@ VictoriaMetrics is developed at a fast pace, so it is recommended periodically c
 
 ### Environment variables
 
-All the VictoriaMetrics components allow referring environment variables in command-line flags via `%{ENV_VAR}` syntax.
+All the VictoriaMetrics components allow referring environment variables in `yaml` configuration files (such as `-promscrape.config`)
+and in command-line flags via `%{ENV_VAR}` syntax.
 For example, `-metricsAuthKey=%{METRICS_AUTH_KEY}` is automatically expanded to `-metricsAuthKey=top-secret`
 if `METRICS_AUTH_KEY=top-secret` environment variable exists at VictoriaMetrics startup.
 This expansion is performed by VictoriaMetrics itself.
diff --git a/app/vmagent/remotewrite/relabel.go b/app/vmagent/remotewrite/relabel.go
index b1cfc16e6..93b4caee7 100644
--- a/app/vmagent/remotewrite/relabel.go
+++ b/app/vmagent/remotewrite/relabel.go
@@ -87,8 +87,8 @@ func initLabelsGlobal() {
 	}
 }
 
-func (rctx *relabelCtx) applyRelabeling(tss []prompbmarshal.TimeSeries, extraLabels []prompbmarshal.Label, pcs *promrelabel.ParsedConfigs) []prompbmarshal.TimeSeries {
-	if len(extraLabels) == 0 && pcs.Len() == 0 && !*usePromCompatibleNaming {
+func (rctx *relabelCtx) applyRelabeling(tss []prompbmarshal.TimeSeries, pcs *promrelabel.ParsedConfigs) []prompbmarshal.TimeSeries {
+	if pcs.Len() == 0 && !*usePromCompatibleNaming {
 		// Nothing to change.
 		return tss
 	}
@@ -98,34 +98,15 @@ func (rctx *relabelCtx) applyRelabeling(tss []prompbmarshal.TimeSeries, extraLab
 		ts := &tss[i]
 		labelsLen := len(labels)
 		labels = append(labels, ts.Labels...)
-		// extraLabels must be added before applying relabeling according to https://prometheus.io/docs/prometheus/latest/configuration/configuration/#remote_write
-		for j := range extraLabels {
-			extraLabel := &extraLabels[j]
-			tmp := promrelabel.GetLabelByName(labels[labelsLen:], extraLabel.Name)
-			if tmp != nil {
-				tmp.Value = extraLabel.Value
-			} else {
-				labels = append(labels, *extraLabel)
-			}
-		}
-		if *usePromCompatibleNaming {
-			// Replace unsupported Prometheus chars in label names and metric names with underscores.
-			tmpLabels := labels[labelsLen:]
-			for j := range tmpLabels {
-				label := &tmpLabels[j]
-				if label.Name == "__name__" {
-					label.Value = promrelabel.SanitizeName(label.Value)
-				} else {
-					label.Name = promrelabel.SanitizeName(label.Name)
-				}
-			}
-		}
 		labels = pcs.Apply(labels, labelsLen)
 		labels = promrelabel.FinalizeLabels(labels[:labelsLen], labels[labelsLen:])
 		if len(labels) == labelsLen {
 			// Drop the current time series, since relabeling removed all the labels.
 			continue
 		}
+		if *usePromCompatibleNaming {
+			fixPromCompatibleNaming(labels[labelsLen:])
+		}
 		tssDst = append(tssDst, prompbmarshal.TimeSeries{
 			Labels:  labels[labelsLen:],
 			Samples: ts.Samples,
@@ -135,6 +116,32 @@ func (rctx *relabelCtx) applyRelabeling(tss []prompbmarshal.TimeSeries, extraLab
 	return tssDst
 }
 
+func (rctx *relabelCtx) appendExtraLabels(tss []prompbmarshal.TimeSeries, extraLabels []prompbmarshal.Label) {
+	if len(extraLabels) == 0 {
+		return
+	}
+	labels := rctx.labels[:0]
+	for i := range tss {
+		ts := &tss[i]
+		labelsLen := len(labels)
+		labels = append(labels, ts.Labels...)
+		for j := range extraLabels {
+			extraLabel := extraLabels[j]
+			if *usePromCompatibleNaming {
+				extraLabel.Name = promrelabel.SanitizeLabelName(extraLabel.Name)
+			}
+			tmp := promrelabel.GetLabelByName(labels[labelsLen:], extraLabel.Name)
+			if tmp != nil {
+				tmp.Value = extraLabel.Value
+			} else {
+				labels = append(labels, extraLabel)
+			}
+		}
+		ts.Labels = labels[labelsLen:]
+	}
+	rctx.labels = labels
+}
+
 type relabelCtx struct {
 	// pool for labels, which are used during the relabeling.
 	labels []prompbmarshal.Label
@@ -159,3 +166,15 @@ func putRelabelCtx(rctx *relabelCtx) {
 	rctx.labels = rctx.labels[:0]
 	relabelCtxPool.Put(rctx)
 }
+
+func fixPromCompatibleNaming(labels []prompbmarshal.Label) {
+	// Replace unsupported Prometheus chars in label names and metric names with underscores.
+	for i := range labels {
+		label := &labels[i]
+		if label.Name == "__name__" {
+			label.Value = promrelabel.SanitizeMetricName(label.Value)
+		} else {
+			label.Name = promrelabel.SanitizeLabelName(label.Name)
+		}
+	}
+}
diff --git a/app/vmagent/remotewrite/remotewrite.go b/app/vmagent/remotewrite/remotewrite.go
index 29099e114..62dd1a908 100644
--- a/app/vmagent/remotewrite/remotewrite.go
+++ b/app/vmagent/remotewrite/remotewrite.go
@@ -355,7 +355,7 @@ func Push(at *auth.Token, wr *prompbmarshal.WriteRequest) {
 	var rctx *relabelCtx
 	rcs := allRelabelConfigs.Load()
 	pcsGlobal := rcs.global
-	if pcsGlobal.Len() > 0 || len(labelsGlobal) > 0 {
+	if pcsGlobal.Len() > 0 {
 		rctx = getRelabelCtx()
 	}
 	tss := wr.Timeseries
@@ -386,7 +386,7 @@ func Push(at *auth.Token, wr *prompbmarshal.WriteRequest) {
 		}
 		if rctx != nil {
 			rowsCountBeforeRelabel := getRowsCount(tssBlock)
-			tssBlock = rctx.applyRelabeling(tssBlock, labelsGlobal, pcsGlobal)
+			tssBlock = rctx.applyRelabeling(tssBlock, pcsGlobal)
 			rowsCountAfterRelabel := getRowsCount(tssBlock)
 			rowsDroppedByGlobalRelabel.Add(rowsCountBeforeRelabel - rowsCountAfterRelabel)
 		}
@@ -668,7 +668,7 @@ func (rwctx *remoteWriteCtx) Push(tss []prompbmarshal.TimeSeries) {
 		v = tssPool.Get().(*[]prompbmarshal.TimeSeries)
 		tss = append(*v, tss...)
 		rowsCountBeforeRelabel := getRowsCount(tss)
-		tss = rctx.applyRelabeling(tss, nil, pcs)
+		tss = rctx.applyRelabeling(tss, pcs)
 		rowsCountAfterRelabel := getRowsCount(tss)
 		rwctx.rowsDroppedByRelabel.Add(rowsCountBeforeRelabel - rowsCountAfterRelabel)
 	}
@@ -719,6 +719,12 @@ func dropAggregatedSeries(src []prompbmarshal.TimeSeries, matchIdxs []byte, drop
 }
 
 func (rwctx *remoteWriteCtx) pushInternal(tss []prompbmarshal.TimeSeries) {
+	if len(labelsGlobal) > 0 {
+		rctx := getRelabelCtx()
+		defer putRelabelCtx(rctx)
+		rctx.appendExtraLabels(tss, labelsGlobal)
+	}
+
 	pss := rwctx.pss
 	idx := atomic.AddUint64(&rwctx.pssNextIdx, 1) % uint64(len(pss))
 	pss[idx].Push(tss)
diff --git a/app/vmauth/README.md b/app/vmauth/README.md
index cf6ea3a19..48d0d8e35 100644
--- a/app/vmauth/README.md
+++ b/app/vmauth/README.md
@@ -240,8 +240,8 @@ Alternatively, [https termination proxy](https://en.wikipedia.org/wiki/TLS_termi
 
 It is recommended protecting the following endpoints with authKeys:
 * `/-/reload` with `-reloadAuthKey` command-line flag, so external users couldn't trigger config reload.
-* `/flags` with `-flagsAuthkey` command-line flag, so unauthorized users couldn't get application command-line flags.
-* `/metrics` with `metricsAuthkey` command-line flag, so unauthorized users couldn't get access to [vmauth metrics](#monitoring).
+* `/flags` with `-flagsAuthKey` command-line flag, so unauthorized users couldn't get application command-line flags.
+* `/metrics` with `metricsAuthKey` command-line flag, so unauthorized users couldn't get access to [vmauth metrics](#monitoring).
 * `/debug/pprof` with `pprofAuthKey` command-line flag, so unauthorized users couldn't get access to [profiling information](#profiling).
 
 `vmauth` also supports the ability to restict access by IP - see [these docs](#ip-filters). See also [concurrency limiting docs](#concurrency-limiting).
diff --git a/app/vmbackup/main.go b/app/vmbackup/main.go
index d0900caa6..602e9275f 100644
--- a/app/vmbackup/main.go
+++ b/app/vmbackup/main.go
@@ -220,7 +220,20 @@ func hasFilepathPrefix(path, prefix string) bool {
 	if err != nil {
 		return false
 	}
-	return strings.HasPrefix(pathAbs, prefixAbs)
+	if prefixAbs == pathAbs {
+		return true
+	}
+	rel, err := filepath.Rel(prefixAbs, pathAbs)
+	if err != nil {
+		// if paths can't be related - they don't match
+		return false
+	}
+	if i := strings.Index(rel, "."); i == 0 {
+		// if path can be related only with . as first char - they still don't match
+		return false
+	}
+	// if paths are related - it is a match
+	return true
 }
 
 func newOriginFS() (common.OriginFS, error) {
diff --git a/app/vmctl/opentsdb/parser.go b/app/vmctl/opentsdb/parser.go
index 6993e8d13..6c2f209e7 100644
--- a/app/vmctl/opentsdb/parser.go
+++ b/app/vmctl/opentsdb/parser.go
@@ -180,7 +180,7 @@ func modifyData(msg Metric, normalize bool) (Metric, error) {
 	/*
 		replace bad characters in metric name with _ per the data model
 	*/
-	finalMsg.Metric = promrelabel.SanitizeName(name)
+	finalMsg.Metric = promrelabel.SanitizeMetricName(name)
 	// replace bad characters in tag keys with _ per the data model
 	for key, value := range msg.Tags {
 		// if normalization requested, lowercase the key and value
@@ -191,7 +191,7 @@ func modifyData(msg Metric, normalize bool) (Metric, error) {
 		/*
 			replace all explicitly bad characters with _
 		*/
-		key = promrelabel.SanitizeName(key)
+		key = promrelabel.SanitizeLabelName(key)
 		// tags that start with __ are considered custom stats for internal prometheus stuff, we should drop them
 		if !strings.HasPrefix(key, "__") {
 			finalMsg.Tags[key] = value
diff --git a/app/vmctl/vm_native.go b/app/vmctl/vm_native.go
index 8d94d2bbe..31bdb6b38 100644
--- a/app/vmctl/vm_native.go
+++ b/app/vmctl/vm_native.go
@@ -201,7 +201,12 @@ func (p *vmNativeProcessor) runBackfilling(ctx context.Context, tenantID string,
 		}
 
 		if len(metrics) == 0 {
-			return fmt.Errorf("no metrics found")
+			errMsg := "no metrics found"
+			if tenantID != "" {
+				errMsg = fmt.Sprintf("%s for tenant id: %s", errMsg, tenantID)
+			}
+			log.Println(errMsg)
+			return nil
 		}
 		foundSeriesMsg = fmt.Sprintf("Found %d metrics to import", len(metrics))
 	}
diff --git a/app/vminsert/relabel/relabel.go b/app/vminsert/relabel/relabel.go
index fcb107c24..3409fb4c0 100644
--- a/app/vminsert/relabel/relabel.go
+++ b/app/vminsert/relabel/relabel.go
@@ -134,9 +134,9 @@ func (ctx *Ctx) ApplyRelabeling(labels []prompb.Label) []prompb.Label {
 		for i := range tmpLabels {
 			label := &tmpLabels[i]
 			if label.Name == "__name__" {
-				label.Value = promrelabel.SanitizeName(label.Value)
+				label.Value = promrelabel.SanitizeMetricName(label.Value)
 			} else {
-				label.Name = promrelabel.SanitizeName(label.Name)
+				label.Name = promrelabel.SanitizeLabelName(label.Name)
 			}
 		}
 	}
diff --git a/app/vmselect/main.go b/app/vmselect/main.go
index 7f10095dd..7ee0a6e90 100644
--- a/app/vmselect/main.go
+++ b/app/vmselect/main.go
@@ -480,6 +480,10 @@ func RequestHandler(w http.ResponseWriter, r *http.Request) bool {
 		expandWithExprsRequests.Inc()
 		prometheus.ExpandWithExprs(w, r)
 		return true
+	case "/prettify-query":
+		prettifyQueryRequests.Inc()
+		prometheus.PrettifyQuery(w, r)
+		return true
 	case "/api/v1/rules", "/rules":
 		rulesRequests.Inc()
 		if len(*vmalertProxyURL) > 0 {
@@ -655,6 +659,7 @@ var (
 	graphiteFunctionDetailsErrors   = metrics.NewCounter(`vm_http_request_errors_total{path="/functions/<func_name>"}`)
 
 	expandWithExprsRequests = metrics.NewCounter(`vm_http_requests_total{path="/expand-with-exprs"}`)
+	prettifyQueryRequests   = metrics.NewCounter(`vm_http_requests_total{path="/prettify-query"}`)
 
 	vmalertRequests = metrics.NewCounter(`vm_http_requests_total{path="/vmalert"}`)
 	rulesRequests   = metrics.NewCounter(`vm_http_requests_total{path="/api/v1/rules"}`)
diff --git a/app/vmselect/prometheus/prometheus.go b/app/vmselect/prometheus/prometheus.go
index 4cbed0d3b..131249570 100644
--- a/app/vmselect/prometheus/prometheus.go
+++ b/app/vmselect/prometheus/prometheus.go
@@ -3,6 +3,7 @@ package prometheus
 import (
 	"flag"
 	"fmt"
+	"github.com/VictoriaMetrics/metricsql"
 	"math"
 	"net/http"
 	"runtime"
@@ -75,6 +76,23 @@ func ExpandWithExprs(w http.ResponseWriter, r *http.Request) {
 	_ = bw.Flush()
 }
 
+// PrettifyQuery handles the request /prettify-query
+func PrettifyQuery(w http.ResponseWriter, r *http.Request) {
+	query := r.FormValue("query")
+	bw := bufferedwriter.Get(w)
+	defer bufferedwriter.Put(bw)
+	w.Header().Set("Content-Type", "application/json")
+	httpserver.EnableCORS(w, r)
+
+	prettyQuery, err := metricsql.Prettify(query)
+	if err != nil {
+		fmt.Fprintf(bw, `{"status": "error", "msg": %q}`, err)
+	} else {
+		fmt.Fprintf(bw, `{"status": "success", "query": %q}`, prettyQuery)
+	}
+	_ = bw.Flush()
+}
+
 // FederateHandler implements /federate . See https://prometheus.io/docs/prometheus/latest/federation/
 func FederateHandler(startTime time.Time, w http.ResponseWriter, r *http.Request) error {
 	defer federateDuration.UpdateDuration(startTime)
diff --git a/app/vmui/packages/vmui/src/components/Chart/BarChart/consts.ts b/app/vmui/packages/vmui/src/components/Chart/BarChart/consts.ts
index f97f1c508..237850349 100644
--- a/app/vmui/packages/vmui/src/components/Chart/BarChart/consts.ts
+++ b/app/vmui/packages/vmui/src/components/Chart/BarChart/consts.ts
@@ -1,6 +1,6 @@
 import { seriesBarsPlugin } from "../../../utils/uplot/plugin";
-import { barDisp, getBarSeries } from "../../../utils/uplot/series";
-import { Fill, Stroke } from "../../../utils/uplot/types";
+import { barDisp, getBarSeries } from "../../../utils/uplot";
+import { Fill, Stroke } from "../../../types";
 import { PaddingSide, Series } from "uplot";
 
 
diff --git a/app/vmui/packages/vmui/src/components/Chart/ChartTooltip/ChartTooltip.tsx b/app/vmui/packages/vmui/src/components/Chart/ChartTooltip/ChartTooltip.tsx
new file mode 100644
index 000000000..8d21888e2
--- /dev/null
+++ b/app/vmui/packages/vmui/src/components/Chart/ChartTooltip/ChartTooltip.tsx
@@ -0,0 +1,178 @@
+import React, { FC, useCallback, useEffect, useRef, useState } from "preact/compat";
+import { MouseEvent as ReactMouseEvent } from "react";
+import useEventListener from "../../../hooks/useEventListener";
+import ReactDOM from "react-dom";
+import classNames from "classnames";
+import uPlot from "uplot";
+import Button from "../../Main/Button/Button";
+import { CloseIcon, DragIcon } from "../../Main/Icons";
+import { SeriesItemStats } from "../../../types";
+
+export interface ChartTooltipProps {
+  u?: uPlot;
+  id: string;
+  title?: string;
+  dates: string[];
+  value: string | number | null;
+  point: { top: number, left: number };
+  unit?: string;
+  stats?: SeriesItemStats;
+  isSticky?: boolean;
+  info?: string;
+  marker?: string;
+  show?: boolean;
+  onClose?: (id: string) => void;
+}
+
+const ChartTooltip: FC<ChartTooltipProps> = ({
+  u,
+  id,
+  title,
+  dates,
+  value,
+  point,
+  unit = "",
+  info,
+  stats,
+  isSticky,
+  marker,
+  onClose
+}) => {
+  const tooltipRef = useRef<HTMLDivElement>(null);
+
+  const [position, setPosition] = useState({ top: -999, left: -999 });
+  const [moving, setMoving] = useState(false);
+  const [moved, setMoved] = useState(false);
+
+  const handleClose = () => {
+    onClose && onClose(id);
+  };
+
+  const handleMouseDown = (e: ReactMouseEvent) => {
+    setMoved(true);
+    setMoving(true);
+    const { clientX, clientY } = e;
+    setPosition({ top: clientY, left: clientX });
+  };
+
+  const handleMouseMove = useCallback((e: MouseEvent) => {
+    if (!moving) return;
+    const { clientX, clientY } = e;
+    setPosition({ top: clientY, left: clientX });
+  }, [moving]);
+
+  const handleMouseUp = () => {
+    setMoving(false);
+  };
+
+  const calcPosition = () => {
+    if (!tooltipRef.current || !u) return;
+
+    const { top, left } = point;
+    const uPlotPosition = {
+      left: parseFloat(u.over.style.left),
+      top: parseFloat(u.over.style.top)
+    };
+
+    const {
+      width: uPlotWidth,
+      height: uPlotHeight
+    } = u.over.getBoundingClientRect();
+
+    const {
+      width: tooltipWidth,
+      height: tooltipHeight
+    } = tooltipRef.current.getBoundingClientRect();
+
+    const margin = 10;
+    const overflowX = left + tooltipWidth >= uPlotWidth ? tooltipWidth + (2 * margin) : 0;
+    const overflowY = top + tooltipHeight >= uPlotHeight ? tooltipHeight + (2 * margin) : 0;
+
+    const position = {
+      top: top + uPlotPosition.top + margin - overflowY,
+      left: left + uPlotPosition.left + margin - overflowX
+    };
+
+    if (position.left < 0) position.left = 20;
+    if (position.top < 0) position.top = 20;
+
+    setPosition(position);
+  };
+
+  useEffect(calcPosition, [u, value, point, tooltipRef]);
+
+  useEventListener("mousemove", handleMouseMove);
+  useEventListener("mouseup", handleMouseUp);
+
+  if (!u) return null;
+
+  return ReactDOM.createPortal((
+    <div
+      className={classNames({
+        "vm-chart-tooltip": true,
+        "vm-chart-tooltip_sticky": isSticky,
+        "vm-chart-tooltip_moved": moved
+
+      })}
+      ref={tooltipRef}
+      style={position}
+    >
+      <div className="vm-chart-tooltip-header">
+        {title && (
+          <div className="vm-chart-tooltip-header__title">
+            {title}
+          </div>
+        )}
+        <div className="vm-chart-tooltip-header__date">
+          {dates.map((date, i) => <span key={i}>{date}</span>)}
+        </div>
+        {isSticky && (
+          <>
+            <Button
+              className="vm-chart-tooltip-header__drag"
+              variant="text"
+              size="small"
+              startIcon={<DragIcon/>}
+              onMouseDown={handleMouseDown}
+            />
+            <Button
+              className="vm-chart-tooltip-header__close"
+              variant="text"
+              size="small"
+              startIcon={<CloseIcon/>}
+              onClick={handleClose}
+            />
+          </>
+        )}
+      </div>
+      <div className="vm-chart-tooltip-data">
+        {marker && (
+          <span
+            className="vm-chart-tooltip-data__marker"
+            style={{ background: marker }}
+          />
+        )}
+        <div>
+          <p className="vm-chart-tooltip-data__value">
+            <b>{value}</b>{unit}
+          </p>
+          {stats && (
+            <p className="vm-chart-tooltip-data__stats">
+              {Object.keys(stats).filter(key => key !== "last").map((key, i) => (
+                <span key={i}>
+                  {key}:<b>{stats[key as keyof SeriesItemStats]}</b>
+                </span>
+              )
+              )}
+            </p>
+          )}
+        </div>
+      </div>
+      <div className="vm-chart-tooltip-info">
+        {info}
+      </div>
+    </div>
+  ), u.root);
+};
+
+export default ChartTooltip;
diff --git a/app/vmui/packages/vmui/src/components/Chart/ChartTooltip/index.tsx b/app/vmui/packages/vmui/src/components/Chart/ChartTooltip/index.tsx
new file mode 100644
index 000000000..1268a48f0
--- /dev/null
+++ b/app/vmui/packages/vmui/src/components/Chart/ChartTooltip/index.tsx
@@ -0,0 +1,26 @@
+import React, { FC } from "preact/compat";
+import ChartTooltip, { ChartTooltipProps } from "./ChartTooltip";
+import "./style.scss";
+
+interface LineTooltipHook {
+  showTooltip: boolean;
+  tooltipProps: ChartTooltipProps;
+  stickyTooltips: ChartTooltipProps[];
+  handleUnStick: (id: string) => void;
+}
+
+const ChartTooltipWrapper: FC<LineTooltipHook> = ({ showTooltip, tooltipProps, stickyTooltips, handleUnStick }) => (
+  <>
+    {showTooltip && tooltipProps && <ChartTooltip {...tooltipProps}/>}
+    {stickyTooltips.map(t => (
+      <ChartTooltip
+        {...t}
+        isSticky
+        key={t.id}
+        onClose={handleUnStick}
+      />
+    ))}
+  </>
+);
+
+export default ChartTooltipWrapper;
diff --git a/app/vmui/packages/vmui/src/components/Chart/Line/ChartTooltip/style.scss b/app/vmui/packages/vmui/src/components/Chart/ChartTooltip/style.scss
similarity index 91%
rename from app/vmui/packages/vmui/src/components/Chart/Line/ChartTooltip/style.scss
rename to app/vmui/packages/vmui/src/components/Chart/ChartTooltip/style.scss
index 63df621bf..a5c00593a 100644
--- a/app/vmui/packages/vmui/src/components/Chart/Line/ChartTooltip/style.scss
+++ b/app/vmui/packages/vmui/src/components/Chart/ChartTooltip/style.scss
@@ -1,4 +1,5 @@
 @use "src/styles/variables" as *;
+
 $chart-tooltip-width: 325px;
 $chart-tooltip-icon-width: 25px;
 $chart-tooltip-half-icon: calc($chart-tooltip-icon-width/2);
@@ -53,10 +54,8 @@ $chart-tooltip-y: -1 * ($padding-small + $chart-tooltip-half-icon);
     }
 
     &__date {
-      &_range {
-        display: grid;
-        gap: 2px;
-      }
+      display: grid;
+      gap: 2px;
     }
   }
 
@@ -68,6 +67,13 @@ $chart-tooltip-y: -1 * ($padding-small + $chart-tooltip-half-icon);
     word-break: break-all;
     line-height: $font-size;
 
+    &__stats {
+      display: flex;
+      align-items: center;
+      flex-wrap: wrap;
+      gap: $padding-small;
+    }
+
     &__marker {
       width: 12px;
       height: 12px;
diff --git a/app/vmui/packages/vmui/src/components/Chart/Heatmap/ChartTooltipHeatmap/ChartTooltipHeatmap.tsx b/app/vmui/packages/vmui/src/components/Chart/Heatmap/ChartTooltipHeatmap/ChartTooltipHeatmap.tsx
deleted file mode 100644
index 3995aa2c9..000000000
--- a/app/vmui/packages/vmui/src/components/Chart/Heatmap/ChartTooltipHeatmap/ChartTooltipHeatmap.tsx
+++ /dev/null
@@ -1,142 +0,0 @@
-import React, { FC, useCallback, useEffect, useRef, useState } from "preact/compat";
-import uPlot from "uplot";
-import ReactDOM from "react-dom";
-import Button from "../../../Main/Button/Button";
-import { CloseIcon, DragIcon } from "../../../Main/Icons";
-import classNames from "classnames";
-import { MouseEvent as ReactMouseEvent } from "react";
-import "../../Line/ChartTooltip/style.scss";
-import useEventListener from "../../../../hooks/useEventListener";
-
-export interface TooltipHeatmapProps  {
-  cursor: {left: number, top: number}
-  startDate: string,
-  endDate: string,
-  bucket: string,
-  value: number,
-  valueFormat: string
-}
-
-export interface ChartTooltipHeatmapProps extends TooltipHeatmapProps {
-  id: string,
-  u: uPlot,
-  unit?: string,
-  isSticky?: boolean,
-  tooltipOffset: { left: number, top: number },
-  onClose?: (id: string) => void
-}
-
-const ChartTooltipHeatmap: FC<ChartTooltipHeatmapProps> = ({
-  u,
-  id,
-  unit = "",
-  cursor,
-  tooltipOffset,
-  isSticky,
-  onClose,
-  startDate,
-  endDate,
-  bucket,
-  valueFormat,
-  value
-}) => {
-  const tooltipRef = useRef<HTMLDivElement>(null);
-
-  const [position, setPosition] = useState({ top: -999, left: -999 });
-  const [moving, setMoving] = useState(false);
-  const [moved, setMoved] = useState(false);
-
-  const handleClose = () => {
-    onClose && onClose(id);
-  };
-
-  const handleMouseDown = (e: ReactMouseEvent) => {
-    setMoved(true);
-    setMoving(true);
-    const { clientX, clientY } = e;
-    setPosition({ top: clientY, left: clientX });
-  };
-
-  const handleMouseMove = useCallback((e: MouseEvent) => {
-    if (!moving) return;
-    const { clientX, clientY } = e;
-    setPosition({ top: clientY, left: clientX });
-  }, [moving]);
-
-  const handleMouseUp = () => {
-    setMoving(false);
-  };
-
-  const calcPosition = () => {
-    if (!tooltipRef.current) return;
-
-    const topOnChart = cursor.top;
-    const leftOnChart = cursor.left;
-    const { width: tooltipWidth, height: tooltipHeight } = tooltipRef.current.getBoundingClientRect();
-    const { width, height } = u.over.getBoundingClientRect();
-
-    const margin = 10;
-    const overflowX = leftOnChart + tooltipWidth >= width ? tooltipWidth + (2 * margin) : 0;
-    const overflowY = topOnChart + tooltipHeight >= height ? tooltipHeight + (2 * margin) : 0;
-
-    setPosition({
-      top: topOnChart + tooltipOffset.top + margin - overflowY,
-      left: leftOnChart + tooltipOffset.left + margin - overflowX
-    });
-  };
-
-  useEffect(calcPosition, [u, cursor, tooltipOffset, tooltipRef]);
-
-  useEventListener("mousemove", handleMouseMove);
-  useEventListener("mouseup", handleMouseUp);
-
-  if (!cursor?.left || !cursor?.top || !value) return null;
-
-  return ReactDOM.createPortal((
-    <div
-      className={classNames({
-        "vm-chart-tooltip": true,
-        "vm-chart-tooltip_sticky": isSticky,
-        "vm-chart-tooltip_moved": moved
-
-      })}
-      ref={tooltipRef}
-      style={position}
-    >
-      <div className="vm-chart-tooltip-header">
-        <div className="vm-chart-tooltip-header__date vm-chart-tooltip-header__date_range">
-          <span>{startDate}</span>
-          <span>{endDate}</span>
-        </div>
-        {isSticky && (
-          <>
-            <Button
-              className="vm-chart-tooltip-header__drag"
-              variant="text"
-              size="small"
-              startIcon={<DragIcon/>}
-              onMouseDown={handleMouseDown}
-            />
-            <Button
-              className="vm-chart-tooltip-header__close"
-              variant="text"
-              size="small"
-              startIcon={<CloseIcon/>}
-              onClick={handleClose}
-            />
-          </>
-        )}
-      </div>
-      <div className="vm-chart-tooltip-data">
-        <p>
-          value: <b className="vm-chart-tooltip-data__value">{valueFormat}</b>{unit}
-        </p>
-      </div>
-      <div className="vm-chart-tooltip-info">
-        {bucket}
-      </div>
-    </div>
-  ), u.root);
-};
-
-export default ChartTooltipHeatmap;
diff --git a/app/vmui/packages/vmui/src/components/Chart/Heatmap/HeatmapChart/HeatmapChart.tsx b/app/vmui/packages/vmui/src/components/Chart/Heatmap/HeatmapChart/HeatmapChart.tsx
index 9afa6e411..f815a03db 100644
--- a/app/vmui/packages/vmui/src/components/Chart/Heatmap/HeatmapChart/HeatmapChart.tsx
+++ b/app/vmui/packages/vmui/src/components/Chart/Heatmap/HeatmapChart/HeatmapChart.tsx
@@ -1,49 +1,44 @@
-import React, { FC, useCallback, useEffect, useMemo, useRef, useState } from "preact/compat";
+import React, { FC, useEffect, useMemo, useRef, useState } from "preact/compat";
 import uPlot, {
   AlignedData as uPlotData,
   Options as uPlotOptions,
-  Range
 } from "uplot";
-import { defaultOptions, sizeAxis } from "../../../../utils/uplot/helpers";
-import { dragChart } from "../../../../utils/uplot/events";
-import { getAxes } from "../../../../utils/uplot/axes";
 import { MetricResult } from "../../../../api/types";
-import { dateFromSeconds, formatDateForNativeInput, limitsDurations } from "../../../../utils/time";
-import throttle from "lodash.throttle";
 import { TimeParams } from "../../../../types";
-import { YaxisState } from "../../../../state/graph/reducer";
 import "uplot/dist/uPlot.min.css";
 import classNames from "classnames";
-import dayjs from "dayjs";
 import { useAppState } from "../../../../state/common/StateContext";
-import { heatmapPaths } from "../../../../utils/uplot/heatmap";
-import { DATE_FULL_TIMEZONE_FORMAT } from "../../../../constants/date";
-import ChartTooltipHeatmap, {
-  ChartTooltipHeatmapProps,
-  TooltipHeatmapProps
-} from "../ChartTooltipHeatmap/ChartTooltipHeatmap";
+import {
+  heatmapPaths,
+  handleDestroy,
+  getDefaultOptions,
+  sizeAxis,
+  getAxes,
+  setSelect,
+} from "../../../../utils/uplot";
 import { ElementSize } from "../../../../hooks/useElementSize";
-import useEventListener from "../../../../hooks/useEventListener";
+import useReadyChart from "../../../../hooks/uplot/useReadyChart";
+import useZoomChart from "../../../../hooks/uplot/useZoomChart";
+import usePlotScale from "../../../../hooks/uplot/usePlotScale";
+import useHeatmapTooltip from "../../../../hooks/uplot/useHeatmapTooltip";
+import ChartTooltipWrapper from "../../ChartTooltip";
+import { ChartTooltipProps } from "../../ChartTooltip/ChartTooltip";
 
 export interface HeatmapChartProps {
   metrics: MetricResult[];
   data: uPlotData;
   period: TimeParams;
-  yaxis: YaxisState;
   unit?: string;
-  setPeriod: ({ from, to }: {from: Date, to: Date}) => void;
+  setPeriod: ({ from, to }: { from: Date, to: Date }) => void;
   layoutSize: ElementSize,
   height?: number;
-  onChangeLegend: (val: TooltipHeatmapProps) => void;
+  onChangeLegend: (val: ChartTooltipProps) => void;
 }
 
-enum typeChartUpdate {xRange = "xRange", yRange = "yRange"}
-
 const HeatmapChart: FC<HeatmapChartProps> = ({
   data,
   metrics = [],
   period,
-  yaxis,
   unit,
   setPeriod,
   layoutSize,
@@ -53,144 +48,39 @@ const HeatmapChart: FC<HeatmapChartProps> = ({
   const { isDarkTheme } = useAppState();
 
   const uPlotRef = useRef<HTMLDivElement>(null);
-  const [isPanning, setPanning] = useState(false);
-  const [xRange, setXRange] = useState({ min: period.start, max: period.end });
   const [uPlotInst, setUPlotInst] = useState<uPlot>();
-  const [startTouchDistance, setStartTouchDistance] = useState(0);
 
-  const [tooltipProps, setTooltipProps] = useState<TooltipHeatmapProps | null>(null);
-  const [tooltipOffset, setTooltipOffset] = useState({ left: 0, top: 0 });
-  const [stickyTooltips, setStickyToolTips] = useState<ChartTooltipHeatmapProps[]>([]);
-  const tooltipId = useMemo(() => {
-    return `${tooltipProps?.bucket}_${tooltipProps?.startDate}`;
-  }, [tooltipProps]);
+  const { xRange, setPlotScale } = usePlotScale({ period, setPeriod });
+  const { onReadyChart, isPanning } = useReadyChart(setPlotScale);
+  useZoomChart({ uPlotInst, xRange, setPlotScale });
+  const {
+    stickyTooltips,
+    handleUnStick,
+    getTooltipProps,
+    setCursor,
+    resetTooltips
+  } = useHeatmapTooltip({ u: uPlotInst, metrics, unit });
 
-  const setScale = ({ min, max }: { min: number, max: number }): void => {
-    if (isNaN(min) || isNaN(max)) return;
-    setPeriod({
-      from: dayjs(min * 1000).toDate(),
-      to: dayjs(max * 1000).toDate()
-    });
-  };
-  const throttledSetScale = useCallback(throttle(setScale, 500), []);
-  const setPlotScale = ({ min, max }: { min: number, max: number }) => {
-    const delta = (max - min) * 1000;
-    if ((delta < limitsDurations.min) || (delta > limitsDurations.max)) return;
-    setXRange({ min, max });
-    throttledSetScale({ min, max });
-  };
+  const tooltipProps = useMemo(() => getTooltipProps(), [getTooltipProps]);
 
-  const onReadyChart = (u: uPlot) => {
-    const factor = 0.9;
-    setTooltipOffset({
-      left: parseFloat(u.over.style.left),
-      top: parseFloat(u.over.style.top)
-    });
+  const getHeatmapAxes = () => {
+    const baseAxes = getAxes([{}], unit);
 
-    u.over.addEventListener("mousedown", e => {
-      const { ctrlKey, metaKey, button } = e;
-      const leftClick = button === 0;
-      const leftClickWithMeta = leftClick && (ctrlKey || metaKey);
-      if (leftClickWithMeta) {
-        // drag pan
-        dragChart({ u, e, setPanning, setPlotScale, factor });
+    return [
+      ...baseAxes,
+      {
+        scale: "y",
+        stroke: baseAxes[0].stroke,
+        font: baseAxes[0].font,
+        size: sizeAxis,
+        splits: metrics.map((m, i) => i),
+        values: metrics.map(m => m.metric.vmrange),
       }
-    });
-
-    u.over.addEventListener("touchstart", e => {
-      dragChart({ u, e, setPanning, setPlotScale, factor });
-    });
-
-    u.over.addEventListener("wheel", e => {
-      if (!e.ctrlKey && !e.metaKey) return;
-      e.preventDefault();
-      const { width } = u.over.getBoundingClientRect();
-      const zoomPos = u.cursor.left && u.cursor.left > 0 ? u.cursor.left : 0;
-      const xVal = u.posToVal(zoomPos, "x");
-      const oxRange = (u.scales.x.max || 0) - (u.scales.x.min || 0);
-      const nxRange = e.deltaY < 0 ? oxRange * factor : oxRange / factor;
-      const min = xVal - (zoomPos / width) * nxRange;
-      const max = min + nxRange;
-      u.batch(() => setPlotScale({ min, max }));
-    });
+    ];
   };
-
-  const handleKeyDown = useCallback((e: KeyboardEvent) => {
-    const { target, ctrlKey, metaKey, key } = e;
-    const isInput = target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement;
-    if (!uPlotInst || isInput) return;
-    const minus = key === "-";
-    const plus = key === "+" || key === "=";
-    if ((minus || plus) && !(ctrlKey || metaKey)) {
-      e.preventDefault();
-      const factor = (xRange.max - xRange.min) / 10 * (plus ? 1 : -1);
-      setPlotScale({
-        min: xRange.min + factor,
-        max: xRange.max - factor
-      });
-    }
-  }, [uPlotInst, xRange]);
-
-  const handleClick = useCallback(() => {
-    if (!tooltipProps?.value) return;
-    const id = `${tooltipProps?.bucket}_${tooltipProps?.startDate}`;
-    const props = {
-      id,
-      unit,
-      tooltipOffset,
-      ...tooltipProps
-    };
-
-    if (!stickyTooltips.find(t => t.id === id)) {
-      const res = JSON.parse(JSON.stringify(props));
-      setStickyToolTips(prev => [...prev, res]);
-    }
-  }, [stickyTooltips, tooltipProps, tooltipOffset, unit]);
-
-  const handleUnStick = (id: string) => {
-    setStickyToolTips(prev => prev.filter(t => t.id !== id));
-  };
-
-  const setCursor = (u: uPlot) => {
-    const left = u.cursor.left && u.cursor.left > 0 ? u.cursor.left : 0;
-    const top = u.cursor.top && u.cursor.top > 0 ? u.cursor.top : 0;
-
-    const xArr = (u.data[1][0] || []) as number[];
-    if (!Array.isArray(xArr)) return;
-    const xVal = u.posToVal(left, "x");
-    const yVal = u.posToVal(top, "y");
-    const xIdx = xArr.findIndex((t, i) => xVal >= t && xVal < xArr[i + 1]) || -1;
-    const second = xArr[xIdx + 1];
-
-    const result = metrics[Math.round(yVal)];
-    if (!result) {
-      setTooltipProps(null);
-      return;
-    }
-
-    const [endTime = 0, value = ""] = result.values.find(v => v[0] === second) || [];
-    const valueFormat = `${+value}%`;
-    const startTime = xArr[xIdx];
-    const startDate = dayjs(startTime * 1000).tz().format(DATE_FULL_TIMEZONE_FORMAT);
-    const endDate = dayjs(endTime * 1000).tz().format(DATE_FULL_TIMEZONE_FORMAT);
-
-    setTooltipProps({
-      cursor: { left, top },
-      startDate,
-      endDate,
-      bucket: result?.metric?.vmrange || "",
-      value: +value,
-      valueFormat: valueFormat,
-    });
-  };
-
-  const getRangeX = (): Range.MinMax => [xRange.min, xRange.max];
-
-  const axes = getAxes( [{}], unit);
   const options: uPlotOptions = {
-    ...defaultOptions,
+    ...getDefaultOptions({ width: layoutSize.width, height }),
     mode: 2,
-    tzDate: ts => dayjs(formatDateForNativeInput(dateFromSeconds(ts))).local().toDate(),
     series: [
       {},
       {
@@ -210,17 +100,7 @@ const HeatmapChart: FC<HeatmapChartProps> = ({
         ],
       },
     ],
-    axes: [
-      ...axes,
-      {
-        scale: "y",
-        stroke: axes[0].stroke,
-        font: axes[0].font,
-        size: sizeAxis,
-        splits: metrics.map((m, i) => i),
-        values: metrics.map(m => m.metric.vmrange),
-      }
-    ],
+    axes: getHeatmapAxes(),
     scales: {
       x: {
         time: true,
@@ -228,88 +108,36 @@ const HeatmapChart: FC<HeatmapChartProps> = ({
       y: {
         log: 2,
         time: false,
-        range: (self, initMin, initMax) => [initMin - 1, initMax + 1]
+        range: (u, initMin, initMax) => [initMin - 1, initMax + 1]
       }
     },
-    width: layoutSize.width || 400,
-    height: height || 500,
-    plugins: [{ hooks: { ready: onReadyChart, setCursor } }],
     hooks: {
-      setSelect: [
-        (u) => {
-          const min = u.posToVal(u.select.left, "x");
-          const max = u.posToVal(u.select.left + u.select.width, "x");
-          setPlotScale({ min, max });
-        }
-      ]
+      ready: [onReadyChart],
+      setCursor: [setCursor],
+      setSelect: [setSelect(setPlotScale)],
+      destroy: [handleDestroy],
     },
   };
 
-  const updateChart = (type: typeChartUpdate): void => {
-    if (!uPlotInst) return;
-    switch (type) {
-      case typeChartUpdate.xRange:
-        uPlotInst.scales.x.range = getRangeX;
-        break;
-    }
-    if (!isPanning) uPlotInst.redraw();
-  };
-
-  useEffect(() => setXRange({ min: period.start, max: period.end }), [period]);
-
   useEffect(() => {
-    setStickyToolTips([]);
-    setTooltipProps(null);
+    resetTooltips();
     const isValidData = data[0] === null && Array.isArray(data[1]);
-    if (!uPlotRef.current || !layoutSize.width || !layoutSize.height || !isValidData) return;
+    if (!uPlotRef.current || !isValidData) return;
     const u = new uPlot(options, data, uPlotRef.current);
     setUPlotInst(u);
-    setXRange({ min: period.start, max: period.end });
     return u.destroy;
-  }, [uPlotRef.current, layoutSize, height, isDarkTheme, data]);
+  }, [uPlotRef, data, isDarkTheme]);
 
-  const handleTouchStart = (e: TouchEvent) => {
-    if (e.touches.length !== 2) return;
-    e.preventDefault();
-
-    const dx = e.touches[0].clientX - e.touches[1].clientX;
-    const dy = e.touches[0].clientY - e.touches[1].clientY;
-    setStartTouchDistance(Math.sqrt(dx * dx + dy * dy));
-  };
-
-  const handleTouchMove = useCallback((e: TouchEvent) => {
-    if (e.touches.length !== 2 || !uPlotInst) return;
-    e.preventDefault();
-
-    const dx = e.touches[0].clientX - e.touches[1].clientX;
-    const dy = e.touches[0].clientY - e.touches[1].clientY;
-    const endTouchDistance = Math.sqrt(dx * dx + dy * dy);
-    const diffDistance = startTouchDistance - endTouchDistance;
-
-    const max = (uPlotInst.scales.x.max || xRange.max);
-    const min = (uPlotInst.scales.x.min || xRange.min);
-    const dur = max - min;
-    const dir = (diffDistance > 0 ? -1 : 1);
-
-    const zoomFactor = dur / 50 * dir;
-    uPlotInst.batch(() => setPlotScale({
-      min: min + zoomFactor,
-      max: max - zoomFactor
-    }));
-  }, [uPlotInst, startTouchDistance, xRange]);
-
-  useEffect(() => updateChart(typeChartUpdate.xRange), [xRange]);
-  useEffect(() => updateChart(typeChartUpdate.yRange), [yaxis]);
+  useEffect(() => {
+    if (!uPlotInst) return;
+    uPlotInst.setSize({ width: layoutSize.width || 400, height: height || 500 });
+    uPlotInst.redraw();
+  }, [height, layoutSize]);
 
   useEffect(() => {
-    if (tooltipProps) onChangeLegend(tooltipProps);
+    onChangeLegend(tooltipProps);
   }, [tooltipProps]);
 
-  useEventListener("click", handleClick);
-  useEventListener("keydown", handleKeyDown);
-  useEventListener("touchmove", handleTouchMove);
-  useEventListener("touchstart", handleTouchStart);
-
   return (
     <div
       className={classNames({
@@ -325,25 +153,13 @@ const HeatmapChart: FC<HeatmapChartProps> = ({
         className="vm-line-chart__u-plot"
         ref={uPlotRef}
       />
-      {uPlotInst && tooltipProps && (
-        <ChartTooltipHeatmap
-          {...tooltipProps}
-          unit={unit}
-          u={uPlotInst}
-          tooltipOffset={tooltipOffset}
-          id={tooltipId}
-        />
-      )}
 
-      {uPlotInst && stickyTooltips.map(t => (
-        <ChartTooltipHeatmap
-          {...t}
-          isSticky
-          u={uPlotInst}
-          key={t.id}
-          onClose={handleUnStick}
-        />
-      ))}
+      <ChartTooltipWrapper
+        showTooltip={!!tooltipProps.show}
+        tooltipProps={tooltipProps}
+        stickyTooltips={stickyTooltips}
+        handleUnStick={handleUnStick}
+      />
     </div>
   );
 };
diff --git a/app/vmui/packages/vmui/src/components/Chart/Heatmap/LegendHeatmap/LegendHeatmap.tsx b/app/vmui/packages/vmui/src/components/Chart/Heatmap/LegendHeatmap/LegendHeatmap.tsx
index 97824cb3f..d1f580b4a 100644
--- a/app/vmui/packages/vmui/src/components/Chart/Heatmap/LegendHeatmap/LegendHeatmap.tsx
+++ b/app/vmui/packages/vmui/src/components/Chart/Heatmap/LegendHeatmap/LegendHeatmap.tsx
@@ -1,39 +1,39 @@
-import React, { FC, useEffect, useState } from "preact/compat";
-import { gradMetal16 } from "../../../../utils/uplot/heatmap";
+import React, { FC, useEffect, useMemo, useState } from "preact/compat";
+import { gradMetal16 } from "../../../../utils/uplot";
+import { SeriesItem, LegendItemType } from "../../../../types";
 import "./style.scss";
-import { TooltipHeatmapProps } from "../ChartTooltipHeatmap/ChartTooltipHeatmap";
-import { SeriesItem } from "../../../../utils/uplot/series";
 import LegendItem from "../../Line/Legend/LegendItem/LegendItem";
-import { LegendItemType } from "../../../../utils/uplot/types";
+import { ChartTooltipProps } from "../../ChartTooltip/ChartTooltip";
 
 interface LegendHeatmapProps {
   min: number
   max: number
-  legendValue: TooltipHeatmapProps | null,
+  legendValue: ChartTooltipProps | null,
   series: SeriesItem[]
 }
 
-const LegendHeatmap: FC<LegendHeatmapProps> = (
-  {
-    min,
-    max,
-    legendValue,
-    series,
-  }
-) => {
+const LegendHeatmap: FC<LegendHeatmapProps> = ({
+  min,
+  max,
+  legendValue,
+  series
+}) => {
 
   const [percent, setPercent] = useState(0);
   const [valueFormat, setValueFormat] = useState("");
   const [minFormat, setMinFormat] = useState("");
   const [maxFormat, setMaxFormat] = useState("");
 
+  const value = useMemo(() => {
+    return parseFloat(String(legendValue?.value || 0).replace("%", ""));
+  }, [legendValue]);
+
   useEffect(() => {
-    const value = legendValue?.value || 0;
     setPercent(value ? (value - min) / (max - min) * 100 : 0);
     setValueFormat(value ? `${value}%` : "");
     setMinFormat(`${min}%`);
     setMaxFormat(`${max}%`);
-  }, [legendValue, min, max]);
+  }, [value, min, max]);
 
   return (
     <div className="vm-legend-heatmap__wrapper">
@@ -42,7 +42,7 @@ const LegendHeatmap: FC<LegendHeatmapProps> = (
           className="vm-legend-heatmap-gradient"
           style={{ background: `linear-gradient(to right, ${gradMetal16.join(", ")})` }}
         >
-          {!!legendValue?.value && (
+          {!!value && (
             <div
               className="vm-legend-heatmap-gradient__value"
               style={{ left: `${percent}%` }}
diff --git a/app/vmui/packages/vmui/src/components/Chart/Line/ChartTooltip/ChartTooltip.tsx b/app/vmui/packages/vmui/src/components/Chart/Line/ChartTooltip/ChartTooltip.tsx
deleted file mode 100644
index 4866be6b6..000000000
--- a/app/vmui/packages/vmui/src/components/Chart/Line/ChartTooltip/ChartTooltip.tsx
+++ /dev/null
@@ -1,180 +0,0 @@
-import React, { FC, useCallback, useEffect, useMemo, useRef, useState } from "preact/compat";
-import uPlot from "uplot";
-import { MetricResult } from "../../../../api/types";
-import { formatPrettyNumber } from "../../../../utils/uplot/helpers";
-import dayjs from "dayjs";
-import { DATE_FULL_TIMEZONE_FORMAT } from "../../../../constants/date";
-import ReactDOM from "react-dom";
-import get from "lodash.get";
-import Button from "../../../Main/Button/Button";
-import { CloseIcon, DragIcon } from "../../../Main/Icons";
-import classNames from "classnames";
-import { MouseEvent as ReactMouseEvent } from "react";
-import "./style.scss";
-import { SeriesItem } from "../../../../utils/uplot/series";
-import useEventListener from "../../../../hooks/useEventListener";
-
-export interface ChartTooltipProps {
-  id: string,
-  u: uPlot,
-  metricItem: MetricResult,
-  seriesItem: SeriesItem,
-  unit?: string,
-  isSticky?: boolean,
-  showQueryNum?: boolean,
-  tooltipOffset: { left: number, top: number },
-  tooltipIdx: { seriesIdx: number, dataIdx: number },
-  onClose?: (id: string) => void
-}
-
-const ChartTooltip: FC<ChartTooltipProps> = ({
-  u,
-  id,
-  unit = "",
-  metricItem,
-  seriesItem,
-  tooltipIdx,
-  tooltipOffset,
-  isSticky,
-  showQueryNum,
-  onClose
-}) => {
-  const tooltipRef = useRef<HTMLDivElement>(null);
-
-  const [position, setPosition] = useState({ top: -999, left: -999 });
-  const [moving, setMoving] = useState(false);
-  const [moved, setMoved] = useState(false);
-
-  const [seriesIdx, setSeriesIdx] = useState(tooltipIdx.seriesIdx);
-  const [dataIdx, setDataIdx] = useState(tooltipIdx.dataIdx);
-
-  const value = get(u, ["data", seriesIdx, dataIdx], 0);
-  const valueFormat = formatPrettyNumber(value, get(u, ["scales", "1", "min"], 0), get(u, ["scales", "1", "max"], 1));
-  const dataTime = u.data[0][dataIdx];
-  const date = dayjs(dataTime * 1000).tz().format(DATE_FULL_TIMEZONE_FORMAT);
-
-  const color = `${seriesItem?.stroke}`;
-  const calculations = seriesItem?.calculations || {};
-  const group = metricItem?.group || 0;
-
-
-  const fullMetricName = useMemo(() => {
-    const metric = metricItem?.metric || {};
-    const labelNames = Object.keys(metric).filter(x => x != "__name__");
-    const labels = labelNames.map(key => `${key}=${JSON.stringify(metric[key])}`);
-    let metricName = metric["__name__"] || "";
-    if (labels.length > 0) {
-      metricName += "{" + labels.join(",") + "}";
-    }
-    return metricName;
-  }, [metricItem]);
-
-  const handleClose = () => {
-    onClose && onClose(id);
-  };
-
-  const handleMouseDown = (e: ReactMouseEvent<HTMLButtonElement, MouseEvent>) => {
-    setMoved(true);
-    setMoving(true);
-    const { clientX, clientY } = e;
-    setPosition({ top: clientY, left: clientX });
-  };
-
-  const handleMouseMove = useCallback((e: MouseEvent) => {
-    if (!moving) return;
-    const { clientX, clientY } = e;
-    setPosition({ top: clientY, left: clientX });
-  }, [moving]);
-
-  const handleMouseUp = () => {
-    setMoving(false);
-  };
-
-  const calcPosition = () => {
-    if (!tooltipRef.current) return;
-
-    const topOnChart = u.valToPos((value || 0), seriesItem?.scale || "1");
-    const leftOnChart = u.valToPos(dataTime, "x");
-    const { width: tooltipWidth, height: tooltipHeight } = tooltipRef.current.getBoundingClientRect();
-    const { width, height } = u.over.getBoundingClientRect();
-
-    const margin = 10;
-    const overflowX = leftOnChart + tooltipWidth >= width ? tooltipWidth + (2 * margin) : 0;
-    const overflowY = topOnChart + tooltipHeight >= height ? tooltipHeight + (2 * margin) : 0;
-
-    const position = {
-      top: topOnChart + tooltipOffset.top + margin - overflowY,
-      left: leftOnChart + tooltipOffset.left + margin - overflowX
-    };
-
-    if (position.left < 0) position.left = 20;
-    if (position.top < 0) position.top = 20;
-
-    setPosition(position);
-  };
-
-  useEffect(calcPosition, [u, value, dataTime, seriesIdx, tooltipOffset, tooltipRef]);
-
-  useEffect(() => {
-    setSeriesIdx(tooltipIdx.seriesIdx);
-    setDataIdx(tooltipIdx.dataIdx);
-  }, [tooltipIdx]);
-
-  useEventListener("mousemove", handleMouseMove);
-  useEventListener("mouseup", handleMouseUp);
-
-  if (tooltipIdx.seriesIdx < 0 || tooltipIdx.dataIdx < 0) return null;
-
-  return ReactDOM.createPortal((
-    <div
-      className={classNames({
-        "vm-chart-tooltip": true,
-        "vm-chart-tooltip_sticky": isSticky,
-        "vm-chart-tooltip_moved": moved
-
-      })}
-      ref={tooltipRef}
-      style={position}
-    >
-      <div className="vm-chart-tooltip-header">
-        <div className="vm-chart-tooltip-header__date">
-          {showQueryNum && (<div>Query {group}</div>)}
-          {date}
-        </div>
-        {isSticky && (
-          <>
-            <Button
-              className="vm-chart-tooltip-header__drag"
-              variant="text"
-              size="small"
-              startIcon={<DragIcon/>}
-              onMouseDown={handleMouseDown}
-            />
-            <Button
-              className="vm-chart-tooltip-header__close"
-              variant="text"
-              size="small"
-              startIcon={<CloseIcon/>}
-              onClick={handleClose}
-            />
-          </>
-        )}
-      </div>
-      <div className="vm-chart-tooltip-data">
-        <div
-          className="vm-chart-tooltip-data__marker"
-          style={{ background: color }}
-        />
-        <div>
-          <b>{valueFormat}{unit}</b><br/>
-          median:<b>{calculations.median}</b>, min:<b>{calculations.min}</b>, max:<b>{calculations.max}</b>
-        </div>
-      </div>
-      <div className="vm-chart-tooltip-info">
-        {fullMetricName}
-      </div>
-    </div>
-  ), u.root);
-};
-
-export default ChartTooltip;
diff --git a/app/vmui/packages/vmui/src/components/Chart/Line/Legend/Legend.tsx b/app/vmui/packages/vmui/src/components/Chart/Line/Legend/Legend.tsx
index 9cdffff43..71484ec12 100644
--- a/app/vmui/packages/vmui/src/components/Chart/Line/Legend/Legend.tsx
+++ b/app/vmui/packages/vmui/src/components/Chart/Line/Legend/Legend.tsx
@@ -1,5 +1,5 @@
 import React, { FC, useMemo } from "preact/compat";
-import { LegendItemType } from "../../../../utils/uplot/types";
+import { LegendItemType } from "../../../../types";
 import LegendItem from "./LegendItem/LegendItem";
 import Accordion from "../../../Main/Accordion/Accordion";
 import "./style.scss";
diff --git a/app/vmui/packages/vmui/src/components/Chart/Line/Legend/LegendItem/LegendItem.tsx b/app/vmui/packages/vmui/src/components/Chart/Line/Legend/LegendItem/LegendItem.tsx
index d455d98c5..2130bbfc2 100644
--- a/app/vmui/packages/vmui/src/components/Chart/Line/Legend/LegendItem/LegendItem.tsx
+++ b/app/vmui/packages/vmui/src/components/Chart/Line/Legend/LegendItem/LegendItem.tsx
@@ -1,6 +1,6 @@
 import React, { FC, useMemo } from "preact/compat";
 import { MouseEvent } from "react";
-import { LegendItemType } from "../../../../../utils/uplot/types";
+import { LegendItemType } from "../../../../../types";
 import "./style.scss";
 import classNames from "classnames";
 import { getFreeFields } from "./helpers";
diff --git a/app/vmui/packages/vmui/src/components/Chart/Line/Legend/LegendItem/helpers.ts b/app/vmui/packages/vmui/src/components/Chart/Line/Legend/LegendItem/helpers.ts
index 6fed3c686..7e6973200 100644
--- a/app/vmui/packages/vmui/src/components/Chart/Line/Legend/LegendItem/helpers.ts
+++ b/app/vmui/packages/vmui/src/components/Chart/Line/Legend/LegendItem/helpers.ts
@@ -1,4 +1,4 @@
-import { LegendItemType } from "../../../../../utils/uplot/types";
+import { LegendItemType } from "../../../../../types";
 
 export const getFreeFields = (legend: LegendItemType) => {
   const keys = Object.keys(legend.freeFormFields).filter(f => f !== "__name__");
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 d1fbc9c64..967a719d9 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
@@ -1,26 +1,33 @@
-import React, { FC, useCallback, useEffect, useRef, useState } from "preact/compat";
+import React, { FC, useEffect, useRef, useState } from "preact/compat";
 import uPlot, {
   AlignedData as uPlotData,
   Options as uPlotOptions,
   Series as uPlotSeries,
 } from "uplot";
-import { defaultOptions } from "../../../../utils/uplot/helpers";
-import { dragChart } from "../../../../utils/uplot/events";
-import { getAxes } from "../../../../utils/uplot/axes";
+import {
+  getDefaultOptions,
+  addSeries,
+  delSeries,
+  getRangeX,
+  getRangeY,
+  getScales,
+  handleDestroy,
+  getAxes,
+  setSelect
+} from "../../../../utils/uplot";
 import { MetricResult } from "../../../../api/types";
-import { dateFromSeconds, formatDateForNativeInput, limitsDurations } from "../../../../utils/time";
 import { TimeParams } from "../../../../types";
 import { YaxisState } from "../../../../state/graph/reducer";
 import "uplot/dist/uPlot.min.css";
 import "./style.scss";
 import classNames from "classnames";
-import ChartTooltip, { ChartTooltipProps } from "../ChartTooltip/ChartTooltip";
-import dayjs from "dayjs";
 import { useAppState } from "../../../../state/common/StateContext";
-import { SeriesItem } from "../../../../utils/uplot/series";
 import { ElementSize } from "../../../../hooks/useElementSize";
-import useEventListener from "../../../../hooks/useEventListener";
-import { getRangeX, getRangeY, getScales } from "../../../../utils/uplot/scales";
+import useReadyChart from "../../../../hooks/uplot/useReadyChart";
+import useZoomChart from "../../../../hooks/uplot/useZoomChart";
+import usePlotScale from "../../../../hooks/uplot/usePlotScale";
+import useLineTooltip from "../../../../hooks/uplot/useLineTooltip";
+import ChartTooltipWrapper from "../../ChartTooltip";
 
 export interface LineChartProps {
   metrics: MetricResult[];
@@ -29,7 +36,7 @@ export interface LineChartProps {
   yaxis: YaxisState;
   series: uPlotSeries[];
   unit?: string;
-  setPeriod: ({ from, to }: {from: Date, to: Date}) => void;
+  setPeriod: ({ from, to }: { from: Date, to: Date }) => void;
   layoutSize: ElementSize;
   height?: number;
 }
@@ -48,208 +55,41 @@ const LineChart: FC<LineChartProps> = ({
   const { isDarkTheme } = useAppState();
 
   const uPlotRef = useRef<HTMLDivElement>(null);
-  const [isPanning, setPanning] = useState(false);
-  const [xRange, setXRange] = useState({ min: period.start, max: period.end });
   const [uPlotInst, setUPlotInst] = useState<uPlot>();
-  const [startTouchDistance, setStartTouchDistance] = useState(0);
 
-  const [showTooltip, setShowTooltip] = useState(false);
-  const [tooltipIdx, setTooltipIdx] = useState({ seriesIdx: -1, dataIdx: -1 });
-  const [tooltipOffset, setTooltipOffset] = useState({ left: 0, top: 0 });
-  const [stickyTooltips, setStickyToolTips] = useState<ChartTooltipProps[]>([]);
-
-  const setPlotScale = ({ min, max }: { min: number, max: number }) => {
-    const delta = (max - min) * 1000;
-    if ((delta < limitsDurations.min) || (delta > limitsDurations.max)) return;
-    setXRange({ min, max });
-    setPeriod({
-      from: dayjs(min * 1000).toDate(),
-      to: dayjs(max * 1000).toDate()
-    });
-  };
-
-  const onReadyChart = (u: uPlot): void => {
-    const factor = 0.9;
-    setTooltipOffset({
-      left: parseFloat(u.over.style.left),
-      top: parseFloat(u.over.style.top)
-    });
-
-    u.over.addEventListener("mousedown", e => {
-      const { ctrlKey, metaKey, button } = e;
-      const leftClick = button === 0;
-      const leftClickWithMeta = leftClick && (ctrlKey || metaKey);
-      if (leftClickWithMeta) {
-        // drag pan
-        dragChart({ u, e, setPanning, setPlotScale, factor });
-      }
-    });
-
-    u.over.addEventListener("touchstart", e => {
-      dragChart({ u, e, setPanning, setPlotScale, factor });
-    });
-
-    u.over.addEventListener("wheel", e => {
-      if (!e.ctrlKey && !e.metaKey) return;
-      e.preventDefault();
-      const { width } = u.over.getBoundingClientRect();
-      const zoomPos = u.cursor.left && u.cursor.left > 0 ? u.cursor.left : 0;
-      const xVal = u.posToVal(zoomPos, "x");
-      const oxRange = (u.scales.x.max || 0) - (u.scales.x.min || 0);
-      const nxRange = e.deltaY < 0 ? oxRange * factor : oxRange / factor;
-      const min = xVal - (zoomPos / width) * nxRange;
-      const max = min + nxRange;
-      u.batch(() => setPlotScale({ min, max }));
-    });
-  };
-
-  const handleKeyDown = useCallback((e: KeyboardEvent) => {
-    const { target, ctrlKey, metaKey, key } = e;
-    const isInput = target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement;
-    if (!uPlotInst || isInput) return;
-    const minus = key === "-";
-    const plus = key === "+" || key === "=";
-    if ((minus || plus) && !(ctrlKey || metaKey)) {
-      e.preventDefault();
-      const factor = (xRange.max - xRange.min) / 10 * (plus ? 1 : -1);
-      setPlotScale({
-        min: xRange.min + factor,
-        max: xRange.max - factor
-      });
-    }
-  }, [uPlotInst, xRange]);
-
-  const getChartProps = useCallback(() => {
-    const { seriesIdx, dataIdx } = tooltipIdx;
-    const id = `${seriesIdx}_${dataIdx}`;
-    const metricItem = metrics[seriesIdx-1];
-    const seriesItem = series[seriesIdx] as SeriesItem;
-
-    const groups = new Set(metrics.map(m => m.group));
-    const showQueryNum = groups.size > 1;
-
-    return {
-      id,
-      unit,
-      seriesItem,
-      metricItem,
-      tooltipIdx,
-      tooltipOffset,
-      showQueryNum,
-    };
-  }, [uPlotInst, metrics, series, tooltipIdx, tooltipOffset, unit]);
-
-  const handleClick = useCallback(() => {
-    if (!showTooltip) return;
-    const props = getChartProps();
-    if (!stickyTooltips.find(t => t.id === props.id)) {
-      setStickyToolTips(prev => [...prev, props as ChartTooltipProps]);
-    }
-  }, [getChartProps, stickyTooltips, showTooltip]);
-
-  const handleUnStick = (id: string) => {
-    setStickyToolTips(prev => prev.filter(t => t.id !== id));
-  };
-
-  const setCursor = (u: uPlot) => {
-    const dataIdx = u.cursor.idx ?? -1;
-    setTooltipIdx(prev => ({ ...prev, dataIdx }));
-  };
-
-  const seriesFocus = (u: uPlot, sidx: (number | null)) => {
-    const seriesIdx = sidx ?? -1;
-    setTooltipIdx(prev => ({ ...prev, seriesIdx }));
-  };
-
-  const addSeries = (u: uPlot, series: uPlotSeries[]) => {
-    series.forEach((s) => {
-      u.addSeries(s);
-    });
-  };
-
-  const delSeries = (u: uPlot) => {
-    for (let i = u.series.length - 1; i >= 0; i--) {
-      u.delSeries(i);
-    }
-  };
-
-  const delHooks = (u: uPlot) => {
-    Object.keys(u.hooks).forEach(hook => {
-      u.hooks[hook as keyof uPlot.Hooks.Arrays] = [];
-    });
-  };
-
-  const handleDestroy = (u: uPlot) => {
-    delSeries(u);
-    delHooks(u);
-    u.setData([]);
-  };
-
-  const setSelect = (u: uPlot) => {
-    const min = u.posToVal(u.select.left, "x");
-    const max = u.posToVal(u.select.left + u.select.width, "x");
-    setPlotScale({ min, max });
-  };
+  const { xRange, setPlotScale } = usePlotScale({ period, setPeriod });
+  const { onReadyChart, isPanning } = useReadyChart(setPlotScale);
+  useZoomChart({ uPlotInst, xRange, setPlotScale });
+  const {
+    showTooltip,
+    stickyTooltips,
+    handleUnStick,
+    getTooltipProps,
+    seriesFocus,
+    setCursor,
+    resetTooltips
+  } = useLineTooltip({ u: uPlotInst, metrics, series, unit });
 
   const options: uPlotOptions = {
-    ...defaultOptions,
-    tzDate: ts => dayjs(formatDateForNativeInput(dateFromSeconds(ts))).local().toDate(),
+    ...getDefaultOptions({ width: layoutSize.width, height }),
     series,
-    axes: getAxes( [{}, { scale: "1" }], unit),
+    axes: getAxes([{}, { scale: "1" }], unit),
     scales: getScales(yaxis, xRange),
-    width: layoutSize.width || 400,
-    height: height || 500,
     hooks: {
       ready: [onReadyChart],
       setSeries: [seriesFocus],
       setCursor: [setCursor],
-      setSelect: [setSelect],
+      setSelect: [setSelect(setPlotScale)],
       destroy: [handleDestroy],
     },
   };
 
-  const handleTouchStart = (e: TouchEvent) => {
-    if (e.touches.length !== 2) return;
-    e.preventDefault();
-
-    const dx = e.touches[0].clientX - e.touches[1].clientX;
-    const dy = e.touches[0].clientY - e.touches[1].clientY;
-    setStartTouchDistance(Math.sqrt(dx * dx + dy * dy));
-  };
-
-  const handleTouchMove = useCallback((e: TouchEvent) => {
-    if (e.touches.length !== 2 || !uPlotInst) return;
-    e.preventDefault();
-
-    const dx = e.touches[0].clientX - e.touches[1].clientX;
-    const dy = e.touches[0].clientY - e.touches[1].clientY;
-    const endTouchDistance = Math.sqrt(dx * dx + dy * dy);
-    const diffDistance = startTouchDistance - endTouchDistance;
-
-    const max = (uPlotInst.scales.x.max || xRange.max);
-    const min = (uPlotInst.scales.x.min || xRange.min);
-    const dur = max - min;
-    const dir = (diffDistance > 0 ? -1 : 1);
-
-    const zoomFactor = dur / 50 * dir;
-    uPlotInst.batch(() => setPlotScale({
-      min: min + zoomFactor,
-      max: max - zoomFactor
-    }));
-  }, [uPlotInst, startTouchDistance, xRange]);
-
-  useEffect(() => {
-    setXRange({ min: period.start, max: period.end });
-  }, [period]);
-
   useEffect(() => {
-    setStickyToolTips([]);
-    setTooltipIdx({ seriesIdx: -1, dataIdx: -1 });
+    resetTooltips();
     if (!uPlotRef.current) return;
     if (uPlotInst) uPlotInst.destroy();
     const u = new uPlot(options, data, uPlotRef.current);
     setUPlotInst(u);
-    setXRange({ min: period.start, max: period.end });
     return u.destroy;
   }, [uPlotRef, isDarkTheme]);
 
@@ -287,15 +127,6 @@ const LineChart: FC<LineChartProps> = ({
     uPlotInst.redraw();
   }, [height, layoutSize]);
 
-  useEffect(() => {
-    setShowTooltip(tooltipIdx.dataIdx !== -1 && tooltipIdx.seriesIdx !== -1);
-  }, [tooltipIdx]);
-
-  useEventListener("click", handleClick);
-  useEventListener("keydown", handleKeyDown);
-  useEventListener("touchmove", handleTouchMove);
-  useEventListener("touchstart", handleTouchStart);
-
   return (
     <div
       className={classNames({
@@ -311,22 +142,12 @@ const LineChart: FC<LineChartProps> = ({
         className="vm-line-chart__u-plot"
         ref={uPlotRef}
       />
-      {uPlotInst && showTooltip && (
-        <ChartTooltip
-          {...getChartProps()}
-          u={uPlotInst}
-        />
-      )}
-
-      {uPlotInst && stickyTooltips.map(t => (
-        <ChartTooltip
-          {...t}
-          isSticky
-          u={uPlotInst}
-          key={t.id}
-          onClose={handleUnStick}
-        />
-      ))}
+      <ChartTooltipWrapper
+        showTooltip={showTooltip}
+        tooltipProps={getTooltipProps()}
+        stickyTooltips={stickyTooltips}
+        handleUnStick={handleUnStick}
+      />
     </div>
   );
 };
diff --git a/app/vmui/packages/vmui/src/components/Main/Button/style.scss b/app/vmui/packages/vmui/src/components/Main/Button/style.scss
index 0a0bc1e2b..24360b758 100644
--- a/app/vmui/packages/vmui/src/components/Main/Button/style.scss
+++ b/app/vmui/packages/vmui/src/components/Main/Button/style.scss
@@ -45,6 +45,10 @@ $button-radius: 6px;
     transform: translateZ(-1px);
   }
 
+  &:active:after {
+    transform: scale(0.9);
+  }
+
   span {
     display: grid;
     align-items: center;
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 82ef532c2..6e96b3cee 100644
--- a/app/vmui/packages/vmui/src/components/Main/Icons/index.tsx
+++ b/app/vmui/packages/vmui/src/components/Main/Icons/index.tsx
@@ -291,6 +291,17 @@ export const VisibilityOffIcon = () => (
   </svg>
 );
 
+export const Prettify = () => (
+  <svg
+    viewBox="0 0 24 24"
+    fill="currentColor"
+  >
+    <path
+      d="M19 9l1.25-2.75L23 5l-2.75-1.25L19 1l-1.25 2.75L15 5l2.75 1.25L19 9zm-7.5.5L9 4 6.5 9.5 1 12l5.5 2.5L9 20l2.5-5.5L17 12l-5.5-2.5zM19 15l-1.25 2.75L15 19l2.75 1.25L19 23l1.25-2.75L23 19l-2.75-1.25L19 15z"
+    ></path>
+  </svg>
+);
+
 export const CopyIcon = () => (
   <svg
     viewBox="0 0 24 24"
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 2bbb027a2..e53e88ddb 100644
--- a/app/vmui/packages/vmui/src/components/Views/GraphView/GraphView.tsx
+++ b/app/vmui/packages/vmui/src/components/Views/GraphView/GraphView.tsx
@@ -8,11 +8,12 @@ import {
   getHideSeries,
   getLegendItem,
   getSeriesItemContext,
-  SeriesItem
-} from "../../../utils/uplot/series";
-import { getLimitsYAxis, getMinMaxBuffer, getTimeSeries } from "../../../utils/uplot/axes";
-import { LegendItemType } from "../../../utils/uplot/types";
-import { TimeParams } from "../../../types";
+  normalizeData,
+  getLimitsYAxis,
+  getMinMaxBuffer,
+  getTimeSeries,
+} from "../../../utils/uplot";
+import { TimeParams, SeriesItem, LegendItemType } from "../../../types";
 import { AxisRange, YaxisState } from "../../../state/graph/reducer";
 import { getAvgFromArray, getMaxFromArray, getMinFromArray } from "../../../utils/math";
 import classNames from "classnames";
@@ -20,10 +21,9 @@ import { useTimeState } from "../../../state/time/TimeStateContext";
 import HeatmapChart from "../../Chart/Heatmap/HeatmapChart/HeatmapChart";
 import "./style.scss";
 import { promValueToNumber } from "../../../utils/metric";
-import { normalizeData } from "../../../utils/uplot/heatmap";
 import useDeviceDetect from "../../../hooks/useDeviceDetect";
-import { TooltipHeatmapProps } from "../../Chart/Heatmap/ChartTooltipHeatmap/ChartTooltipHeatmap";
 import useElementSize from "../../../hooks/useElementSize";
+import { ChartTooltipProps } from "../../Chart/ChartTooltip/ChartTooltip";
 
 export interface GraphViewProps {
   data?: MetricResult[];
@@ -35,7 +35,7 @@ export interface GraphViewProps {
   unit?: string;
   showLegend?: boolean;
   setYaxisLimits: (val: AxisRange) => void
-  setPeriod: ({ from, to }: {from: Date, to: Date}) => void
+  setPeriod: ({ from, to }: { from: Date, to: Date }) => void
   fullWidth?: boolean
   height?: number
   isHistogram?: boolean
@@ -48,7 +48,7 @@ const GraphView: FC<GraphViewProps> = ({
   query,
   yaxis,
   unit,
-  showLegend= true,
+  showLegend = true,
   setYaxisLimits,
   setPeriod,
   alias = [],
@@ -66,13 +66,13 @@ const GraphView: FC<GraphViewProps> = ({
   const [series, setSeries] = useState<uPlotSeries[]>([]);
   const [legend, setLegend] = useState<LegendItemType[]>([]);
   const [hideSeries, setHideSeries] = useState<string[]>([]);
-  const [legendValue, setLegendValue] = useState<TooltipHeatmapProps | null>(null);
+  const [legendValue, setLegendValue] = useState<ChartTooltipProps | null>(null);
 
   const getSeriesItem = useMemo(() => {
     return getSeriesItemContext(data, hideSeries, alias);
   }, [data, hideSeries, alias]);
 
-  const setLimitsYaxis = (values: {[key: string]: number[]}) => {
+  const setLimitsYaxis = (values: { [key: string]: number[] }) => {
     const limits = getLimitsYAxis(values, !isHistogram);
     setYaxisLimits(limits);
   };
@@ -105,7 +105,7 @@ const GraphView: FC<GraphViewProps> = ({
 
   useEffect(() => {
     const tempTimes: number[] = [];
-    const tempValues: {[key: string]: number[]} = {};
+    const tempValues: { [key: string]: number[] } = {};
     const tempLegend: LegendItemType[] = [];
     const tempSeries: uPlotSeries[] = [{}];
 
@@ -199,7 +199,6 @@ const GraphView: FC<GraphViewProps> = ({
           data={dataChart}
           metrics={data}
           period={period}
-          yaxis={yaxis}
           unit={unit}
           setPeriod={setPeriod}
           layoutSize={containerSize}
diff --git a/app/vmui/packages/vmui/src/hooks/uplot/useDragChart.ts b/app/vmui/packages/vmui/src/hooks/uplot/useDragChart.ts
new file mode 100644
index 000000000..12f3ed153
--- /dev/null
+++ b/app/vmui/packages/vmui/src/hooks/uplot/useDragChart.ts
@@ -0,0 +1,65 @@
+import { useRef } from "preact/compat";
+import uPlot from "uplot";
+import { SetMinMax } from "../../types";
+
+interface DragHookArgs {
+  dragSpeed: number,
+  setPanning: (enable: boolean) => void,
+  setPlotScale: SetMinMax
+}
+
+interface DragArgs {
+  e: MouseEvent | TouchEvent,
+  u: uPlot,
+}
+
+const isMouseEvent = (e: MouseEvent | TouchEvent): e is MouseEvent => e instanceof MouseEvent;
+const getClientX = (e: MouseEvent | TouchEvent) => isMouseEvent(e) ? e.clientX : e.touches[0].clientX;
+
+const useDragChart = ({ dragSpeed = 0.85, setPanning, setPlotScale }: DragHookArgs) => {
+  const dragState = useRef({
+    leftStart: 0,
+    xUnitsPerPx: 0,
+    scXMin: 0,
+    scXMax: 0,
+  });
+
+  const mouseMove = (e: MouseEvent | TouchEvent) => {
+    e.preventDefault();
+    const clientX = getClientX(e);
+    const { leftStart, xUnitsPerPx, scXMin, scXMax } = dragState.current;
+    const dx = xUnitsPerPx * ((clientX - leftStart) * dragSpeed);
+    setPlotScale({ min: scXMin - dx, max: scXMax - dx });
+  };
+
+  const mouseUp = () => {
+    setPanning(false);
+    document.removeEventListener("mousemove", mouseMove);
+    document.removeEventListener("mouseup", mouseUp);
+    document.removeEventListener("touchmove", mouseMove);
+    document.removeEventListener("touchend", mouseUp);
+  };
+
+  const mouseDown = () => {
+    document.addEventListener("mousemove", mouseMove);
+    document.addEventListener("mouseup", mouseUp);
+    document.addEventListener("touchmove", mouseMove);
+    document.addEventListener("touchend", mouseUp);
+  };
+
+  return ({ e, u }: DragArgs): void => {
+    e.preventDefault();
+    setPanning(true);
+
+    dragState.current = {
+      leftStart: getClientX(e),
+      xUnitsPerPx: u.posToVal(1, "x") - u.posToVal(0, "x"),
+      scXMin: u.scales.x.min || 0,
+      scXMax: u.scales.x.max || 0,
+    };
+
+    mouseDown();
+  };
+};
+
+export default useDragChart;
diff --git a/app/vmui/packages/vmui/src/hooks/uplot/useHeatmapTooltip.ts b/app/vmui/packages/vmui/src/hooks/uplot/useHeatmapTooltip.ts
new file mode 100644
index 000000000..b6d6e7bca
--- /dev/null
+++ b/app/vmui/packages/vmui/src/hooks/uplot/useHeatmapTooltip.ts
@@ -0,0 +1,82 @@
+import uPlot from "uplot";
+import { useCallback, useState } from "preact/compat";
+import { ChartTooltipProps } from "../../components/Chart/ChartTooltip/ChartTooltip";
+import dayjs from "dayjs";
+import { DATE_FULL_TIMEZONE_FORMAT } from "../../constants/date";
+import { MetricResult } from "../../api/types";
+import useEventListener from "../useEventListener";
+import get from "lodash.get";
+
+interface LineTooltipHook {
+  u?: uPlot;
+  metrics: MetricResult[];
+  unit?: string;
+}
+
+const useLineTooltip = ({ u, metrics, unit }: LineTooltipHook) => {
+  const [point, setPoint] = useState({ left: 0, top: 0 });
+  const [stickyTooltips, setStickyToolTips] = useState<ChartTooltipProps[]>([]);
+
+  const resetTooltips = () => {
+    setStickyToolTips([]);
+    setPoint({ left: 0, top: 0 });
+  };
+
+  const setCursor = (u: uPlot) => {
+    const left = u.cursor.left || 0;
+    const top = u.cursor.top || 0;
+    setPoint({ left, top });
+  };
+
+  const getTooltipProps = useCallback((): ChartTooltipProps => {
+    const { left, top } = point;
+    const xArr = (get(u, ["data", 1, 0], []) || []) as number[];
+    const xVal = u ? u.posToVal(left, "x") : 0;
+    const yVal = u ? u.posToVal(top, "y") : 0;
+    const xIdx = xArr.findIndex((t, i) => xVal >= t && xVal < xArr[i + 1]) || -1;
+    const second = xArr[xIdx + 1];
+    const result = metrics[Math.round(yVal)] || { values: [] };
+
+    const [endTime = 0, value = ""] = result.values.find(v => v[0] === second) || [];
+    const startTime = xArr[xIdx];
+    const startDate = dayjs(startTime * 1000).tz().format(DATE_FULL_TIMEZONE_FORMAT);
+    const endDate = dayjs(endTime * 1000).tz().format(DATE_FULL_TIMEZONE_FORMAT);
+
+    const bucket = result?.metric?.vmrange || "";
+
+    return {
+      unit,
+      point,
+      u: u,
+      id: `${bucket}_${startDate}`,
+      dates: [startDate, endDate],
+      value: `${value}%`,
+      info: bucket,
+      show: +value > 0
+    };
+  }, [u, point, metrics, unit]);
+
+  const handleClick = useCallback(() => {
+    const props = getTooltipProps();
+    if (!props.show) return;
+    if (!stickyTooltips.find(t => t.id === props.id)) {
+      setStickyToolTips(prev => [...prev, props]);
+    }
+  }, [getTooltipProps, stickyTooltips]);
+
+  const handleUnStick = (id: string) => {
+    setStickyToolTips(prev => prev.filter(t => t.id !== id));
+  };
+
+  useEventListener("click", handleClick);
+
+  return {
+    stickyTooltips,
+    handleUnStick,
+    getTooltipProps,
+    setCursor,
+    resetTooltips
+  };
+};
+
+export default useLineTooltip;
diff --git a/app/vmui/packages/vmui/src/hooks/uplot/useLineTooltip.ts b/app/vmui/packages/vmui/src/hooks/uplot/useLineTooltip.ts
new file mode 100644
index 000000000..ac2efb3bb
--- /dev/null
+++ b/app/vmui/packages/vmui/src/hooks/uplot/useLineTooltip.ts
@@ -0,0 +1,101 @@
+import uPlot, { Series as uPlotSeries } from "uplot";
+import { useCallback, useEffect, useState } from "preact/compat";
+import { ChartTooltipProps } from "../../components/Chart/ChartTooltip/ChartTooltip";
+import { SeriesItem } from "../../types";
+import get from "lodash.get";
+import dayjs from "dayjs";
+import { DATE_FULL_TIMEZONE_FORMAT } from "../../constants/date";
+import { formatPrettyNumber, getMetricName } from "../../utils/uplot";
+import { MetricResult } from "../../api/types";
+import useEventListener from "../useEventListener";
+
+interface LineTooltipHook {
+  u?: uPlot;
+  metrics: MetricResult[];
+  series: uPlotSeries[];
+  unit?: string;
+}
+
+const useLineTooltip = ({ u, metrics, series, unit }: LineTooltipHook) => {
+  const [showTooltip, setShowTooltip] = useState(false);
+  const [tooltipIdx, setTooltipIdx] = useState({ seriesIdx: -1, dataIdx: -1 });
+  const [stickyTooltips, setStickyToolTips] = useState<ChartTooltipProps[]>([]);
+
+  const resetTooltips = () => {
+    setStickyToolTips([]);
+    setTooltipIdx({ seriesIdx: -1, dataIdx: -1 });
+  };
+
+  const setCursor = (u: uPlot) => {
+    const dataIdx = u.cursor.idx ?? -1;
+    setTooltipIdx(prev => ({ ...prev, dataIdx }));
+  };
+
+  const seriesFocus = (u: uPlot, sidx: (number | null)) => {
+    const seriesIdx = sidx ?? -1;
+    setTooltipIdx(prev => ({ ...prev, seriesIdx }));
+  };
+
+  const getTooltipProps = useCallback((): ChartTooltipProps => {
+    const { seriesIdx, dataIdx } = tooltipIdx;
+    const metricItem = metrics[seriesIdx - 1];
+    const seriesItem = series[seriesIdx] as SeriesItem;
+
+    const groups = new Set(metrics.map(m => m.group));
+    const group = metricItem?.group || 0;
+
+    const value = get(u, ["data", seriesIdx, dataIdx], 0);
+    const min = get(u, ["scales", "1", "min"], 0);
+    const max = get(u, ["scales", "1", "max"], 1);
+
+    const date = get(u, ["data", 0, dataIdx], 0);
+
+    const point = {
+      top: u ? u.valToPos((value || 0), seriesItem?.scale || "1") : 0,
+      left: u ? u.valToPos(date, "x") : 0,
+    };
+
+    return {
+      unit,
+      point,
+      u: u,
+      id: `${seriesIdx}_${dataIdx}`,
+      title: groups.size > 1 ? `Query ${group}` : "",
+      dates: [date ? dayjs(date * 1000).tz().format(DATE_FULL_TIMEZONE_FORMAT) : "-"],
+      value: formatPrettyNumber(value, min, max),
+      info: getMetricName(metricItem),
+      stats: seriesItem?.calculations,
+      marker: `${seriesItem?.stroke}`,
+    };
+  }, [u, tooltipIdx, metrics, series, unit]);
+
+  const handleClick = useCallback(() => {
+    if (!showTooltip) return;
+    const props = getTooltipProps();
+    if (!stickyTooltips.find(t => t.id === props.id)) {
+      setStickyToolTips(prev => [...prev, props]);
+    }
+  }, [getTooltipProps, stickyTooltips, showTooltip]);
+
+  const handleUnStick = (id: string) => {
+    setStickyToolTips(prev => prev.filter(t => t.id !== id));
+  };
+
+  useEffect(() => {
+    setShowTooltip(tooltipIdx.dataIdx !== -1 && tooltipIdx.seriesIdx !== -1);
+  }, [tooltipIdx]);
+
+  useEventListener("click", handleClick);
+
+  return {
+    showTooltip,
+    stickyTooltips,
+    handleUnStick,
+    getTooltipProps,
+    seriesFocus,
+    setCursor,
+    resetTooltips,
+  };
+};
+
+export default useLineTooltip;
diff --git a/app/vmui/packages/vmui/src/hooks/uplot/usePlotScale.ts b/app/vmui/packages/vmui/src/hooks/uplot/usePlotScale.ts
new file mode 100644
index 000000000..cd3aac35c
--- /dev/null
+++ b/app/vmui/packages/vmui/src/hooks/uplot/usePlotScale.ts
@@ -0,0 +1,34 @@
+import { MinMax } from "../../types";
+import { limitsDurations } from "../../utils/time";
+import { useEffect, useState } from "preact/compat";
+import { TimeParams } from "../../types";
+import dayjs from "dayjs";
+
+interface PlotScaleHook {
+  setPeriod: ({ from, to }: { from: Date, to: Date }) => void;
+  period: TimeParams;
+}
+
+const usePlotScale = ({ period, setPeriod }: PlotScaleHook) => {
+  const [xRange, setXRange] = useState({ min: period.start, max: period.end });
+
+  const setPlotScale = ({ min, max }: MinMax) => {
+    const delta = (max - min) * 1000;
+    if ((delta < limitsDurations.min) || (delta > limitsDurations.max)) return;
+    setPeriod({
+      from: dayjs(min * 1000).toDate(),
+      to: dayjs(max * 1000).toDate()
+    });
+  };
+
+  useEffect(() => {
+    setXRange({ min: period.start, max: period.end });
+  }, [period]);
+
+  return {
+    xRange,
+    setPlotScale
+  };
+};
+
+export default usePlotScale;
diff --git a/app/vmui/packages/vmui/src/hooks/uplot/useReadyChart.ts b/app/vmui/packages/vmui/src/hooks/uplot/useReadyChart.ts
new file mode 100644
index 000000000..4388183e9
--- /dev/null
+++ b/app/vmui/packages/vmui/src/hooks/uplot/useReadyChart.ts
@@ -0,0 +1,53 @@
+import { useState } from "preact/compat";
+import uPlot from "uplot";
+import useDragChart from "./useDragChart";
+import { SetMinMax } from "../../types";
+
+const isLiftClickWithMeta = (e: MouseEvent) => {
+  const { ctrlKey, metaKey, button } = e;
+  const leftClick = button === 0;
+  return leftClick && (ctrlKey || metaKey);
+};
+
+// coefficient for drag speed; the higher the value, the faster the graph moves.
+const dragSpeed = 0.9;
+
+const useReadyChart = (setPlotScale: SetMinMax) => {
+  const [isPanning, setPanning] = useState(false);
+
+  const dragChart = useDragChart({ dragSpeed, setPanning, setPlotScale });
+
+  const onReadyChart = (u: uPlot): void => {
+    const handleInteractionStart = (e: MouseEvent | TouchEvent) => {
+      const dragByMouse = e instanceof MouseEvent && isLiftClickWithMeta(e);
+      const dragByTouch = e instanceof TouchEvent && e.touches.length > 1;
+      if (dragByMouse || dragByTouch) {
+        dragChart({ u, e });
+      }
+    };
+
+    const handleWheel = (e: WheelEvent) => {
+      if (!e.ctrlKey && !e.metaKey) return;
+      e.preventDefault();
+      const { width } = u.over.getBoundingClientRect();
+      const zoomPos = u.cursor.left && u.cursor.left > 0 ? u.cursor.left : 0;
+      const xVal = u.posToVal(zoomPos, "x");
+      const oxRange = (u.scales.x.max || 0) - (u.scales.x.min || 0);
+      const nxRange = e.deltaY < 0 ? oxRange * dragSpeed : oxRange / dragSpeed;
+      const min = xVal - (zoomPos / width) * nxRange;
+      const max = min + nxRange;
+      u.batch(() => setPlotScale({ min, max }));
+    };
+
+    u.over.addEventListener("mousedown", handleInteractionStart);
+    u.over.addEventListener("touchstart", handleInteractionStart);
+    u.over.addEventListener("wheel", handleWheel);
+  };
+
+  return {
+    onReadyChart,
+    isPanning,
+  };
+};
+
+export default useReadyChart;
diff --git a/app/vmui/packages/vmui/src/hooks/uplot/useZoomChart.ts b/app/vmui/packages/vmui/src/hooks/uplot/useZoomChart.ts
new file mode 100644
index 000000000..bddb870fb
--- /dev/null
+++ b/app/vmui/packages/vmui/src/hooks/uplot/useZoomChart.ts
@@ -0,0 +1,66 @@
+import { useCallback, useState } from "preact/compat";
+import useEventListener from "../useEventListener";
+import { MinMax, SetMinMax } from "../../types";
+
+interface ZoomChartHook {
+  uPlotInst?: uPlot;
+  xRange: MinMax;
+  setPlotScale: SetMinMax;
+}
+
+const calculateDistance = (touches: TouchList) => {
+  const dx = touches[0].clientX - touches[1].clientX;
+  const dy = touches[0].clientY - touches[1].clientY;
+  return Math.sqrt(dx * dx + dy * dy);
+};
+
+const useZoomChart = ({ uPlotInst, xRange, setPlotScale }: ZoomChartHook) => {
+  const [startTouchDistance, setStartTouchDistance] = useState(0);
+
+  const handleKeyDown = useCallback((e: KeyboardEvent) => {
+    const { target, ctrlKey, metaKey, key } = e;
+    const isInput = target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement;
+    if (!uPlotInst || isInput) return;
+
+    const isPlus = key === "+" || key === "=";
+    const isMinus = key === "-";
+    const isNotControlKey = !(ctrlKey || metaKey);
+
+    if ((isMinus || isPlus) && isNotControlKey) {
+      e.preventDefault();
+      const factor = (xRange.max - xRange.min) / 10 * (isPlus ? 1 : -1);
+      setPlotScale({ min: xRange.min + factor, max: xRange.max - factor });
+    }
+  }, [uPlotInst, xRange]);
+
+  const handleTouchStart = (e: TouchEvent) => {
+    if (e.touches.length === 2) {
+      e.preventDefault();
+      setStartTouchDistance(calculateDistance(e.touches));
+    }
+  };
+
+  const handleTouchMove = useCallback((e: TouchEvent) => {
+    if (!uPlotInst || e.touches.length !== 2) return;
+    e.preventDefault();
+
+    const endTouchDistance = calculateDistance(e.touches);
+    const diffDistance = startTouchDistance - endTouchDistance;
+
+    const max = (uPlotInst.scales.x.max || xRange.max);
+    const min = (uPlotInst.scales.x.min || xRange.min);
+    const dur = max - min;
+    const dir = (diffDistance > 0 ? -1 : 1);
+
+    const zoomFactor = dur / 50 * dir;
+    uPlotInst.batch(() => setPlotScale({ min: min + zoomFactor, max: max - zoomFactor }));
+  }, [uPlotInst, startTouchDistance, xRange]);
+
+  useEventListener("keydown", handleKeyDown);
+  useEventListener("touchmove", handleTouchMove);
+  useEventListener("touchstart", handleTouchStart);
+
+  return null;
+};
+
+export default useZoomChart;
diff --git a/app/vmui/packages/vmui/src/hooks/useFetchQuery.ts b/app/vmui/packages/vmui/src/hooks/useFetchQuery.ts
index 7576ecda2..be014a298 100644
--- a/app/vmui/packages/vmui/src/hooks/useFetchQuery.ts
+++ b/app/vmui/packages/vmui/src/hooks/useFetchQuery.ts
@@ -1,4 +1,4 @@
-import { useCallback, useEffect, useMemo, useState } from "preact/compat";
+import { StateUpdater, useCallback, useEffect, useMemo, useState } from "preact/compat";
 import { getQueryRangeUrl, getQueryUrl } from "../api/query-range";
 import { useAppState } from "../state/common/StateContext";
 import { InstantMetricResult, MetricBase, MetricResult, QueryStats } from "../api/types";
@@ -28,6 +28,7 @@ interface FetchQueryReturn {
   liveData?: InstantMetricResult[],
   error?: ErrorTypes | string,
   queryErrors: (ErrorTypes | string)[],
+  setQueryErrors: StateUpdater<string[]>,
   queryStats: QueryStats[],
   warning?: string,
   traces?: Trace[],
@@ -62,12 +63,13 @@ export const useFetchQuery = ({
   const [liveData, setLiveData] = useState<InstantMetricResult[]>();
   const [traces, setTraces] = useState<Trace[]>();
   const [error, setError] = useState<ErrorTypes | string>();
-  const [queryErrors, setQueryErrors] = useState<(ErrorTypes | string)[]>([]);
+  const [queryErrors, setQueryErrors] = useState<string[]>([]);
   const [queryStats, setQueryStats] = useState<QueryStats[]>([]);
   const [warning, setWarning] = useState<string>();
   const [fetchQueue, setFetchQueue] = useState<AbortController[]>([]);
   const [isHistogram, setIsHistogram] = useState(false);
 
+
   const fetchData = async ({
     fetchUrl,
     fetchQueue,
@@ -81,11 +83,13 @@ export const useFetchQuery = ({
     setFetchQueue([...fetchQueue, controller]);
     try {
       const isDisplayChart = displayType === "chart";
-      let seriesLimit = showAllSeries ? Infinity : (+stateSeriesLimits[displayType] || Infinity);
+      const defaultLimit = showAllSeries ? Infinity : (+stateSeriesLimits[displayType] || Infinity);
+      let seriesLimit = defaultLimit;
       const tempData: MetricBase[] = [];
       const tempTraces: Trace[] = [];
       let counter = 1;
       let totalLength = 0;
+      let isHistogramResult = false;
 
       for await (const url of fetchUrl) {
 
@@ -113,9 +117,8 @@ export const useFetchQuery = ({
             tempTraces.push(trace);
           }
 
-          const isHistogramResult = isDisplayChart && isHistogramData(resp.data.result);
-          if (resp.data.result.length) setIsHistogram(isHistogramResult);
-          if (isHistogramResult) seriesLimit = Infinity;
+          isHistogramResult = isDisplayChart && isHistogramData(resp.data.result);
+          seriesLimit = isHistogramResult ? Infinity : Math.max(totalLength, defaultLimit);
           const freeTempSize = seriesLimit - tempData.length;
           resp.data.result.slice(0, freeTempSize).forEach((d: MetricBase) => {
             d.group = counter;
@@ -134,6 +137,7 @@ export const useFetchQuery = ({
       setWarning(totalLength > seriesLimit ? limitText : "");
       isDisplayChart ? setGraphData(tempData as MetricResult[]) : setLiveData(tempData as InstantMetricResult[]);
       setTraces(tempTraces);
+      setIsHistogram(totalLength ? isHistogramResult: false);
     } catch (e) {
       if (e instanceof Error && e.name !== "AbortError") {
         setError(`${e.name}: ${e.message}`);
@@ -193,5 +197,5 @@ export const useFetchQuery = ({
     setFetchQueue(fetchQueue.filter(f => !f.signal.aborted));
   }, [fetchQueue]);
 
-  return { fetchUrl, isLoading, graphData, liveData, error, queryErrors, queryStats, warning, traces, isHistogram };
+  return { fetchUrl, isLoading, graphData, liveData, error, queryErrors, setQueryErrors, queryStats, warning, traces, isHistogram };
 };
diff --git a/app/vmui/packages/vmui/src/pages/CustomPanel/QueryConfigurator/QueryConfigurator.tsx b/app/vmui/packages/vmui/src/pages/CustomPanel/QueryConfigurator/QueryConfigurator.tsx
index 23e581a22..b35cbc5e4 100644
--- a/app/vmui/packages/vmui/src/pages/CustomPanel/QueryConfigurator/QueryConfigurator.tsx
+++ b/app/vmui/packages/vmui/src/pages/CustomPanel/QueryConfigurator/QueryConfigurator.tsx
@@ -1,12 +1,18 @@
-import React, { FC, useState, useEffect } from "preact/compat";
+import React, { FC, StateUpdater, useEffect, useState } from "preact/compat";
 import QueryEditor from "../../../components/Configurators/QueryEditor/QueryEditor";
 import AdditionalSettings from "../../../components/Configurators/AdditionalSettings/AdditionalSettings";
-import { ErrorTypes } from "../../../types";
 import usePrevious from "../../../hooks/usePrevious";
 import { MAX_QUERY_FIELDS } from "../../../constants/graph";
 import { useQueryDispatch, useQueryState } from "../../../state/query/QueryStateContext";
 import { useTimeDispatch } from "../../../state/time/TimeStateContext";
-import { DeleteIcon, PlayIcon, PlusIcon, VisibilityIcon, VisibilityOffIcon } from "../../../components/Main/Icons";
+import {
+  DeleteIcon,
+  PlayIcon,
+  PlusIcon,
+  Prettify,
+  VisibilityIcon,
+  VisibilityOffIcon
+} from "../../../components/Main/Icons";
 import Button from "../../../components/Main/Button/Button";
 import "./style.scss";
 import Tooltip from "../../../components/Main/Tooltip/Tooltip";
@@ -15,9 +21,12 @@ import { MouseEvent as ReactMouseEvent } from "react";
 import { arrayEquals } from "../../../utils/array";
 import useDeviceDetect from "../../../hooks/useDeviceDetect";
 import { QueryStats } from "../../../api/types";
+import { usePrettifyQuery } from "./hooks/usePrettifyQuery";
 
 export interface QueryConfiguratorProps {
-  errors: (ErrorTypes | string)[];
+  queryErrors: string[];
+  setQueryErrors: StateUpdater<string[]>;
+  setHideError: StateUpdater<boolean>;
   stats: QueryStats[];
   queryOptions: string[]
   onHideQuery: (queries: number[]) => void
@@ -25,12 +34,15 @@ export interface QueryConfiguratorProps {
 }
 
 const QueryConfigurator: FC<QueryConfiguratorProps> = ({
-  errors,
+  queryErrors,
+  setQueryErrors,
+  setHideError,
   stats,
   queryOptions,
   onHideQuery,
   onRunQuery
 }) => {
+
   const { isMobile } = useDeviceDetect();
 
   const { query, queryHistory, autocomplete } = useQueryState();
@@ -41,6 +53,8 @@ const QueryConfigurator: FC<QueryConfiguratorProps> = ({
   const [hideQuery, setHideQuery] = useState<number[]>([]);
   const prevStateQuery = usePrevious(stateQuery) as (undefined | string[]);
 
+  const getPrettifiedQuery = usePrettifyQuery();
+
   const updateHistory = () => {
     queryDispatch({
       type: "SET_QUERY_HISTORY", payload: stateQuery.map((q, i) => {
@@ -106,13 +120,25 @@ const QueryConfigurator: FC<QueryConfiguratorProps> = ({
 
   const createHandlerRemoveQuery = (i: number) => () => {
     handleRemoveQuery(i);
-    setHideQuery(prev => prev.includes(i) ? prev.filter(n => n !== i) : prev.map(n => n > i ? n - 1: n));
+    setHideQuery(prev => prev.includes(i) ? prev.filter(n => n !== i) : prev.map(n => n > i ? n - 1 : n));
   };
 
   const createHandlerHideQuery = (i: number) => (e: ReactMouseEvent<HTMLButtonElement, MouseEvent>) => {
     handleToggleHideQuery(e, i);
   };
 
+  const handlePrettifyQuery = async (i:number) => {
+    const prettyQuery = await getPrettifiedQuery(stateQuery[i]);
+    setHideError(false);
+
+    handleChangeQuery(prettyQuery.query, i);
+
+    setQueryErrors((qe) => {
+      qe[i] = prettyQuery.error;
+      return [...qe];
+    });
+  };
+
   useEffect(() => {
     if (prevStateQuery && (stateQuery.length < prevStateQuery.length)) {
       handleRunQuery();
@@ -144,7 +170,7 @@ const QueryConfigurator: FC<QueryConfiguratorProps> = ({
             value={stateQuery[i]}
             autocomplete={autocomplete}
             options={queryOptions}
-            error={errors[i]}
+            error={queryErrors[i]}
             stats={stats[i]}
             onArrowUp={createHandlerArrow(-1, i)}
             onArrowDown={createHandlerArrow(1, i)}
@@ -163,6 +189,19 @@ const QueryConfigurator: FC<QueryConfiguratorProps> = ({
               />
             </div>
           </Tooltip>
+
+          <Tooltip title={"Prettify query"}>
+            <div className="vm-query-configurator-list-row__button">
+              <Button
+                variant={"text"}
+                color={"gray"}
+                startIcon={<Prettify/>}
+                onClick={async () => await handlePrettifyQuery(i)}
+                className="prettify"
+              />
+            </div>
+          </Tooltip>
+
           {stateQuery.length > 1 && (
             <Tooltip title="Remove Query">
               <div className="vm-query-configurator-list-row__button">
diff --git a/app/vmui/packages/vmui/src/pages/CustomPanel/QueryConfigurator/hooks/usePrettifyQuery.ts b/app/vmui/packages/vmui/src/pages/CustomPanel/QueryConfigurator/hooks/usePrettifyQuery.ts
new file mode 100644
index 000000000..da6589fe7
--- /dev/null
+++ b/app/vmui/packages/vmui/src/pages/CustomPanel/QueryConfigurator/hooks/usePrettifyQuery.ts
@@ -0,0 +1,49 @@
+import { useAppState } from "../../../../state/common/StateContext";
+
+export interface PrettyQuery {
+  query: string;
+  error: string;
+}
+
+export const usePrettifyQuery = () => {
+  const { serverUrl } = useAppState();
+
+  const getPrettifiedQuery = async (query: string): Promise<PrettyQuery> => {
+    try {
+      const oldQuery = encodeURIComponent(query);
+      const fetchUrl = `${serverUrl}/prettify-query?query=${oldQuery}`;
+
+      // {"status": "success", "query": "metrics"}
+      // {"status": "error", "msg": "labelFilterExpr: unexpected token ..."}
+      const response = await fetch(fetchUrl);
+
+      if (response.status != 200) {
+        return {
+          query: query,
+          error: "Error requesting /prettify-query, status: " + response.status,
+        };
+      }
+
+      const data = await response.json();
+
+      if (data["status"] != "success") {
+        return {
+          query: query,
+          error: String(data.msg) };
+      }
+
+      return {
+        query: String(data.query),
+        error: "" };
+
+    } catch (e) {
+      console.error(e);
+      if (e instanceof Error && e.name !== "AbortError") {
+        return { query: query, error: `${e.name}: ${e.message}` };
+      }
+      return { query: query, error: String(e) };
+    }
+  };
+
+  return getPrettifiedQuery;
+};
diff --git a/app/vmui/packages/vmui/src/pages/CustomPanel/QueryConfigurator/style.scss b/app/vmui/packages/vmui/src/pages/CustomPanel/QueryConfigurator/style.scss
index fb39cfb0a..a6a564da4 100644
--- a/app/vmui/packages/vmui/src/pages/CustomPanel/QueryConfigurator/style.scss
+++ b/app/vmui/packages/vmui/src/pages/CustomPanel/QueryConfigurator/style.scss
@@ -9,7 +9,7 @@
 
     &-row {
       display: grid;
-      grid-template-columns: 1fr auto auto;
+      grid-template-columns: 1fr auto auto auto;
       align-items: center;
       gap: $padding-small;
 
diff --git a/app/vmui/packages/vmui/src/pages/CustomPanel/hooks/useSetQueryParams.ts b/app/vmui/packages/vmui/src/pages/CustomPanel/hooks/useSetQueryParams.ts
index 62f235f2c..0b3f22feb 100644
--- a/app/vmui/packages/vmui/src/pages/CustomPanel/hooks/useSetQueryParams.ts
+++ b/app/vmui/packages/vmui/src/pages/CustomPanel/hooks/useSetQueryParams.ts
@@ -6,7 +6,7 @@ import { useQueryState } from "../../../state/query/QueryStateContext";
 import { displayTypeTabs } from "../DisplayTypeSwitch";
 import { compactObject } from "../../../utils/object";
 import { useGraphState } from "../../../state/graph/GraphStateContext";
-import useSearchParamsFromObject from "../../../hooks/useSearchParamsFromObject";
+import { useSearchParams } from "react-router-dom";
 
 export const useSetQueryParams = () => {
   const { tenantId } = useAppState();
@@ -14,7 +14,7 @@ export const useSetQueryParams = () => {
   const { query } = useQueryState();
   const { duration, relativeTime, period: { date, step } } = useTimeState();
   const { customStep } = useGraphState();
-  const { setSearchParamsFromKeys } = useSearchParamsFromObject();
+  const [, setSearchParams] = useSearchParams();
 
   const setSearchParamsFromState = () => {
     const params: Record<string, unknown> = {};
@@ -30,7 +30,7 @@ export const useSetQueryParams = () => {
       if ((step !== customStep) && customStep) params[`${group}.step_input`] = customStep;
     });
 
-    setSearchParamsFromKeys(compactObject(params) as Record<string, string>);
+    setSearchParams(compactObject(params) as Record<string, string>);
   };
 
   useEffect(setSearchParamsFromState, [tenantId, displayType, query, duration, relativeTime, date, step, customStep]);
diff --git a/app/vmui/packages/vmui/src/pages/CustomPanel/index.tsx b/app/vmui/packages/vmui/src/pages/CustomPanel/index.tsx
index 4e169c1e8..fb170f9bd 100644
--- a/app/vmui/packages/vmui/src/pages/CustomPanel/index.tsx
+++ b/app/vmui/packages/vmui/src/pages/CustomPanel/index.tsx
@@ -57,6 +57,7 @@ const CustomPanel: FC = () => {
     graphData,
     error,
     queryErrors,
+    setQueryErrors,
     queryStats,
     warning,
     traces,
@@ -128,7 +129,9 @@ const CustomPanel: FC = () => {
       })}
     >
       <QueryConfigurator
-        errors={!hideError ? queryErrors : []}
+        queryErrors={!hideError ? queryErrors : []}
+        setQueryErrors={setQueryErrors}
+        setHideError={setHideError}
         stats={queryStats}
         queryOptions={queryOptions}
         onHideQuery={handleHideQuery}
diff --git a/app/vmui/packages/vmui/src/pages/TopQueries/index.tsx b/app/vmui/packages/vmui/src/pages/TopQueries/index.tsx
index 76efce676..23c7948b0 100644
--- a/app/vmui/packages/vmui/src/pages/TopQueries/index.tsx
+++ b/app/vmui/packages/vmui/src/pages/TopQueries/index.tsx
@@ -2,7 +2,7 @@ import React, { FC, useEffect, useMemo, KeyboardEvent } from "react";
 import { useFetchTopQueries } from "./hooks/useFetchTopQueries";
 import Spinner from "../../components/Main/Spinner/Spinner";
 import TopQueryPanel from "./TopQueryPanel/TopQueryPanel";
-import { formatPrettyNumber } from "../../utils/uplot/helpers";
+import { formatPrettyNumber } from "../../utils/uplot";
 import { isSupportedDuration } from "../../utils/time";
 import dayjs from "dayjs";
 import { TopQueryStats } from "../../types";
diff --git a/app/vmui/packages/vmui/src/types/index.ts b/app/vmui/packages/vmui/src/types/index.ts
index 33235ea1c..e77ec9329 100644
--- a/app/vmui/packages/vmui/src/types/index.ts
+++ b/app/vmui/packages/vmui/src/types/index.ts
@@ -1,4 +1,5 @@
 import { MetricBase } from "../api/types";
+export * from "./uplot";
 
 declare global {
   interface Window {
diff --git a/app/vmui/packages/vmui/src/utils/uplot/types.ts b/app/vmui/packages/vmui/src/types/uplot.ts
similarity index 65%
rename from app/vmui/packages/vmui/src/utils/uplot/types.ts
rename to app/vmui/packages/vmui/src/types/uplot.ts
index 083b67c97..afa594b2b 100644
--- a/app/vmui/packages/vmui/src/utils/uplot/types.ts
+++ b/app/vmui/packages/vmui/src/types/uplot.ts
@@ -1,4 +1,16 @@
-import uPlot, { Series } from "uplot";
+import { Axis, Series } from "uplot";
+
+export interface SeriesItemStats {
+    min: string,
+    max: string,
+    median: string,
+    last: string
+}
+
+export interface SeriesItem extends Series {
+    freeFormFields: {[key: string]: string};
+    calculations: SeriesItemStats
+}
 
 export interface HideSeriesArgs {
     hideSeries: string[],
@@ -7,13 +19,9 @@ export interface HideSeriesArgs {
     series: Series[]
 }
 
-export interface DragArgs {
-    e: MouseEvent | TouchEvent,
-    u: uPlot,
-    factor: number,
-    setPanning: (enable: boolean) => void,
-    setPlotScale: ({ min, max }: { min: number, max: number }) => void
-}
+export type MinMax = { min: number, max: number }
+
+export type SetMinMax = ({ min, max }: MinMax) => void
 
 export interface LegendItemType {
     group: number;
@@ -53,3 +61,7 @@ export interface Fill {
 }
 
 export type ArrayRGB = [number, number, number]
+
+export interface AxisExtend extends Axis {
+    _size?: number;
+}
diff --git a/app/vmui/packages/vmui/src/utils/color.ts b/app/vmui/packages/vmui/src/utils/color.ts
index d6faef962..03776d7e0 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 "./uplot/types";
+import { ArrayRGB } from "../types";
 
 export const baseContrastColors = [
   "#e54040",
diff --git a/app/vmui/packages/vmui/src/utils/uplot/axes.ts b/app/vmui/packages/vmui/src/utils/uplot/axes.ts
index 5d649a9b2..c4c8f9b82 100644
--- a/app/vmui/packages/vmui/src/utils/uplot/axes.ts
+++ b/app/vmui/packages/vmui/src/utils/uplot/axes.ts
@@ -2,9 +2,10 @@ import uPlot, { Axis, Series } from "uplot";
 import { getMaxFromArray, getMinFromArray } from "../math";
 import { getSecondsFromDuration, roundToMilliseconds } from "../time";
 import { AxisRange } from "../../state/graph/reducer";
-import { formatTicks, sizeAxis } from "./helpers";
+import { formatTicks, getTextWidth } from "./helpers";
 import { TimeParams } from "../../types";
 import { getCssVariable } from "../theme";
+import { AxisExtend } from "../../types";
 
 // see https://github.com/leeoniya/uPlot/tree/master/docs#axis--grid-opts
 const timeValues = [
@@ -77,3 +78,16 @@ export const getLimitsYAxis = (values: { [key: string]: number[] }, buffer: bool
   result[key] = buffer ? getMinMaxBuffer(min, max) : [min, max];
   return result;
 };
+
+export const sizeAxis = (u: uPlot, values: string[], axisIdx: number, cycleNum: number): number => {
+  const axis = u.axes[axisIdx] as AxisExtend;
+
+  if (cycleNum > 1) return axis._size || 60;
+
+  let axisSize = 6 + (axis?.ticks?.size || 0) + (axis.gap || 0);
+
+  const longestVal = (values ?? []).reduce((acc, val) => val?.length > acc.length ? val : acc, "");
+  if (longestVal != "") axisSize += getTextWidth(longestVal, "10px Arial");
+
+  return Math.ceil(axisSize);
+};
diff --git a/app/vmui/packages/vmui/src/utils/uplot/events.ts b/app/vmui/packages/vmui/src/utils/uplot/events.ts
deleted file mode 100644
index 9bf4d3c11..000000000
--- a/app/vmui/packages/vmui/src/utils/uplot/events.ts
+++ /dev/null
@@ -1,34 +0,0 @@
-import { DragArgs } from "./types";
-
-export const dragChart = ({ e, factor = 0.85, u, setPanning, setPlotScale }: DragArgs): void => {
-  e.preventDefault();
-  const isMouseEvent = e instanceof MouseEvent;
-
-  setPanning(true);
-  const leftStart = isMouseEvent ? e.clientX : e.touches[0].clientX;
-  const xUnitsPerPx = u.posToVal(1, "x") - u.posToVal(0, "x");
-  const scXMin = u.scales.x.min || 0;
-  const scXMax = u.scales.x.max || 0;
-
-  const mouseMove = (e: MouseEvent | TouchEvent) => {
-    const isMouseEvent = e instanceof MouseEvent;
-    if (!isMouseEvent && e.touches.length > 1) return;
-    e.preventDefault();
-
-    const clientX = isMouseEvent ? e.clientX : e.touches[0].clientX;
-    const dx = xUnitsPerPx * ((clientX - leftStart) * factor);
-    setPlotScale({ min: scXMin - dx, max: scXMax - dx });
-  };
-  const mouseUp = () => {
-    setPanning(false);
-    document.removeEventListener("mousemove", mouseMove);
-    document.removeEventListener("mouseup", mouseUp);
-    document.removeEventListener("touchmove", mouseMove);
-    document.removeEventListener("touchend", mouseUp);
-  };
-
-  document.addEventListener("mousemove", mouseMove);
-  document.addEventListener("mouseup", mouseUp);
-  document.addEventListener("touchmove", mouseMove);
-  document.addEventListener("touchend", mouseUp);
-};
diff --git a/app/vmui/packages/vmui/src/utils/uplot/helpers.ts b/app/vmui/packages/vmui/src/utils/uplot/helpers.ts
index 0a947d57d..acd0d1dc1 100644
--- a/app/vmui/packages/vmui/src/utils/uplot/helpers.ts
+++ b/app/vmui/packages/vmui/src/utils/uplot/helpers.ts
@@ -1,27 +1,5 @@
-import uPlot, { Axis } from "uplot";
-
-export const defaultOptions = {
-  legend: {
-    show: false
-  },
-  cursor: {
-    drag: {
-      x: true,
-      y: false
-    },
-    focus: {
-      prox: 30
-    },
-    points: {
-      size: 5.6,
-      width: 1.4
-    },
-    bind: {
-      click: (): null => null,
-      dblclick: (): null => null,
-    },
-  },
-};
+import uPlot from "uplot";
+import { MetricResult } from "../../api/types";
 
 export const formatTicks = (u: uPlot, ticks: number[], unit = ""): string[] => {
   const min = ticks[0];
@@ -32,7 +10,11 @@ export const formatTicks = (u: uPlot, ticks: number[], unit = ""): string[] => {
   return ticks.map(v => `${formatPrettyNumber(v, min, max)} ${unit}`);
 };
 
-export const formatPrettyNumber = (n: number | null | undefined, min: number | null | undefined, max: number | null | undefined): string => {
+export const formatPrettyNumber = (
+  n: number | null | undefined,
+  min: number | null | undefined,
+  max: number | null | undefined
+): string => {
   if (n === undefined || n === null) {
     return "";
   }
@@ -59,11 +41,7 @@ export const formatPrettyNumber = (n: number | null | undefined, min: number | n
   });
 };
 
-interface AxisExtend extends Axis {
-  _size?: number;
-}
-
-const getTextWidth = (val: string, font: string): number => {
+export const getTextWidth = (val: string, font: string): number => {
   const span = document.createElement("span");
   span.innerText = val;
   span.style.cssText = `position: absolute; z-index: -1; pointer-events: none; opacity: 0; font: ${font}`;
@@ -73,17 +51,17 @@ const getTextWidth = (val: string, font: string): number => {
   return width;
 };
 
-export const sizeAxis = (u: uPlot, values: string[], axisIdx: number, cycleNum: number): number => {
-  const axis = u.axes[axisIdx] as AxisExtend;
-
-  if (cycleNum > 1) return axis._size || 60;
-
-  let axisSize = 6 + (axis?.ticks?.size || 0) + (axis.gap || 0);
-
-  const longestVal = (values ?? []).reduce((acc, val) => val?.length > acc.length ? val : acc, "");
-  if (longestVal != "") axisSize += getTextWidth(longestVal, "10px Arial");
-
-  return Math.ceil(axisSize);
+export const getDashLine = (group: number): number[] => {
+  return group <= 1 ? [] : [group*4, group*1.2];
 };
 
-export const getDashLine = (group: number): number[] => group <= 1 ? [] : [group*4, group*1.2];
+export const getMetricName = (metricItem: MetricResult) => {
+  const metric = metricItem?.metric || {};
+  const labelNames = Object.keys(metric).filter(x => x != "__name__");
+  const labels = labelNames.map(key => `${key}=${JSON.stringify(metric[key])}`);
+  let metricName = metric["__name__"] || "";
+  if (labels.length > 0) {
+    metricName += "{" + labels.join(",") + "}";
+  }
+  return metricName;
+};
diff --git a/app/vmui/packages/vmui/src/utils/uplot/hooks.ts b/app/vmui/packages/vmui/src/utils/uplot/hooks.ts
new file mode 100644
index 000000000..5eb64c33c
--- /dev/null
+++ b/app/vmui/packages/vmui/src/utils/uplot/hooks.ts
@@ -0,0 +1,7 @@
+import uPlot from "uplot";
+
+export const delHooks = (u: uPlot) => {
+  Object.keys(u.hooks).forEach(hook => {
+    u.hooks[hook as keyof uPlot.Hooks.Arrays] = [];
+  });
+};
diff --git a/app/vmui/packages/vmui/src/utils/uplot/index.ts b/app/vmui/packages/vmui/src/utils/uplot/index.ts
new file mode 100644
index 000000000..47f727870
--- /dev/null
+++ b/app/vmui/packages/vmui/src/utils/uplot/index.ts
@@ -0,0 +1,7 @@
+export * from "./axes";
+export * from "./heatmap";
+export * from "./helpers";
+export * from "./hooks";
+export * from "./instnance";
+export * from "./scales";
+export * from "./series";
diff --git a/app/vmui/packages/vmui/src/utils/uplot/instnance.ts b/app/vmui/packages/vmui/src/utils/uplot/instnance.ts
new file mode 100644
index 000000000..a789e8481
--- /dev/null
+++ b/app/vmui/packages/vmui/src/utils/uplot/instnance.ts
@@ -0,0 +1,43 @@
+import uPlot, { Options as uPlotOptions } from "uplot";
+import { delSeries } from "./series";
+import { delHooks } from "./hooks";
+import dayjs from "dayjs";
+import { dateFromSeconds, formatDateForNativeInput } from "../time";
+
+interface InstanceOptions {
+  width?: number,
+  height?: number
+}
+
+export const getDefaultOptions = ({ width = 400, height = 500 }: InstanceOptions): uPlotOptions => ({
+  width,
+  height,
+  series: [],
+  tzDate: ts => dayjs(formatDateForNativeInput(dateFromSeconds(ts))).local().toDate(),
+  legend: {
+    show: false
+  },
+  cursor: {
+    drag: {
+      x: true,
+      y: false
+    },
+    focus: {
+      prox: 30
+    },
+    points: {
+      size: 5.6,
+      width: 1.4
+    },
+    bind: {
+      click: (): null => null,
+      dblclick: (): null => null,
+    },
+  },
+});
+
+export const handleDestroy = (u: uPlot) => {
+  delSeries(u);
+  delHooks(u);
+  u.setData([]);
+};
diff --git a/app/vmui/packages/vmui/src/utils/uplot/scales.ts b/app/vmui/packages/vmui/src/utils/uplot/scales.ts
index 59a7535af..eb0d4c024 100644
--- a/app/vmui/packages/vmui/src/utils/uplot/scales.ts
+++ b/app/vmui/packages/vmui/src/utils/uplot/scales.ts
@@ -1,22 +1,16 @@
 import uPlot, { Range, Scale, Scales } from "uplot";
 import { getMinMaxBuffer } from "./axes";
 import { YaxisState } from "../../state/graph/reducer";
+import { MinMax, SetMinMax } from "../../types";
 
-interface XRangeType {
-  min: number,
-  max: number
-}
-
-export const getRangeX = (xRange: XRangeType): Range.MinMax => {
-  return [xRange.min, xRange.max];
-};
+export const getRangeX = ({ min, max }: MinMax): Range.MinMax => [min, max];
 
 export const getRangeY = (u: uPlot, min = 0, max = 1, axis: string, yaxis: YaxisState): Range.MinMax => {
   if (yaxis.limits.enable) return yaxis.limits.range[axis];
   return getMinMaxBuffer(min, max);
 };
 
-export const getScales = (yaxis: YaxisState, xRange: XRangeType): Scales => {
+export const getScales = (yaxis: YaxisState, xRange: MinMax): Scales => {
   const scales: { [key: string]: { range: Scale.Range } } = { x: { range: () => getRangeX(xRange) } };
   const ranges = Object.keys(yaxis.limits.range);
   (ranges.length ? ranges : ["1"]).forEach(axis => {
@@ -24,3 +18,9 @@ export const getScales = (yaxis: YaxisState, xRange: XRangeType): Scales => {
   });
   return scales;
 };
+
+export const setSelect = (setPlotScale: SetMinMax) => (u: uPlot) => {
+  const min = u.posToVal(u.select.left, "x");
+  const max = u.posToVal(u.select.left + u.select.width, "x");
+  setPlotScale({ min, max });
+};
diff --git a/app/vmui/packages/vmui/src/utils/uplot/series.ts b/app/vmui/packages/vmui/src/utils/uplot/series.ts
index 36e751c00..6d3602ebe 100644
--- a/app/vmui/packages/vmui/src/utils/uplot/series.ts
+++ b/app/vmui/packages/vmui/src/utils/uplot/series.ts
@@ -1,22 +1,11 @@
 import { MetricResult } from "../../api/types";
-import { Series } from "uplot";
+import uPlot, { Series as uPlotSeries } from "uplot";
 import { getNameForMetric, promValueToNumber } from "../metric";
-import { BarSeriesItem, Disp, Fill, LegendItemType, Stroke } from "./types";
-import { HideSeriesArgs } from "./types";
+import { HideSeriesArgs, BarSeriesItem, Disp, Fill, LegendItemType, Stroke, SeriesItem } from "../../types";
 import { baseContrastColors, getColorFromString } from "../color";
 import { getMedianFromArray, getMaxFromArray, getMinFromArray, getLastFromArray } from "../math";
 import { formatPrettyNumber } from "./helpers";
 
-export interface SeriesItem extends Series {
-  freeFormFields: {[key: string]: string};
-  calculations: {
-    min: string,
-    max: string,
-    median: string,
-    last: string
-  }
-}
-
 export const getSeriesItemContext = (data: MetricResult[], hideSeries: string[], alias: string[]) => {
   const colorState: {[key: string]: string} = {};
   const calculations = data.map(d => {
@@ -108,3 +97,15 @@ export const barDisp = (stroke: Stroke, fill: Fill): Disp => {
     fill: fill
   };
 };
+
+export const delSeries = (u: uPlot) => {
+  for (let i = u.series.length - 1; i >= 0; i--) {
+    u.delSeries(i);
+  }
+};
+
+export const addSeries = (u: uPlot, series: uPlotSeries[]) => {
+  series.forEach((s) => {
+    u.addSeries(s);
+  });
+};
diff --git a/dashboards/vmagent.json b/dashboards/vmagent.json
index 8fa59ce02..4c66b13b9 100644
--- a/dashboards/vmagent.json
+++ b/dashboards/vmagent.json
@@ -257,7 +257,7 @@
             "uid": "$ds"
           },
           "editorMode": "code",
-          "expr": "sum(rate(vm_ingestserver_requests_total{job=~\"$job\", instance=~\"$instance\", path!~\"/favicon.ico\"}[$__rate_interval]))",
+          "expr": "sum(rate(vmagent_rows_inserted_total{job=~\"$job\", instance=~\"$instance\"}[$__rate_interval]))",
           "interval": "",
           "legendFormat": "__auto",
           "range": true,
@@ -864,7 +864,7 @@
             "uid": "$ds"
           },
           "editorMode": "code",
-          "expr": "sum(rate(vm_promscrape_scraped_samples_sum{job=~\"$job\", instance=~\"$instance\"}[$__rate_interval])) by(job)\n+ sum(rate(vmagent_rows_inserted_total{job=~\"$job\", instance=~\"$instance\"}[$__rate_interval])) by(job)",
+          "expr": "sum(rate({__name__=~\"vmagent_rows_inserted_total|vm_promscrape_scraped_samples_sum\", job=~\"$job\", instance=~\"$instance\"}[$__rate_interval])) by(job)",
           "hide": false,
           "interval": "",
           "legendFormat": "in {{job}}",
@@ -4091,7 +4091,7 @@
               },
               "editorMode": "code",
               "exemplar": true,
-              "expr": "sum(rate(vm_ingestserver_requests_total{job=~\"$job\", instance=~\"$instance\", path!~\"/favicon.ico\"}[$__rate_interval])) by(job, type, net) > 0",
+              "expr": "sum(rate(vm_ingestserver_requests_total{job=~\"$job\", instance=~\"$instance\"}[$__rate_interval])) by(job, type, net) > 0",
               "interval": "",
               "legendFormat": "{{net}}: {{ type }} ({{job}})",
               "range": true,
@@ -4119,7 +4119,7 @@
             "type": "prometheus",
             "uid": "$ds"
           },
-          "description": "Shows the rate of write errors in ingestserver (UDP, TCP connections) and HTTP server.",
+          "description": "Shows the rate of rows ingested in vmagent via push protocols.",
           "fieldConfig": {
             "defaults": {
               "color": {
@@ -4181,7 +4181,7 @@
             "x": 12,
             "y": 38
           },
-          "id": 77,
+          "id": 131,
           "links": [],
           "options": {
             "legend": {
@@ -4206,25 +4206,16 @@
                 "type": "prometheus",
                 "uid": "$ds"
               },
+              "editorMode": "code",
               "exemplar": true,
-              "expr": "sum(rate(vm_ingestserver_request_errors_total{job=~\"$job\", instance=~\"$instance\", path!~\"/favicon.ico\"}[$__rate_interval])) by(type, net) > 0",
+              "expr": "sum(rate(vmagent_rows_inserted_total{job=~\"$job\", instance=~\"$instance\"}[$__rate_interval])) by(job, type) > 0",
               "interval": "",
-              "legendFormat": "{{ type }} ({{net}})",
+              "legendFormat": "{{ type }} ({{job}})",
+              "range": true,
               "refId": "A"
-            },
-            {
-              "datasource": {
-                "type": "prometheus",
-                "uid": "$ds"
-              },
-              "exemplar": true,
-              "expr": "sum(rate(vmagent_http_request_errors_total{job=~\"$job\", instance=~\"$instance\", protocol!=\"\"}[$__rate_interval])) by(protocol) > 0",
-              "interval": "",
-              "legendFormat": "{{ protocol }} (http)",
-              "refId": "B"
             }
           ],
-          "title": "Error rate  ($instance)",
+          "title": "Rows rate  ($instance)",
           "type": "timeseries"
         },
         {
@@ -4343,6 +4334,119 @@
           ],
           "title": "Concurrent inserts ($instance)",
           "type": "timeseries"
+        },
+        {
+          "datasource": {
+            "type": "prometheus",
+            "uid": "$ds"
+          },
+          "description": "Shows the rate of write errors in ingestserver (UDP, TCP connections) and HTTP server.",
+          "fieldConfig": {
+            "defaults": {
+              "color": {
+                "mode": "palette-classic"
+              },
+              "custom": {
+                "axisCenteredZero": false,
+                "axisColorMode": "text",
+                "axisLabel": "",
+                "axisPlacement": "auto",
+                "barAlignment": 0,
+                "drawStyle": "line",
+                "fillOpacity": 0,
+                "gradientMode": "none",
+                "hideFrom": {
+                  "legend": false,
+                  "tooltip": false,
+                  "viz": false
+                },
+                "lineInterpolation": "linear",
+                "lineWidth": 1,
+                "pointSize": 5,
+                "scaleDistribution": {
+                  "type": "linear"
+                },
+                "showPoints": "never",
+                "spanNulls": false,
+                "stacking": {
+                  "group": "A",
+                  "mode": "none"
+                },
+                "thresholdsStyle": {
+                  "mode": "off"
+                }
+              },
+              "links": [],
+              "mappings": [],
+              "min": 0,
+              "thresholds": {
+                "mode": "absolute",
+                "steps": [
+                  {
+                    "color": "green",
+                    "value": null
+                  },
+                  {
+                    "color": "red",
+                    "value": 80
+                  }
+                ]
+              },
+              "unit": "short"
+            },
+            "overrides": []
+          },
+          "gridPos": {
+            "h": 8,
+            "w": 12,
+            "x": 12,
+            "y": 46
+          },
+          "id": 77,
+          "links": [],
+          "options": {
+            "legend": {
+              "calcs": [
+                "mean",
+                "lastNotNull",
+                "max"
+              ],
+              "displayMode": "table",
+              "placement": "bottom",
+              "showLegend": true
+            },
+            "tooltip": {
+              "mode": "multi",
+              "sort": "desc"
+            }
+          },
+          "pluginVersion": "9.2.6",
+          "targets": [
+            {
+              "datasource": {
+                "type": "prometheus",
+                "uid": "$ds"
+              },
+              "exemplar": true,
+              "expr": "sum(rate(vm_ingestserver_request_errors_total{job=~\"$job\", instance=~\"$instance\", path!~\"/favicon.ico\"}[$__rate_interval])) by(type, net) > 0",
+              "interval": "",
+              "legendFormat": "{{ type }} ({{net}})",
+              "refId": "A"
+            },
+            {
+              "datasource": {
+                "type": "prometheus",
+                "uid": "$ds"
+              },
+              "exemplar": true,
+              "expr": "sum(rate(vmagent_http_request_errors_total{job=~\"$job\", instance=~\"$instance\", protocol!=\"\"}[$__rate_interval])) by(protocol) > 0",
+              "interval": "",
+              "legendFormat": "{{ protocol }} (http)",
+              "refId": "B"
+            }
+          ],
+          "title": "Error rate  ($instance)",
+          "type": "timeseries"
         }
       ],
       "targets": [
@@ -4420,8 +4524,7 @@
                 "mode": "absolute",
                 "steps": [
                   {
-                    "color": "green",
-                    "value": null
+                    "color": "green"
                   },
                   {
                     "color": "red",
@@ -4523,8 +4626,7 @@
                 "mode": "absolute",
                 "steps": [
                   {
-                    "color": "green",
-                    "value": null
+                    "color": "green"
                   },
                   {
                     "color": "red",
@@ -4626,8 +4728,7 @@
                 "mode": "absolute",
                 "steps": [
                   {
-                    "color": "green",
-                    "value": null
+                    "color": "green"
                   },
                   {
                     "color": "red",
@@ -4729,8 +4830,7 @@
                 "mode": "absolute",
                 "steps": [
                   {
-                    "color": "green",
-                    "value": null
+                    "color": "green"
                   },
                   {
                     "color": "red",
@@ -4831,8 +4931,7 @@
                 "mode": "absolute",
                 "steps": [
                   {
-                    "color": "transparent",
-                    "value": null
+                    "color": "transparent"
                   },
                   {
                     "color": "red",
@@ -4930,8 +5029,7 @@
                 "mode": "absolute",
                 "steps": [
                   {
-                    "color": "transparent",
-                    "value": null
+                    "color": "transparent"
                   },
                   {
                     "color": "red",
@@ -5032,8 +5130,7 @@
                 "mode": "absolute",
                 "steps": [
                   {
-                    "color": "transparent",
-                    "value": null
+                    "color": "transparent"
                   },
                   {
                     "color": "red",
@@ -5722,8 +5819,8 @@
       {
         "current": {
           "selected": true,
-          "text": "VictoriaMetrics - cluster",
-          "value": "VictoriaMetrics - cluster"
+          "text": "VictoriaMetrics",
+          "value": "VictoriaMetrics"
         },
         "hide": 0,
         "includeAll": false,
diff --git a/docs/Articles.md b/docs/Articles.md
index 2f9c2ec56..4517cb50c 100644
--- a/docs/Articles.md
+++ b/docs/Articles.md
@@ -1,11 +1,11 @@
 ---
-sort: 17
-weight: 17
+sort: 20
+weight: 20
 title: Articles
 menu:
   docs:
     parent: "victoriametrics"
-    weight: 17
+    weight: 20
 aliases:
 - /Articles.html
 ---
@@ -76,6 +76,7 @@ See also [case studies](https://docs.victoriametrics.com/CaseStudies.html).
 * [Evaluating Backend Options For Prometheus Metrics](https://www.techetio.com/2022/08/21/evaluating-backend-options-for-prometheus-metrics/)
 * [Time Series in the Multiverse of Madness (in Korean)](https://www.youtube.com/watch?v=OUyXPgVcdw4), plus [these slides](https://deview.kr/data/deview/session/attach/%5B2B4%5DVictoriaMetrics_%E1%84%89%E1%85%B5%E1%84%80%E1%85%A8%E1%84%8B%E1%85%A7%E1%86%AF_%E1%84%83%E1%85%A6%E1%84%8B%E1%85%B5%E1%84%90%E1%85%A5_%E1%84%83%E1%85%A2%E1%84%92%E1%85%A9%E1%86%AB%E1%84%83%E1%85%A9%E1%86%AB%E1%84%8B%E1%85%B4_%E1%84%86%E1%85%A5%E1%86%AF%E1%84%90%E1%85%B5%E1%84%87%E1%85%A5%E1%84%89%E1%85%B3_Kor+Eng.pdf)
 * [VictoriaMetrics: an overview and its use instead of Prometheus](https://rtfm.co.ua/en/victoriametrics-an-overview-and-its-use-instead-of-prometheus/)
+* [VictoriaMetrics: deploying a Kubernetes monitoring stack](https://rtfm.co.ua/en/victoriametrics-deploying-a-kubernetes-monitoring-stack/)
 
 ## Our articles
 
diff --git a/docs/BestPractices.md b/docs/BestPractices.md
index d76633b95..d2737840d 100644
--- a/docs/BestPractices.md
+++ b/docs/BestPractices.md
@@ -1,11 +1,11 @@
 ---
-sort: 20
-weight: 20
+sort: 23
+weight: 23
 title: VictoriaMetrics best practices
 menu:
   docs:
     parent: "victoriametrics"
-    weight: 20
+    weight: 23
 aliases:
 - /BestPractices.html
 ---
diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md
index 77cea2fd5..69f0506de 100644
--- a/docs/CHANGELOG.md
+++ b/docs/CHANGELOG.md
@@ -25,6 +25,16 @@ The following `tip` changes can be tested by building VictoriaMetrics components
 ## tip
 
 * FEATURE: [vmbackup](https://docs.victoriametrics.com/vmbackup.html): add support for server-side copy of existing backups. See [these docs](https://docs.victoriametrics.com/vmbackup.html#server-side-copy-of-the-existing-backup) for details.
+* FEATURE: [vmagent](https://docs.victoriametrics.com/vmagent.html): properly handle `unexpected EOF` error when parsing metrics in Prometheus exposition format. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/4817). 
+* FEATURE: [vmui](https://docs.victoriametrics.com/#vmui): add button for auto-formatting PromQL/MetricsQL queries. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/4681). Thanks to @aramattamara for the [pull request](https://github.com/VictoriaMetrics/VictoriaMetrics/pull/4694).
+
+* BUGFIX: do not allow starting VictoriaMetrics components with improperly set boolean command-line flags in the form `-boolFlagName value`, since this leads to silent incomplete flags' parsing. This form should be replaced with `-boolFlagName=value`. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/4845).
+* BUGFIX: [vmagent](https://docs.victoriametrics.com/vmagent.html): properly set labels from `-remoteWrite.label` command-line flag just before sending samples to the configured `-remoteWrite.url` according to [these docs](https://docs.victoriametrics.com/vmagent.html#adding-labels-to-metrics). Previously these labels were incorrectly set before [the relabeling](https://docs.victoriametrics.com/vmagent.html#relabeling) configured via `-remoteWrite.urlRelabelConfigs` and [the stream aggregation](https://docs.victoriametrics.com/stream-aggregation.html) configured via `-remoteWrite.streamAggr.config`, so these labels could be lost or incorrectly transformed before sending the samples to remote storage. The fix allows using `-remoteWrite.label` for identifying `vmagent` instances in [cluster mode](https://docs.victoriametrics.com/vmagent.html#scraping-big-number-of-targets). See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/4247) and [these docs](https://docs.victoriametrics.com/stream-aggregation.html#cluster-mode) for more details.
+* BUGFIX: remove `DEBUG` logging when parsing `if` filters inside [relabeling rules](https://docs.victoriametrics.com/vmagent.html#relabeling-enhancements) and when parsing `match` filters inside [stream aggregation rules](https://docs.victoriametrics.com/stream-aggregation.html).
+* BUGFIX: properly replace `:` chars in label names with `_` when `-usePromCompatibleNaming` command-line flag is passed to `vmagent`, `vminsert` or single-node VictoriaMetrics. This addresses [this comment](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/3113#issuecomment-1275077071).
+* BUGFIX: [Official Grafana dashboards for VictoriaMetrics](https://grafana.com/orgs/victoriametrics): fix display of ingested rows rate for `Samples ingested/s` and `Samples rate` panels for vmagent's dasbhoard. Previously, not all ingested protocols were accounted in these panels. An extra panel `Rows rate` was added to `Ingestion` section to display the split for rows ingested rate by protocol.
+* BUGFIX: [vmbackup](https://docs.victoriametrics.com/vmbackup.html): correctly check if specified `-dst` belongs to specified `-storageDataPath`. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/4837).
+* BUGFIX: [vmctl](https://docs.victoriametrics.com/vmctl.html): don't interrupt the migration process if no metrics were found for a specific tenant. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/4796).
 
 ## [v1.93.0](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v1.93.0)
 
diff --git a/docs/CHANGELOG_2020.md b/docs/CHANGELOG_2020.md
index 3ea468fd6..b7c913928 100644
--- a/docs/CHANGELOG_2020.md
+++ b/docs/CHANGELOG_2020.md
@@ -1,3 +1,15 @@
+---
+sort: 19
+weight: 19
+title: CHANGELOG for the year 2020
+menu:
+  docs:
+    parent: "victoriametrics"
+    weight: 19
+aliases:
+- /CHANGELOG.html
+---
+
 # CHANGELOG for the year 2020
 
 ## [v1.51.0](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v1.51.0)
diff --git a/docs/CHANGELOG_2021.md b/docs/CHANGELOG_2021.md
index 9af80a57d..e02c10238 100644
--- a/docs/CHANGELOG_2021.md
+++ b/docs/CHANGELOG_2021.md
@@ -1,3 +1,15 @@
+---
+sort: 18
+weight: 18
+title: CHANGELOG for the year 2021
+menu:
+  docs:
+    parent: "victoriametrics"
+    weight: 18
+aliases:
+- /CHANGELOG.html
+---
+
 # CHANGELOG for the year 2021
 
 ## [v1.71.0](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v1.71.0)
diff --git a/docs/CHANGELOG_2022.md b/docs/CHANGELOG_2022.md
index 31604ff4b..00cd751eb 100644
--- a/docs/CHANGELOG_2022.md
+++ b/docs/CHANGELOG_2022.md
@@ -1,3 +1,15 @@
+---
+sort: 17
+weight: 17
+title: CHANGELOG for the year 2022
+menu:
+  docs:
+    parent: "victoriametrics"
+    weight: 17
+aliases:
+- /CHANGELOG.html
+---
+
 # CHANGELOG for the year 2022
 
 ## [v1.85.3](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v1.85.3)
diff --git a/docs/PerTenantStatistic.md b/docs/PerTenantStatistic.md
index 41d60ca0a..4bac224b6 100644
--- a/docs/PerTenantStatistic.md
+++ b/docs/PerTenantStatistic.md
@@ -1,11 +1,11 @@
 ---
-sort: 19
-weight: 19
+sort: 22
+weight: 22
 title: VictoriaMetrics Cluster Per Tenant Statistic
 menu:
   docs:
     parent: "victoriametrics"
-    weight: 19
+    weight: 22
 aliases:
 - /PerTenantStatistic.html
 ---
diff --git a/docs/README.md b/docs/README.md
index 448d59f56..34412c343 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -176,7 +176,8 @@ VictoriaMetrics is developed at a fast pace, so it is recommended periodically c
 
 ### Environment variables
 
-All the VictoriaMetrics components allow referring environment variables in command-line flags via `%{ENV_VAR}` syntax.
+All the VictoriaMetrics components allow referring environment variables in `yaml` configuration files (such as `-promscrape.config`)
+and in command-line flags via `%{ENV_VAR}` syntax.
 For example, `-metricsAuthKey=%{METRICS_AUTH_KEY}` is automatically expanded to `-metricsAuthKey=top-secret`
 if `METRICS_AUTH_KEY=top-secret` environment variable exists at VictoriaMetrics startup.
 This expansion is performed by VictoriaMetrics itself.
diff --git a/docs/Release-Guide.md b/docs/Release-Guide.md
index 179b26638..eaa21b0e2 100644
--- a/docs/Release-Guide.md
+++ b/docs/Release-Guide.md
@@ -1,11 +1,11 @@
 ---
-sort: 18
-weight: 18
+sort: 21
+weight: 21
 title: Release process guidance
 menu:
   docs:
     parent: "victoriametrics"
-    weight: 18
+    weight: 21
 aliases:
 - /Release-Guide.html
 ---
diff --git a/docs/Single-server-VictoriaMetrics.md b/docs/Single-server-VictoriaMetrics.md
index b3e54edf7..d26ed865d 100644
--- a/docs/Single-server-VictoriaMetrics.md
+++ b/docs/Single-server-VictoriaMetrics.md
@@ -184,7 +184,8 @@ VictoriaMetrics is developed at a fast pace, so it is recommended periodically c
 
 ### Environment variables
 
-All the VictoriaMetrics components allow referring environment variables in command-line flags via `%{ENV_VAR}` syntax.
+All the VictoriaMetrics components allow referring environment variables in `yaml` configuration files (such as `-promscrape.config`)
+and in command-line flags via `%{ENV_VAR}` syntax.
 For example, `-metricsAuthKey=%{METRICS_AUTH_KEY}` is automatically expanded to `-metricsAuthKey=top-secret`
 if `METRICS_AUTH_KEY=top-secret` environment variable exists at VictoriaMetrics startup.
 This expansion is performed by VictoriaMetrics itself.
diff --git a/docs/Troubleshooting.md b/docs/Troubleshooting.md
index 04e221ba4..8204f0877 100644
--- a/docs/Troubleshooting.md
+++ b/docs/Troubleshooting.md
@@ -1,11 +1,11 @@
 ---
-sort: 23
-weight: 23
+sort: 26
+weight: 26
 title: Troubleshooting
 menu:
   docs:
     parent: "victoriametrics"
-    weight: 23
+    weight: 26
 aliases:
 - /Troubleshooting.html
 ---
diff --git a/docs/_config.yml b/docs/_config.yml
index bb8598883..a58d23767 100644
--- a/docs/_config.yml
+++ b/docs/_config.yml
@@ -9,6 +9,12 @@ remote_theme: rundocs/jekyll-rtd-theme
 readme_index:
   with_frontmatter: true
 
+
+exclude:
+  # exclude assets/README.md from publishing on the docs website
+  # as its purpose is different to other docs.
+  - assets/README.md
+
 google:
   gtag: G-N9SVT8S3HK
 
diff --git a/docs/keyConcepts.md b/docs/keyConcepts.md
index 7a2fb94a9..aca976cfe 100644
--- a/docs/keyConcepts.md
+++ b/docs/keyConcepts.md
@@ -1,11 +1,11 @@
 ---
-sort: 22
-weight: 22
+sort: 25
+weight: 25
 title: Key concepts
 menu:
   docs:
     parent: "victoriametrics"
-    weight: 22
+    weight: 25
 aliases:
 - /keyConcepts.html
 ---
diff --git a/docs/operator/CHANGELOG.md b/docs/operator/CHANGELOG.md
index 1df8d24b9..5b662c681 100644
--- a/docs/operator/CHANGELOG.md
+++ b/docs/operator/CHANGELOG.md
@@ -40,7 +40,7 @@
 - [vmrule](https://docs.victoriametrics.com/operator/api.html#vmrule): support `update_entries_limit` field in rules, refer to [alerting rules](https://docs.victoriametrics.com/vmalert.html#alerting-rules). See [this PR](https://github.com/VictoriaMetrics/operator/pull/691) for details.
 - [vmrule](https://docs.victoriametrics.com/operator/api.html#vmrule): support `keep_firing_for` field in rules, refer to [alerting rules](https://docs.victoriametrics.com/vmalert.html#alerting-rules). See [this PR](https://github.com/VictoriaMetrics/operator/pull/711) for details.
 - [vmoperator parameters](https://docs.victoriametrics.com/operator/vars.html): Add option `VM_ENABLESTRICTSECURITY` and enable strict security context by default. See [this issue](https://github.com/VictoriaMetrics/operator/issues/637), [this](https://github.com/VictoriaMetrics/operator/pull/692/) and [this](https://github.com/VictoriaMetrics/operator/pull/712) PR for details.
-
+- [vmoperator parameters](https://docs.victoriametrics.com/operator/vars.html): change option `VM_PSPAUTOCREATEENABLED` default value from `true` to `false` cause PodSecurityPolicy already got deprecated since [kubernetes v1.25](https://kubernetes.io/docs/reference/using-api/deprecation-guide/#psp-v125). See [this pr](https://github.com/VictoriaMetrics/operator/pull/726) for details.
 
 <a name="v0.35.1"></a>
 ## [v0.35.1](https://github.com/VictoriaMetrics/operator/releases/tag/v0.35.1) - 12 Jul 2023
@@ -60,14 +60,14 @@
 * [vmuser](https://docs.victoriametrics.com/operator/api.html#vmuser): fix vmselect url_map in vmuser. See [this issue for details](https://github.com/VictoriaMetrics/operator/issues/655). Thanks [@Haleygo](https://github.com/Haleygo)
 * [vmalert](https://docs.victoriametrics.com/operator/api.html#vmalert): correctly set default port for vmauth components discovery. See [this issue for details](https://github.com/VictoriaMetrics/operator/issues/658). Thanks [@Haleygo](https://github.com/Haleygo)
 * [vmuser](https://docs.victoriametrics.com/operator/api.html#vmuser): remove rate limit on delete. In https://github.com/VictoriaMetrics/operator/pull/672. Thanks [@Haleygo](https://github.com/Haleygo)
-* [vmcluster](https://docs.victoriametrics.com/operator/api.html#vmuser): fix spec change check. See [this issue for details](https://github.com/VictoriaMetrics/operator/issues/677). Thanks [@Haleygo](https://github.com/Haleygo)
+* [vmcluster](https://docs.victoriametrics.com/operator/api.html#vmcluster): fix spec change check. See [this issue for details](https://github.com/VictoriaMetrics/operator/issues/677). Thanks [@Haleygo](https://github.com/Haleygo)
 * Correctly publish multi-arch release at https://github.com/VictoriaMetrics/operator/pull/681. Thanks [@Haleygo](https://github.com/Haleygo)
 
 ### Features
 
-* [vmagent](https://docs.victoriametrics.com/operator/api.html#vmagent):add validation when generate static scrape config. See [this issue for details](https://github.com/VictoriaMetrics/operator/issues/677). Thanks [@Haleygo](https://github.com/Haleygo)
-* [vmalertmanagerconfig](https://docs.victoriametrics.com/operator/api.html#vmalertmanagerconfig) : add validation for slack receiver url. See [this issue for details](https://github.com/VictoriaMetrics/operator/issues/661). Thanks [@Haleygo](https://github.com/Haleygo)
-* [vmauth/vmagent](https://docs.victoriametrics.com/operator/api.html#vmagent): implement configuration initiation for custom config reloader. See [this issue for details](https://github.com/VictoriaMetrics/operator/issues/619). Thanks [@Haleygo](https://github.com/Haleygo)
+* [vmagent](https://docs.victoriametrics.com/operator/api.html#vmagent): add validation when generate static scrape config. See [this issue for details](https://github.com/VictoriaMetrics/operator/issues/677). Thanks [@Haleygo](https://github.com/Haleygo)
+* [vmalertmanagerconfig](https://docs.victoriametrics.com/operator/api.html#vmalertmanagerconfig): add validation for slack receiver url. See [this issue for details](https://github.com/VictoriaMetrics/operator/issues/661). Thanks [@Haleygo](https://github.com/Haleygo)
+* [vmauth](https://docs.victoriametrics.com/operator/api.html#vmauth)/[vmagent](https://docs.victoriametrics.com/operator/api.html#vmagent): implement configuration initiation for custom config reloader. See [this issue for details](https://github.com/VictoriaMetrics/operator/issues/619). Thanks [@Haleygo](https://github.com/Haleygo)
 * add more generators  Thanks [@Haleygo](https://github.com/Haleygo) in https://github.com/VictoriaMetrics/operator/pull/668
 * [vmsingle](https://docs.victoriametrics.com/operator/api.html#vmsingle): add status field. See [this issue for details](https://github.com/VictoriaMetrics/operator/issues/670). Thanks [@Haleygo](https://github.com/Haleygo)
 
@@ -79,9 +79,9 @@
 
 ### Fixes
 
-- [VMCluster] - fail fast on misconfigured or missing kubernetes pods. It should prevent rare bug with cascade pod deletion. See this [issue](https://github.com/VictoriaMetrics/operator/issues/643) for details
-- [VMAuth/VMAgent]- correctly renders initConfig image with global container registry domain. See this [issue](https://github.com/VictoriaMetrics/operator/issues/654) for details.
-- [VMAgent] - correctly set RBAC permissions for single namespace mode and custom config reloader image. See this [issue](https://github.com/VictoriaMetrics/operator/issues/653) for details.
+- [vmcluster](https://docs.victoriametrics.com/operator/api.html#vmcluster): fail fast on misconfigured or missing kubernetes pods. It should prevent rare bug with cascade pod deletion. See this [issue](https://github.com/VictoriaMetrics/operator/issues/643) for details
+- [vmauth](https://docs.victoriametrics.com/operator/api.html#vmauth)/[vmagent](https://docs.victoriametrics.com/operator/api.html#vmagent): correctly renders initConfig image with global container registry domain. See this [issue](https://github.com/VictoriaMetrics/operator/issues/654) for details.
+- [vmagent](https://docs.victoriametrics.com/operator/api.html#vmagent): correctly set RBAC permissions for single namespace mode and custom config reloader image. See this [issue](https://github.com/VictoriaMetrics/operator/issues/653) for details.
 
 [Changes][v0.34.1]
 
@@ -95,18 +95,18 @@
 
 ### Fixes
 
-- [VMNodeScrape]: fixed selectors for Exists and NotExists operators with empty label Thanks [@Amper](https://github.com/Amper) in https://github.com/VictoriaMetrics/operator/pull/646
-- [VMRule]: Add config for vmrule in validating webhook Thanks in https://github.com/VictoriaMetrics/operator/pull/650
-- [VMAgent]: skips misconfigured objects with missed secret references: https://github.com/VictoriaMetrics/operator/issues/648
-- [VMAgent]: correctly renders initContainer for configuration download: https://github.com/VictoriaMetrics/operator/issues/649
+- [vmnodescrape](https://docs.victoriametrics.com/operator/api.html#vmnodescrape): fixed selectors for Exists and NotExists operators with empty label Thanks [@Amper](https://github.com/Amper) in https://github.com/VictoriaMetrics/operator/pull/646
+- [vmrule](https://docs.victoriametrics.com/operator/api.html#vmrule): Add config for vmrule in validating webhook Thanks in https://github.com/VictoriaMetrics/operator/pull/650
+- [vmagent](https://docs.victoriametrics.com/operator/api.html#vmagent): skips misconfigured objects with missed secret references: https://github.com/VictoriaMetrics/operator/issues/648
+- [vmagent](https://docs.victoriametrics.com/operator/api.html#vmagent): correctly renders initContainer for configuration download: https://github.com/VictoriaMetrics/operator/issues/649
 
 ### Features
 
-- [VMAlertmanager]: Bump alertmanager to v0.25.0 Thanks [@tamcore](https://github.com/tamcore) in https://github.com/VictoriaMetrics/operator/pull/636
-- [VMCluster]: added `clusterNativePort` field to VMSelect/VMInsert for multi-level cluster setup ([#634](https://github.com/VictoriaMetrics/operator/issues/634)) Thanks [@Amper](https://github.com/Amper) in https://github.com/VictoriaMetrics/operator/pull/639
-- [VMRule]: add notifierHeader field in vmrule spec Thanks [@Haleygo](https://github.com/Haleygo) in https://github.com/VictoriaMetrics/operator/pull/622
-- [VMPodScrape]: adds FilterRunning option as prometheus does in https://github.com/VictoriaMetrics/operator/pull/640
-- [VMAuth]: adds latest features in https://github.com/VictoriaMetrics/operator/pull/642
+- [vmalertmanager](https://docs.victoriametrics.com/operator/api.html#vmalertmanager): Bump alertmanager to v0.25.0 Thanks [@tamcore](https://github.com/tamcore) in https://github.com/VictoriaMetrics/operator/pull/636
+- [vmcluster](https://docs.victoriametrics.com/operator/api.html#vmcluster): added `clusterNativePort` field to VMSelect/VMInsert for multi-level cluster setup ([#634](https://github.com/VictoriaMetrics/operator/issues/634)) Thanks [@Amper](https://github.com/Amper) in https://github.com/VictoriaMetrics/operator/pull/639
+- [vmrule](https://docs.victoriametrics.com/operator/api.html#vmrule): add notifierHeader field in vmrule spec Thanks [@Haleygo](https://github.com/Haleygo) in https://github.com/VictoriaMetrics/operator/pull/622
+- [vmpodscrape](https://docs.victoriametrics.com/operator/api.html#vmpodscrape): adds FilterRunning option as prometheus does in https://github.com/VictoriaMetrics/operator/pull/640
+- [vmauth](https://docs.victoriametrics.com/operator/api.html#vmauth): adds latest features in https://github.com/VictoriaMetrics/operator/pull/642
 
 [Changes][v0.34.0]
 
@@ -116,22 +116,22 @@
 
 ### Fixes
 
-- [VMAlert]: skip bad rules and improve logging for rules exceed max configmap size https://github.com/VictoriaMetrics/operator/commit/bb754d5c20bb371a197cd6ff5afac1ba86a4d92b
-- [VMAlertmanagerConfig]: fixed error with headers in VMAlertmanagerConfig.Receivers.EmailConfigs.Headers unmarshalling. Thanks [@Amper](https://github.com/Amper) in https://github.com/VictoriaMetrics/operator/pull/610
-- [VMAgent]: fixed keepInput setting for streaming aggregation. Thanks [@Amper](https://github.com/Amper) in https://github.com/VictoriaMetrics/operator/pull/618
-- [VMAlertmanagerConfig]: fix webhook config maxAlerts not work. Thanks [@Haleygo](https://github.com/Haleygo) in https://github.com/VictoriaMetrics/operator/pull/625
-- [VMAgent]: Remove single quotes from remote write headers. Thanks [@axelsccp](https://github.com/axelsccp) in https://github.com/VictoriaMetrics/operator/pull/613
-- [VMAlertmanagerConfig]: fix parse route error and some comments. Thanks [@Haleygo](https://github.com/Haleygo) in https://github.com/VictoriaMetrics/operator/pull/630
-- [VMUser]: properly removes finalizers for objects https://github.com/VictoriaMetrics/operator/commit/8f10113920a353f21fbcc8637076905f2e57bb34
+- [vmalert](https://docs.victoriametrics.com/operator/api.html#vmalert): skip bad rules and improve logging for rules exceed max configmap size https://github.com/VictoriaMetrics/operator/commit/bb754d5c20bb371a197cd6ff5afac1ba86a4d92b
+- [vmalertmanagerconfig](https://docs.victoriametrics.com/operator/api.html#vmalertmanagerconfig): fixed error with headers in VMAlertmanagerConfig.Receivers.EmailConfigs.Headers unmarshalling. Thanks [@Amper](https://github.com/Amper) in https://github.com/VictoriaMetrics/operator/pull/610
+- [vmagent](https://docs.victoriametrics.com/operator/api.html#vmagent): fixed keepInput setting for streaming aggregation. Thanks [@Amper](https://github.com/Amper) in https://github.com/VictoriaMetrics/operator/pull/618
+- [vmalertmanagerconfig](https://docs.victoriametrics.com/operator/api.html#vmalertmanagerconfig): fix webhook config maxAlerts not work. Thanks [@Haleygo](https://github.com/Haleygo) in https://github.com/VictoriaMetrics/operator/pull/625
+- [vmagent](https://docs.victoriametrics.com/operator/api.html#vmagent): Remove single quotes from remote write headers. Thanks [@axelsccp](https://github.com/axelsccp) in https://github.com/VictoriaMetrics/operator/pull/613
+- [vmalertmanagerconfig](https://docs.victoriametrics.com/operator/api.html#vmalertmanagerconfig): fix parse route error and some comments. Thanks [@Haleygo](https://github.com/Haleygo) in https://github.com/VictoriaMetrics/operator/pull/630
+- [vmuser](https://docs.victoriametrics.com/operator/api.html#vmuser): properly removes finalizers for objects https://github.com/VictoriaMetrics/operator/commit/8f10113920a353f21fbcc8637076905f2e57bb34
 
 ### Features
 
-- [VMAlertmanager]: add option to disable route continue enforce. Thanks [@Haleygo](https://github.com/Haleygo) in https://github.com/VictoriaMetrics/operator/pull/621
-- [VMAlertmanagerConfig]: support set require_tls to false. Thanks [@Haleygo](https://github.com/Haleygo) in https://github.com/VictoriaMetrics/operator/pull/624
-- [VMAlertmanagerConfig]: add sanity check. Thanks [@Haleygo](https://github.com/Haleygo) in https://github.com/VictoriaMetrics/operator/pull/627
+- [vmalertmanager](https://docs.victoriametrics.com/operator/api.html#vmalertmanager): add option to disable route continue enforce. Thanks [@Haleygo](https://github.com/Haleygo) in https://github.com/VictoriaMetrics/operator/pull/621
+- [vmalertmanagerconfig](https://docs.victoriametrics.com/operator/api.html#vmalertmanagerconfig): support set require_tls to false. Thanks [@Haleygo](https://github.com/Haleygo) in https://github.com/VictoriaMetrics/operator/pull/624
+- [vmalertmanagerconfig](https://docs.victoriametrics.com/operator/api.html#vmalertmanagerconfig): add sanity check. Thanks [@Haleygo](https://github.com/Haleygo) in https://github.com/VictoriaMetrics/operator/pull/627
 - Makefile: bump Alpine base image to latest v3.17.3. Thanks [@denisgolius](https://github.com/denisgolius) in https://github.com/VictoriaMetrics/operator/pull/628
-- [VMAlertmanagerConfig]: support sound field in pushover config. Thanks [@Haleygo](https://github.com/Haleygo) in https://github.com/VictoriaMetrics/operator/pull/631
-- [VMAgent/VMAuth]: download initial config with initContainer https://github.com/VictoriaMetrics/operator/commit/612e7c8f40659731e7938ef9556eb088c67eb4b7
+- [vmalertmanagerconfig](https://docs.victoriametrics.com/operator/api.html#vmalertmanagerconfig): support sound field in pushover config. Thanks [@Haleygo](https://github.com/Haleygo) in https://github.com/VictoriaMetrics/operator/pull/631
+- [vmagent](https://docs.victoriametrics.com/operator/api.html#vmagent)/[vmauth](https://docs.victoriametrics.com/operator/api.html#vmauth): download initial config with initContainer https://github.com/VictoriaMetrics/operator/commit/612e7c8f40659731e7938ef9556eb088c67eb4b7
 
 [Changes][v0.33.0]
 
@@ -141,8 +141,8 @@
 
 ### Fixes
 
-- [config]: fixes typo at default vm apps version https://github.com/VictoriaMetrics/operator/issues/608
-- [vmsingle]: conditionally adds stream aggregation config https://github.com/VictoriaMetrics/operator/commit/4a0ca54113afcde439ca4c77e22d3ef1c0d36241
+- config: fixes typo at default vm apps version https://github.com/VictoriaMetrics/operator/issues/608
+- [vmsingle](https://docs.victoriametrics.com/operator/api.html#vmsingle): conditionally adds stream aggregation config https://github.com/VictoriaMetrics/operator/commit/4a0ca54113afcde439ca4c77e22d3ef1c0d36241
 
 [Changes][v0.32.1]
 
@@ -152,14 +152,14 @@
 
 ### Fixes
 
-- [security]: builds docker image with latest `alpine` base image and go `v1.20`.
+- security: builds docker image with latest `alpine` base image and go `v1.20`.
 
 ### Features
 
-- [vmauth]: automatically configures `proxy-protocol` client and `reloadAuthKey` for `config-reloader` container. https://github.com/VictoriaMetrics/operator/commit/611819233bf595a4dbd04b07d7be24b7e994379c
-- [vmagent]: adds `scrapeTimeout` global configuration for `VMAgent` https://github.com/VictoriaMetrics/operator/commit/d1d5024c6befa0961f8d56c82a0554935a4b1878
-- [vmagent]: adds [streaming aggregation](https://docs.victoriametrics.com/stream-aggregation.html) for `remoteWrite` targets https://github.com/VictoriaMetrics/operator/commit/b8baa6c2b72bdda64ebfcc9c3d86d846cd9b3c98 Thanks [@Amper](https://github.com/Amper)
-- [vmsingle]: adds [streaming aggregation](https://docs.victoriametrics.com/stream-aggregation.html) as global configuration for database https://github.com/VictoriaMetrics/operator/commit/b8baa6c2b72bdda64ebfcc9c3d86d846cd9b3c98 Thanks [@Amper](https://github.com/Amper)
+- [vmauth](https://docs.victoriametrics.com/operator/api.html#vmauth): automatically configures `proxy-protocol` client and `reloadAuthKey` for `config-reloader` container. https://github.com/VictoriaMetrics/operator/commit/611819233bf595a4dbd04b07d7be24b7e994379c
+- [vmagent](https://docs.victoriametrics.com/operator/api.html#vmagent): adds `scrapeTimeout` global configuration for `VMAgent` https://github.com/VictoriaMetrics/operator/commit/d1d5024c6befa0961f8d56c82a0554935a4b1878
+- [vmagent](https://docs.victoriametrics.com/operator/api.html#vmagent): adds [streaming aggregation](https://docs.victoriametrics.com/stream-aggregation.html) for `remoteWrite` targets https://github.com/VictoriaMetrics/operator/commit/b8baa6c2b72bdda64ebfcc9c3d86d846cd9b3c98 Thanks [@Amper](https://github.com/Amper)
+- [vmsingle](https://docs.victoriametrics.com/operator/api.html#vmsingle): adds [streaming aggregation](https://docs.victoriametrics.com/stream-aggregation.html) as global configuration for database https://github.com/VictoriaMetrics/operator/commit/b8baa6c2b72bdda64ebfcc9c3d86d846cd9b3c98 Thanks [@Amper](https://github.com/Amper)
 
 [Changes][v0.32.0]
 
@@ -169,13 +169,13 @@
 
 ### Fixes
 
-- [hpa]: Fix hpa object since v2beta deprecated in 1.26+ Thanks [@Haleygo](https://github.com/Haleygo) in https://github.com/VictoriaMetrics/operator/pull/593
-- [api]: adds missing generated client CRD entities https://github.com/VictoriaMetrics/operator/issues/599
+- hpa: Fix hpa object since v2beta deprecated in 1.26+ Thanks [@Haleygo](https://github.com/Haleygo) in https://github.com/VictoriaMetrics/operator/pull/593
+- api: adds missing generated client CRD entities https://github.com/VictoriaMetrics/operator/issues/599
 
 ### Features
 
-- [vmalertmanager]: Add support of vmalertmanager.spec.templates and autoreload dirs for templates and configmaps  thanks [@Amper](https://github.com/Amper) https://github.com/VictoriaMetrics/operator/issues/590 https://github.com/VictoriaMetrics/operator/issues/592
-- [vmalertmanager]: Add support "%SHARD_NUM%" placeholder for vmagent sts/deployment  Thanks [@Amper](https://github.com/Amper) https://github.com/VictoriaMetrics/operator/issues/508
+- [vmalertmanager](https://docs.victoriametrics.com/operator/api.html#vmalertmanager): Add support of vmalertmanager.spec.templates and autoreload dirs for templates and configmaps  thanks [@Amper](https://github.com/Amper) https://github.com/VictoriaMetrics/operator/issues/590 https://github.com/VictoriaMetrics/operator/issues/592
+- [vmalertmanager](https://docs.victoriametrics.com/operator/api.html#vmalertmanager): Add support "%SHARD_NUM%" placeholder for vmagent sts/deployment  Thanks [@Amper](https://github.com/Amper) https://github.com/VictoriaMetrics/operator/issues/508
 
 [Changes][v0.31.0]
 
diff --git a/docs/operator/vars.md b/docs/operator/vars.md
index f9b4bef55..be3fd0952 100644
--- a/docs/operator/vars.md
+++ b/docs/operator/vars.md
@@ -10,14 +10,15 @@ aliases:
 - /operator/vars.html
 ---
 # Auto Generated vars for package config 
- updated at Thu Aug 10 14:33:38 UTC 2023 
+ updated at Fri Aug 18 07:32:53 UTC 2023 
+
 
 | varible name | variable default value | variable required | variable description |
 | --- | --- | --- | --- |
 | VM_USECUSTOMCONFIGRELOADER | false | false | enables custom config reloader for vmauth and vmagent,it should speed-up config reloading process. |
 | VM_CONTAINERREGISTRY | - | false | container registry name prefix, e.g. docker.io |
 | VM_CUSTOMCONFIGRELOADERIMAGE | victoriametrics/operator:config-reloader-v0.32.0 | false | - |
-| VM_PSPAUTOCREATEENABLED | true | false | - |
+| VM_PSPAUTOCREATEENABLED | false | false | - |
 | VM_VMALERTDEFAULT_IMAGE | victoriametrics/vmalert | false | - |
 | VM_VMALERTDEFAULT_VERSION | v1.91.3 | false | - |
 | VM_VMALERTDEFAULT_PORT | 8080 | false | - |
diff --git a/docs/relabeling.md b/docs/relabeling.md
index 8224835f2..97b103913 100644
--- a/docs/relabeling.md
+++ b/docs/relabeling.md
@@ -1,6 +1,6 @@
 ---
-sort: 25
-weight: 25
+sort: 28
+weight: 28
 title: Relabeling cookbook
 menu:
   docs:
diff --git a/docs/sd_configs.md b/docs/sd_configs.md
index fc35c5409..d697e5eec 100644
--- a/docs/sd_configs.md
+++ b/docs/sd_configs.md
@@ -1,11 +1,11 @@
 ---
-sort: 24
-weight: 24
+sort: 27
+weight: 27
 title: Prometheus service discovery
 menu:
   docs:
     parent: "victoriametrics"
-    weight: 24
+    weight: 27
 aliases:
 - /sd_configs.html
 ---
diff --git a/docs/stream-aggregation.md b/docs/stream-aggregation.md
index 9be338bac..80756a3f0 100644
--- a/docs/stream-aggregation.md
+++ b/docs/stream-aggregation.md
@@ -1,7 +1,7 @@
 ---
 sort: 98
 weight: 98
-title: streaming aggregation
+title: Streaming aggregation
 menu:
   docs:
     parent: "victoriametrics"
@@ -10,7 +10,7 @@ aliases:
 - /stream-aggregation.html
 ---
 
-# streaming aggregation
+# Streaming aggregation
 
 [vmagent](https://docs.victoriametrics.com/vmagent.html) and [single-node VictoriaMetrics](https://docs.victoriametrics.com/Single-server-VictoriaMetrics.html)
 can aggregate incoming [samples](https://docs.victoriametrics.com/keyConcepts.html#raw-samples) in streaming mode by time and by labels before data is written to remote storage.
@@ -673,3 +673,17 @@ support the following approaches for hot reloading stream aggregation configs fr
   ```
 
 * By sending HTTP request to `/-/reload` endpoint (e.g. `http://vmagent:8429/-/reload` or `http://victoria-metrics:8428/-/reload).
+
+## Cluster mode
+
+If you use [vmagent in cluster mode](https://docs.victoriametrics.com/vmagent.html#scraping-big-number-of-targets) for streaming aggregation
+(with `-promscrape.cluster.*` parameters or with `VMAgent.spec.shardCount > 1` for [vmoperator](https://docs.victoriametrics.com/operator))
+then be careful when aggregating metrics via `by`, `without` or modifying via `*_relabel_configs` parameters, since incorrect usage
+may result in duplicates and data collision. For example, if more than one `vmagent` instance calculates `increase` for metric `http_requests_total`
+with `by: [path]` directive, then all the `vmagent` instances will aggregate samples to the same set of time series with different `path` labels.
+The proper fix would be to add an unique [`-remoteWrite.label`](https://docs.victoriametrics.com/vmagent.html#adding-labels-to-metrics) per each `vmagent`,
+so every `vmagent` aggregates data into distinct set of time series. These time series then can be aggregated later as needed during querying.
+
+For example, if `vmagent` instances run in Docker or Kubernetes, then you can refer `POD_NAME` or `HOSTNAME` environment variables
+as an unique label value per each `vmagent`: `-remoteWrite.label='vmagent=%{HOSTNAME}` . See [these docs](https://docs.victoriametrics.com/#environment-variables)
+on how to refer environment variables in VictoriaMetrics components.
diff --git a/docs/url-examples.md b/docs/url-examples.md
index 9124aa99f..f1bded5aa 100644
--- a/docs/url-examples.md
+++ b/docs/url-examples.md
@@ -1,11 +1,11 @@
 ---
-sort: 21
-weight: 21
+sort: 24
+weight: 24
 title: VictoriaMetrics API examples
 menu:
   docs:
     parent: "victoriametrics"
-    weight: 21
+    weight: 24
 ---
 
 # VictoriaMetrics API examples
diff --git a/docs/vmanomaly.md b/docs/vmanomaly.md
index aa383aa9a..fda6ebc06 100644
--- a/docs/vmanomaly.md
+++ b/docs/vmanomaly.md
@@ -135,3 +135,97 @@ It is also possible to split up config into multiple files, just list them all i
 ```sh
 python3 -m vmanomaly model_prophet.yaml io_csv.yaml scheduler_oneoff.yaml
 ```
+
+### Monitoring
+
+vmanomaly can be monitored by using push or pull approach.
+It can push metrics to VictoriaMetrics or expose metrics in Prometheus exposition format.
+
+#### Push approach
+
+vmanomaly can push metrics to VictoriaMetrics single-node or cluster version.
+In order to enable push approach, specify `push` section in config file:
+
+```yaml
+monitoring:
+   push:
+      url: "http://victoriametrics:8428/"
+      extra_labels:
+         job: "vmanomaly-push"
+```
+
+#### Pull approach
+
+vmanomaly can export internal metrics in Prometheus exposition format at `/metrics` page.
+These metrics can be scraped via [vmagent](https://docs.victoriametrics.com/vmagent.html) or Prometheus.
+
+In order to enable pull approach, specify `pull` section in config file:
+
+```yaml
+monitoring:
+   pull:
+      enable: true
+      port: 8080
+```
+
+This will expose metrics at `http://0.0.0.0:8080/metrics` page.
+
+### Licensing
+
+Starting from v1.5.0 vmanomaly requires a license key to run. You can obtain a trial license
+key [here](https://victoriametrics.com/products/enterprise/trial/).
+
+The license key can be passed via the following command-line flags:
+```
+  --license LICENSE     See https://victoriametrics.com/products/enterprise/
+                        for trial license
+  --license-file LICENSE_FILE
+                        See https://victoriametrics.com/products/enterprise/
+                        for trial license
+  --license-verify-offline LICENSE_VERIFY_OFFLINE
+                        Force offline verification of license code.
+                        License is verified online by default. This flag runs license verification
+                        offline.
+```
+
+Usage example:
+```
+python3 -m vmanomaly --license-file /path/to/license_file.yaml config.yaml
+```
+
+In order to make it easier to monitor the license expiration date, the following metrics are exposed(see
+[Monitoring](#monitoring) section for details on how to scrape them):
+
+```
+# HELP vm_license_expires_at When the license expires as a Unix timestamp in seconds
+# TYPE vm_license_expires_at gauge
+vm_license_expires_at 1.6963776e+09
+# HELP vm_license_expires_in_seconds Amount of seconds until the license expires
+# TYPE vm_license_expires_in_seconds gauge
+vm_license_expires_in_seconds 4.886608e+06
+```
+
+You can find example alerts for [vmalert](https://docs.victoriametrics.com/vmalert.html):
+```yaml
+groups:
+  - name: vm-license
+    # note the `job` filter and update accordingly to your setup
+    rules:
+      - alert: LicenseExpiresInLessThan30Days
+        expr: vm_license_expires_in_seconds < 30 * 24 * 3600
+        labels:
+          severity: warning
+        annotations:
+          summary: "{{ $labels.job }} instance {{ $labels.instance }} license expires in less than 30 days"
+          description: "{{ $labels.instance }} of job {{ $labels.job }} license expires in {{ $value | humanizeDuration }}. 
+            Please make sure to update the license before it expires."
+
+      - alert: LicenseExpiresInLessThan7Days
+        expr: vm_license_expires_in_seconds < 7 * 24 * 3600
+        labels:
+          severity: critical
+        annotations:
+          summary: "{{ $labels.job }} instance {{ $labels.instance }} license expires in less than 7 days"
+          description: "{{ $labels.instance }} of job {{ $labels.job }} license expires in {{ $value | humanizeDuration }}. 
+            Please make sure to update the license before it expires."
+```
diff --git a/docs/vmauth.md b/docs/vmauth.md
index cf198945f..fd03d86f2 100644
--- a/docs/vmauth.md
+++ b/docs/vmauth.md
@@ -251,8 +251,8 @@ Alternatively, [https termination proxy](https://en.wikipedia.org/wiki/TLS_termi
 
 It is recommended protecting the following endpoints with authKeys:
 * `/-/reload` with `-reloadAuthKey` command-line flag, so external users couldn't trigger config reload.
-* `/flags` with `-flagsAuthkey` command-line flag, so unauthorized users couldn't get application command-line flags.
-* `/metrics` with `metricsAuthkey` command-line flag, so unauthorized users couldn't get access to [vmauth metrics](#monitoring).
+* `/flags` with `-flagsAuthKey` command-line flag, so unauthorized users couldn't get application command-line flags.
+* `/metrics` with `metricsAuthKey` command-line flag, so unauthorized users couldn't get access to [vmauth metrics](#monitoring).
 * `/debug/pprof` with `pprofAuthKey` command-line flag, so unauthorized users couldn't get access to [profiling information](#profiling).
 
 `vmauth` also supports the ability to restict access by IP - see [these docs](#ip-filters). See also [concurrency limiting docs](#concurrency-limiting).
diff --git a/lib/envflag/envflag.go b/lib/envflag/envflag.go
index ab2b3fd9a..6603a127d 100644
--- a/lib/envflag/envflag.go
+++ b/lib/envflag/envflag.go
@@ -32,6 +32,11 @@ func ParseFlagSet(fs *flag.FlagSet, args []string) {
 		// Do not use lib/logger here, since it is uninitialized yet.
 		log.Fatalf("cannot parse flags %q: %s", args, err)
 	}
+	if fs.NArg() > 0 {
+		// See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/4845
+		log.Fatalf("unprocessed command-line args left: %s; the most likely reason is missing `=` between boolean flag name and value; "+
+			"see https://pkg.go.dev/flag#hdr-Command_line_flag_syntax", fs.Args())
+	}
 	if !*enable {
 		return
 	}
diff --git a/lib/promrelabel/if_expression.go b/lib/promrelabel/if_expression.go
index 78519d3e8..1f3506dfb 100644
--- a/lib/promrelabel/if_expression.go
+++ b/lib/promrelabel/if_expression.go
@@ -80,7 +80,6 @@ func (ie *IfExpression) UnmarshalYAML(f func(interface{}) error) error {
 }
 
 func (ie *IfExpression) unmarshalFromInterface(v interface{}) error {
-	logger.Infof("DEBUG: unmarshaling ifExpr from %#v", v)
 	ies := ie.ies[:0]
 	switch t := v.(type) {
 	case string:
@@ -89,7 +88,6 @@ func (ie *IfExpression) unmarshalFromInterface(v interface{}) error {
 			return fmt.Errorf("unexpected `match` option: %w", err)
 		}
 		ies = append(ies, ieLocal)
-		logger.Infof("DEBUG: unmarshaled ifExpr from %#v to %s", t, ieLocal)
 	case []interface{}:
 		for _, x := range t {
 			s, ok := x.(string)
@@ -102,7 +100,6 @@ func (ie *IfExpression) unmarshalFromInterface(v interface{}) error {
 			}
 			ies = append(ies, ieLocal)
 		}
-		logger.Infof("DEBUG: unmarshaled ifExpr from %#v to %s", t, ies)
 	default:
 		return fmt.Errorf("unexpected `match` type; got %#v; want string or an array of strings", t)
 	}
diff --git a/lib/promrelabel/relabel.go b/lib/promrelabel/relabel.go
index c6c17b4c9..e2d5d68da 100644
--- a/lib/promrelabel/relabel.go
+++ b/lib/promrelabel/relabel.go
@@ -619,15 +619,28 @@ func fillLabelReferences(dst []byte, replacement string, labels []prompbmarshal.
 	return dst
 }
 
-// SanitizeName replaces unsupported by Prometheus chars in metric names and label names with _.
+// SanitizeLabelName replaces unsupported by Prometheus chars in label names with _.
 //
 // See https://prometheus.io/docs/concepts/data_model/#metric-names-and-labels
-func SanitizeName(name string) string {
-	return promSanitizer.Transform(name)
+func SanitizeLabelName(name string) string {
+	return labelNameSanitizer.Transform(name)
 }
 
-var promSanitizer = bytesutil.NewFastStringTransformer(func(s string) string {
-	return unsupportedPromChars.ReplaceAllString(s, "_")
+var labelNameSanitizer = bytesutil.NewFastStringTransformer(func(s string) string {
+	return unsupportedLabelNameChars.ReplaceAllString(s, "_")
 })
 
-var unsupportedPromChars = regexp.MustCompile(`[^a-zA-Z0-9_:]`)
+var unsupportedLabelNameChars = regexp.MustCompile(`[^a-zA-Z0-9_]`)
+
+// SanitizeMetricName replaces unsupported by Prometheus chars in metric names with _.
+//
+// See https://prometheus.io/docs/concepts/data_model/#metric-names-and-labels
+func SanitizeMetricName(value string) string {
+	return metricNameSanitizer.Transform(value)
+}
+
+var metricNameSanitizer = bytesutil.NewFastStringTransformer(func(s string) string {
+	return unsupportedMetricNameChars.ReplaceAllString(s, "_")
+})
+
+var unsupportedMetricNameChars = regexp.MustCompile(`[^a-zA-Z0-9_:]`)
diff --git a/lib/protoparser/common/lines_reader.go b/lib/protoparser/common/lines_reader.go
index 1549a2079..a1fe0d3c6 100644
--- a/lib/protoparser/common/lines_reader.go
+++ b/lib/protoparser/common/lines_reader.go
@@ -93,7 +93,7 @@ again:
 }
 
 func isEOFLikeError(err error) bool {
-	if errors.Is(err, io.EOF) {
+	if errors.Is(err, io.EOF) || errors.Is(err, io.ErrUnexpectedEOF) {
 		return true
 	}
 	s := err.Error()


Test patch:
diff --git a/app/vmagent/remotewrite/relabel_test.go b/app/vmagent/remotewrite/relabel_test.go
index eb3540002..c84a31610 100644
--- a/app/vmagent/remotewrite/relabel_test.go
+++ b/app/vmagent/remotewrite/relabel_test.go
@@ -10,18 +10,16 @@ import (
 )
 
 func TestApplyRelabeling(t *testing.T) {
-	f := func(extraLabels []prompbmarshal.Label, pcs *promrelabel.ParsedConfigs, sTss, sExpTss string) {
+	f := func(pcs *promrelabel.ParsedConfigs, sTss, sExpTss string) {
 		rctx := &relabelCtx{}
 		tss, expTss := parseSeries(sTss), parseSeries(sExpTss)
-		gotTss := rctx.applyRelabeling(tss, extraLabels, pcs)
+		gotTss := rctx.applyRelabeling(tss, pcs)
 		if !reflect.DeepEqual(gotTss, expTss) {
 			t.Fatalf("expected to have: \n%v;\ngot: \n%v", expTss, gotTss)
 		}
 	}
 
-	f(nil, nil, "up", "up")
-	f([]prompbmarshal.Label{{Name: "foo", Value: "bar"}}, nil, "up", `up{foo="bar"}`)
-	f([]prompbmarshal.Label{{Name: "foo", Value: "bar"}}, nil, `up{foo="baz"}`, `up{foo="bar"}`)
+	f(nil, "up", "up")
 
 	pcs, err := promrelabel.ParseRelabelConfigsData([]byte(`
 - target_label: "foo"
@@ -32,11 +30,32 @@ func TestApplyRelabeling(t *testing.T) {
 	if err != nil {
 		t.Fatalf("unexpected error: %s", err)
 	}
-	f(nil, pcs, `up{foo="baz", env="prod"}`, `up{foo="aaa"}`)
+	f(pcs, `up{foo="baz", env="prod"}`, `up{foo="aaa"}`)
 
 	oldVal := *usePromCompatibleNaming
 	*usePromCompatibleNaming = true
-	f(nil, nil, `foo.bar`, `foo_bar`)
+	f(nil, `foo.bar`, `foo_bar`)
+	*usePromCompatibleNaming = oldVal
+}
+
+func TestAppendExtraLabels(t *testing.T) {
+	f := func(extraLabels []prompbmarshal.Label, sTss, sExpTss string) {
+		rctx := &relabelCtx{}
+		tss, expTss := parseSeries(sTss), parseSeries(sExpTss)
+		rctx.appendExtraLabels(tss, extraLabels)
+		if !reflect.DeepEqual(tss, expTss) {
+			t.Fatalf("expected to have: \n%v;\ngot: \n%v", expTss, tss)
+		}
+	}
+
+	f(nil, "up", "up")
+	f([]prompbmarshal.Label{{Name: "foo", Value: "bar"}}, "up", `up{foo="bar"}`)
+	f([]prompbmarshal.Label{{Name: "foo", Value: "bar"}}, `up{foo="baz"}`, `up{foo="bar"}`)
+	f([]prompbmarshal.Label{{Name: "baz", Value: "qux"}}, `up{foo="baz"}`, `up{foo="baz",baz="qux"}`)
+
+	oldVal := *usePromCompatibleNaming
+	*usePromCompatibleNaming = true
+	f([]prompbmarshal.Label{{Name: "foo.bar", Value: "baz"}}, "up", `up{foo_bar="baz"}`)
 	*usePromCompatibleNaming = oldVal
 }
 
diff --git a/app/vmbackup/main_test.go b/app/vmbackup/main_test.go
index f3288fb49..c1a87e51d 100644
--- a/app/vmbackup/main_test.go
+++ b/app/vmbackup/main_test.go
@@ -26,4 +26,9 @@ func TestHasFilepathPrefix(t *testing.T) {
 	f("fs://"+pwd+"/foo", pwd+"/foo/bar", false)
 	f("fs://"+pwd+"/foo/bar", pwd+"/foo", true)
 	f("fs://"+pwd+"/foo", pwd+"/bar", false)
+	f("fs:///data1", "/data", false)
+	f("fs:///data", "/data1", false)
+	f("fs:///data", "/data/foo", false)
+	f("fs:///data/foo", "/data", true)
+	f("fs:///data/foo/", "/data/", true)
 }
diff --git a/lib/promrelabel/relabel_test.go b/lib/promrelabel/relabel_test.go
index 987374252..df8080af4 100644
--- a/lib/promrelabel/relabel_test.go
+++ b/lib/promrelabel/relabel_test.go
@@ -9,13 +9,13 @@ import (
 	"github.com/VictoriaMetrics/VictoriaMetrics/lib/promutils"
 )
 
-func TestSanitizeName(t *testing.T) {
+func TestSanitizeMetricName(t *testing.T) {
 	f := func(s, resultExpected string) {
 		t.Helper()
 		for i := 0; i < 5; i++ {
-			result := SanitizeName(s)
+			result := SanitizeMetricName(s)
 			if result != resultExpected {
-				t.Fatalf("unexpected result for SanitizeName(%q) at iteration %d; got %q; want %q", s, i, result, resultExpected)
+				t.Fatalf("unexpected result for SanitizeMetricName(%q) at iteration %d; got %q; want %q", s, i, result, resultExpected)
 			}
 		}
 	}
@@ -25,6 +25,22 @@ func TestSanitizeName(t *testing.T) {
 	f("foo...bar", "foo___bar")
 }
 
+func TestSanitizeLabelName(t *testing.T) {
+	f := func(s, resultExpected string) {
+		t.Helper()
+		for i := 0; i < 5; i++ {
+			result := SanitizeLabelName(s)
+			if result != resultExpected {
+				t.Fatalf("unexpected result for SanitizeLabelName(%q) at iteration %d; got %q; want %q", s, i, result, resultExpected)
+			}
+		}
+	}
+	f("", "")
+	f("a", "a")
+	f("foo.bar/baz:a", "foo_bar_baz_a")
+	f("foo...bar", "foo___bar")
+}
+
 func TestLabelsToString(t *testing.T) {
 	f := func(labels []prompbmarshal.Label, sExpected string) {
 		t.Helper()
diff --git a/lib/promrelabel/relabel_timing_test.go b/lib/promrelabel/relabel_timing_test.go
index 31682e787..485002020 100644
--- a/lib/promrelabel/relabel_timing_test.go
+++ b/lib/promrelabel/relabel_timing_test.go
@@ -8,20 +8,39 @@ import (
 	"github.com/VictoriaMetrics/VictoriaMetrics/lib/prompbmarshal"
 )
 
-func BenchmarkSanitizeName(b *testing.B) {
+func BenchmarkSanitizeMetricName(b *testing.B) {
 	for _, name := range []string{"", "foo", "foo-bar-baz", "http_requests_total"} {
 		b.Run(name, func(b *testing.B) {
-			benchmarkSanitizeName(b, name)
+			benchmarkSanitizeMetricName(b, name)
 		})
 	}
 }
 
-func benchmarkSanitizeName(b *testing.B, name string) {
+func benchmarkSanitizeMetricName(b *testing.B, name string) {
 	b.ReportAllocs()
 	b.SetBytes(1)
 	b.RunParallel(func(pb *testing.PB) {
 		for pb.Next() {
-			sanitizedName := SanitizeName(name)
+			sanitizedName := SanitizeMetricName(name)
+			GlobalSink += len(sanitizedName)
+		}
+	})
+}
+
+func BenchmarkSanitizeLabelName(b *testing.B) {
+	for _, name := range []string{"", "foo", "foo-bar-baz", "http_requests_total"} {
+		b.Run(name, func(b *testing.B) {
+			benchmarkSanitizeLabelName(b, name)
+		})
+	}
+}
+
+func benchmarkSanitizeLabelName(b *testing.B, name string) {
+	b.ReportAllocs()
+	b.SetBytes(1)
+	b.RunParallel(func(pb *testing.PB) {
+		for pb.Next() {
+			sanitizedName := SanitizeLabelName(name)
 			GlobalSink += len(sanitizedName)
 		}
 	})
diff --git a/lib/protoparser/common/lines_reader_test.go b/lib/protoparser/common/lines_reader_test.go
index a083322c1..e37ac720c 100644
--- a/lib/protoparser/common/lines_reader_test.go
+++ b/lib/protoparser/common/lines_reader_test.go
@@ -2,6 +2,7 @@ package common
 
 import (
 	"bytes"
+	"errors"
 	"fmt"
 	"io"
 	"reflect"
@@ -25,6 +26,20 @@ func TestReadLinesBlockFailure(t *testing.T) {
 		if _, _, err := ReadLinesBlock(fr, nil, nil); err == nil {
 			t.Fatalf("expecting non-nil error")
 		}
+
+		un := &unexpectedEOF{}
+		if _, _, err := ReadLinesBlock(un, nil, nil); err != nil {
+			if !errors.Is(err, io.EOF) {
+				t.Fatalf("get unexpected error, expecting io.EOF")
+			}
+		}
+
+		ef := eofErr{}
+		if _, _, err := ReadLinesBlock(ef, nil, nil); err != nil {
+			if !errors.Is(err, io.EOF) {
+				t.Fatalf("get unexpected error, expecting io.EOF")
+			}
+		}
 	}
 
 	// empty string
@@ -41,6 +56,18 @@ func (fr *failureReader) Read(p []byte) (int, error) {
 	return 0, fmt.Errorf("some error")
 }
 
+type unexpectedEOF struct{}
+
+func (un unexpectedEOF) Read(p []byte) (int, error) {
+	return 0, io.ErrUnexpectedEOF
+}
+
+type eofErr struct{}
+
+func (eo eofErr) Read(p []byte) (int, error) {
+	return 0, io.EOF
+}
+
 func TestReadLinesBlockMultiLinesSingleByteReader(t *testing.T) {
 	f := func(s string, linesExpected []string) {
 		t.Helper()