Repository: VictoriaMetrics/VictoriaMetrics
Pull request #4864 (2023-08-21 11:54:12)

vmalert: correctly re-instantinate HTTP req on retries
Previosly, request retry to datasource re-used existing HTTP request. But if request object was already partially processed (body was read), then retry will be unsuccessful.

The change re-instantinates HTTP request object before retry.

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/VictoriaMetrics/VictoriaMetrics/pull/4864?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=VictoriaMetrics) Report
Patch coverage: **`88.00%`** and project coverage change: **`+0.01%`** :tada:
> Comparison is base [(`7349f18`)](https://app.codecov.io/gh/VictoriaMetrics/VictoriaMetrics/commit/7349f18c55642306729366a493d422732d330bfd?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=VictoriaMetrics) 60.56% compared to head [(`792b0f8`)](https://app.codecov.io/gh/VictoriaMetrics/VictoriaMetrics/pull/4864?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=VictoriaMetrics) 60.57%.
> Report is 4 commits behind head on master.

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


```diff
@@            Coverage Diff             @@
##           master    #4864      +/-   ##
==========================================
+ Coverage   60.56%   60.57%   +0.01%     
==========================================
  Files         384      384              
  Lines       72274    72275       +1     
==========================================
+ Hits        43775    43783       +8     
+ Misses      26101    26099       -2     
+ Partials     2398     2393       -5     
```


| [Files Changed](https://app.codecov.io/gh/VictoriaMetrics/VictoriaMetrics/pull/4864?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=VictoriaMetrics) | Coverage Δ | |
|---|---|---|
| [app/vmalert/datasource/vm.go](https://app.codecov.io/gh/VictoriaMetrics/VictoriaMetrics/pull/4864?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=VictoriaMetrics#diff-YXBwL3ZtYWxlcnQvZGF0YXNvdXJjZS92bS5nbw==) | `83.80% <88.00%> (+4.37%)` | :arrow_up: |

... and [9 files with indirect coverage changes](https://app.codecov.io/gh/VictoriaMetrics/VictoriaMetrics/pull/4864/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/4864?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/app/vmalert/datasource/vm.go b/app/vmalert/datasource/vm.go
index cf6a43d3e..6bac40be5 100644
--- a/app/vmalert/datasource/vm.go
+++ b/app/vmalert/datasource/vm.go
@@ -127,21 +127,14 @@ func NewVMStorage(baseURL string, authCfg *promauth.Config, lookBack time.Durati
 
 // Query executes the given query and returns parsed response
 func (s *VMStorage) Query(ctx context.Context, query string, ts time.Time) (Result, *http.Request, error) {
-	req, err := s.newRequestPOST()
-	if err != nil {
-		return Result{}, nil, err
-	}
-
-	switch s.dataSourceType {
-	case "", datasourcePrometheus:
-		s.setPrometheusInstantReqParams(req, query, ts)
-	case datasourceGraphite:
-		s.setGraphiteReqParams(req, query, ts)
-	default:
-		return Result{}, nil, fmt.Errorf("engine not found: %q", s.dataSourceType)
-	}
-
+	req := s.newQueryRequest(query, ts)
 	resp, err := s.do(ctx, req)
+	if errors.Is(err, io.EOF) || errors.Is(err, io.ErrUnexpectedEOF) {
+		// something in the middle between client and datasource might be closing
+		// the connection. So we do a one more attempt in hope request will succeed.
+		req = s.newQueryRequest(query, ts)
+		resp, err = s.do(ctx, req)
+	}
 	if err != nil {
 		return Result{}, req, err
 	}
@@ -164,18 +157,20 @@ func (s *VMStorage) QueryRange(ctx context.Context, query string, start, end tim
 	if s.dataSourceType != datasourcePrometheus {
 		return res, fmt.Errorf("%q is not supported for QueryRange", s.dataSourceType)
 	}
-	req, err := s.newRequestPOST()
-	if err != nil {
-		return res, err
-	}
 	if start.IsZero() {
 		return res, fmt.Errorf("start param is missing")
 	}
 	if end.IsZero() {
 		return res, fmt.Errorf("end param is missing")
 	}
-	s.setPrometheusRangeReqParams(req, query, start, end)
+	req := s.newQueryRangeRequest(query, start, end)
 	resp, err := s.do(ctx, req)
+	if errors.Is(err, io.EOF) || errors.Is(err, io.ErrUnexpectedEOF) {
+		// something in the middle between client and datasource might be closing
+		// the connection. So we do a one more attempt in hope request will succeed.
+		req = s.newQueryRangeRequest(query, start, end)
+		resp, err = s.do(ctx, req)
+	}
 	if err != nil {
 		return res, err
 	}
@@ -190,11 +185,6 @@ func (s *VMStorage) do(ctx context.Context, req *http.Request) (*http.Response,
 		logger.Infof("DEBUG datasource request: executing %s request with params %q", req.Method, req.URL.RawQuery)
 	}
 	resp, err := s.c.Do(req.WithContext(ctx))
-	if errors.Is(err, io.EOF) || errors.Is(err, io.ErrUnexpectedEOF) {
-		// something in the middle between client and datasource might be closing
-		// the connection. So we do a one more attempt in hope request will succeed.
-		resp, err = s.c.Do(req.WithContext(ctx))
-	}
 	if err != nil {
 		return nil, fmt.Errorf("error getting response from %s: %w", req.URL.Redacted(), err)
 	}
@@ -206,10 +196,29 @@ func (s *VMStorage) do(ctx context.Context, req *http.Request) (*http.Response,
 	return resp, nil
 }
 
-func (s *VMStorage) newRequestPOST() (*http.Request, error) {
+func (s *VMStorage) newQueryRangeRequest(query string, start, end time.Time) *http.Request {
+	req := s.newRequest()
+	s.setPrometheusRangeReqParams(req, query, start, end)
+	return req
+}
+
+func (s *VMStorage) newQueryRequest(query string, ts time.Time) *http.Request {
+	req := s.newRequest()
+	switch s.dataSourceType {
+	case "", datasourcePrometheus:
+		s.setPrometheusInstantReqParams(req, query, ts)
+	case datasourceGraphite:
+		s.setGraphiteReqParams(req, query, ts)
+	default:
+		logger.Panicf("BUG: engine not found: %q", s.dataSourceType)
+	}
+	return req
+}
+
+func (s *VMStorage) newRequest() *http.Request {
 	req, err := http.NewRequest(http.MethodPost, s.datasourceURL, nil)
 	if err != nil {
-		return nil, err
+		logger.Panicf("BUG: unexpected error from http.NewRequest(%q): %s", s.datasourceURL, err)
 	}
 	req.Header.Set("Content-Type", "application/json")
 	if s.authCfg != nil {
@@ -218,5 +227,5 @@ func (s *VMStorage) newRequestPOST() (*http.Request, error) {
 	for _, h := range s.extraHeaders {
 		req.Header.Set(h.key, h.value)
 	}
-	return req, nil
+	return req
 }
diff --git a/app/vmselect/prometheus/prometheus.go b/app/vmselect/prometheus/prometheus.go
index ee1197967..131249570 100644
--- a/app/vmselect/prometheus/prometheus.go
+++ b/app/vmselect/prometheus/prometheus.go
@@ -76,7 +76,7 @@ func ExpandWithExprs(w http.ResponseWriter, r *http.Request) {
 	_ = bw.Flush()
 }
 
-// PrettifyQuery implements /prettify-query. Takes a MetricsQL query and returns it formatted.
+// PrettifyQuery handles the request /prettify-query
 func PrettifyQuery(w http.ResponseWriter, r *http.Request) {
 	query := r.FormValue("query")
 	bw := bufferedwriter.Get(w)
@@ -90,7 +90,6 @@ func PrettifyQuery(w http.ResponseWriter, r *http.Request) {
 	} else {
 		fmt.Fprintf(bw, `{"status": "success", "query": %q}`, prettyQuery)
 	}
-
 	_ = bw.Flush()
 }
 
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/Configurators/QueryEditor/QueryEditor.tsx b/app/vmui/packages/vmui/src/components/Configurators/QueryEditor/QueryEditor.tsx
index 863918c2a..78e3053f0 100644
--- a/app/vmui/packages/vmui/src/components/Configurators/QueryEditor/QueryEditor.tsx
+++ b/app/vmui/packages/vmui/src/components/Configurators/QueryEditor/QueryEditor.tsx
@@ -5,8 +5,6 @@ import TextField from "../../Main/TextField/TextField";
 import Autocomplete from "../../Main/Autocomplete/Autocomplete";
 import "./style.scss";
 import { QueryStats } from "../../../api/types";
-import Tooltip from "../../Main/Tooltip/Tooltip";
-import { WarningIcon } from "../../Main/Icons";
 import { partialWarning, seriesFetchedWarning } from "./warningText";
 
 export interface QueryEditorProps {
@@ -41,7 +39,7 @@ const QueryEditor: FC<QueryEditorProps> = ({
   const [openAutocomplete, setOpenAutocomplete] = useState(false);
   const autocompleteAnchorEl = useRef<HTMLDivElement>(null);
 
-  const warnings = [
+  const warning = [
     {
       show: stats?.seriesFetched === "0" && !stats.resultLength,
       text: seriesFetchedWarning
@@ -50,7 +48,7 @@ const QueryEditor: FC<QueryEditorProps> = ({
       show: stats?.isPartial,
       text: partialWarning
     }
-  ].filter((warning) => warning.show);
+  ].filter((w) => w.show).map(w => w.text).join("");
 
   const handleSelect = (val: string) => {
     onChange(val);
@@ -104,6 +102,7 @@ const QueryEditor: FC<QueryEditorProps> = ({
       type={"textarea"}
       autofocus={!!value}
       error={error}
+      warning={warning}
       onKeyDown={handleKeyDown}
       onChange={onChange}
       disabled={disabled}
@@ -119,20 +118,6 @@ const QueryEditor: FC<QueryEditorProps> = ({
         onFoundOptions={handleChangeFoundOptions}
       />
     )}
-    {!!warnings.length && (
-      <div className="vm-query-editor-warning">
-        <Tooltip
-          placement="bottom-right"
-          title={(
-            <div className="vm-query-editor-warning__tooltip">
-              {warnings.map((warning, index) => <p key={index}>{warning.text}</p>)}
-            </div>
-          )}
-        >
-          <WarningIcon/>
-        </Tooltip>
-      </div>
-    )}
   </div>;
 };
 
diff --git a/app/vmui/packages/vmui/src/components/Configurators/QueryEditor/style.scss b/app/vmui/packages/vmui/src/components/Configurators/QueryEditor/style.scss
index 7c8354845..6e9d4277f 100644
--- a/app/vmui/packages/vmui/src/components/Configurators/QueryEditor/style.scss
+++ b/app/vmui/packages/vmui/src/components/Configurators/QueryEditor/style.scss
@@ -7,29 +7,4 @@
     max-height: 300px;
     overflow: auto;
   }
-
-  &-warning {
-    position: absolute;
-    top: 50%;
-    right: $padding-global;
-    transform: translateY(-50%);
-    display: grid;
-    align-items: center;
-    justify-content: center;
-    width: 18px;
-    height: 18px;
-    color: $color-warning;
-
-    &__tooltip {
-      white-space: pre-line;
-
-      p {
-        margin-bottom: $padding-small;
-
-        &:last-child {
-          margin-bottom: 0;
-        }
-      }
-    }
-  }
 }
diff --git a/app/vmui/packages/vmui/src/components/Main/TextField/TextField.tsx b/app/vmui/packages/vmui/src/components/Main/TextField/TextField.tsx
index 2514bfb9d..7e6211e3b 100644
--- a/app/vmui/packages/vmui/src/components/Main/TextField/TextField.tsx
+++ b/app/vmui/packages/vmui/src/components/Main/TextField/TextField.tsx
@@ -3,7 +3,7 @@ import classNames from "classnames";
 import { useMemo } from "preact/compat";
 import { useAppState } from "../../../state/common/StateContext";
 import useDeviceDetect from "../../../hooks/useDeviceDetect";
-import TextFieldError from "./TextFieldError";
+import TextFieldMessage from "./TextFieldMessage";
 import "./style.scss";
 
 interface TextFieldProps {
@@ -11,6 +11,7 @@ interface TextFieldProps {
   value?: string | number
   type?: HTMLInputTypeAttribute | "textarea"
   error?: string
+  warning?: string
   placeholder?: string
   endIcon?: ReactNode
   startIcon?: ReactNode
@@ -30,12 +31,13 @@ const TextField: FC<TextFieldProps> = ({
   value,
   type = "text",
   error = "",
+  warning = "",
+  helperText = "",
   placeholder,
   endIcon,
   startIcon,
   disabled = false,
   autofocus = false,
-  helperText,
   inputmode = "text",
   onChange,
   onEnter,
@@ -53,6 +55,7 @@ const TextField: FC<TextFieldProps> = ({
   const inputClasses = classNames({
     "vm-text-field__input": true,
     "vm-text-field__input_error": error,
+    "vm-text-field__input_warning": !error && warning,
     "vm-text-field__input_icon-start": startIcon,
     "vm-text-field__input_disabled": disabled,
     "vm-text-field__input_textarea": type === "textarea",
@@ -133,12 +136,11 @@ const TextField: FC<TextFieldProps> = ({
       )
     }
     {label && <span className="vm-text-field__label">{label}</span>}
-    <TextFieldError error={error}/>
-    {helperText && !error && (
-      <span className="vm-text-field__helper-text">
-        {helperText}
-      </span>
-    )}
+    <TextFieldMessage
+      error={error}
+      warning={warning}
+      info={helperText}
+    />
   </label>
   ;
 };
diff --git a/app/vmui/packages/vmui/src/components/Main/TextField/TextFieldError.tsx b/app/vmui/packages/vmui/src/components/Main/TextField/TextFieldError.tsx
deleted file mode 100644
index 3d3b1a95a..000000000
--- a/app/vmui/packages/vmui/src/components/Main/TextField/TextFieldError.tsx
+++ /dev/null
@@ -1,56 +0,0 @@
-import React, { FC, useEffect, useRef, useState } from "react";
-import useEventListener from "../../../hooks/useEventListener";
-import classNames from "classnames";
-import "./style.scss";
-
-interface TextFieldErrorProps {
-  error: string;
-}
-
-const TextFieldError: FC<TextFieldErrorProps> = ({ error }) => {
-  const errorRef = useRef<HTMLSpanElement>(null);
-  const [isErrorTruncated, setIsErrorTruncated] = useState(false);
-  const [showFull, setShowFull] = useState(false);
-
-  const checkIfTextTruncated = () => {
-    const el = errorRef.current;
-    if (el) {
-      const { offsetWidth, scrollWidth, offsetHeight, scrollHeight } = el;
-      // The "+1" is for the scrollbar in Firefox
-      const overflowed = (offsetWidth + 1) < scrollWidth || (offsetHeight + 1) < scrollHeight;
-      setIsErrorTruncated(overflowed);
-    } else {
-      setIsErrorTruncated(false);
-    }
-  };
-
-  const handleClickError = () => {
-    if (!isErrorTruncated) return;
-    setShowFull(true);
-    setIsErrorTruncated(false);
-  };
-
-  useEffect(() => {
-    setShowFull(false);
-    checkIfTextTruncated();
-  }, [errorRef, error]);
-
-  useEventListener("resize", checkIfTextTruncated);
-
-  return (
-    <span
-      className={classNames({
-        "vm-text-field__error": true,
-        "vm-text-field__error_overflowed": isErrorTruncated,
-        "vm-text-field__error_full": showFull,
-      })}
-      data-show={!!error}
-      ref={errorRef}
-      onClick={handleClickError}
-    >
-      {error}
-    </span>
-  );
-};
-
-export default TextFieldError;
diff --git a/app/vmui/packages/vmui/src/components/Main/TextField/TextFieldMessage.tsx b/app/vmui/packages/vmui/src/components/Main/TextField/TextFieldMessage.tsx
new file mode 100644
index 000000000..db1c2f1c7
--- /dev/null
+++ b/app/vmui/packages/vmui/src/components/Main/TextField/TextFieldMessage.tsx
@@ -0,0 +1,72 @@
+import React, { FC, useEffect, useRef, useState } from "react";
+import useEventListener from "../../../hooks/useEventListener";
+import classNames from "classnames";
+import "./style.scss";
+import { useMemo } from "preact/compat";
+
+interface TextFieldErrorProps {
+  error: string;
+  warning: string;
+  info: string;
+}
+
+const TextFieldMessage: FC<TextFieldErrorProps> = ({ error, warning, info }) => {
+  console.log(warning);
+  const messageRef = useRef<HTMLSpanElement>(null);
+  const [isMessageTruncated, setIsMessageTruncated] = useState(false);
+  const [showFull, setShowFull] = useState(false);
+
+  const prefix = useMemo(() => {
+    if (error) return "ERROR: ";
+    if (warning) return "WARNING: ";
+    return "";
+  }, [error, warning]);
+
+  const message = `${prefix}${error || warning || info}`;
+
+  const checkIfTextTruncated = () => {
+    const el = messageRef.current;
+    if (el) {
+      const { offsetWidth, scrollWidth, offsetHeight, scrollHeight } = el;
+      // The "+1" is for the scrollbar in Firefox
+      const overflowed = (offsetWidth + 1) < scrollWidth || (offsetHeight + 1) < scrollHeight;
+      setIsMessageTruncated(overflowed);
+    } else {
+      setIsMessageTruncated(false);
+    }
+  };
+
+  const handleClickError = () => {
+    if (!isMessageTruncated) return;
+    setShowFull(true);
+    setIsMessageTruncated(false);
+  };
+
+  useEffect(() => {
+    setShowFull(false);
+    checkIfTextTruncated();
+  }, [messageRef, message]);
+
+  useEventListener("resize", checkIfTextTruncated);
+
+  if (!error && !warning && !info) return null;
+
+  return (
+    <span
+      className={classNames({
+        "vm-text-field__error": true,
+        "vm-text-field__warning": warning && !error,
+        "vm-text-field__helper-text": !warning && !error,
+        "vm-text-field__error_overflowed": isMessageTruncated,
+        "vm-text-field__error_full": showFull,
+      })}
+      data-show={!!message}
+      ref={messageRef}
+      onClick={handleClickError}
+    >
+      {message}
+    </span>
+  );
+};
+
+export default TextFieldMessage;
diff --git a/app/vmui/packages/vmui/src/components/Main/TextField/style.scss b/app/vmui/packages/vmui/src/components/Main/TextField/style.scss
index 44a854bbf..d94b61e90 100644
--- a/app/vmui/packages/vmui/src/components/Main/TextField/style.scss
+++ b/app/vmui/packages/vmui/src/components/Main/TextField/style.scss
@@ -26,6 +26,7 @@
 
   &__label,
   &__error,
+  &__warning,
   &__helper-text, {
     position: absolute;
     left: calc($padding-global/2);
@@ -50,12 +51,13 @@
     color: $color-text-secondary;
   }
 
+  &__helper-text,
+  &__warning,
   &__error {
     position: relative;
     top: calc($font-size-small/-2);
     width: fit-content;
     overflow-wrap: anywhere;
-    color: $color-error;
     pointer-events: auto;
     user-select: text;
 
@@ -69,8 +71,15 @@
     }
   }
 
+  &__error {
+    color: $color-error;
+  }
+
+  &__warning {
+    color: $color-warning;
+  }
+
   &__helper-text {
-    bottom: calc($font-size-small/-2);
     color: $color-text-secondary;
   }
 
@@ -93,14 +102,26 @@
     }
 
     &_error {
-      border: 1px solid $color-error;
+      border-color: $color-error;
+
+      &:hover {
+        border-color: $color-error;
+      }
+
+      &:focus {
+        border-color: $color-error;
+      }
+    }
+
+    &_warning {
+      border-color: $color-warning;
 
       &:hover {
-        border: 1px solid $color-error;
+        border-color: $color-warning;
       }
 
       &:focus {
-        border: 1px solid $color-error;
+        border-color: $color-warning;
       }
     }
 
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 e4f418983..be014a298 100644
--- a/app/vmui/packages/vmui/src/hooks/useFetchQuery.ts
+++ b/app/vmui/packages/vmui/src/hooks/useFetchQuery.ts
@@ -83,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) {
 
@@ -115,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;
@@ -136,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}`);
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/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/deployment/docker/docker-compose-cluster.yml b/deployment/docker/docker-compose-cluster.yml
index a659dd14b..6706ba967 100644
--- a/deployment/docker/docker-compose-cluster.yml
+++ b/deployment/docker/docker-compose-cluster.yml
@@ -2,7 +2,7 @@ version: '3.5'
 services:
   vmagent:
     container_name: vmagent
-    image: victoriametrics/vmagent:v1.93.0
+    image: victoriametrics/vmagent:v1.93.1
     depends_on:
       - "vminsert"
     ports:
@@ -32,7 +32,7 @@ services:
 
   vmstorage-1:
     container_name: vmstorage-1
-    image: victoriametrics/vmstorage:v1.93.0-cluster
+    image: victoriametrics/vmstorage:v1.93.1-cluster
     ports:
       - 8482
       - 8400
@@ -44,7 +44,7 @@ services:
     restart: always
   vmstorage-2:
     container_name: vmstorage-2
-    image: victoriametrics/vmstorage:v1.93.0-cluster
+    image: victoriametrics/vmstorage:v1.93.1-cluster
     ports:
       - 8482
       - 8400
@@ -56,7 +56,7 @@ services:
     restart: always
   vminsert:
     container_name: vminsert
-    image: victoriametrics/vminsert:v1.93.0-cluster
+    image: victoriametrics/vminsert:v1.93.1-cluster
     depends_on:
       - "vmstorage-1"
       - "vmstorage-2"
@@ -68,7 +68,7 @@ services:
     restart: always
   vmselect:
     container_name: vmselect
-    image: victoriametrics/vmselect:v1.93.0-cluster
+    image: victoriametrics/vmselect:v1.93.1-cluster
     depends_on:
       - "vmstorage-1"
       - "vmstorage-2"
@@ -82,7 +82,7 @@ services:
 
   vmalert:
     container_name: vmalert
-    image: victoriametrics/vmalert:v1.93.0
+    image: victoriametrics/vmalert:v1.93.1
     depends_on:
       - "vmselect"
     ports:
diff --git a/deployment/docker/docker-compose.yml b/deployment/docker/docker-compose.yml
index dc4b90929..a1a59dd33 100644
--- a/deployment/docker/docker-compose.yml
+++ b/deployment/docker/docker-compose.yml
@@ -2,7 +2,7 @@ version: "3.5"
 services:
   vmagent:
     container_name: vmagent
-    image: victoriametrics/vmagent:v1.93.0
+    image: victoriametrics/vmagent:v1.93.1
     depends_on:
       - "victoriametrics"
     ports:
@@ -18,7 +18,7 @@ services:
     restart: always
   victoriametrics:
     container_name: victoriametrics
-    image: victoriametrics/victoria-metrics:v1.93.0
+    image: victoriametrics/victoria-metrics:v1.93.1
     ports:
       - 8428:8428
       - 8089:8089
@@ -56,7 +56,7 @@ services:
     restart: always
   vmalert:
     container_name: vmalert
-    image: victoriametrics/vmalert:v1.93.0
+    image: victoriametrics/vmalert:v1.93.1
     depends_on:
       - "victoriametrics"
       - "alertmanager"
diff --git a/deployment/logs-benchmark/docker-compose.yml b/deployment/logs-benchmark/docker-compose.yml
index 416046a35..6da6e164e 100644
--- a/deployment/logs-benchmark/docker-compose.yml
+++ b/deployment/logs-benchmark/docker-compose.yml
@@ -105,7 +105,7 @@ services:
       - '--config=/config.yml'
 
   vmsingle:
-    image: victoriametrics/victoria-metrics:v1.93.0
+    image: victoriametrics/victoria-metrics:v1.93.1
     ports:
       - '8428:8428'
     command:
diff --git a/deployment/marketplace/digitialocean/one-click-droplet/RELEASE_GUIDE.md b/deployment/marketplace/digitialocean/one-click-droplet/RELEASE_GUIDE.md
index 41fc7cd72..fdf6ed943 100644
--- a/deployment/marketplace/digitialocean/one-click-droplet/RELEASE_GUIDE.md
+++ b/deployment/marketplace/digitialocean/one-click-droplet/RELEASE_GUIDE.md
@@ -8,7 +8,7 @@
 4. Set variables `DIGITALOCEAN_API_TOKEN` with `VM_VERSION` for `packer` environment and run make from example below:
 
 ```console
-make release-victoria-metrics-digitalocean-oneclick-droplet DIGITALOCEAN_API_TOKEN="dop_v23_2e46f4759ceeeba0d0248" VM_VERSION="1.93.0"
+make release-victoria-metrics-digitalocean-oneclick-droplet DIGITALOCEAN_API_TOKEN="dop_v23_2e46f4759ceeeba0d0248" VM_VERSION="1.93.1"
 ```
 
 
diff --git a/deployment/marketplace/digitialocean/one-click-droplet/files/etc/update-motd.d/99-one-click b/deployment/marketplace/digitialocean/one-click-droplet/files/etc/update-motd.d/99-one-click
index e0765d5e2..8e8eeea1d 100755
--- a/deployment/marketplace/digitialocean/one-click-droplet/files/etc/update-motd.d/99-one-click
+++ b/deployment/marketplace/digitialocean/one-click-droplet/files/etc/update-motd.d/99-one-click
@@ -19,8 +19,8 @@ On the server:
   * VictoriaMetrics is running on ports: 8428, 8089, 4242, 2003 and they are bound to the local interface.
 
 ********************************************************************************
-  # This image includes 1.93.0 version of VictoriaMetrics.
-  # See Release notes https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v1.93.0
+  # This image includes 1.93.1 version of VictoriaMetrics.
+  # See Release notes https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v1.93.1
 
   # Welcome to VictoriaMetrics droplet!
 
diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md
index f59db25ed..22b04dadb 100644
--- a/docs/CHANGELOG.md
+++ b/docs/CHANGELOG.md
@@ -26,19 +26,32 @@ The following `tip` changes can be tested by building VictoriaMetrics components
 
 * 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): make the warning message more noticeable for text fields. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/4848).
+* 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).
 
+## [v1.93.1](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v1.93.1)
+
+Released at 2023-08-23
+
+**v1.93.x is a line of LTS releases (e.g. long-time support). It contains important up-to-date bugfixes.
+The v1.93.x line will be supported for at least 12 months since [v1.93.0](https://docs.victoriametrics.com/CHANGELOG.html#v1930) release**
+
+* BUGFIX: [storage](https://docs.victoriametrics.com/Single-server-VictoriaMetrics.html): properly set next retention time for indexDB. Previously it may enter into endless retention loop. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/4873) for details.
 * 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).
+* BUGFIX: [vmalert](https://docs.victoriametrics.com/vmalert.html): correctly re-use HTTP request object on `EOF` retries when querying the configured datasource. Previously, there was a small chance that query retry wouldn't succeed.
+
 
 ## [v1.93.0](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v1.93.0)
 
 Released at 2023-08-12
 
+**It is recommended upgrading to [VictoriaMetrics v1.93.1](https://docs.victoriametrics.com/CHANGELOG.html#v1931) because v1.93.0 contains a bug, which could lead to incorrect indexdb rotation. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/4873) for details.**
+
 **v1.93.x is a line of LTS releases (e.g. long-time support). It contains important up-to-date bugfixes.
 The v1.93.x line will be supported for at least 12 months since [v1.93.0](https://docs.victoriametrics.com/CHANGELOG.html#v1930) release**
 
@@ -60,8 +73,8 @@ The v1.93.x line will be supported for at least 12 months since [v1.93.0](https:
 * FEATURE: [Official Grafana dashboards for VictoriaMetrics](https://grafana.com/orgs/victoriametrics): add panels for absolute Mem and CPU usage by vmalert. See related issue [here](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/4627).
 * FEATURE: [Official Grafana dashboards for VictoriaMetrics](https://grafana.com/orgs/victoriametrics): correctly calculate `Bytes per point` value for single-server and cluster VM dashboards. Before, the calculation mistakenly accounted for the number of entries in indexdb in denominator, which could have shown lower values than expected.
 * FEATURE: [Alerting rules for VictoriaMetrics](https://github.com/VictoriaMetrics/VictoriaMetrics/tree/master/deployment/docker#alerts): `ConcurrentFlushesHitTheLimit` alerting rule was moved from [single-server](https://github.com/VictoriaMetrics/VictoriaMetrics/blob/master/deployment/docker/alerts.yml) and [cluster](https://github.com/VictoriaMetrics/VictoriaMetrics/blob/master/deployment/docker/alerts-cluster.yml) alerts to the [list of "health" alerts](https://github.com/VictoriaMetrics/VictoriaMetrics/blob/master/deployment/docker/alerts-health.yml) as it could be related to many VictoriaMetrics components. 
-* FEATURE: [vmui](https://docs.victoriametrics.com/#vmui): added Prettify query functionality. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/4681)
 
+* BUGFIX: [storage](https://docs.victoriametrics.com/Single-server-VictoriaMetrics.html): properly set next retention time for indexDB. Previously it may enter into endless retention loop. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/4873) for details.
 * BUGFIX: [vmagent](https://docs.victoriametrics.com/vmagent.html): return human readable error if opentelemetry has json encoding. Follow-up after [PR](https://github.com/VictoriaMetrics/VictoriaMetrics/pull/2570).
 * BUGFIX: [vmagent](https://docs.victoriametrics.com/vmagent.html): properly validate scheme for `proxy_url` field at the scrape config. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/4811) for details.
 * BUGFIX: [vmagent](https://docs.victoriametrics.com/vmagent.html): properly apply `if` filters during [relabeling](https://docs.victoriametrics.com/vmagent.html#relabeling-enhancements). Previously the `if` filter could improperly work. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/4806) and [this pull request](https://github.com/VictoriaMetrics/VictoriaMetrics/pull/4816).
@@ -85,6 +98,9 @@ Released at 2023-07-28
 
 Released at 2023-07-27
 
+**Update note: this release contains backwards-incompatible change to indexdb,
+so rolling back to the previous versions of VictoriaMetrics may result in partial data loss of entries in indexdb.**
+
 **Update note**: starting from this release, [stream aggregation](https://docs.victoriametrics.com/stream-aggregation.html) writes
 the following samples to the configured remote storage by default:
 
diff --git a/docs/operator/CHANGELOG.md b/docs/operator/CHANGELOG.md
index 5b662c681..ff3be5a25 100644
--- a/docs/operator/CHANGELOG.md
+++ b/docs/operator/CHANGELOG.md
@@ -1,6 +1,13 @@
 # CHANGELOG
 
-## tip
+## Next release
+
+### Fixes
+
+### Features
+
+<a name="v0.36.0"></a>
+## [v0.36.0](https://github.com/VictoriaMetrics/operator/releases/tag/v0.36.0) - 23 Aug 2023
 
 ### Breaking changes
 
@@ -42,6 +49,8 @@
 - [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.
 
+[Changes][v0.36.0]
+
 <a name="v0.35.1"></a>
 ## [v0.35.1](https://github.com/VictoriaMetrics/operator/releases/tag/v0.35.1) - 12 Jul 2023
 
diff --git a/docs/operator/vars.md b/docs/operator/vars.md
index be3fd0952..2529047f2 100644
--- a/docs/operator/vars.md
+++ b/docs/operator/vars.md
@@ -10,7 +10,7 @@ aliases:
 - /operator/vars.html
 ---
 # Auto Generated vars for package config 
- updated at Fri Aug 18 07:32:53 UTC 2023 
+ updated at Wed Aug 23 17:26:30 UTC 2023 
 
 
 | varible name | variable default value | variable required | variable description |
@@ -20,7 +20,7 @@ aliases:
 | VM_CUSTOMCONFIGRELOADERIMAGE | victoriametrics/operator:config-reloader-v0.32.0 | false | - |
 | VM_PSPAUTOCREATEENABLED | false | false | - |
 | VM_VMALERTDEFAULT_IMAGE | victoriametrics/vmalert | false | - |
-| VM_VMALERTDEFAULT_VERSION | v1.91.3 | false | - |
+| VM_VMALERTDEFAULT_VERSION | v1.93.1 | false | - |
 | VM_VMALERTDEFAULT_PORT | 8080 | false | - |
 | VM_VMALERTDEFAULT_USEDEFAULTRESOURCES | true | false | - |
 | VM_VMALERTDEFAULT_RESOURCE_LIMIT_MEM | 500Mi | false | - |
@@ -31,7 +31,7 @@ aliases:
 | VM_VMALERTDEFAULT_CONFIGRELOADERMEMORY | 25Mi | false | - |
 | VM_VMALERTDEFAULT_CONFIGRELOADIMAGE | jimmidyson/configmap-reload:v0.3.0 | false | - |
 | VM_VMAGENTDEFAULT_IMAGE | victoriametrics/vmagent | false | - |
-| VM_VMAGENTDEFAULT_VERSION | v1.91.3 | false | - |
+| VM_VMAGENTDEFAULT_VERSION | v1.93.1 | false | - |
 | VM_VMAGENTDEFAULT_CONFIGRELOADIMAGE | quay.io/prometheus-operator/prometheus-config-reloader:v0.58.0 | false | - |
 | VM_VMAGENTDEFAULT_PORT | 8429 | false | - |
 | VM_VMAGENTDEFAULT_USEDEFAULTRESOURCES | true | false | - |
@@ -42,7 +42,7 @@ aliases:
 | VM_VMAGENTDEFAULT_CONFIGRELOADERCPU | 100m | false | - |
 | VM_VMAGENTDEFAULT_CONFIGRELOADERMEMORY | 25Mi | false | - |
 | VM_VMSINGLEDEFAULT_IMAGE | victoriametrics/victoria-metrics | false | - |
-| VM_VMSINGLEDEFAULT_VERSION | v1.91.3 | false | - |
+| VM_VMSINGLEDEFAULT_VERSION | v1.93.1 | false | - |
 | VM_VMSINGLEDEFAULT_PORT | 8429 | false | - |
 | VM_VMSINGLEDEFAULT_USEDEFAULTRESOURCES | true | false | - |
 | VM_VMSINGLEDEFAULT_RESOURCE_LIMIT_MEM | 1500Mi | false | - |
@@ -53,14 +53,14 @@ aliases:
 | VM_VMSINGLEDEFAULT_CONFIGRELOADERMEMORY | 25Mi | false | - |
 | VM_VMCLUSTERDEFAULT_USEDEFAULTRESOURCES | true | false | - |
 | VM_VMCLUSTERDEFAULT_VMSELECTDEFAULT_IMAGE | victoriametrics/vmselect | false | - |
-| VM_VMCLUSTERDEFAULT_VMSELECTDEFAULT_VERSION | v1.91.3-cluster | false | - |
+| VM_VMCLUSTERDEFAULT_VMSELECTDEFAULT_VERSION | v1.93.1-cluster | false | - |
 | VM_VMCLUSTERDEFAULT_VMSELECTDEFAULT_PORT | 8481 | false | - |
 | VM_VMCLUSTERDEFAULT_VMSELECTDEFAULT_RESOURCE_LIMIT_MEM | 1000Mi | false | - |
 | VM_VMCLUSTERDEFAULT_VMSELECTDEFAULT_RESOURCE_LIMIT_CPU | 500m | false | - |
 | VM_VMCLUSTERDEFAULT_VMSELECTDEFAULT_RESOURCE_REQUEST_MEM | 500Mi | false | - |
 | VM_VMCLUSTERDEFAULT_VMSELECTDEFAULT_RESOURCE_REQUEST_CPU | 100m | false | - |
 | VM_VMCLUSTERDEFAULT_VMSTORAGEDEFAULT_IMAGE | victoriametrics/vmstorage | false | - |
-| VM_VMCLUSTERDEFAULT_VMSTORAGEDEFAULT_VERSION | v1.91.3-cluster | false | - |
+| VM_VMCLUSTERDEFAULT_VMSTORAGEDEFAULT_VERSION | v1.93.1-cluster | false | - |
 | VM_VMCLUSTERDEFAULT_VMSTORAGEDEFAULT_VMINSERTPORT | 8400 | false | - |
 | VM_VMCLUSTERDEFAULT_VMSTORAGEDEFAULT_VMSELECTPORT | 8401 | false | - |
 | VM_VMCLUSTERDEFAULT_VMSTORAGEDEFAULT_PORT | 8482 | false | - |
@@ -69,7 +69,7 @@ aliases:
 | VM_VMCLUSTERDEFAULT_VMSTORAGEDEFAULT_RESOURCE_REQUEST_MEM | 500Mi | false | - |
 | VM_VMCLUSTERDEFAULT_VMSTORAGEDEFAULT_RESOURCE_REQUEST_CPU | 250m | false | - |
 | VM_VMCLUSTERDEFAULT_VMINSERTDEFAULT_IMAGE | victoriametrics/vminsert | false | - |
-| VM_VMCLUSTERDEFAULT_VMINSERTDEFAULT_VERSION | v1.91.3-cluster | false | - |
+| VM_VMCLUSTERDEFAULT_VMINSERTDEFAULT_VERSION | v1.93.1-cluster | false | - |
 | VM_VMCLUSTERDEFAULT_VMINSERTDEFAULT_PORT | 8480 | false | - |
 | VM_VMCLUSTERDEFAULT_VMINSERTDEFAULT_RESOURCE_LIMIT_MEM | 500Mi | false | - |
 | VM_VMCLUSTERDEFAULT_VMINSERTDEFAULT_RESOURCE_LIMIT_CPU | 500m | false | - |
@@ -88,7 +88,7 @@ aliases:
 | VM_VMALERTMANAGER_RESOURCE_REQUEST_CPU | 30m | false | - |
 | VM_DISABLESELFSERVICESCRAPECREATION | false | false | - |
 | VM_VMBACKUP_IMAGE | victoriametrics/vmbackupmanager | false | - |
-| VM_VMBACKUP_VERSION | v1.91.3-enterprise | false | - |
+| VM_VMBACKUP_VERSION | v1.93.1-enterprise | false | - |
 | VM_VMBACKUP_PORT | 8300 | false | - |
 | VM_VMBACKUP_USEDEFAULTRESOURCES | true | false | - |
 | VM_VMBACKUP_RESOURCE_LIMIT_MEM | 500Mi | false | - |
@@ -97,7 +97,7 @@ aliases:
 | VM_VMBACKUP_RESOURCE_REQUEST_CPU | 150m | false | - |
 | VM_VMBACKUP_LOGLEVEL | INFO | false | - |
 | VM_VMAUTHDEFAULT_IMAGE | victoriametrics/vmauth | false | - |
-| VM_VMAUTHDEFAULT_VERSION | v1.91.3 | false | - |
+| VM_VMAUTHDEFAULT_VERSION | v1.93.1 | false | - |
 | VM_VMAUTHDEFAULT_CONFIGRELOADIMAGE | quay.io/prometheus-operator/prometheus-config-reloader:v0.48.1 | false | - |
 | VM_VMAUTHDEFAULT_PORT | 8427 | false | - |
 | VM_VMAUTHDEFAULT_USEDEFAULTRESOURCES | true | false | - |
diff --git a/docs/stream-aggregation.md b/docs/stream-aggregation.md
index 80756a3f0..5970c5ce2 100644
--- a/docs/stream-aggregation.md
+++ b/docs/stream-aggregation.md
@@ -451,6 +451,12 @@ For example, see below time series produced by config with aggregation interval
 Aggregating irregular and sporadic metrics (received from [Lambdas](https://aws.amazon.com/lambda/)
 or [Cloud Functions](https://cloud.google.com/functions)) can be controlled via [staleness_inteval](#stream-aggregation-config).
 
+`increase` can be used as an alternative for [rate](https://docs.victoriametrics.com/MetricsQL.html#rate) function.
+For example, if we have `increase` with `interval` of `5m` for a counter `some_counter`, then to get `rate` we should divide
+the resulting aggregation by the `interval` in seconds: `some_counter:5m_increase / 300` is similar to `rate(some_counter[5m])`.
+Please note, opposite to [rate](https://docs.victoriametrics.com/MetricsQL.html#rate), `increase` aggregations can be 
+combined safely afterwards. This is helpful when the aggregation is calculated by more than one vmagent.
+
 ### count_series
 
 `count_series` counts the number of unique [time series](https://docs.victoriametrics.com/keyConcepts.html#time-series).
diff --git a/docs/vmanomaly.md b/docs/vmanomaly.md
index fda6ebc06..7c745c325 100644
--- a/docs/vmanomaly.md
+++ b/docs/vmanomaly.md
@@ -182,10 +182,10 @@ The license key can be passed via the following command-line flags:
   --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.
+  --license-verify-offline {true,false}
+                        Force offline verification of license code. License is
+                        verified online by default. This flag runs license
+                        verification offline.
 ```
 
 Usage example:
diff --git a/lib/storage/storage.go b/lib/storage/storage.go
index 5ef0a8290..e61c0f0c0 100644
--- a/lib/storage/storage.go
+++ b/lib/storage/storage.go
@@ -757,7 +757,7 @@ func (s *Storage) mustRotateIndexDB(currentTime time.Time) {
 	idbNew := mustOpenIndexDB(idbNewPath, s, &s.isReadOnly)
 
 	// Update nextRotationTimestamp
-	nextRotationTimestamp := currentTime.UnixMilli() + s.retentionMsecs/1000
+	nextRotationTimestamp := currentTime.Unix() + s.retentionMsecs/1000
 	atomic.StoreInt64(&s.nextRotationTimestamp, nextRotationTimestamp)
 
 	// Set idbNext to idbNew


Test patch:
diff --git a/app/vmalert/datasource/vm_test.go b/app/vmalert/datasource/vm_test.go
index 68c2f6e8f..7b06f42db 100644
--- a/app/vmalert/datasource/vm_test.go
+++ b/app/vmalert/datasource/vm_test.go
@@ -629,10 +629,7 @@ func TestRequestParams(t *testing.T) {
 
 	for _, tc := range testCases {
 		t.Run(tc.name, func(t *testing.T) {
-			req, err := tc.vm.newRequestPOST()
-			if err != nil {
-				t.Fatalf("unexpected error: %s", err)
-			}
+			req := tc.vm.newRequest()
 			switch tc.vm.dataSourceType {
 			case "", datasourcePrometheus:
 				if tc.queryRange {
@@ -727,10 +724,7 @@ func TestHeaders(t *testing.T) {
 	for _, tt := range testCases {
 		t.Run(tt.name, func(t *testing.T) {
 			vm := tt.vmFn()
-			req, err := vm.newRequestPOST()
-			if err != nil {
-				t.Fatalf("unexpected error: %s", err)
-			}
+			req := vm.newQueryRequest("foo", time.Now())
 			tt.checkFn(t, req)
 		})
 	}