Repository: VictoriaMetrics/VictoriaMetrics
Pull request #8882: app/vmui: add live tailing tab to Victoria Logs
Author: login: arturminchukov
Created: 2025-05-06T10:26:32Z | Merged: 2025-05-16T13:37:55Z

### Describe Your Changes

Related issue: VictoriaMetrics/VictoriaLogs#298 

Changes:
 - add JSX automatic runtime import of React, you don't need to import React anymore in JSX files.
 - add unused imports eslint rule
 - add headers to ProcessLiveTailRequest to enable client-side connection setup
 - refactor ExploreLogs: divided the component into several separate components for better readability and code maintenance
 - add live tailing tab to VictoriaLogs

short demo:

https://github.com/user-attachments/assets/3e5f57ee-8e72-4835-9fc6-35c6f38bc9ef



### Checklist

The following checks are **mandatory**:

- [ ] My change adheres to [VictoriaMetrics contributing guidelines](https://docs.victoriametrics.com/victoriametrics/contributing/).



Diff:
diff --git a/app/vlselect/logsql/logsql.go b/app/vlselect/logsql/logsql.go
index 7f54f5fc06263..f754fe36f3354 100644
--- a/app/vlselect/logsql/logsql.go
+++ b/app/vlselect/logsql/logsql.go
@@ -514,6 +514,11 @@ func ProcessLiveTailRequest(ctx context.Context, w http.ResponseWriter, r *http.
 	if !ok {
 		logger.Panicf("BUG: it is expected that http.ResponseWriter (%T) supports http.Flusher interface", w)
 	}
+
+	w.Header().Set("Content-Type", "application/x-ndjson")
+	w.Header().Set("Access-Control-Allow-Origin", "*")
+	flusher.Flush()
+
 	qOrig := q
 	for {
 		q = qOrig.CloneWithTimeFilter(end, start, end)
diff --git a/app/vmui/packages/vmui/eslint.config.js b/app/vmui/packages/vmui/eslint.config.js
index 0630c79ce8492..559f3bf541d86 100644
--- a/app/vmui/packages/vmui/eslint.config.js
+++ b/app/vmui/packages/vmui/eslint.config.js
@@ -6,6 +6,7 @@ import path from "node:path";
 import { fileURLToPath } from "node:url";
 import js from "@eslint/js";
 import { FlatCompat } from "@eslint/eslintrc";
+import unusedImports from "eslint-plugin-unused-imports";
 
 const __filename = fileURLToPath(import.meta.url);
 const __dirname = path.dirname(__filename);
@@ -23,6 +24,7 @@ export default [...compat.extends(
   plugins: {
     react,
     "@typescript-eslint": typescriptEslint,
+    "unused-imports": unusedImports,
   },
 
   languageOptions: {
@@ -59,7 +61,7 @@ export default [...compat.extends(
       allowTernary: true
     }],
 
-    "@typescript-eslint/no-unused-vars": ["warn", {
+    "@typescript-eslint/no-unused-vars": ["error", {
       "argsIgnorePattern": "^_",
       "caughtErrors": "none",
       "caughtErrorsIgnorePattern": "^_",
@@ -67,6 +69,8 @@ export default [...compat.extends(
       "varsIgnorePattern": "^_",
       "ignoreRestSiblings": true
     }],
+    
+    "unused-imports/no-unused-imports": "error",
 
     "react/jsx-closing-bracket-location": [1, "line-aligned"],
 
@@ -85,6 +89,7 @@ export default [...compat.extends(
     quotes: ["error", "double"],
     semi: ["error", "always"],
     "react/prop-types": 0,
+    "react/react-in-jsx-scope": "off",
 
   },
 }];
diff --git a/app/vmui/packages/vmui/package-lock.json b/app/vmui/packages/vmui/package-lock.json
index 6a7cc8e68aae9..c3718cdc57e3d 100644
--- a/app/vmui/packages/vmui/package-lock.json
+++ b/app/vmui/packages/vmui/package-lock.json
@@ -42,6 +42,7 @@
         "cross-env": "^7.0.3",
         "eslint": "^9.24.0",
         "eslint-plugin-react": "^7.37.5",
+        "eslint-plugin-unused-imports": "^4.1.4",
         "globals": "^16.0.0",
         "http-proxy-middleware": "^3.0.5",
         "jsdom": "^26.1.0",
@@ -4199,6 +4200,22 @@
         "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7"
       }
     },
+    "node_modules/eslint-plugin-unused-imports": {
+      "version": "4.1.4",
+      "resolved": "https://registry.npmjs.org/eslint-plugin-unused-imports/-/eslint-plugin-unused-imports-4.1.4.tgz",
+      "integrity": "sha512-YptD6IzQjDardkl0POxnnRBhU1OEePMV0nd6siHaRBbd+lyh6NAhFEobiznKU7kTsSsDeSD62Pe7kAM1b7dAZQ==",
+      "dev": true,
+      "license": "MIT",
+      "peerDependencies": {
+        "@typescript-eslint/eslint-plugin": "^8.0.0-0 || ^7.0.0 || ^6.0.0 || ^5.0.0",
+        "eslint": "^9.0.0 || ^8.0.0"
+      },
+      "peerDependenciesMeta": {
+        "@typescript-eslint/eslint-plugin": {
+          "optional": true
+        }
+      }
+    },
     "node_modules/eslint-scope": {
       "version": "8.3.0",
       "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.3.0.tgz",
diff --git a/app/vmui/packages/vmui/package.json b/app/vmui/packages/vmui/package.json
index 3de13af014471..df6bf5b7b7e21 100644
--- a/app/vmui/packages/vmui/package.json
+++ b/app/vmui/packages/vmui/package.json
@@ -66,6 +66,7 @@
     "cross-env": "^7.0.3",
     "eslint": "^9.24.0",
     "eslint-plugin-react": "^7.37.5",
+    "eslint-plugin-unused-imports": "^4.1.4",
     "globals": "^16.0.0",
     "http-proxy-middleware": "^3.0.5",
     "jsdom": "^26.1.0",
diff --git a/app/vmui/packages/vmui/src/components/Main/Icons/index.tsx b/app/vmui/packages/vmui/src/components/Main/Icons/index.tsx
index e08ff2877c34b..16ba67f468ae4 100644
--- a/app/vmui/packages/vmui/src/components/Main/Icons/index.tsx
+++ b/app/vmui/packages/vmui/src/components/Main/Icons/index.tsx
@@ -21,7 +21,7 @@ export const LogoLogsIcon = () => (
       d="M11.12 10.48c.36.28.8.43 1.26.43h.05c.48 0 .96-.19 1.25-.44 1.5-1.28 5.88-5.29 5.88-5.29 1.17-1.09-2.1-2.17-7.13-2.18h-.06c-5.04 0-8.3 1.1-7.13 2.18 0 0 4.37 4 5.88 5.3Zm2.56 2.16c-.36.28-.8.44-1.26.45h-.04c-.46 0-.9-.17-1.26-.45-1.04-.88-4.74-4.22-6.12-5.5v1.94c0 .21.08.5.22.63l.07.06c1.05.96 4.55 4.16 5.83 5.25.36.28.8.43 1.26.44h.04c.49-.02.96-.2 1.26-.44 1.3-1.11 4.94-4.45 5.88-5.31.15-.14.23-.42.23-.63V7.15a455.13 455.13 0 0 1-6.11 5.5Zm-1.26 4.99c.46 0 .9-.16 1.26-.44 2.05-1.82 4.09-3.65 6.1-5.5v1.94c0 .2-.07.48-.22.62-.94.87-4.57 4.2-5.88 5.3-.3.26-.77.44-1.26.45h-.04c-.46 0-.9-.16-1.26-.44-1.2-1.02-4.38-3.92-5.62-5.06l-.28-.25c-.14-.14-.22-.42-.22-.62v-1.94c1.38 1.26 5.08 4.6 6.12 5.5.36.28.8.43 1.26.44h.04ZM40 5l-5.84 14.46h-2.43L25.89 5h2.16a.9.9 0 0 1 .9.61l3.41 8.82a18.8 18.8 0 0 1 .62 2.02 19.44 19.44 0 0 1 .57-2.02l3.39-8.82c.05-.15.16-.3.31-.42a.9.9 0 0 1 .58-.19H40Zm17.18 0v14.46H54.8v-9.34c0-.37.02-.78.06-1.21l-4.37 8.21c-.21.4-.53.59-.95.59h-.38c-.43 0-.75-.2-.95-.59L43.8 8.88a22.96 22.96 0 0 1 .07 1.24v9.34H41.5V5h2.03l.3.01c.1 0 .17.02.24.05.07.03.13.07.19.13a1 1 0 0 1 .17.24l4.33 8.03a16.97 16.97 0 0 1 .6 1.36 14.34 14.34 0 0 1 .6-1.38l4.28-8.01c.05-.1.1-.18.17-.24.06-.06.12-.1.19-.13a.9.9 0 0 1 .24-.05l.3-.01h2.04Zm8.88 13.73a4.5 4.5 0 0 0 1.82-.35 3.96 3.96 0 0 0 2.22-2.47c.2-.57.3-1.19.3-1.85V5.31h1.02v8.75c0 .78-.12 1.51-.37 2.19a4.88 4.88 0 0 1-2.76 2.95c-.66.29-1.4.43-2.23.43-.82 0-1.57-.14-2.24-.43a5.01 5.01 0 0 1-2.75-2.95 6.37 6.37 0 0 1-.37-2.19V5.31h1.03v8.74c0 .66.1 1.28.3 1.85a3.98 3.98 0 0 0 2.21 2.47c.53.24 1.14.36 1.82.36Zm10.38.73h-1.03V5.31h1.03v14.15ZM1.73 36v-5.17l-.67-.07a.6.6 0 0 1-.21-.1.23.23 0 0 1-.08-.18v-.44h.96v-.59c0-.34.05-.65.14-.92a1.79 1.79 0 0 1 1.08-1.11 2.45 2.45 0 0 1 1.62-.02l-.03.53c0 .1-.06.15-.16.16H4c-.18 0-.35.03-.5.08a.95.95 0 0 0-.39.23c-.1.11-.19.25-.25.43-.05.18-.08.4-.08.65v.56h1.75v.78H2.8V36H1.73Zm6.17-6.17c.45 0 .85.07 1.2.22a2.57 2.57 0 0 1 1.5 1.62c.13.38.2.81.2 1.29s-.07.91-.2 1.3a2.57 2.57 0 0 1-1.49 1.61c-.36.14-.76.21-1.2.21-.45 0-.86-.07-1.22-.21a2.57 2.57 0 0 1-1.5-1.62c-.12-.38-.19-.81-.19-1.3 0-.47.07-.9.2-1.28a2.57 2.57 0 0 1 1.5-1.62c.35-.15.76-.22 1.2-.22Zm0 5.42c.6 0 1.05-.2 1.35-.6.3-.4.44-.97.44-1.69s-.15-1.28-.44-1.69c-.3-.4-.75-.6-1.35-.6-.3 0-.57.05-.8.15-.22.1-.4.26-.56.45-.15.2-.26.44-.33.73-.08.28-.11.6-.11.96 0 .72.15 1.29.44 1.69.3.4.76.6 1.36.6Zm5.26-4.11c.2-.42.43-.74.71-.97.28-.24.62-.36 1.03-.36.13 0 .25.02.36.05.12.02.23.07.32.13l-.08.8c-.02.1-.08.15-.18.15l-.24-.04a1.7 1.7 0 0 0-.88.05c-.15.05-.29.14-.4.25-.12.1-.23.24-.32.4-.1.17-.18.35-.26.56V36h-1.07v-6.08h.61c.12 0 .2.02.24.07.05.04.08.12.1.23l.06.92Zm13.73-3.82L23.39 36h-1.46l-3.5-8.68h1.29a.54.54 0 0 1 .54.37l2.04 5.3a11.31 11.31 0 0 1 .37 1.21 11.65 11.65 0 0 1 .35-1.22l2.03-5.29c.03-.1.1-.18.19-.25.1-.08.21-.12.35-.12h1.3Zm2.2 2.52V36H27.6v-6.16h1.49Zm.2-1.79c0 .13-.02.25-.08.36a1 1 0 0 1-.51.5.96.96 0 0 1-.73 0 1.02 1.02 0 0 1-.5-.5.96.96 0 0 1 0-.73.93.93 0 0 1 .86-.58.9.9 0 0 1 .37.08c.12.05.22.11.3.2a.94.94 0 0 1 .3.67Zm5.72 3.1a.68.68 0 0 1-.13.13c-.04.03-.1.05-.18.05a.42.42 0 0 1-.22-.07 3.95 3.95 0 0 0-.62-.31c-.14-.05-.3-.07-.51-.07-.26 0-.5.04-.69.14-.2.1-.36.23-.49.4-.13.18-.22.4-.29.64-.06.25-.1.53-.1.85 0 .33.04.62.1.88.08.25.18.47.32.64.13.18.29.3.48.4.18.09.4.13.63.13a1.6 1.6 0 0 0 .94-.27l.26-.2a.4.4 0 0 1 .25-.09.3.3 0 0 1 .27.14l.43.54a2.76 2.76 0 0 1-1.77.96c-.22.03-.43.05-.65.05a2.57 2.57 0 0 1-1.96-.83c-.25-.28-.45-.6-.6-1-.14-.4-.21-.85-.21-1.35 0-.45.06-.87.2-1.25a2.61 2.61 0 0 1 1.51-1.67c.37-.16.8-.24 1.28-.24.46 0 .86.07 1.2.22.35.15.66.36.94.64l-.4.54Zm3.43 4.95c-.54 0-.95-.15-1.24-.45-.28-.3-.42-.73-.42-1.26v-3.44h-.63a.29.29 0 0 1-.2-.07c-.06-.06-.09-.13-.09-.24v-.59l.99-.16.31-1.68a.33.33 0 0 1 .12-.18.34.34 0 0 1 .21-.07h.77v1.94h1.64v1.05h-1.64v3.34c0 .2.05.34.14.45.1.1.22.16.39.16a.73.73 0 0 0 .39-.1l.12-.07a.2.2 0 0 1 .11-.03c.05 0 .08.01.11.03l.09.1.44.72c-.21.18-.46.32-.74.4-.28.1-.57.15-.87.15Zm5.09-6.35c.46 0 .87.07 1.24.22a2.7 2.7 0 0 1 1.58 1.63c.14.39.22.83.22 1.31 0 .49-.08.93-.22 1.32-.14.4-.35.73-.62 1-.26.28-.58.49-.96.64-.37.15-.78.22-1.24.22a3.4 3.4 0 0 1-1.25-.22 2.71 2.71 0 0 1-1.59-1.64 3.8 3.8 0 0 1-.21-1.32c0-.48.07-.92.21-1.31a2.75 2.75 0 0 1 1.58-1.63c.38-.15.8-.22 1.26-.22Zm0 5.2c.51 0 .89-.17 1.13-.52.25-.34.38-.84.38-1.5a2.6 2.6 0 0 0-.38-1.53c-.24-.34-.62-.52-1.13-.52-.52 0-.9.18-1.16.53-.25.35-.37.85-.37 1.51s.12 1.17.37 1.51c.25.35.64.52 1.16.52Zm5.56-4.04c.2-.37.42-.65.69-.86.26-.21.57-.32.94-.32.28 0 .5.06.68.19l-.1 1.1a.3.3 0 0 1-.09.16.24.24 0 0 1-.15.04 1.8 1.8 0 0 1-.27-.03 2.01 2.01 0 0 0-.34-.03c-.16 0-.3.03-.44.08a1.1 1.1 0 0 0-.34.2c-.1.1-.2.2-.27.33-.08.13-.15.27-.22.44V36H47.7v-6.16h.87c.15 0 .26.03.31.09.06.05.1.15.13.29l.09.7Zm4.62-1.07V36h-1.49v-6.16h1.49Zm.2-1.79c0 .13-.02.25-.07.36a1 1 0 0 1-.51.5.96.96 0 0 1-.74 0 1.02 1.02 0 0 1-.5-.5.96.96 0 0 1 0-.73.93.93 0 0 1 .86-.58.9.9 0 0 1 .38.08c.11.05.21.11.3.2a.94.94 0 0 1 .28.67Zm4.56 5.32a7.8 7.8 0 0 0-1.08.12c-.29.05-.52.12-.7.2a.92.92 0 0 0-.38.3.64.64 0 0 0-.11.36c0 .26.07.45.23.56.15.11.35.17.6.17.3 0 .57-.06.79-.17.22-.1.44-.28.65-.5v-1.04Zm-3.4-2.67c.71-.65 1.57-.97 2.56-.97.36 0 .68.06.97.18a1.99 1.99 0 0 1 1.16 1.24c.1.3.16.61.16.96V36h-.67a.7.7 0 0 1-.33-.06c-.07-.04-.13-.13-.18-.26l-.13-.44c-.16.14-.3.26-.46.37a2.8 2.8 0 0 1-.97.43 2.77 2.77 0 0 1-1.32-.05 1.62 1.62 0 0 1-.57-.31 1.41 1.41 0 0 1-.38-.53 1.85 1.85 0 0 1-.05-1.18c.05-.16.14-.3.25-.45.12-.14.28-.27.46-.4a3 3 0 0 1 .7-.32 9.19 9.19 0 0 1 2.2-.33v-.36c0-.41-.09-.71-.26-.91-.18-.2-.43-.3-.76-.3a1.84 1.84 0 0 0-1.02.28l-.33.18c-.1.06-.2.09-.32.09-.1 0-.2-.03-.27-.08a.72.72 0 0 1-.17-.2l-.26-.47Zm11.49 4.32V36h-4.88v-8.6h1.16v7.62h3.72Zm3.16-5.2c.44 0 .84.08 1.2.23a2.57 2.57 0 0 1 1.49 1.62c.13.38.2.81.2 1.29s-.07.91-.2 1.3a2.57 2.57 0 0 1-1.49 1.61c-.36.14-.76.21-1.2.21-.45 0-.85-.07-1.21-.21a2.57 2.57 0 0 1-1.5-1.62c-.13-.38-.2-.81-.2-1.3 0-.47.07-.9.2-1.28.14-.39.33-.72.59-1 .25-.26.55-.47.9-.62.37-.15.77-.22 1.22-.22Zm0 5.43c.6 0 1.05-.2 1.34-.6.3-.4.45-.97.45-1.69s-.15-1.28-.45-1.69c-.3-.4-.74-.6-1.34-.6-.3 0-.57.05-.8.15-.22.1-.4.26-.56.45-.15.2-.26.44-.34.73-.07.28-.1.6-.1.96 0 .72.14 1.29.44 1.69.3.4.75.6 1.36.6Zm6.33-2.22c.22 0 .4-.03.57-.09.16-.06.3-.14.41-.25.12-.11.2-.24.26-.39.05-.15.08-.31.08-.5 0-.37-.11-.66-.34-.88-.23-.22-.55-.33-.98-.33-.43 0-.76.1-.99.33-.22.22-.34.51-.34.89 0 .18.03.34.09.5a1.1 1.1 0 0 0 .67.63c.16.06.35.09.57.09Zm1.93 3.3a.51.51 0 0 0-.13-.36.84.84 0 0 0-.34-.22 8.57 8.57 0 0 0-1.73-.2 7.5 7.5 0 0 1-.62-.05c-.23.1-.41.23-.56.4a.8.8 0 0 0-.1.92c.07.12.18.22.32.3.14.1.32.16.54.21a3.5 3.5 0 0 0 1.55 0c.23-.05.42-.12.57-.22.16-.1.29-.21.37-.34a.8.8 0 0 0 .13-.44Zm1.08-6.17v.4c0 .13-.08.21-.25.25l-.69.09c.14.26.2.56.2.88a1.86 1.86 0 0 1-1.36 1.82 3.07 3.07 0 0 1-1.72.04c-.12.08-.22.16-.29.25a.44.44 0 0 0-.1.27c0 .15.06.26.17.33.12.08.28.13.47.16a5 5 0 0 0 .66.06 16.56 16.56 0 0 1 1.5.13c.26.05.48.12.67.22.19.1.34.24.46.41.12.18.18.4.18.69 0 .26-.07.5-.2.75s-.31.46-.56.65c-.24.2-.54.34-.9.46a4.57 4.57 0 0 1-2.36.04c-.33-.09-.6-.2-.82-.36a1.56 1.56 0 0 1-.5-.51c-.1-.2-.16-.4-.16-.6 0-.3.1-.56.28-.77.19-.2.45-.37.77-.5a1.15 1.15 0 0 1-.43-.32.88.88 0 0 1-.15-.54c0-.09.01-.18.04-.27.04-.1.08-.2.15-.28a1.55 1.55 0 0 1 .58-.5c-.3-.16-.53-.39-.7-.66-.17-.28-.25-.6-.25-.97 0-.3.05-.57.16-.8.12-.25.28-.46.48-.63.2-.17.45-.3.73-.4a3 3 0 0 1 2.3.21h1.64Zm4.65.76a.24.24 0 0 1-.23.14.42.42 0 0 1-.2-.07 3.59 3.59 0 0 0-.67-.3 1.8 1.8 0 0 0-1.03 0c-.14.05-.27.11-.37.2a.87.87 0 0 0-.23.27.75.75 0 0 0-.08.35c0 .15.04.28.13.39.1.1.21.19.36.27.15.07.32.14.5.2a13.63 13.63 0 0 1 1.16.4c.2.08.36.18.5.3a1.33 1.33 0 0 1 .5 1.07 2 2 0 0 1-.15.78c-.1.24-.25.44-.45.62-.2.17-.43.3-.72.4a3.1 3.1 0 0 1-2.14-.05 2.97 2.97 0 0 1-.87-.53l.25-.41c.04-.05.07-.1.12-.12a.3.3 0 0 1 .17-.04.4.4 0 0 1 .22.08l.3.19a1.91 1.91 0 0 0 1.03.27c.2 0 .38-.03.54-.08.16-.06.29-.13.4-.22a.96.96 0 0 0 .3-.7c0-.17-.05-.31-.14-.42-.09-.11-.2-.2-.36-.28a2.6 2.6 0 0 0-.5-.2l-.59-.19c-.2-.06-.39-.14-.58-.22a2.14 2.14 0 0 1-.5-.3 1.45 1.45 0 0 1-.36-.46c-.1-.19-.14-.41-.14-.67a1.6 1.6 0 0 1 .57-1.23c.18-.16.4-.3.68-.39.26-.1.57-.14.91-.14a2.84 2.84 0 0 1 1.9.7l-.23.4Z"
     />
     <defs>
-      <path d="M0 0h85v38H0z"/>
+      <path d="M0 0h85v38H0z" />
     </defs>
   </svg>
 );
@@ -634,3 +634,12 @@ export const ModalIcon = () => (
     <path d="M19 4H5c-1.11 0-2 .9-2 2v12c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2V6c0-1.1-.89-2-2-2m0 14H5V8h14z"></path>
   </svg>
 );
+
+export const PauseIcon = () => (
+  <svg
+    viewBox="0 0 24 24"
+    fill="currentColor"
+  >
+    <path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z" />
+  </svg>
+);
diff --git a/app/vmui/packages/vmui/src/components/Main/Pagination/SelectLimit/SelectLimit.tsx b/app/vmui/packages/vmui/src/components/Main/Pagination/SelectLimit/SelectLimit.tsx
index ebfb0a02d110a..9329bb644d0af 100644
--- a/app/vmui/packages/vmui/src/components/Main/Pagination/SelectLimit/SelectLimit.tsx
+++ b/app/vmui/packages/vmui/src/components/Main/Pagination/SelectLimit/SelectLimit.tsx
@@ -1,4 +1,4 @@
-import React, { FC, useMemo, useRef } from "preact/compat";
+import { FC, useMemo, useRef } from "preact/compat";
 import { ArrowDropDownIcon } from "../../Icons";
 import useBoolean from "../../../../hooks/useBoolean";
 import Popper from "../../Popper/Popper";
@@ -10,11 +10,12 @@ interface SelectLimitProps {
   limit: number | string;
   allowUnlimited?: boolean;
   onChange: (val: number) => void;
+  onOpenSelect?: () => void;
 }
 
 const defaultLimits = [10, 25, 50, 100, 250, 500, 1000];
 
-const SelectLimit: FC<SelectLimitProps> = ({ limit, allowUnlimited, onChange }) => {
+const SelectLimit: FC<SelectLimitProps> = ({ limit, allowUnlimited, onChange, onOpenSelect }) => {
   const { isMobile } = useDeviceDetect();
   const buttonRef = useRef<HTMLDivElement>(null);
 
@@ -28,6 +29,11 @@ const SelectLimit: FC<SelectLimitProps> = ({ limit, allowUnlimited, onChange })
     setFalse: handleClose,
   } = useBoolean(false);
 
+  const handleClickSelect = () => {
+    toggleOpenList();
+    if(!openList) onOpenSelect?.();
+  };
+
   const handleChangeLimit = (n: number) => () => {
     onChange(n);
     handleClose();
@@ -37,7 +43,7 @@ const SelectLimit: FC<SelectLimitProps> = ({ limit, allowUnlimited, onChange })
     <>
       <div
         className="vm-select-limits-button"
-        onClick={toggleOpenList}
+        onClick={handleClickSelect}
         ref={buttonRef}
       >
         <div>
diff --git a/app/vmui/packages/vmui/src/layouts/Header/style.scss b/app/vmui/packages/vmui/src/layouts/Header/style.scss
index 0b5d3901235dc..a0f58626b7ad6 100644
--- a/app/vmui/packages/vmui/src/layouts/Header/style.scss
+++ b/app/vmui/packages/vmui/src/layouts/Header/style.scss
@@ -24,6 +24,7 @@
   &_sidebar {
     display: grid;
     grid-template-columns: 40px auto 1fr;
+    box-shadow: $color-background-body 0 1px 1px 0px;
   }
 
   &_mobile {
diff --git a/app/vmui/packages/vmui/src/pages/ExploreLogs/DownloadLogsButton/DownloadLogsButton.tsx b/app/vmui/packages/vmui/src/pages/ExploreLogs/DownloadLogsButton/DownloadLogsButton.tsx
index 340704cc5ac65..c123687783e1c 100644
--- a/app/vmui/packages/vmui/src/pages/ExploreLogs/DownloadLogsButton/DownloadLogsButton.tsx
+++ b/app/vmui/packages/vmui/src/pages/ExploreLogs/DownloadLogsButton/DownloadLogsButton.tsx
@@ -1,4 +1,4 @@
-import React, { FC, useMemo } from "preact/compat";
+import { FC, useMemo } from "preact/compat";
 import { useCallback } from "react";
 import dayjs from "dayjs";
 import DownloadButton from "../../../components/DownloadButton/DownloadButton";
@@ -7,10 +7,11 @@ import { downloadCSV, downloadJSON } from "../../../utils/file";
 import { Logs } from "../../../api/types";
 
 interface DownloadLogsButtonProps {
-  logs: Logs[];
+  /** Callback to get logs to download */
+  getLogs: () => Logs[];
 }
 
-const DownloadLogsButton: FC<DownloadLogsButtonProps> = ({ logs }) => {
+const DownloadLogsButton: FC<DownloadLogsButtonProps> = ({ getLogs }) => {
   const { fileExtensions, getDownloaderByExtension } = useMemo(() => {
     const downloadFileOptions: {
       extension: string;
@@ -39,12 +40,13 @@ const DownloadLogsButton: FC<DownloadLogsButtonProps> = ({ logs }) => {
       return;
     }
 
+    const logs = getLogs();
     const downloader = getDownloaderByExtension(fileExtension);
     if (downloader){
       const timestamp = dayjs().utc().format(DATE_FILENAME_FORMAT);
       downloader(logs, `vmui_logs_${timestamp}.${fileExtension}`);
     }
-  }, [logs]);
+  }, [getLogs]);
 
   return <DownloadButton
     title={"Download logs"}
@@ -53,4 +55,4 @@ const DownloadLogsButton: FC<DownloadLogsButtonProps> = ({ logs }) => {
   />;
 };
 
-export default DownloadLogsButton;
\ No newline at end of file
+export default DownloadLogsButton;
diff --git a/app/vmui/packages/vmui/src/pages/ExploreLogs/ExploreLogsBody/ExploreLogsBody.tsx b/app/vmui/packages/vmui/src/pages/ExploreLogs/ExploreLogsBody/ExploreLogsBody.tsx
index a8611595ca681..8d85c34e2628f 100644
--- a/app/vmui/packages/vmui/src/pages/ExploreLogs/ExploreLogsBody/ExploreLogsBody.tsx
+++ b/app/vmui/packages/vmui/src/pages/ExploreLogs/ExploreLogsBody/ExploreLogsBody.tsx
@@ -1,5 +1,5 @@
-import React, { FC, useState, useMemo, useRef } from "preact/compat";
-import { CodeIcon, ListIcon, TableIcon } from "../../../components/Main/Icons";
+import { FC, useRef } from "preact/compat";
+import { CodeIcon, ListIcon, TableIcon, PlayIcon } from "../../../components/Main/Icons";
 import Tabs from "../../../components/Main/Tabs/Tabs";
 import "./style.scss";
 import classNames from "classnames";
@@ -7,18 +7,11 @@ import useDeviceDetect from "../../../hooks/useDeviceDetect";
 import { Logs } from "../../../api/types";
 import useStateSearchParams from "../../../hooks/useStateSearchParams";
 import useSearchParamsFromObject from "../../../hooks/useSearchParamsFromObject";
-import TableSettings from "../../../components/Table/TableSettings/TableSettings";
-import useBoolean from "../../../hooks/useBoolean";
-import TableLogs from "./TableLogs";
-import GroupLogs from "../GroupLogs/GroupLogs";
-import JsonView from "../../../components/Views/JsonView/JsonView";
 import LineLoader from "../../../components/Main/LineLoader/LineLoader";
-import SelectLimit from "../../../components/Main/Pagination/SelectLimit/SelectLimit";
-import DownloadLogsButton from "../DownloadLogsButton/DownloadLogsButton";
-
-const MemoizedTableLogs = React.memo(TableLogs);
-const MemoizedGroupLogs = React.memo(GroupLogs);
-const MemoizedJsonView = React.memo(JsonView);
+import GroupView from "./views/GroupView/GroupView";
+import TableView from "./views/TableView/TableView";
+import JsonView from "./views/JsonView/JsonView";
+import LiveTailingView from "./views/LiveTailingView/LiveTailingView";
 
 export interface ExploreLogBodyProps {
   data: Logs[];
@@ -29,44 +22,28 @@ enum DisplayType {
   group = "group",
   table = "table",
   json = "json",
+  liveTailing = "liveTailing",
 }
 
 const tabs = [
-  { label: "Group", value: DisplayType.group, icon: <ListIcon/> },
-  { label: "Table", value: DisplayType.table, icon: <TableIcon/> },
-  { label: "JSON", value: DisplayType.json, icon: <CodeIcon/> },
+  { label: "Group", value: DisplayType.group, icon: <ListIcon/>, Component: GroupView },
+  { label: "Table", value: DisplayType.table, icon: <TableIcon/>, Component: TableView },
+  { label: "JSON", value: DisplayType.json, icon: <CodeIcon/>, Component: JsonView },
+  { label: "Live", value: DisplayType.liveTailing, icon: <PlayIcon/>, Component: LiveTailingView },
 ];
 
 const ExploreLogsBody: FC<ExploreLogBodyProps> = ({ data, isLoading }) => {
   const { isMobile } = useDeviceDetect();
   const { setSearchParamsFromKeys } = useSearchParamsFromObject();
-  const groupSettingsRef = useRef<HTMLDivElement>(null);
-
   const [activeTab, setActiveTab] = useStateSearchParams(DisplayType.group, "view");
-  const [displayColumns, setDisplayColumns] = useState<string[]>([]);
-  const [rowsPerPage, setRowsPerPage] = useStateSearchParams(1000, "rows_per_page");
-  const { value: tableCompact, toggle: toggleTableCompact } = useBoolean(false);
-
-  const columns = useMemo(() => {
-    if (!data?.length || activeTab !== DisplayType.table) return [];
-    const keys = new Set<string>();
-    for (const item of data) {
-      for (const key in item) {
-        keys.add(key);
-      }
-    }
-    return Array.from(keys);
-  }, [data, activeTab]);
+  const settingsRef = useRef<HTMLDivElement>(null);
 
   const handleChangeTab = (view: string) => {
     setActiveTab(view as DisplayType);
     setSearchParamsFromKeys({ view });
   };
 
-  const handleSetRowsPerPage = (limit: number) => {
-    setRowsPerPage(limit);
-    setSearchParamsFromKeys({ rows_per_page: limit });
-  };
+  const ActiveTabComponent = tabs.find(tab => tab.value === activeTab)?.Component;
 
   return (
     <div
@@ -95,39 +72,16 @@ const ExploreLogsBody: FC<ExploreLogBodyProps> = ({ data, isLoading }) => {
             items={tabs}
             onChange={handleChangeTab}
           />
-          <div className="vm-explore-logs-body-header__log-info">
-            Total logs returned: <b>{data.length}</b>
-          </div>
-        </div>
-        {activeTab === DisplayType.table && (
-          <div className="vm-explore-logs-body-header__settings">
-            <SelectLimit
-              limit={rowsPerPage}
-              onChange={handleSetRowsPerPage}
-            />
-            <div className="vm-explore-logs-body-header__table-settings">
-              {data.length > 0 && <DownloadLogsButton logs={data} />}
-              <TableSettings
-                columns={columns}
-                selectedColumns={displayColumns}
-                onChangeColumns={setDisplayColumns}
-                tableCompact={tableCompact}
-                toggleTableCompact={toggleTableCompact}
-              />
+          {activeTab !== DisplayType.liveTailing && (
+            <div className="vm-explore-logs-body-header__log-info">
+              Total logs returned: <b>{data.length}</b>
             </div>
-          </div>
-        )}
-        {activeTab === DisplayType.group && (
-          <>
-            <div
-              className="vm-explore-logs-body-header__settings"
-              ref={groupSettingsRef}
-            />
-          </>
-        )}
-        {activeTab === DisplayType.json && data.length > 0 && (
-          <DownloadLogsButton logs={data} />
-        )}
+          )}
+        </div>
+        <div
+          className="vm-explore-logs-body-header__settings"
+          ref={settingsRef}
+        />
       </div>
 
       <div
@@ -136,29 +90,12 @@ const ExploreLogsBody: FC<ExploreLogBodyProps> = ({ data, isLoading }) => {
           "vm-explore-logs-body__table_mobile": isMobile,
         })}
       >
-        {!data.length && <div className="vm-explore-logs-body__empty">No logs found</div>}
-        {!!data.length && (
-          <>
-            {activeTab === DisplayType.table && (
-              <MemoizedTableLogs
-                logs={data}
-                displayColumns={displayColumns}
-                tableCompact={tableCompact}
-                columns={columns}
-                rowsPerPage={Number(rowsPerPage)}
-              />
-            )}
-            {activeTab === DisplayType.group && (
-              <MemoizedGroupLogs
-                logs={data}
-                settingsRef={groupSettingsRef}
-              />
-            )}
-            {activeTab === DisplayType.json && (
-              <MemoizedJsonView data={data}/>
-            )}
-          </>
-        )}
+        {ActiveTabComponent &&
+            <ActiveTabComponent
+              data={data}
+              settingsRef={settingsRef}
+            />
+        }
       </div>
     </div>
   );
diff --git a/app/vmui/packages/vmui/src/pages/ExploreLogs/ExploreLogsBody/style.scss b/app/vmui/packages/vmui/src/pages/ExploreLogs/ExploreLogsBody/style.scss
index 8be0313f8c5b6..e1eae97b4ada8 100644
--- a/app/vmui/packages/vmui/src/pages/ExploreLogs/ExploreLogsBody/style.scss
+++ b/app/vmui/packages/vmui/src/pages/ExploreLogs/ExploreLogsBody/style.scss
@@ -4,7 +4,15 @@
   position: relative;
 
   &-header {
+    background-color: $color-background-block;
+    z-index: 1;
     margin: -$padding-medium 0-$padding-medium 0;
+    position: sticky;
+    top: 0;
+
+    @media (max-width:1000px) {
+      top: 51px;
+    }
 
     &_mobile {
       margin: -$padding-global 0-$padding-global 0;
@@ -19,11 +27,6 @@
       justify-content: flex-end;
     }
 
-    &__table-settings {
-      display: flex;
-      flex-direction: row;
-    }
-
     &__log-info {
       flex-grow: 1;
       text-align: right;
diff --git a/app/vmui/packages/vmui/src/pages/ExploreLogs/ExploreLogsBody/types.ts b/app/vmui/packages/vmui/src/pages/ExploreLogs/ExploreLogsBody/types.ts
new file mode 100644
index 0000000000000..8f8dd29a874ed
--- /dev/null
+++ b/app/vmui/packages/vmui/src/pages/ExploreLogs/ExploreLogsBody/types.ts
@@ -0,0 +1,6 @@
+import { Logs } from "../../../api/types";
+
+export interface ViewProps {
+  data: Logs[];
+  settingsRef: React.RefObject<HTMLDivElement>;
+}
\ No newline at end of file
diff --git a/app/vmui/packages/vmui/src/pages/ExploreLogs/ExploreLogsBody/views/GroupView/GroupView.tsx b/app/vmui/packages/vmui/src/pages/ExploreLogs/ExploreLogsBody/views/GroupView/GroupView.tsx
new file mode 100644
index 0000000000000..be7c17378cff3
--- /dev/null
+++ b/app/vmui/packages/vmui/src/pages/ExploreLogs/ExploreLogsBody/views/GroupView/GroupView.tsx
@@ -0,0 +1,21 @@
+import React, { FC } from "preact/compat";
+import GroupLogs from "../../../GroupLogs/GroupLogs";
+import { ViewProps } from "../../types";
+import EmptyLogs from "../components/EmptyLogs/EmptyLogs";
+
+const MemoizedGroupLogs = React.memo(GroupLogs);
+
+const GroupView: FC<ViewProps> = ({ data, settingsRef }) => {
+  if (!data.length) return <EmptyLogs />;
+
+  return (
+    <>
+      <MemoizedGroupLogs
+        logs={data}
+        settingsRef={settingsRef}
+      />
+    </>
+  );
+};
+
+export default GroupView; 
diff --git a/app/vmui/packages/vmui/src/pages/ExploreLogs/ExploreLogsBody/views/JsonView/JsonView.tsx b/app/vmui/packages/vmui/src/pages/ExploreLogs/ExploreLogsBody/views/JsonView/JsonView.tsx
new file mode 100644
index 0000000000000..042c6ca35de17
--- /dev/null
+++ b/app/vmui/packages/vmui/src/pages/ExploreLogs/ExploreLogsBody/views/JsonView/JsonView.tsx
@@ -0,0 +1,33 @@
+import React, { FC } from "preact/compat";
+import DownloadLogsButton from "../../../DownloadLogsButton/DownloadLogsButton";
+import { createPortal } from "preact/compat";
+import JsonViewComponent from "../../../../../components/Views/JsonView/JsonView";
+import { ViewProps } from "../../types";
+import EmptyLogs from "../components/EmptyLogs/EmptyLogs";
+import { useCallback } from "react";
+
+const MemoizedJsonView = React.memo(JsonViewComponent);
+
+const JsonView: FC<ViewProps> = ({ data, settingsRef }) => {
+  const getLogs = useCallback(() => data, [data]);
+
+  const renderSettings = () => {
+    if (!settingsRef.current) return null;
+
+    return createPortal(
+      data.length > 0 && <DownloadLogsButton getLogs={getLogs} />,
+      settingsRef.current
+    );
+  };
+
+  if (!data.length) return <EmptyLogs />;
+
+  return (
+    <>
+      {renderSettings()}
+      <MemoizedJsonView data={data} />
+    </>
+  );
+};
+
+export default JsonView; 
diff --git a/app/vmui/packages/vmui/src/pages/ExploreLogs/ExploreLogsBody/views/LiveTailingView/LiveTailingSettings.tsx b/app/vmui/packages/vmui/src/pages/ExploreLogs/ExploreLogsBody/views/LiveTailingView/LiveTailingSettings.tsx
new file mode 100644
index 0000000000000..d75b0e4b38ec4
--- /dev/null
+++ b/app/vmui/packages/vmui/src/pages/ExploreLogs/ExploreLogsBody/views/LiveTailingView/LiveTailingSettings.tsx
@@ -0,0 +1,125 @@
+import { FC, RefObject, useCallback, useRef } from "preact/compat";
+import { createPortal } from "preact/compat";
+import DownloadLogsButton from "../../../DownloadLogsButton/DownloadLogsButton";
+import Button from "../../../../../components/Main/Button/Button";
+import SelectLimit from "../../../../../components/Main/Pagination/SelectLimit/SelectLimit";
+import { DeleteIcon, PauseIcon, PlayCircleOutlineIcon, SettingsIcon } from "../../../../../components/Main/Icons";
+import Tooltip from "../../../../../components/Main/Tooltip/Tooltip";
+import Modal from "../../../../../components/Main/Modal/Modal";
+import Switch from "../../../../../components/Main/Switch/Switch";
+import useBoolean from "../../../../../hooks/useBoolean";
+import { Logs } from "../../../../../api/types";
+
+interface LiveTailingSettingsProps {
+  settingsRef: RefObject<HTMLDivElement>;
+  rowsPerPage: number;
+  handleSetRowsPerPage: (limit: number) => void;
+  logs: Logs[];
+  isPaused: boolean;
+  handleResumeLiveTailing: () => void;
+  pauseLiveTailing: () => void;
+  clearLogs: () => void;
+  isCompactTailingNumber: boolean;
+  handleSetCompactTailing: (value: boolean) => void;
+}
+
+const LiveTailingSettings: FC<LiveTailingSettingsProps> = ({
+  settingsRef,
+  rowsPerPage,
+  handleSetRowsPerPage,
+  logs,
+  isPaused,
+  handleResumeLiveTailing,
+  pauseLiveTailing,
+  clearLogs,
+  isCompactTailingNumber,
+  handleSetCompactTailing
+}) => {
+  const settingButtonRef = useRef<HTMLDivElement>(null);
+  const { value: isSettingsOpen, setFalse: closeSettings, setTrue: openSettings } = useBoolean(false);
+
+  const getLogs = useCallback(() => logs.map(({ _log_id, ...log }) => log), [logs]);
+
+  if (!settingsRef.current) return null;
+
+  return createPortal(
+    <div className="vm-live-tailing-view__settings">
+      <SelectLimit
+        limit={rowsPerPage}
+        onChange={handleSetRowsPerPage}
+        onOpenSelect={pauseLiveTailing}
+      />
+      <div className="vm-live-tailing-view__settings-buttons">
+        {logs.length > 0 && <DownloadLogsButton getLogs={getLogs}/>}
+        {isPaused ? (
+          <Tooltip
+            title={"Resume live tailing"}
+          >
+            <Button
+              variant="text"
+              color="primary"
+              onClick={handleResumeLiveTailing}
+              startIcon={<PlayCircleOutlineIcon/>}
+              ariaLabel={"Resume live tailing"}
+            />
+          </Tooltip>
+        ) : (
+          <Tooltip
+            title={"Pause live tailing"}
+          >
+            <Button
+              variant="text"
+              color="primary"
+              onClick={pauseLiveTailing}
+              startIcon={<PauseIcon/>}
+              ariaLabel={"Pause live tailing"}
+            />
+          </Tooltip>
+        )}
+        <Tooltip
+          title={"Clear logs"}
+        >
+          <Button
+            variant="text"
+            color="secondary"
+            onClick={clearLogs}
+            startIcon={<DeleteIcon/>}
+            ariaLabel={"Clear logs"}
+          />
+        </Tooltip>
+        <Tooltip
+          title={"Settings"}
+        >
+          <Button
+            ref={settingButtonRef}
+            variant="text"
+            color="secondary"
+            onClick={openSettings}
+            startIcon={<SettingsIcon/>}
+            ariaLabel={"Settings"}
+          />
+        </Tooltip>
+        {isSettingsOpen && <Modal
+          onClose={closeSettings}
+          title={"Live tailing settings"}
+        >
+          <div className="vm-live-tailing-view__settings-modal">
+            <div className={"vm-live-tailing-view__settings-modal-item"}>
+              <Switch
+                label={"Expandable Properties View"}
+                value={isCompactTailingNumber}
+                onChange={handleSetCompactTailing}
+              />
+              <span className="vm-group-logs-configurator-item__info">
+                Switches log display to expandable properties view with additional visualization settings. Please note: when processing large volumes of data, it may increase system response time.
+              </span>
+            </div>
+          </div>
+        </Modal>}
+      </div>
+    </div>,
+    settingsRef.current
+  );
+};
+
+export default LiveTailingSettings;
diff --git a/app/vmui/packages/vmui/src/pages/ExploreLogs/ExploreLogsBody/views/LiveTailingView/LiveTailingView.tsx b/app/vmui/packages/vmui/src/pages/ExploreLogs/ExploreLogsBody/views/LiveTailingView/LiveTailingView.tsx
new file mode 100644
index 0000000000000..e2e0ae060d35c
--- /dev/null
+++ b/app/vmui/packages/vmui/src/pages/ExploreLogs/ExploreLogsBody/views/LiveTailingView/LiveTailingView.tsx
@@ -0,0 +1,145 @@
+import { FC, useCallback, useEffect, useRef, useState } from "preact/compat";
+import { ViewProps } from "../../types";
+import useStateSearchParams from "../../../../../hooks/useStateSearchParams";
+import useSearchParamsFromObject from "../../../../../hooks/useSearchParamsFromObject";
+import "./style.scss";
+import { useLiveTailingLogs } from "./useLiveTailingLogs";
+import { LOGS_DISPLAY_FIELDS, LOGS_URL_PARAMS } from "../../../../../constants/logs";
+import { useMemo } from "react";
+import { useSearchParams } from "react-router-dom";
+import throttle from "lodash/throttle";
+import GroupLogsItem from "../../../GroupLogs/GroupLogsItem";
+import LiveTailingSettings from "./LiveTailingSettings";
+
+const SCROLL_THRESHOLD = 100;
+const scrollToBottom = () => window.scrollTo({
+  top: document.documentElement.scrollHeight,
+  behavior: "instant"
+});
+const throttledScrollToBottom = throttle(scrollToBottom, 200);
+
+const LiveTailingView: FC<ViewProps> = ({ settingsRef }) => {
+  const containerRef = useRef<HTMLDivElement>(null);
+
+  const [isAtBottom, setIsAtBottom] = useState(true);
+  const [searchParams] = useSearchParams();
+  const { setSearchParamsFromKeys } = useSearchParamsFromObject();
+  const [rowsPerPage, setRowsPerPage] = useStateSearchParams(100, "rows_per_page");
+  const [query, _setQuery] = useStateSearchParams("*", "query");
+  const [isCompactTailingStr] = useStateSearchParams(0, "compact_tailing");
+  const isCompactTailingNumber = Boolean(Number(isCompactTailingStr));
+  const {
+    logs,
+    isPaused,
+    error,
+    startLiveTailing,
+    stopLiveTailing,
+    pauseLiveTailing,
+    resumeLiveTailing,
+    clearLogs
+  } = useLiveTailingLogs(query, rowsPerPage);
+
+  const displayFieldsString = searchParams.get(LOGS_URL_PARAMS.DISPLAY_FIELDS) || LOGS_DISPLAY_FIELDS;
+  const displayFields = useMemo(() => displayFieldsString.split(","), [displayFieldsString]);
+
+  const handleResumeLiveTailing = useCallback(() => {
+    throttledScrollToBottom();
+    resumeLiveTailing();
+  }, [resumeLiveTailing]);
+
+  const handleSetRowsPerPage = useCallback((limit: number) => {
+    setSearchParamsFromKeys({ rows_per_page: limit });
+  }, [setRowsPerPage, setSearchParamsFromKeys]);
+
+  const handleSetCompactTailing = useCallback((value: boolean) => {
+    setSearchParamsFromKeys({ compact_tailing: Number(value) });
+  }, [setSearchParamsFromKeys]);
+
+  useEffect(() => {
+    startLiveTailing();
+    return () => stopLiveTailing();
+  }, [startLiveTailing, stopLiveTailing]);
+
+  useEffect(() => {
+    const container = containerRef.current;
+    if (!container) return;
+
+    const handleScroll = () => {
+      const { scrollTop, scrollHeight, clientHeight } = document.documentElement;
+      const isBottom = Math.abs(scrollHeight - scrollTop - clientHeight) < SCROLL_THRESHOLD;
+
+      setIsAtBottom(isBottom);
+
+      if (!isBottom && !isPaused) {
+        pauseLiveTailing();
+      }
+    };
+
+    document.addEventListener("scroll", handleScroll);
+    return () => document.removeEventListener("scroll", handleScroll);
+  }, [isPaused, pauseLiveTailing, resumeLiveTailing]);
+
+  useEffect(() => {
+    if (isAtBottom && !isPaused) {
+      throttledScrollToBottom();
+    }
+  }, [logs, isAtBottom]);
+
+  useEffect(() => {
+    handleResumeLiveTailing();
+  }, [rowsPerPage]);
+
+
+
+  if (error) {
+    return <div className="vm-live-tailing-view__error">{error}</div>;
+  }
+
+  return (
+    <>
+      <LiveTailingSettings
+        settingsRef={settingsRef}
+        rowsPerPage={rowsPerPage}
+        handleSetRowsPerPage={handleSetRowsPerPage}
+        logs={logs}
+        isPaused={isPaused}
+        handleResumeLiveTailing={handleResumeLiveTailing}
+        pauseLiveTailing={pauseLiveTailing}
+        clearLogs={clearLogs}
+        isCompactTailingNumber={isCompactTailingNumber}
+        handleSetCompactTailing={handleSetCompactTailing}
+      />
+      <div
+        ref={containerRef}
+        className="vm-live-tailing-view__container"
+      >
+        {logs.length === 0
+          ? (<div className="vm-live-tailing-view__empty">Waiting for logs...</div>)
+          : (<div className="vm-live-tailing-view__logs">
+            {logs.map(({ _log_id, ...log }, idx) =>
+              isCompactTailingNumber
+                ? (
+                  <GroupLogsItem
+                    key={_log_id}
+                    log={log}
+                    onItemClick={pauseLiveTailing}
+                    hideGroupButton={true}
+                    displayFields={displayFields}
+                  />
+                ) : (
+                  <pre
+                    key={idx}
+                    className="vm-live-tailing-view__log-row"
+                  >
+                    {JSON.stringify(log)}
+                  </pre>
+                )
+            )}
+          </div>
+          )}
+      </div>
+    </>
+  );
+};
+
+export default LiveTailingView;
diff --git a/app/vmui/packages/vmui/src/pages/ExploreLogs/ExploreLogsBody/views/LiveTailingView/style.scss b/app/vmui/packages/vmui/src/pages/ExploreLogs/ExploreLogsBody/views/LiveTailingView/style.scss
new file mode 100644
index 0000000000000..287d565304f3f
--- /dev/null
+++ b/app/vmui/packages/vmui/src/pages/ExploreLogs/ExploreLogsBody/views/LiveTailingView/style.scss
@@ -0,0 +1,76 @@
+@use "src/styles/variables" as *;
+
+.vm-live-tailing-view {
+  &__settings {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    width: 100%;
+  }
+
+  &__settings-modal {
+    max-width: 500px;
+    min-width: 300px;
+  }
+
+  &__settings-modal-item {
+    display: flex;
+    flex-direction: column;
+    gap: $padding-small;
+  }
+
+  &__settings-modal-item-info {
+    font-size: $font-size-small;
+    color: $color-text-secondary;
+    line-height: 130%;
+  }
+
+  &__settings-buttons {
+    display: flex;
+    align-items: center;
+  }
+
+  &__container {
+    width: 100%;
+    height: 100%;
+    overflow: auto;
+    padding: $padding-global;
+    min-height: 200px;
+    font-family: $font-family-monospace;
+  }
+
+  &__empty {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    height: 100%;
+    color: $color-text-secondary;
+  }
+
+  &__error {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    height: 100%;
+    color: $color-error;
+  }
+
+  &__logs {
+    .vm-group-logs-row {
+      animation: highlight-fade 1s ease-out forwards;
+    }
+  }
+
+  &__log-row {
+    margin-top: $padding-small;
+  }
+}
+
+@keyframes highlight-fade {
+  0% {
+    background-color: $color-tropical-blue;
+  }
+  100% {
+    background-color: $color-background-block;
+  }
+}
diff --git a/app/vmui/packages/vmui/src/pages/ExploreLogs/ExploreLogsBody/views/LiveTailingView/useLiveTailingLogs.ts b/app/vmui/packages/vmui/src/pages/ExploreLogs/ExploreLogsBody/views/LiveTailingView/useLiveTailingLogs.ts
new file mode 100644
index 0000000000000..a617e1f40cfd5
--- /dev/null
+++ b/app/vmui/packages/vmui/src/pages/ExploreLogs/ExploreLogsBody/views/LiveTailingView/useLiveTailingLogs.ts
@@ -0,0 +1,152 @@
+import { useCallback, useEffect, useRef, useState } from "preact/compat";
+import { ErrorTypes } from "../../../../../types";
+import { Logs } from "../../../../../api/types";
+import { useAppState } from "../../../../../state/common/StateContext";
+import { useSearchParams } from "react-router-dom";
+import useBoolean from "../../../../../hooks/useBoolean";
+
+export const useLiveTailingLogs = (query: string, limit: number) => {
+  const { serverUrl } = useAppState();
+  const [searchParams] = useSearchParams();
+
+  const [logs, setLogs] = useState<Logs[]>([]);
+  const { value: isPaused, setTrue: pauseLiveTailing, setFalse: resumeLiveTailing } = useBoolean(false);
+  const [error, setError] = useState<ErrorTypes | string>();
+
+  const counterRef = useRef<bigint>(0n);
+  const abortControllerRef = useRef(new AbortController());
+  const readerRef = useRef<ReadableStreamDefaultReader<Uint8Array> | null>(null);
+  const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
+  const bufferRef = useRef<string>("");
+
+  const stopLiveTailing = useCallback(() => {
+    if (readerRef.current) {
+      readerRef.current.cancel();
+      readerRef.current = null;
+    }
+    if (intervalRef.current) {
+      clearInterval(intervalRef.current);
+    }
+    if (bufferRef.current) {
+      bufferRef.current = "";
+    }
+    abortControllerRef.current.abort();
+  }, []);
+
+  const startLiveTailing = useCallback(async () => {
+    stopLiveTailing();
+
+    abortControllerRef.current = new AbortController();
+    const { signal } = abortControllerRef.current;
+
+    setError(undefined);
+    setLogs([]);
+
+    try {
+      const tenant = {
+        AccountID: searchParams.get("accountID") || "0",
+        ProjectID: searchParams.get("projectID") || "0"
+      };
+      const response = await fetch(`${serverUrl}/select/logsql/tail`, {
+        signal,
+        method: "POST",
+        headers: {
+          ...tenant,
+        },
+        body: new URLSearchParams({
+          query: query.trim(),
+        })
+      });
+
+      if (!response.ok || !response.body) {
+        const text = await response.text();
+        setError(text);
+        setLogs([]);
+        return false;
+      }
+
+      const reader = response.body.getReader();
+      readerRef.current = reader;
+
+      const processStream = async () => {
+        try {
+          while (true) {
+            const { done, value } = await reader.read();
+            if (done) break;
+
+            // Convert the Uint8Array to a string
+            const chunk = new TextDecoder().decode(value);
+            bufferRef.current += chunk;
+          }
+        } catch (e) {
+          if (e instanceof Error && e.name !== "AbortError") {
+            console.error("Stream processing error:", e);
+            setError(String(e));
+          }
+        }
+      };
+
+      processStream();
+      return true;
+    } catch (e) {
+      if (e instanceof Error && e.name !== "AbortError") {
+        setError(String(e));
+        console.error(e);
+        setLogs([]);
+      }
+      return false;
+    }
+  }, [query, stopLiveTailing]);
+
+
+  useEffect(() => {
+    if (isPaused) return;
+
+    /**
+     * Process incoming log data at a throttled rate (every 1s)
+     * This interval-based approach prevents CPU overload by:
+     * 1. Batching log processing instead of processing each chunk immediately
+     * 2. Limiting UI updates to a reasonable frequency (1/sec) even when data streams in rapidly
+     * 3. Reducing performance impact when handling large volumes of incoming logs
+     * 4. Allowing efficient garbage collection between processing cycles
+     */
+    const timerId = setInterval(() => {
+      const lines = bufferRef.current.split("\n");
+      bufferRef.current = lines.pop() || "";
+
+      const newLogs = lines
+        .map(line => {
+          try {
+            const parsedLine = line && JSON.parse(line);
+            parsedLine._log_id = counterRef.current++;
+            return parsedLine;
+          } catch (e) {
+            console.error(`Failed to parse "${line}" to JSON\n`, e);
+            return null;
+          }
+        })
+        .filter(Boolean) as Logs[];
+
+      setLogs(prevLogs => {
+        const combinedLogs = [...prevLogs, ...newLogs];
+        return combinedLogs.length > limit ? combinedLogs.slice(-limit) : combinedLogs;
+      });
+    }, 1000);
+    return () => clearInterval(timerId);
+  }, [limit, isPaused]);
+
+  const clearLogs = useCallback(() => {
+    setLogs([]);
+  }, []);
+
+  return {
+    logs,
+    isPaused,
+    error,
+    startLiveTailing,
+    stopLiveTailing,
+    pauseLiveTailing,
+    resumeLiveTailing,
+    clearLogs
+  };
+}; 
\ No newline at end of file
diff --git a/app/vmui/packages/vmui/src/pages/ExploreLogs/ExploreLogsBody/views/TableView/TableView.tsx b/app/vmui/packages/vmui/src/pages/ExploreLogs/ExploreLogsBody/views/TableView/TableView.tsx
new file mode 100644
index 0000000000000..b230cc19f42a4
--- /dev/null
+++ b/app/vmui/packages/vmui/src/pages/ExploreLogs/ExploreLogsBody/views/TableView/TableView.tsx
@@ -0,0 +1,80 @@
+import React, { FC, useMemo, useState } from "preact/compat";
+import DownloadLogsButton from "../../../DownloadLogsButton/DownloadLogsButton";
+import { createPortal } from "preact/compat";
+import "./style.scss";
+import { ViewProps } from "../../types";
+import useBoolean from "../../../../../hooks/useBoolean";
+import useStateSearchParams from "../../../../../hooks/useStateSearchParams";
+import TableLogs from "../../TableLogs";
+import SelectLimit from "../../../../../components/Main/Pagination/SelectLimit/SelectLimit";
+import TableSettings from "../../../../../components/Table/TableSettings/TableSettings";
+import useSearchParamsFromObject from "../../../../../hooks/useSearchParamsFromObject";
+import EmptyLogs from "../components/EmptyLogs/EmptyLogs";
+import { useCallback } from "react";
+
+const MemoizedTableView = React.memo(TableLogs);
+
+const TableView: FC<ViewProps> = ({ data, settingsRef }) => {
+  const { setSearchParamsFromKeys } = useSearchParamsFromObject();
+  const [displayColumns, setDisplayColumns] = useState<string[]>([]);
+  const [rowsPerPage, setRowsPerPage] = useStateSearchParams(100, "rows_per_page");
+  const { value: tableCompact, toggle: toggleTableCompact } = useBoolean(false);
+
+  const columns = useMemo(() => {
+    const keys = new Set<string>();
+    for (const item of data) {
+      for (const key in item) {
+        keys.add(key);
+      }
+    }
+    return Array.from(keys);
+  }, [data]);
+
+  const handleSetRowsPerPage = (limit: number) => {
+    setRowsPerPage(limit);
+    setSearchParamsFromKeys({ rows_per_page: limit });
+  };
+
+  const getLogs = useCallback(() => data, [data]);
+
+  const renderSettings = () => {
+    if (!settingsRef.current) return null;
+
+    return createPortal(
+      <div className="vm-table-view__settings">
+        <SelectLimit
+          limit={rowsPerPage}
+          onChange={handleSetRowsPerPage}
+        />
+        <div className="vm-table-view__settings-buttons">
+          {data.length > 0 && <DownloadLogsButton getLogs={getLogs} />}
+          <TableSettings
+            columns={columns}
+            selectedColumns={displayColumns}
+            onChangeColumns={setDisplayColumns}
+            tableCompact={tableCompact}
+            toggleTableCompact={toggleTableCompact}
+          />
+        </div>
+      </div>,
+      settingsRef.current
+    );
+  };
+
+  if (!data.length) return <EmptyLogs />;
+
+  return (
+    <>
+      {renderSettings()}
+      <MemoizedTableView
+        logs={data}
+        displayColumns={displayColumns}
+        tableCompact={tableCompact}
+        columns={columns}
+        rowsPerPage={Number(rowsPerPage)}
+      />
+    </>
+  );
+};
+
+export default TableView; 
diff --git a/app/vmui/packages/vmui/src/pages/ExploreLogs/ExploreLogsBody/views/TableView/style.scss b/app/vmui/packages/vmui/src/pages/ExploreLogs/ExploreLogsBody/views/TableView/style.scss
new file mode 100644
index 0000000000000..35382696f909f
--- /dev/null
+++ b/app/vmui/packages/vmui/src/pages/ExploreLogs/ExploreLogsBody/views/TableView/style.scss
@@ -0,0 +1,10 @@
+@use "src/styles/variables" as *;
+
+.vm-table-view {
+  &__settings,
+  &__settings-buttons {
+    display: flex;
+    align-items: center;
+    gap: $padding-small;
+  }
+}
\ No newline at end of file
diff --git a/app/vmui/packages/vmui/src/pages/ExploreLogs/ExploreLogsBody/views/components/EmptyLogs/EmptyLogs.tsx b/app/vmui/packages/vmui/src/pages/ExploreLogs/ExploreLogsBody/views/components/EmptyLogs/EmptyLogs.tsx
new file mode 100644
index 0000000000000..bf3122133ce7f
--- /dev/null
+++ b/app/vmui/packages/vmui/src/pages/ExploreLogs/ExploreLogsBody/views/components/EmptyLogs/EmptyLogs.tsx
@@ -0,0 +1,10 @@
+import { FC } from "preact/compat";
+import "./style.scss";
+
+const EmptyLogs: FC = () => {
+  return (
+    <div className="vm-explore-logs-body__empty">No logs found</div>
+  );
+};
+
+export default EmptyLogs;
diff --git a/app/vmui/packages/vmui/src/pages/ExploreLogs/ExploreLogsBody/views/components/EmptyLogs/style.scss b/app/vmui/packages/vmui/src/pages/ExploreLogs/ExploreLogsBody/views/components/EmptyLogs/style.scss
new file mode 100644
index 0000000000000..eb54dc35a9e2f
--- /dev/null
+++ b/app/vmui/packages/vmui/src/pages/ExploreLogs/ExploreLogsBody/views/components/EmptyLogs/style.scss
@@ -0,0 +1,12 @@
+@use "src/styles/variables" as *;
+
+.vm-explore-logs-body {
+  &__empty {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    min-height: 120px;
+    color: $color-text-disabled;
+    text-align: center;
+  }
+}
\ No newline at end of file
diff --git a/app/vmui/packages/vmui/src/pages/ExploreLogs/GroupLogs/GroupLogs.tsx b/app/vmui/packages/vmui/src/pages/ExploreLogs/GroupLogs/GroupLogs.tsx
index 3ebede636edae..035fc15595f26 100644
--- a/app/vmui/packages/vmui/src/pages/ExploreLogs/GroupLogs/GroupLogs.tsx
+++ b/app/vmui/packages/vmui/src/pages/ExploreLogs/GroupLogs/GroupLogs.tsx
@@ -96,6 +96,8 @@ const GroupLogs: FC<Props> = ({ logs, settingsRef }) => {
     window.scrollTo({ top: 0 });
   };
 
+  const getLogs = useCallback(() => logs, [logs]);
+
   useEffect(() => {
     setExpandGroups(new Array(groupData.length).fill(!isMobile));
   }, [groupData]);
@@ -162,7 +164,7 @@ const GroupLogs: FC<Props> = ({ logs, settingsRef }) => {
               ariaLabel={expandAll ? "Collapse All" : "Expand All"}
             />
           </Tooltip>
-          <DownloadLogsButton logs={logs} />
+          <DownloadLogsButton getLogs={getLogs} />
           <GroupLogsConfigurators logs={logs}/>
         </div>
       ), settingsRef.current)}
diff --git a/app/vmui/packages/vmui/src/pages/ExploreLogs/GroupLogs/GroupLogsFieldRow.tsx b/app/vmui/packages/vmui/src/pages/ExploreLogs/GroupLogs/GroupLogsFieldRow.tsx
index fa4ee56249d91..c8d22d991bcc6 100644
--- a/app/vmui/packages/vmui/src/pages/ExploreLogs/GroupLogs/GroupLogsFieldRow.tsx
+++ b/app/vmui/packages/vmui/src/pages/ExploreLogs/GroupLogs/GroupLogsFieldRow.tsx
@@ -1,4 +1,4 @@
-import React, { FC, memo, useCallback, useEffect, useState } from "preact/compat";
+import { FC, memo, useCallback, useEffect, useState } from "preact/compat";
 import Tooltip from "../../../components/Main/Tooltip/Tooltip";
 import Button from "../../../components/Main/Button/Button";
 import { CopyIcon, StorageIcon, VisibilityIcon } from "../../../components/Main/Icons";
@@ -9,9 +9,10 @@ import { LOGS_GROUP_BY, LOGS_URL_PARAMS } from "../../../constants/logs";
 interface Props {
   field: string;
   value: string;
+  hideGroupButton?: boolean;
 }
 
-const GroupLogsFieldRow: FC<Props> = ({ field, value }) => {
+const GroupLogsFieldRow: FC<Props> = ({ field, value, hideGroupButton }) => {
   const copyToClipboard = useCopyToClipboard();
   const [searchParams, setSearchParams] = useSearchParams();
 
@@ -75,20 +76,22 @@ const GroupLogsFieldRow: FC<Props> = ({ field, value }) => {
               size="small"
               startIcon={isSelectedField ? <VisibilityIcon/> : <VisibilityIcon/>}
               onClick={handleSelectDisplayField}
-              ariaLabel="copy to clipboard"
-            />
-          </Tooltip>
-          <Tooltip title={isGroupByField ? "Ungroup this field" : "Group by this field"}>
-            <Button
-              className="vm-group-logs-row-fields-item-controls__button"
-              variant="text"
-              color={isGroupByField ? "secondary" : "gray"}
-              size="small"
-              startIcon={<StorageIcon/>}
-              onClick={handleSelectGroupBy}
-              ariaLabel="copy to clipboard"
+              ariaLabel={isSelectedField ? "Hide this field" : "Show this field instead of the message"}
             />
           </Tooltip>
+          {!hideGroupButton && (
+            <Tooltip title={isGroupByField ? "Ungroup this field" : "Group by this field"}>
+              <Button
+                className="vm-group-logs-row-fields-item-controls__button"
+                variant="text"
+                color={isGroupByField ? "secondary" : "gray"}
+                size="small"
+                startIcon={<StorageIcon/>}
+                onClick={handleSelectGroupBy}
+                ariaLabel={isGroupByField ? "Ungroup this field" : "Group by this field"}
+              />
+            </Tooltip>
+          )}
         </div>
       </td>
       <td className="vm-group-logs-row-fields-item__key">{field}</td>
diff --git a/app/vmui/packages/vmui/src/pages/ExploreLogs/GroupLogs/GroupLogsFields.tsx b/app/vmui/packages/vmui/src/pages/ExploreLogs/GroupLogs/GroupLogsFields.tsx
index 2e1137f251c23..227846e4a96ab 100644
--- a/app/vmui/packages/vmui/src/pages/ExploreLogs/GroupLogs/GroupLogsFields.tsx
+++ b/app/vmui/packages/vmui/src/pages/ExploreLogs/GroupLogs/GroupLogsFields.tsx
@@ -8,9 +8,10 @@ import { getFromStorage } from "../../../utils/storage";
 
 interface Props {
   log: Logs;
+  hideGroupButton?: boolean;
 }
 
-const GroupLogsFields: FC<Props> = ({ log }) => {
+const GroupLogsFields: FC<Props> = ({ log, hideGroupButton }) => {
   const sortedFields = useMemo(() => {
     return Object.entries(log)
       .sort(([aKey], [bKey]) => aKey.localeCompare(bKey));
@@ -41,6 +42,7 @@ const GroupLogsFields: FC<Props> = ({ log }) => {
               key={key}
               field={key}
               value={value}
+              hideGroupButton={hideGroupButton}
             />
           ))}
         </tbody>
diff --git a/app/vmui/packages/vmui/src/pages/ExploreLogs/GroupLogs/GroupLogsItem.tsx b/app/vmui/packages/vmui/src/pages/ExploreLogs/GroupLogs/GroupLogsItem.tsx
index 8fd75a4e6bd9f..00b11522914fe 100644
--- a/app/vmui/packages/vmui/src/pages/ExploreLogs/GroupLogs/GroupLogsItem.tsx
+++ b/app/vmui/packages/vmui/src/pages/ExploreLogs/GroupLogs/GroupLogsItem.tsx
@@ -18,9 +18,11 @@ import GroupLogsFields from "./GroupLogsFields";
 interface Props {
   log: Logs;
   displayFields?: string[];
+  hideGroupButton?: boolean;
+  onItemClick?: (log: Logs) => void;
 }
 
-const GroupLogsItem: FC<Props> = ({ log, displayFields = ["_msg"] }) => {
+const GroupLogsItem: FC<Props> = ({ log, displayFields = ["_msg"], onItemClick, hideGroupButton }) => {
   const {
     value: isOpenFields,
     toggle: toggleOpenFields,
@@ -75,6 +77,11 @@ const GroupLogsItem: FC<Props> = ({ log, displayFields = ["_msg"] }) => {
     }
   };
 
+  const handleClick = () => {
+    toggleOpenFields();
+    onItemClick?.(log);
+  };
+
   useEventListener("storage", handleUpdateStage);
 
   return (
@@ -84,7 +91,7 @@ const GroupLogsItem: FC<Props> = ({ log, displayFields = ["_msg"] }) => {
           "vm-group-logs-row-content": true,
           "vm-group-logs-row-content_interactive": !disabledHovers,
         })}
-        onClick={toggleOpenFields}
+        onClick={handleClick}
       >
         {hasFields && (
           <div
@@ -123,7 +130,10 @@ const GroupLogsItem: FC<Props> = ({ log, displayFields = ["_msg"] }) => {
           ))}
         </div>
       </div>
-      {hasFields && isOpenFields && <GroupLogsFields log={log}/>}
+      {hasFields && isOpenFields && <GroupLogsFields
+        hideGroupButton={hideGroupButton}
+        log={log}
+      />}
     </div>
   );
 };
diff --git a/app/vmui/packages/vmui/tsconfig.json b/app/vmui/packages/vmui/tsconfig.json
index a4361fdf0e830..3b26389509e17 100644
--- a/app/vmui/packages/vmui/tsconfig.json
+++ b/app/vmui/packages/vmui/tsconfig.json
@@ -20,7 +20,9 @@
     "isolatedModules": true,
     "noEmit": true,
     "jsx": "react-jsx",
+    "jsxImportSource": "preact",
     "downlevelIteration": true,
+    "noUnusedLocals": true,
     "paths": {
       "react": ["./node_modules/preact/compat/"],
       "react/jsx-runtime": ["./node_modules/preact/jsx-runtime"],
diff --git a/docs/victorialogs/CHANGELOG.md b/docs/victorialogs/CHANGELOG.md
index fa201c905d911..0bf30ef2c70bd 100644
--- a/docs/victorialogs/CHANGELOG.md
+++ b/docs/victorialogs/CHANGELOG.md
@@ -18,6 +18,8 @@ according to [these docs](https://docs.victoriametrics.com/victorialogs/quicksta
 
 ## tip
 
+* FEATURE: [web UI](https://docs.victoriametrics.com/victorialogs/querying/#web-ui): add "Live" tab that allows monitoring logs in real-time as they arrive. This feature helps users to observe the most recent log entries without manual refreshing, making troubleshooting and monitoring more efficient. See [#7046](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/7046).
+
 * BUGFIX: [OpenTelemetry](https://docs.victoriametrics.com/victorialogs/data-ingestion/opentelemetry/): properly handle nested attributes by expanding them into separate top-level fields. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/8862).
 * BUGFIX: [Datadog](https://docs.victoriametrics.com/victorialogs/data-ingestion/datadog/): respond HTTP 202 instead of HTTP 200 on successful Datadog endpoint ingestion as it's strictly required by Datadog agent. See [#8956](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/8956).
 
@@ -173,7 +175,6 @@ It is safe upgrading to this release and all the future releases from older rele
 * FEATURE: improve per-field data locality on disk. This reduces overhead related to reading data from unrelated fields during queries. This improves query performance over structured logs with big number of fields (aka [wide events](https://jeremymorrell.dev/blog/a-practitioners-guide-to-wide-events/)) when only a small portion of fields are used in the query.
 * FEATURE: [data ingestion](https://docs.victoriametrics.com/victorialogs/data-ingestion/): reduce memory usage by up to 4x when ingesting [wide events](https://jeremymorrell.dev/blog/a-practitioners-guide-to-wide-events/) at high rate into VictoriaLogs.
 * FEATURE: [web UI](https://docs.victoriametrics.com/victorialogs/querying/#web-ui): add ability to limit the number of logs per page in the Group view (client-side). See [this pull request](https://github.com/VictoriaMetrics/VictoriaMetrics/pull/8334).
-* FEATURE: [web UI](https://docs.victoriametrics.com/victorialogs/querying/#web-ui): add ability to disable the hover effect in the Group view to reduce load when viewing many records. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/8135).
 
 * BUGFIX: [data ingestion](https://docs.victoriametrics.com/victorialogs/data-ingestion/): Fixed journald ingestion to support entities with single-character names. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/8314).
 


Reviews:
user: login: Loori-R
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-06T13:46:14Z
user: login: arturminchukov
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-07T06:17:52Z
user: login: arturminchukov
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-07T06:51:14Z
user: login: hagen1778
body: Looks good to me! Thanks @arturminchukov !

The only thing that could be confusing is that LiveTailing doesn't affect the bar chart in the top of the page. I don't expect it to be affected, though. So maybe it should be suppressed with some darker color and message that it is not supported in Live? Or if you think we can update it too - that would be great as well. But this deserves a separate PR.
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-05-16T13:37:34Z

Review comments:
path: docs/victorialogs/CHANGELOG.md
user: login: Loori-R
body: Please fix the CHANGELOG — the new entry is empty, and an unrelated old entry was modified.
created_at: 2025-05-06T13:44:25Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,226 @@
+import { FC, useCallback, useEffect, useRef, useState } from "preact/compat";
+import { createPortal } from "preact/compat";
+import DownloadLogsButton from "../../../DownloadLogsButton/DownloadLogsButton";
+import { ViewProps } from "../../types";
+import useStateSearchParams from "../../../../../hooks/useStateSearchParams";
+import useSearchParamsFromObject from "../../../../../hooks/useSearchParamsFromObject";
+import Button from "../../../../../components/Main/Button/Button";
+import SelectLimit from "../../../../../components/Main/Pagination/SelectLimit/SelectLimit";
+import "./style.scss";
+import { useLiveTailingLogs } from "./useLiveTailingLogs";
+import { DeleteIcon, PauseIcon, PlayCircleOutlineIcon, SettingsIcon } from "../../../../../components/Main/Icons";
+import { LOGS_DISPLAY_FIELDS, LOGS_URL_PARAMS } from "../../../../../constants/logs";
+import { useMemo } from "react";
+import { useSearchParams } from "react-router-dom";
+import Tooltip from "../../../../../components/Main/Tooltip/Tooltip";
+import throttle from "lodash/throttle";
+import useBoolean from "../../../../../hooks/useBoolean";
+import Modal from "../../../../../components/Main/Modal/Modal";
+import Switch from "../../../../../components/Main/Switch/Switch";
+import GroupLogsItem from "../../../GroupLogs/GroupLogsItem";
+
+const SCROLL_THRESHOLD = 100;
+const scrollToBottom = () => window.scrollTo({
+  top: document.documentElement.scrollHeight,
+  behavior: "instant"
+});
+const throttledScrollToBottom = throttle(scrollToBottom, 200);
+
+const LiveTailingView: FC<ViewProps> = ({ settingsRef }) => {
+  const containerRef = useRef<HTMLDivElement>(null);
+  const settingButtonRef = useRef<HTMLDivElement>(null);
+
+  const [isAtBottom, setIsAtBottom] = useState(true);
+  const { value: isSettingsOpen, setFalse: closeSettings, setTrue: openSettings } = useBoolean(false);
+  const [searchParams] = useSearchParams();
+  const { setSearchParamsFromKeys } = useSearchParamsFromObject();
+  const [rowsPerPage, setRowsPerPage] = useStateSearchParams(100, "rows_per_page");
+  const [query, _setQuery] = useStateSearchParams("*", "query");
+  const [isCompactTailingStr] = useStateSearchParams(0, "compact_tailing");
+  const isCompactTailingNumber = Boolean(Number(isCompactTailingStr));
+  const {
+    logs,
+    isPaused,
+    error,
+    startLiveTailing,
+    stopLiveTailing,
+    pauseLiveTailing,
+    resumeLiveTailing,
+    clearLogs
+  } = useLiveTailingLogs(query, rowsPerPage);
+
+  const displayFieldsString = searchParams.get(LOGS_URL_PARAMS.DISPLAY_FIELDS) || LOGS_DISPLAY_FIELDS;
+  const displayFields = useMemo(() => displayFieldsString.split(","), [displayFieldsString]);
+
+  const handleResumeLiveTailing = useCallback(() => {
+    throttledScrollToBottom();
+    resumeLiveTailing();
+  }, [resumeLiveTailing]);
+
+  const handleSetRowsPerPage = useCallback((limit: number) => {
+    setSearchParamsFromKeys({ rows_per_page: limit });
+  }, [setRowsPerPage, setSearchParamsFromKeys]);
+
+  const handleSetCompactTailing = useCallback((value: boolean) => {
+    setSearchParamsFromKeys({ compact_tailing: Number(value) });
+  }, [setSearchParamsFromKeys]);
+
+  useEffect(() => {
+    startLiveTailing();
+    return () => stopLiveTailing();
+  }, [startLiveTailing, stopLiveTailing]);
+
+  useEffect(() => {
+    const container = containerRef.current;
+    if (!container) return;
+
+    const handleScroll = () => {
+      const { scrollTop, scrollHeight, clientHeight } = document.documentElement;
+      const isBottom = Math.abs(scrollHeight - scrollTop - clientHeight) < SCROLL_THRESHOLD;
+
+      setIsAtBottom(isBottom);
+
+      if (!isBottom && !isPaused) {
+        pauseLiveTailing();
+      }
+    };
+
+    document.addEventListener("scroll", handleScroll);
+    return () => document.removeEventListener("scroll", handleScroll);
+  }, [isPaused, pauseLiveTailing, resumeLiveTailing]);
+
+  useEffect(() => {
+    if (isAtBottom && !isPaused) {
+      throttledScrollToBottom();
+    }
+  }, [logs, isAtBottom]);
+
+  useEffect(() => {
+    handleResumeLiveTailing();
+  }, [rowsPerPage]);
+
+
+  const renderSettings = () => {
path: app/vmui/packages/vmui/src/pages/ExploreLogs/ExploreLogsBody/views/LiveTailingView/LiveTailingView.tsx
user: login: Loori-R
body: What do you think about moving the settings into a separate component?
created_at: 2025-05-06T13:45:57Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,226 @@
+import { FC, useCallback, useEffect, useRef, useState } from "preact/compat";
+import { createPortal } from "preact/compat";
+import DownloadLogsButton from "../../../DownloadLogsButton/DownloadLogsButton";
+import { ViewProps } from "../../types";
+import useStateSearchParams from "../../../../../hooks/useStateSearchParams";
+import useSearchParamsFromObject from "../../../../../hooks/useSearchParamsFromObject";
+import Button from "../../../../../components/Main/Button/Button";
+import SelectLimit from "../../../../../components/Main/Pagination/SelectLimit/SelectLimit";
+import "./style.scss";
+import { useLiveTailingLogs } from "./useLiveTailingLogs";
+import { DeleteIcon, PauseIcon, PlayCircleOutlineIcon, SettingsIcon } from "../../../../../components/Main/Icons";
+import { LOGS_DISPLAY_FIELDS, LOGS_URL_PARAMS } from "../../../../../constants/logs";
+import { useMemo } from "react";
+import { useSearchParams } from "react-router-dom";
+import Tooltip from "../../../../../components/Main/Tooltip/Tooltip";
+import throttle from "lodash/throttle";
+import useBoolean from "../../../../../hooks/useBoolean";
+import Modal from "../../../../../components/Main/Modal/Modal";
+import Switch from "../../../../../components/Main/Switch/Switch";
+import GroupLogsItem from "../../../GroupLogs/GroupLogsItem";
+
+const SCROLL_THRESHOLD = 100;
+const scrollToBottom = () => window.scrollTo({
+  top: document.documentElement.scrollHeight,
+  behavior: "instant"
+});
+const throttledScrollToBottom = throttle(scrollToBottom, 200);
+
+const LiveTailingView: FC<ViewProps> = ({ settingsRef }) => {
+  const containerRef = useRef<HTMLDivElement>(null);
+  const settingButtonRef = useRef<HTMLDivElement>(null);
+
+  const [isAtBottom, setIsAtBottom] = useState(true);
+  const { value: isSettingsOpen, setFalse: closeSettings, setTrue: openSettings } = useBoolean(false);
+  const [searchParams] = useSearchParams();
+  const { setSearchParamsFromKeys } = useSearchParamsFromObject();
+  const [rowsPerPage, setRowsPerPage] = useStateSearchParams(100, "rows_per_page");
+  const [query, _setQuery] = useStateSearchParams("*", "query");
+  const [isCompactTailingStr] = useStateSearchParams(0, "compact_tailing");
+  const isCompactTailingNumber = Boolean(Number(isCompactTailingStr));
+  const {
+    logs,
+    isPaused,
+    error,
+    startLiveTailing,
+    stopLiveTailing,
+    pauseLiveTailing,
+    resumeLiveTailing,
+    clearLogs
+  } = useLiveTailingLogs(query, rowsPerPage);
+
+  const displayFieldsString = searchParams.get(LOGS_URL_PARAMS.DISPLAY_FIELDS) || LOGS_DISPLAY_FIELDS;
+  const displayFields = useMemo(() => displayFieldsString.split(","), [displayFieldsString]);
+
+  const handleResumeLiveTailing = useCallback(() => {
+    throttledScrollToBottom();
+    resumeLiveTailing();
+  }, [resumeLiveTailing]);
+
+  const handleSetRowsPerPage = useCallback((limit: number) => {
+    setSearchParamsFromKeys({ rows_per_page: limit });
+  }, [setRowsPerPage, setSearchParamsFromKeys]);
+
+  const handleSetCompactTailing = useCallback((value: boolean) => {
+    setSearchParamsFromKeys({ compact_tailing: Number(value) });
+  }, [setSearchParamsFromKeys]);
+
+  useEffect(() => {
+    startLiveTailing();
+    return () => stopLiveTailing();
+  }, [startLiveTailing, stopLiveTailing]);
+
+  useEffect(() => {
+    const container = containerRef.current;
+    if (!container) return;
+
+    const handleScroll = () => {
+      const { scrollTop, scrollHeight, clientHeight } = document.documentElement;
+      const isBottom = Math.abs(scrollHeight - scrollTop - clientHeight) < SCROLL_THRESHOLD;
+
+      setIsAtBottom(isBottom);
+
+      if (!isBottom && !isPaused) {
+        pauseLiveTailing();
+      }
+    };
+
+    document.addEventListener("scroll", handleScroll);
+    return () => document.removeEventListener("scroll", handleScroll);
+  }, [isPaused, pauseLiveTailing, resumeLiveTailing]);
+
+  useEffect(() => {
+    if (isAtBottom && !isPaused) {
+      throttledScrollToBottom();
+    }
+  }, [logs, isAtBottom]);
+
+  useEffect(() => {
+    handleResumeLiveTailing();
+  }, [rowsPerPage]);
+
+
+  const renderSettings = () => {
path: app/vmui/packages/vmui/src/pages/ExploreLogs/ExploreLogsBody/views/LiveTailingView/LiveTailingView.tsx
user: login: arturminchukov
body: I thought about this when I created this component, but decided to leave it because almost all of the component's states need to be passed as props.

✅ But will try to do it, I think it will be better then keep all code in separate components.
created_at: 2025-05-07T06:17:52Z
author_association: MEMBER
path: docs/victorialogs/CHANGELOG.md
user: login: arturminchukov
body: thanks, fixed it ✅
created_at: 2025-05-07T06:51:14Z
author_association: MEMBER

PR comments:
user: login: hagen1778
created_at: 2025-05-08T11:52:18Z
body: Looks good! A few comments, though:
1. The Play/Pause button sometimes pauses automatically, and I can't understand why:

https://github.com/user-attachments/assets/7b0b2fa1-dc70-42e8-b6ff-52533293dc52

2. The Play/Pause button is too small to notice. Sometimes I don't know when tailing stopped, because the icon change is unnoticeable to me.
3. The export button in Live mode always returns an error:
<img width="489" alt="image" src="https://github.com/user-attachments/assets/477170a0-2d9f-4a27-ad95-83698879e8bc" />


@Loori-R would you mind taking a look too?
author_association: COLLABORATOR
user: login: arturminchukov
created_at: 2025-05-08T12:45:59Z
body: > Looks good! A few comments, though:
> 
> 1. The Play/Pause button sometimes pauses automatically, and I can't understand why:

when you scroll up, the log stream is automatically paused. Also it will be paused when you decide to expand a log in expandable mode. I took this behavior from the grafana live mode.

> 2. The Play/Pause button is too small to notice. Sometimes I don't know when tailing stopped, because the icon change is unnoticeable to me.

So, according to the first point, if you are not in the bottom of logs you always will have pause. I'm not sure, I could add some notification about pause, but I would leave it as it is.

> 3. The export button in Live mode always returns an error:

My bad, it happens due to JSON.stringify of _log_id bigint, parameter which I use only for UI view, and need to remove it when prepare logs for downloading.
UPD: ✅ fixed it


author_association: MEMBER