pw_web: Implement virtualizer, basic log search and log clearing

This CL also includes the following changes:
- Removes one-line height limit for log entries
- Sets max number of entries to 1000
- Applies CSS Grid for table styling
- Updates Prettier formatter config to include `singleQuote` rule
- Adds favicon to HTML page

Change-Id: I1a53d7e0b670979caa0903522b9fe052187edc4c
Reviewed-on: https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/151230
Commit-Queue: Luis Flores <lesprit@google.com>
Reviewed-by: Asad Memon <asadmemon@google.com>
diff --git a/pw_web/log-viewer/.gitignore b/pw_web/log-viewer/.gitignore
index f764736..b8214fe 100644
--- a/pw_web/log-viewer/.gitignore
+++ b/pw_web/log-viewer/.gitignore
@@ -11,6 +11,7 @@
 dist
 dist-ssr
 *.local
+*.tsbuildinfo
 
 # Editor directories and files
 .vscode/*
diff --git a/pw_web/log-viewer/.prettierrc.cjs b/pw_web/log-viewer/.prettierrc.cjs
index 50f1d1c..a9d85ef 100644
--- a/pw_web/log-viewer/.prettierrc.cjs
+++ b/pw_web/log-viewer/.prettierrc.cjs
@@ -1,4 +1,5 @@
 // Prettier configuration
 module.exports = {
     tabWidth: 4,
+    singleQuote: true,
 };
diff --git a/pw_web/log-viewer/index.html b/pw_web/log-viewer/index.html
index 6cd849d..dae501e 100644
--- a/pw_web/log-viewer/index.html
+++ b/pw_web/log-viewer/index.html
@@ -19,20 +19,15 @@
     <head>
         <meta charset="UTF-8" />
         <meta name="viewport" content="width=device-width, initial-scale=1.0" />
-        <title>Pigweed Log Viewer</title>
+        <title>Log Viewer</title>
         <link rel="stylesheet" href="./src/index.css" />
+        <link rel="icon" href="src/assets/favicon.svg" />
         <script type="module" src="./src/index.ts"></script>
         <script
             type="module"
             src="/src/components/log-viewer.ts"
         ></script>
 
-        <!-- Legacy component: -->
-        <script
-            type="module"
-            src="/src/legacy/log_viewer.ts"
-        ></script>
-
         <style>
             @import url('https://fonts.googleapis.com/css2?family=Google+Sans&family=Roboto+Mono:wght@400;500&family=Material+Symbols+Outlined&display=swap');
         </style>
diff --git a/pw_web/log-viewer/package-lock.json b/pw_web/log-viewer/package-lock.json
index 46dfef4..e619e4c 100644
--- a/pw_web/log-viewer/package-lock.json
+++ b/pw_web/log-viewer/package-lock.json
@@ -8,6 +8,7 @@
             "name": "log-viewer",
             "version": "0.0.0",
             "dependencies": {
+                "@lit-labs/virtualizer": "^2.0.3",
                 "@material/web": "^1.0.0-pre.8",
                 "lit": "^3.0.0-pre.0",
                 "peggy": "^3.0.2"
@@ -555,18 +556,18 @@
             }
         },
         "node_modules/@eslint/js": {
-            "version": "8.41.0",
-            "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.41.0.tgz",
-            "integrity": "sha512-LxcyMGxwmTh2lY9FwHPGWOHmYFCZvbrFCBZL4FzSSsxsRPuhrYUg/49/0KDfW8tnIEaEHtfmn6+NPN+1DqaNmA==",
+            "version": "8.42.0",
+            "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.42.0.tgz",
+            "integrity": "sha512-6SWlXpWU5AvId8Ac7zjzmIOqMOba/JWY8XZ4A7q7Gn1Vlfg/SFFIlrtHXt9nPn4op9ZPAkl91Jao+QQv3r/ukw==",
             "dev": true,
             "engines": {
                 "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
             }
         },
         "node_modules/@humanwhocodes/config-array": {
-            "version": "0.11.8",
-            "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.8.tgz",
-            "integrity": "sha512-UybHIJzJnR5Qc/MsD9Kr+RpO2h+/P1GhOwdiLPXK5TWk5sgTdu88bTD9UP+CKbPPh5Rni1u0GjAdYQLemG8g+g==",
+            "version": "0.11.10",
+            "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.10.tgz",
+            "integrity": "sha512-KVVjQmNUepDVGXNuoRRdmmEjruj0KfiGSbS8LVc12LMsWDQzRXJ0qdhN8L8uUigKpfEHRhlaQFY0ib1tnUbNeQ==",
             "dev": true,
             "dependencies": {
                 "@humanwhocodes/object-schema": "^1.2.1",
@@ -601,6 +602,56 @@
             "resolved": "https://registry.npmjs.org/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.1.2-pre.0.tgz",
             "integrity": "sha512-3FSKQV90k20guBluMFzd9paVuzZTxeL1vDuqNc8SbwpiCmZkY7CJH7HH4HVD35D4hU8d8JTKKL51eXRWzXBZXQ=="
         },
+        "node_modules/@lit-labs/virtualizer": {
+            "version": "2.0.3",
+            "resolved": "https://registry.npmjs.org/@lit-labs/virtualizer/-/virtualizer-2.0.3.tgz",
+            "integrity": "sha512-/D8dYN0LmwMwXqPdKGPK7EKJjZiLHGtX1GwyNJX/dpOeztixliPrtG4KGAqzRTbVom8gXbM3N010fe7ssWrpOw==",
+            "dependencies": {
+                "lit": "^2.7.0",
+                "tslib": "^2.0.3"
+            }
+        },
+        "node_modules/@lit-labs/virtualizer/node_modules/@lit-labs/ssr-dom-shim": {
+            "version": "1.1.1",
+            "resolved": "https://registry.npmjs.org/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.1.1.tgz",
+            "integrity": "sha512-kXOeFbfCm4fFf2A3WwVEeQj55tMZa8c8/f9AKHMobQMkzNUfUj+antR3fRPaZJawsa1aZiP/Da3ndpZrwEe4rQ=="
+        },
+        "node_modules/@lit-labs/virtualizer/node_modules/@lit/reactive-element": {
+            "version": "1.6.2",
+            "resolved": "https://registry.npmjs.org/@lit/reactive-element/-/reactive-element-1.6.2.tgz",
+            "integrity": "sha512-rDfl+QnCYjuIGf5xI2sVJWdYIi56CTCwWa+nidKYX6oIuBYwUbT/vX4qbUDlHiZKJ/3FRNQ/tWJui44p6/stSA==",
+            "dependencies": {
+                "@lit-labs/ssr-dom-shim": "^1.0.0"
+            }
+        },
+        "node_modules/@lit-labs/virtualizer/node_modules/lit": {
+            "version": "2.7.5",
+            "resolved": "https://registry.npmjs.org/lit/-/lit-2.7.5.tgz",
+            "integrity": "sha512-i/cH7Ye6nBDUASMnfwcictBnsTN91+aBjXoTHF2xARghXScKxpD4F4WYI+VLXg9lqbMinDfvoI7VnZXjyHgdfQ==",
+            "dependencies": {
+                "@lit/reactive-element": "^1.6.0",
+                "lit-element": "^3.3.0",
+                "lit-html": "^2.7.0"
+            }
+        },
+        "node_modules/@lit-labs/virtualizer/node_modules/lit-element": {
+            "version": "3.3.2",
+            "resolved": "https://registry.npmjs.org/lit-element/-/lit-element-3.3.2.tgz",
+            "integrity": "sha512-xXAeVWKGr4/njq0rGC9dethMnYCq5hpKYrgQZYTzawt9YQhMiXfD+T1RgrdY3NamOxwq2aXlb0vOI6e29CKgVQ==",
+            "dependencies": {
+                "@lit-labs/ssr-dom-shim": "^1.1.0",
+                "@lit/reactive-element": "^1.3.0",
+                "lit-html": "^2.7.0"
+            }
+        },
+        "node_modules/@lit-labs/virtualizer/node_modules/lit-html": {
+            "version": "2.7.4",
+            "resolved": "https://registry.npmjs.org/lit-html/-/lit-html-2.7.4.tgz",
+            "integrity": "sha512-/Jw+FBpeEN+z8X6PJva5n7+0MzCVAH2yypN99qHYYkq8bI+j7I39GH+68Z/MZD6rGKDK9RpzBw7CocfmHfq6+g==",
+            "dependencies": {
+                "@types/trusted-types": "^2.0.2"
+            }
+        },
         "node_modules/@lit/reactive-element": {
             "version": "2.0.0-pre.0",
             "resolved": "https://registry.npmjs.org/@lit/reactive-element/-/reactive-element-2.0.0-pre.0.tgz",
@@ -625,17 +676,17 @@
             "integrity": "sha512-kXOeFbfCm4fFf2A3WwVEeQj55tMZa8c8/f9AKHMobQMkzNUfUj+antR3fRPaZJawsa1aZiP/Da3ndpZrwEe4rQ=="
         },
         "node_modules/@material/web/node_modules/@lit/reactive-element": {
-            "version": "1.6.1",
-            "resolved": "https://registry.npmjs.org/@lit/reactive-element/-/reactive-element-1.6.1.tgz",
-            "integrity": "sha512-va15kYZr7KZNNPZdxONGQzpUr+4sxVu7V/VG7a8mRfPPXUyhEYj5RzXCQmGrlP3tAh0L3HHm5AjBMFYRqlM9SA==",
+            "version": "1.6.2",
+            "resolved": "https://registry.npmjs.org/@lit/reactive-element/-/reactive-element-1.6.2.tgz",
+            "integrity": "sha512-rDfl+QnCYjuIGf5xI2sVJWdYIi56CTCwWa+nidKYX6oIuBYwUbT/vX4qbUDlHiZKJ/3FRNQ/tWJui44p6/stSA==",
             "dependencies": {
                 "@lit-labs/ssr-dom-shim": "^1.0.0"
             }
         },
         "node_modules/@material/web/node_modules/lit": {
-            "version": "2.7.4",
-            "resolved": "https://registry.npmjs.org/lit/-/lit-2.7.4.tgz",
-            "integrity": "sha512-cgD7xrZoYr21mbrkZIuIrj98YTMw/snJPg52deWVV4A8icLyNHI3bF70xsJeAgwTuiq5Kkd+ZR8gybSJDCPB7g==",
+            "version": "2.7.5",
+            "resolved": "https://registry.npmjs.org/lit/-/lit-2.7.5.tgz",
+            "integrity": "sha512-i/cH7Ye6nBDUASMnfwcictBnsTN91+aBjXoTHF2xARghXScKxpD4F4WYI+VLXg9lqbMinDfvoI7VnZXjyHgdfQ==",
             "dependencies": {
                 "@lit/reactive-element": "^1.6.0",
                 "lit-element": "^3.3.0",
@@ -1700,16 +1751,16 @@
             }
         },
         "node_modules/eslint": {
-            "version": "8.41.0",
-            "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.41.0.tgz",
-            "integrity": "sha512-WQDQpzGBOP5IrXPo4Hc0814r4/v2rrIsB0rhT7jtunIalgg6gYXWhRMOejVO8yH21T/FGaxjmFjBMNqcIlmH1Q==",
+            "version": "8.42.0",
+            "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.42.0.tgz",
+            "integrity": "sha512-ulg9Ms6E1WPf67PHaEY4/6E2tEn5/f7FXGzr3t9cBMugOmf1INYvuUwwh1aXQN4MfJ6a5K2iNwP3w4AColvI9A==",
             "dev": true,
             "dependencies": {
                 "@eslint-community/eslint-utils": "^4.2.0",
                 "@eslint-community/regexpp": "^4.4.0",
                 "@eslint/eslintrc": "^2.0.3",
-                "@eslint/js": "8.41.0",
-                "@humanwhocodes/config-array": "^0.11.8",
+                "@eslint/js": "8.42.0",
+                "@humanwhocodes/config-array": "^0.11.10",
                 "@humanwhocodes/module-importer": "^1.0.1",
                 "@nodelib/fs.walk": "^1.2.8",
                 "ajv": "^6.10.0",
@@ -4058,9 +4109,9 @@
             }
         },
         "node_modules/tslib": {
-            "version": "2.5.0",
-            "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz",
-            "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg=="
+            "version": "2.5.3",
+            "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.3.tgz",
+            "integrity": "sha512-mSxlJJwl3BMEQCUNnxXBU9jP4JBktcEGhURcPR6VQVlnP0FdDEsIaz0C35dXNGLyRfrATNofF0F5p2KPxQgB+w=="
         },
         "node_modules/tsutils": {
             "version": "3.21.0",
diff --git a/pw_web/log-viewer/package.json b/pw_web/log-viewer/package.json
index d4c88e6..8387a49 100644
--- a/pw_web/log-viewer/package.json
+++ b/pw_web/log-viewer/package.json
@@ -10,6 +10,7 @@
         "lint": "eslint --max-warnings=0 src"
     },
     "dependencies": {
+        "@lit-labs/virtualizer": "^2.0.3",
         "@material/web": "^1.0.0-pre.8",
         "lit": "^3.0.0-pre.0",
         "peggy": "^3.0.2"
diff --git a/pw_web/log-viewer/src/assets/favicon.svg b/pw_web/log-viewer/src/assets/favicon.svg
new file mode 100644
index 0000000..4ebaf2a
--- /dev/null
+++ b/pw_web/log-viewer/src/assets/favicon.svg
@@ -0,0 +1,97 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   width="82.519623"
+   height="96"
+   viewBox="0 0 21.833318 25.4"
+   version="1.1"
+   id="svg5"
+   inkscape:version="1.1.2 (0a00cf5339, 2022-02-04)"
+   sodipodi:docname="pw-logo.svg"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:svg="http://www.w3.org/2000/svg">
+  <sodipodi:namedview
+     id="namedview7"
+     pagecolor="#ffffff"
+     bordercolor="#000000"
+     borderopacity="0.25"
+     inkscape:showpageshadow="2"
+     inkscape:pageopacity="0.0"
+     inkscape:pagecheckerboard="0"
+     inkscape:deskcolor="#d1d1d1"
+     inkscape:document-units="px"
+     showgrid="true"
+     inkscape:zoom="5.1754899"
+     inkscape:cx="34.29627"
+     inkscape:cy="64.824781"
+     inkscape:window-width="2131"
+     inkscape:window-height="1334"
+     inkscape:window-x="1307"
+     inkscape:window-y="72"
+     inkscape:window-maximized="0"
+     inkscape:current-layer="layer1"
+     inkscape:pageshadow="2"
+     fit-margin-top="0"
+     fit-margin-left="0.25981"
+     fit-margin-right="0"
+     fit-margin-bottom="0"
+     width="96px">
+    <inkscape:grid
+       type="xygrid"
+       id="grid899"
+       originx="-0.29938562"
+       originy="0.007612793" />
+  </sodipodi:namedview>
+  <defs
+     id="defs2" />
+  <g
+     inkscape:label="Layer 1"
+     inkscape:groupmode="layer"
+     id="layer1"
+     transform="translate(-0.29938553,0.00761281)">
+    <g
+       id="g934"
+       transform="matrix(6.0036869,0,0,6.0036869,-1.8419922,0.03809209)">
+      <path
+         style="fill:#f100f7;fill-opacity:1;stroke-width:0.216574"
+         d="M 2.1616449,3.4336061 C 1.9091824,3.5326629 1.5875433,1.5193339 1.8309272,0.85077532 1.9385896,0.55503414 2.031071,0.2463175 2.4114224,-0.00401171 2.8544419,-0.05203268 2.6565454,0.38084903 2.5067113,1.256888 2.3568774,2.1329272 2.1616453,3.4336062 2.1616449,3.4336061 Z"
+         id="path2515"
+         sodipodi:nodetypes="cscsc" />
+      <path
+         style="fill:#b700d7;fill-opacity:1;stroke-width:0.216574"
+         d="M 2.2191106,3.4109307 C 2.0678012,3.6359974 2.0167095,3.0174321 1.7528477,2.3566876 1.4889859,1.6959431 0.94922328,2.1927191 1.0001916,1.7425692 1.1506181,1.5574268 1.8838262,1.5874003 2.2588512,2.3931613 2.6338765,3.1989223 2.219111,3.4109305 2.2191106,3.4109307 Z"
+         id="path2413"
+         sodipodi:nodetypes="cscsc" />
+      <path
+         style="fill:#951798;fill-opacity:1;stroke-width:0.216574"
+         d="M 2.1237722,3.5148295 C 2.3150351,3.7071003 2.3599863,3.0564818 2.6763817,2.4192218 2.9059426,1.9568575 3.3825927,1.9923461 3.1850672,1.6011417 2.9975756,1.4621979 2.4269145,1.8007494 2.2115779,2.6630282 1.9962408,3.525307 2.1237717,3.5148295 2.1237722,3.5148295 Z"
+         id="path2513"
+         sodipodi:nodetypes="cscsc"
+         inkscape:transform-center-x="-0.045186222"
+         inkscape:transform-center-y="-0.11748418" />
+      <path
+         style="fill:#b700d7;fill-opacity:1;stroke-width:0.216574"
+         d="M 2.16657,3.1630056 C 1.935386,3.3047925 2.3927779,2.3796051 2.1943537,1.717073 2.0826164,1.3439858 1.8924319,1.040618 2.0299315,0.81735074 2.252724,0.72377787 2.5506558,1.2084909 2.5567396,2.0972302 2.5628236,2.9859696 2.1665704,3.1630056 2.16657,3.1630056 Z"
+         id="path2517"
+         sodipodi:nodetypes="cscsc" />
+      <path
+         style="fill:#fb71fe;fill-opacity:1;stroke-width:0.216574"
+         d="M 2.1694107,3.3555634 C 1.972559,3.5421082 1.8165702,2.9074538 1.703537,2.2050082 1.5905038,1.5025626 1.0219277,1.2858513 1.2514214,1.001359 1.637517,0.63319073 1.9169663,1.6332865 2.1067716,2.5015425 c 0.1898056,0.8682561 0.062639,0.8540209 0.062639,0.8540209 z"
+         id="path2519"
+         sodipodi:nodetypes="cscsc" />
+      <path
+         style="fill:#00a100;fill-opacity:1;stroke-width:0.264583"
+         d="M 2.1055809,4.223121 C 2.0551089,3.1942573 2.0383098,2.9291347 2.6324934,2.5000643 3.2266767,2.070994 3.6923741,2.4674508 3.9933288,2.5699226 3.718952,2.7370717 3.4647904,2.6395555 3.1496058,2.781203 2.7064121,2.9803792 2.738338,3.0037867 2.6187891,3.2423382 2.2503826,3.9774682 2.4762183,4.1592185 2.1055809,4.223121 Z"
+         id="path2415"
+         sodipodi:nodetypes="cscssc" />
+      <path
+         style="fill:#00cf00;fill-opacity:1;stroke-width:0.272503"
+         d="M 2.0975886,4.2112405 C 1.7892123,4.1687886 2.0455122,3.5896312 1.4981867,2.9104616 1.0529822,2.3580126 0.74373118,3.1368263 0.36812698,2.8108117 0.69766349,1.9826211 1.4696657,1.6318979 1.8391765,2.643645 c 0.097896,0.2680453 0.1875581,0.3804525 0.2399067,0.5618191 0.1452432,0.5032081 0.2932885,1.0610147 0.018505,1.0057764 z"
+         id="path2411"
+         sodipodi:nodetypes="cscssc" />
+    </g>
+  </g>
+</svg>
diff --git a/pw_web/log-viewer/src/components/createLogViewer.ts b/pw_web/log-viewer/src/components/createLogViewer.ts
index 1944b80..9d9b5c1 100644
--- a/pw_web/log-viewer/src/components/createLogViewer.ts
+++ b/pw_web/log-viewer/src/components/createLogViewer.ts
@@ -12,37 +12,36 @@
 // License for the specific language governing permissions and limitations under
 // the License.
 
-import {LogViewer as RootComponent} from "./log-viewer";
-import {LogEntry} from "../shared/interfaces";
+import { LogViewer as RootComponent } from './log-viewer';
+import { LogEntry } from '../shared/interfaces';
 
-import "@material/web/button/filled-button.js";
-import "@material/web/button/outlined-button.js";
-import "@material/web/checkbox/checkbox.js";
-import "@material/web/field/outlined-field.js";
-import "@material/web/textfield/outlined-text-field.js";
-import "@material/web/textfield/filled-text-field.js";
-import "@material/web/iconbutton/standard-icon-button.js";
-import "@material/web/icon/icon.js";
-import {LogSource} from "../log-source";
+import '@material/web/button/filled-button.js';
+import '@material/web/button/outlined-button.js';
+import '@material/web/checkbox/checkbox.js';
+import '@material/web/field/outlined-field.js';
+import '@material/web/textfield/outlined-text-field.js';
+import '@material/web/textfield/filled-text-field.js';
+import '@material/web/iconbutton/standard-icon-button.js';
+import '@material/web/icon/icon.js';
+import { LogSource } from '../log-source';
 
 export function createLogViewer(logSource: LogSource, root: HTMLElement) {
-  const logViewer = new RootComponent();
-  const logs: LogEntry[] = [];
-  root.appendChild(logViewer);
+    const logViewer = new RootComponent();
+    const logs: LogEntry[] = [];
+    root.appendChild(logViewer);
 
+    // Define an event listener for the 'logEntry' event
+    const logEntryListener = (logEntry: LogEntry) => {
+        logs.push(logEntry);
+        logViewer.logs = logs;
+        logViewer.requestUpdate('logs', []);
+    };
 
-  // Define an event listener for the 'logEntry' event
-  const logEntryListener = (logEntry: LogEntry) => {
-    logs.push(logEntry);
-    logViewer.logs = logs;
-    logViewer.requestUpdate("logs", []);
-  };
+    // Add the event listener to the LogSource instance
+    logSource.addEventListener('logEntry', logEntryListener);
 
-  // Add the event listener to the LogSource instance
-  logSource.addEventListener("logEntry", logEntryListener);
-
-  // Method to destroy and unsubscribe
-  return () => {
-    logSource.removeEventListener("logEntry", logEntryListener);
-  }
+    // Method to destroy and unsubscribe
+    return () => {
+        logSource.removeEventListener('logEntry', logEntryListener);
+    };
 }
diff --git a/pw_web/log-viewer/src/components/log-view/controls/controls.styles.ts b/pw_web/log-viewer/src/components/log-view/controls/controls.styles.ts
index 8bff193..c3a9cdd 100644
--- a/pw_web/log-viewer/src/components/log-view/controls/controls.styles.ts
+++ b/pw_web/log-viewer/src/components/log-view/controls/controls.styles.ts
@@ -29,6 +29,10 @@
         display: flex;
     }
 
+    p {
+        white-space: nowrap;
+    }
+
     button {
         color: white;
     }
diff --git a/pw_web/log-viewer/src/components/log-view/controls/controls.ts b/pw_web/log-viewer/src/components/log-view/controls/controls.ts
index d2e2db5..6513448 100644
--- a/pw_web/log-viewer/src/components/log-view/controls/controls.ts
+++ b/pw_web/log-viewer/src/components/log-view/controls/controls.ts
@@ -12,20 +12,32 @@
 // License for the specific language governing permissions and limitations under
 // the License.
 
-import { LitElement, html } from "lit";
-import { customElement, property } from "lit/decorators.js";
-import { styles } from "./controls.styles";
+import { LitElement, html } from 'lit';
+import { customElement, property } from 'lit/decorators.js';
+import { styles } from './controls.styles';
 
 /**
  * Description of LogViewControls.
  */
-@customElement("log-view-controls")
+@customElement('log-view-controls')
 export class LogViewControls extends LitElement {
     static styles = styles;
 
+    /**
+     * Description of viewId.
+     */
+    @property()
+    viewId = '';
+
     @property({ type: Array })
     fieldKeys: string[];
 
+    @property()
+    hideCloseButton = false;
+
+    private _inputDebounceTimer: number | null = null;
+    private readonly _inputDebounceDelay = 50; // ms
+
     constructor() {
         super();
         this.fieldKeys = [];
@@ -33,65 +45,141 @@
 
     render() {
         return html`
-            <p class="host-name">Host name</p>
-            
+            <p class="host-name">Log View</p>
+
             <div class="input-container">
-                <input placeholder="Search" type="text"></input>
+                <input placeholder="Search" type="text" @input=${
+                    this.handleInput
+                }></input>
             </div>
 
             <div class="actions-container">
-                <md-standard-icon-button>
-                    <md-icon>pause_circle</md-icon>
-                </md-standard-icon-button>
+                <span class="action-button" hidden>
+                    <md-standard-icon-button>
+                        <md-icon>pause_circle</md-icon>
+                    </md-standard-icon-button>
+                </span>
 
-                <md-standard-icon-button>
-                    <md-icon>wrap_text</md-icon>
-                </md-standard-icon-button>
+                <span class="action-button" hidden>
+                    <md-standard-icon-button>
+                        <md-icon>wrap_text</md-icon>
+                    </md-standard-icon-button>
+                </span>
 
-                <md-standard-icon-button>
-                    <md-icon>delete_sweep</md-icon>
-                </md-standard-icon-button>
+                <span class="action-button" title="Clear logs">
+                    <md-standard-icon-button @click=${
+                        this.handleClearLogsClick
+                    }>
+                        <md-icon>delete_sweep</md-icon>
+                    </md-standard-icon-button>
+                </span>
 
-                <div class='field-toggle'>
-                    <md-standard-icon-button @click=${this.toggleFieldsDropdown}>
+                <div class='field-toggle' title="Toggle fields">
+                    <md-standard-icon-button @click=${
+                        this.toggleFieldsDropdown
+                    }>
                         <md-icon>view_column</md-icon>
                     </md-standard-icon-button>
                     <menu class='field-menu' hidden>
-                        ${Array.from(this.fieldKeys).map((field) => html`
-                        <li class='field-menu-item'>
-                        <input class='fields' @click=${this.handleFieldToggle} checked type='checkbox' value=${field}>
-                            <label for=${field}>${field}</label>
-                        </li>
-                        `)}
+                        ${Array.from(this.fieldKeys).map(
+                            (field) => html`
+                                <li class="field-menu-item">
+                                    <input
+                                        class="fields"
+                                        @click=${this.handleFieldToggle}
+                                        checked
+                                        type="checkbox"
+                                        value=${field}
+                                    />
+                                    <label for=${field}>${field}</label>
+                                </li>
+                            `
+                        )}
                     </menu>
                 </div>
 
-                <md-standard-icon-button>
-                    <md-icon>more_horiz</md-icon>
-                </md-standard-icon-button>
+                <span class="action-button" title="Close view" ?hidden=${
+                    this.hideCloseButton
+                }>
+                    <md-standard-icon-button  @click=${
+                        this.handleCloseViewClick
+                    }>
+                        <md-icon>close</md-icon>
+                    </md-standard-icon-button>
+                </span>
+
+                <span class="action-button" hidden>
+                    <md-standard-icon-button>
+                        <md-icon>more_horiz</md-icon>
+                    </md-standard-icon-button>
+                </span>
             </div>
         `;
     }
 
+    handleInput = (event: Event) => {
+        if (this._inputDebounceTimer) {
+            clearTimeout(this._inputDebounceTimer);
+        }
+
+        const inputElement = event.target as HTMLInputElement;
+        const filterValue = inputElement.value;
+
+        this._inputDebounceTimer = window.setTimeout(() => {
+            const customEvent = new CustomEvent('filter-change', {
+                detail: { filterValue },
+                bubbles: true,
+                composed: true,
+            });
+
+            this.dispatchEvent(customEvent);
+        }, this._inputDebounceDelay);
+    };
+
+    handleClearLogsClick() {
+        const event = new CustomEvent('clear-logs', {
+            bubbles: true,
+            composed: true,
+        });
+
+        this.dispatchEvent(event);
+    }
+
+    handleCloseViewClick() {
+        const event = new CustomEvent('close-view', {
+            bubbles: true,
+            composed: true,
+            detail: {
+                viewId: this.viewId,
+            },
+        });
+
+        this.dispatchEvent(event);
+    }
+
     handleFieldToggle(e: Event) {
         // TODO(b/283505711): Handle select all/none condition
         const inputEl = e.target as HTMLInputElement;
-        let fieldToggle = new CustomEvent('field-toggle', {
+        const fieldToggle = new CustomEvent('field-toggle', {
             detail: {
                 bubbles: true,
                 composed: true,
                 message: 'visible fields have changed',
                 field: inputEl.value,
                 isChecked: inputEl.checked,
-            }
+            },
         });
         this.dispatchEvent(fieldToggle);
     }
 
     toggleFieldsDropdown() {
-        const dropdownElement = this.renderRoot.querySelector('.field-menu') as HTMLElement;
-        const dropdownButton = this.renderRoot.querySelector('.field-toggle') as HTMLElement;
-        dropdownElement.hidden = (dropdownElement!.hidden == true) ? false : true;
+        const dropdownElement = this.renderRoot.querySelector(
+            '.field-menu'
+        ) as HTMLElement;
+        const dropdownButton = this.renderRoot.querySelector(
+            '.field-toggle'
+        ) as HTMLElement;
+        dropdownElement.hidden = dropdownElement.hidden == true ? false : true;
         dropdownButton.classList.toggle('button-toggle');
     }
 }
diff --git a/pw_web/log-viewer/src/components/log-view/log-list/log-list.styles.ts b/pw_web/log-viewer/src/components/log-view/log-list/log-list.styles.ts
index 505b59e..9d539a4 100644
--- a/pw_web/log-viewer/src/components/log-view/log-list/log-list.styles.ts
+++ b/pw_web/log-viewer/src/components/log-view/log-list/log-list.styles.ts
@@ -12,121 +12,136 @@
 // License for the specific language governing permissions and limitations under
 // the License.
 
-import { css } from "lit";
+import { css } from 'lit';
 
 export const styles = css`
-    :host {
-        display: block;
-        font-family: "Roboto Mono", monospace;
-        overflow: scroll;
-        height: 100%;
-        position: relative;
-    }
-
     * {
         box-sizing: border-box;
     }
 
+    :host {
+        position: relative;
+        display: block;
+        height: 100%;
+        font-family: 'Roboto Mono', monospace;
+    }
+
     .table-container {
-        overflow: auto;
         width: 100%;
         height: 100%;
+        overflow: auto;
+        padding-bottom: 3rem;
+        scroll-behavior: auto;
     }
 
     table {
-        min-width: 100%;
         width: auto;
+        height: 100%;
+        min-width: 100vw;
         table-layout: fixed;
         border-collapse: collapse;
     }
 
-    tr:hover {
+    thead,
+    th {
+        position: sticky;
+        top: 0;
+        z-index: 1;
+    }
+
+    thead {
+        background: #2d3134;
+    }
+
+    tr {
+        display: grid;
+        width: 100%;
+        justify-content: flex-start;
+        border-bottom: 1px solid #4b5054;
+    }
+
+    tr:hover > td {
         background: rgb(47 47 47);
     }
 
     th,
     td {
-        width: auto;
         padding: 0.5rem 1rem;
         text-align: left;
-        border-bottom: 1px solid #4b5054;
-        text-align: left;
-        position: relative;
+        display: block;
+        overflow: hidden;
+        text-overflow: ellipsis;
+        grid-row: 1;
+    }
+
+    th[hidden],
+    td[hidden] {
+        display: none;
     }
 
     th {
-        position: sticky;
-        top: 0;
-        z-index: 1;
-        background: #2d3134;
+        grid-row: 1;
         white-space: nowrap;
     }
 
     td {
+        position: relative;
         vertical-align: top;
-        max-width: 96ch;
     }
 
     .resize-handle {
-        content: "";
+        content: '';
         position: absolute;
         top: 0;
         right: 0;
         bottom: 0;
         left: 0;
+        z-index: 1;
         width: 1px;
         height: 100%;
-        cursor: col-resize;
-        z-index: 1;
         opacity: 1;
         background-color: #4b5054;
-        transition: opacity 0.3s ease;
+        cursor: col-resize;
         pointer-events: auto;
+        transition: opacity 300ms ease;
     }
 
     .resize-handle:hover {
-        background-color: #4cdada;
-        outline: 1px solid #4cdada;
+        background-color: var(--md-sys-color-primary);
+        outline: 1px solid var(--md-sys-color-primary);
     }
 
     .resize-handle::before {
-        content: "";
-        display: block;
+        content: '';
         position: absolute;
-        top: 0px;
-        bottom: 0px;
-        right: -8px;
+        top: 0;
+        right: -0.5rem;
+        bottom: 0;
         width: 1rem;
-        /* background: pink; */
-    }
-
-    .cell-content {
         display: block;
-        overflow: hidden;
-        text-overflow: ellipsis;
-        white-space: nowrap;
     }
 
     .overflow-indicator {
         position: absolute;
-        width: 10px;
-        height: 10px;
+        top: 0;
+        width: 4rem;
+        height: 100%;
         pointer-events: none;
     }
 
     .right-indicator {
-        height: 100%;
-        width: 4rem;
-        top: 0;
         right: 0;
         background: linear-gradient(to right, transparent, rgb(47 47 47));
     }
 
     .left-indicator {
-        height: 100%;
-        width: 4rem;
-        top: 0;
         left: 0;
         background: linear-gradient(to left, transparent, rgb(47 47 47));
     }
+
+    mark {
+        background-color: var(--md-sys-color-primary);
+        outline: 1px solid var(--md-sys-color-primary);
+        border-radius: 2px;
+    }
 `;
diff --git a/pw_web/log-viewer/src/components/log-view/log-list/log-list.ts b/pw_web/log-viewer/src/components/log-view/log-list/log-list.ts
index 73399b3..103d6fa 100644
--- a/pw_web/log-viewer/src/components/log-view/log-list/log-list.ts
+++ b/pw_web/log-viewer/src/components/log-view/log-list/log-list.ts
@@ -12,166 +12,312 @@
 // License for the specific language governing permissions and limitations under
 // the License.
 
-import { LitElement, html, PropertyValues } from "lit";
-import { customElement, property, state } from "lit/decorators.js";
-import { LogEntry } from "../../../shared/interfaces";
-import { styles } from "./log-list.styles";
+import { LitElement, html, PropertyValues, TemplateResult } from 'lit';
+import { customElement, property, state } from 'lit/decorators.js';
+import { FieldData, LogEntry } from '../../../shared/interfaces';
+import { styles } from './log-list.styles';
+import '@lit-labs/virtualizer';
+import { virtualize } from '@lit-labs/virtualizer/virtualize.js';
 
 /**
  * Description of LogList.
  */
-@customElement("log-list")
+@customElement('log-list')
 export class LogList extends LitElement {
     static styles = styles;
 
     /**
+     * Description of viewId.
+     */
+    @property()
+    viewId = '';
+
+    /**
      * Description of logs.
      */
     @property({ type: Array })
-    logs: LogEntry[];
+    logs: LogEntry[] = [];
 
     /**
-     * Description of filter.
+     * Description of filterValue.
+     */
+    @property({ type: String })
+    filterValue = '';
+
+    /**
+     * Description of _maxEntries.
      */
     @state()
-    private _maxEntries: number;
+    private _maxEntries = 100_000;
 
+    /**
+     * Description of _fieldNames.
+     */
     @state()
-    private _fieldKeys = new Set<string>();
+    private _fieldNames = new Set<string>();
 
+    /**
+     * Description of _isOverflowingToRight.
+     */
     @state()
-    private _isOverflowingToRight: boolean;
+    private _isOverflowingToRight = false;
 
+    /**
+     * Description of _scrollPercentageHorizontal.
+     */
     @state()
-    private scrollPercentageHorizontal: number;
+    private _scrollPercentageHorizontal = 0;
 
+    /**
+     * Description of _autoscrollIsEnabled.
+     */
+    @state()
+    private _autoscrollIsEnabled = true;
+
+    /**
+     * Description of columnResizeData.
+     */
     private columnResizeData: {
         columnIndex: number;
         startX: number;
         startWidth: number;
     } | null = null;
 
+    @property({ type: Array })
+    colsHidden: boolean[] = [];
+
     constructor() {
         super();
-        this.logs = [];
-        this._maxEntries = 32;
-        this._isOverflowingToRight = false;
-        this.scrollPercentageHorizontal = 0;
+    }
+
+    firstUpdated() {
+        window.addEventListener('scroll', this.handleTableScroll);
+        this.renderRoot
+            .querySelector('tbody')
+            ?.addEventListener('rangeChanged', this.updateGridTemplateColumns);
+
+        if (this.logs.length > 0) {
+            this.performUpdate();
+        }
     }
 
     updated(changedProperties: PropertyValues) {
         super.updated(changedProperties);
         setInterval(() => this.updateOverflowIndicators(), 1000);
 
-        window.addEventListener("scroll", this.handleScroll);
-        // this.addResizeListeners();
-
         if (
-            changedProperties.has("offsetWidth") ||
-            changedProperties.has("scrollWidth")
+            changedProperties.has('offsetWidth') ||
+            changedProperties.has('scrollWidth')
         ) {
             this.updateOverflowIndicators();
         }
+
+        if (changedProperties.has('logs')) {
+            this.updateTableView();
+        }
     }
 
-    firstUpdated() {
-        this.updateWidth();
+    private updateTableView() {
+        const container = this.renderRoot.querySelector(
+            '.table-container'
+        ) as HTMLElement;
+
+        if (container && this._autoscrollIsEnabled) {
+            container.scrollTop = container.scrollHeight;
+        }
     }
 
+    clearGridTemplateColumns() {
+        const table = this.renderRoot.querySelector(
+            'table'
+        ) as HTMLTableElement;
+        const rows = Array.from(table.querySelectorAll('tr'));
+
+        rows.forEach((row) => {
+            row.style.gridTemplateColumns = '';
+        });
+    }
+
+    updateGridTemplateColumns = () => {
+        const table = this.renderRoot.querySelector(
+            'table'
+        ) as HTMLTableElement;
+        const rows = Array.from(table.querySelectorAll('tr'));
+
+        // TODO(b/287277404): move this logic into a separate function
+        // Set column visibility based on `colsHidden` array
+        rows.forEach((row) => {
+            const cells = Array.from(row.querySelectorAll('td, th'));
+            cells.forEach((cell, index: number) => {
+                const colHidden = this.colsHidden[index];
+
+                const cellEl = cell as HTMLElement;
+                cellEl.hidden = colHidden;
+            });
+        });
+
+        // Get the number of visible columns
+        const columnCount =
+            Array.from(rows[0]?.children || []).filter(
+                (child) => !child.hasAttribute('hidden')
+            ).length || 0;
+
+        // Initialize an array to store the maximum width of each column
+        const columnWidths: number[] = new Array(columnCount).fill(0);
+
+        // Iterate through each row to find the maximum width in each column
+        rows.forEach((row) => {
+            const cells = Array.from(row.children).filter(
+                (cell) => !cell.hasAttribute('hidden')
+            ) as HTMLTableCellElement[];
+
+            cells.forEach((cell, columnIndex) => {
+                const cellWidth = cell.getBoundingClientRect().width;
+                columnWidths[columnIndex] = Math.max(
+                    columnWidths[columnIndex],
+                    cellWidth
+                );
+            });
+        });
+
+        // Generate the gridTemplateColumns value for each row
+        rows.forEach((row) => {
+            const gridTemplateColumns = columnWidths
+                .map((width, index) => {
+                    if (index === columnWidths.length - 1) {
+                        return '1fr';
+                    } else {
+                        return `${width}px`;
+                    }
+                })
+                .join(' ');
+            row.style.gridTemplateColumns = gridTemplateColumns;
+        });
+    };
+
     disconnectedCallback() {
         super.disconnectedCallback();
 
-        const table = this.renderRoot.querySelector(".table-container");
-        table?.removeEventListener("scroll", this.handleScroll);
+        const container = this.renderRoot.querySelector('.table-container');
+        container?.removeEventListener('scroll', this.handleTableScroll);
+    }
+
+    highlightMatchedText(text: string): TemplateResult[] {
+        if (!this.filterValue) {
+            return [html`${text}`];
+        }
+
+        const escapedFilterValue = this.filterValue.replace(
+            /[.*+?^${}()|[\]\\]/g,
+            '\\$&'
+        );
+        const regex = new RegExp(`(${escapedFilterValue})`, 'gi');
+        const parts = text.split(regex);
+        return parts.map((part) =>
+            regex.test(part) ? html`<mark>${part}</mark>` : html`${part}`
+        );
     }
 
     render() {
-        const logsDisplayed = this.logs.slice(0, this._maxEntries);
-
-        logsDisplayed.forEach((logEntry) => {
-            logEntry.fields.forEach((fieldData) => {
-                this._fieldKeys.add(fieldData.key);
-            });
-        });
+        const logsDisplayed: LogEntry[] = this.logs.slice(0, this._maxEntries);
+        this.setFieldNames(logsDisplayed);
 
         return html`
-            <div class="table-container" role="region" tabindex="0">
-                <table @wheel="${this.handleScroll}">
-                    <tr>
-                        ${Array.from(this._fieldKeys).map(
-                            (fieldKey, columnIndex) => html`
-                                <th>
-                                    <span class="cell-content"
-                                        >${fieldKey}</span
-                                    >
-                                    ${columnIndex > 0
-                                        ? html`
-                                              <div
-                                                  class="resize-handle"
-                                                  @mousedown="${(
-                                                      event: MouseEvent
-                                                  ) =>
-                                                      this.handleColumnResizeStart(
-                                                          event,
-                                                          columnIndex - 1
-                                                      )}"
-                                              ></div>
-                                          `
-                                        : html``}
-                                </th>
-                            `
-                        )}
-                    </tr>
+            <div
+                class="table-container"
+                role="log"
+                @wheel="${this.handleTableScroll}"
+            >
+                <table>
+                    <thead>
+                        ${this.tableHeaderRow()}
+                    </thead>
 
-                    ${logsDisplayed.map(
-                        (log: LogEntry) => html` <tr>
-                            ${log.fields.map(
-                                (field, columnIndex) =>
-                                    html`
-                                        <td>
-                                            <span class="cell-content"
-                                                >${field.value}</span
-                                            >
-
-                                            ${columnIndex > 0
-                                                ? html`
-                                                      <div
-                                                          class="resize-handle"
-                                                          @mousedown="${(
-                                                              event: MouseEvent
-                                                          ) =>
-                                                              this.handleColumnResizeStart(
-                                                                  event,
-                                                                  columnIndex -
-                                                                      1
-                                                              )}"
-                                                      ></div>
-                                                  `
-                                                : html``}
-                                        </td>
-                                    `
-                            )}
-                        </tr>`
-                    )}
+                    <tbody>
+                        ${virtualize({
+                            items: logsDisplayed,
+                            renderItem: (log) =>
+                                html`${this.tableDataRow(log)}`,
+                        })}
+                    </tbody>
                 </table>
 
-                <div
-                    class="overflow-indicator right-indicator"
-                    style="opacity: ${1 - this.scrollPercentageHorizontal}"
-                    ?hidden="${!this._isOverflowingToRight}"
-                ></div>
-                <div
-                    class="overflow-indicator left-indicator"
-                    style="opacity: ${this.scrollPercentageHorizontal}"
-                    ?hidden="${!this._isOverflowingToRight}"
-                ></div>
+                ${this.overflowIndicators()}
             </div>
         `;
     }
 
-    handleScroll = () => {
+    setFieldNames(logs: LogEntry[]) {
+        logs.forEach((logEntry) => {
+            logEntry.fields.forEach((fieldData) => {
+                this._fieldNames.add(fieldData.key);
+            });
+        });
+    }
+
+    tableHeaderRow() {
+        return html`
+            <tr>
+                ${Array.from(this._fieldNames).map((fieldKey, columnIndex) =>
+                    this.tableHeaderCell(fieldKey, columnIndex)
+                )}
+            </tr>
+        `;
+    }
+
+    tableHeaderCell(fieldKey: string, columnIndex: number) {
+        return html`
+            <th>
+                ${fieldKey}
+                ${columnIndex > 0 ? this.resizeHandle(columnIndex - 1) : html``}
+            </th>
+        `;
+    }
+
+    resizeHandle(columnIndex: number) {
+        return html`
+            <span
+                class="resize-handle"
+                @mousedown="${(event: MouseEvent) =>
+                    this.handleColumnResizeStart(event, columnIndex)}"
+            ></span>
+        `;
+    }
+
+    tableDataRow(log: LogEntry) {
+        return html`
+            <tr>
+                ${log.fields.map((field, columnIndex) =>
+                    this.tableDataCell(field, columnIndex)
+                )}
+            </tr>
+        `;
+    }
+
+    tableDataCell = (field: FieldData, columnIndex: number) => html`
+        <td>
+            ${this.highlightMatchedText(field.value.toString())}
+            ${columnIndex > 0 ? this.resizeHandle(columnIndex - 1) : html``}
+        </td>
+    `;
+
+    overflowIndicators = () => html`
+        <div
+            class="overflow-indicator left-indicator"
+            style="opacity: ${this._scrollPercentageHorizontal}"
+            ?hidden="${!this._isOverflowingToRight}"
+        ></div>
+
+        <div
+            class="overflow-indicator right-indicator"
+            style="opacity: ${1 - this._scrollPercentageHorizontal}"
+            ?hidden="${!this._isOverflowingToRight}"
+        ></div>
+    `;
+
+    handleTableScroll = () => {
         const container = this.renderRoot.querySelector(
-            ".table-container"
+            '.table-container'
         ) as HTMLElement;
 
         if (!container) {
@@ -179,55 +325,47 @@
         }
 
         const containerWidth = container.offsetWidth;
-        const containerHeight = container.offsetHeight;
         const scrollLeft = container.scrollLeft;
-        const scrollTop = container.scrollTop;
         const maxScrollLeft = container.scrollWidth - containerWidth;
-        const maxScrollTop = container.scrollHeight - containerHeight;
+        const threshold = 128;
 
-        this.scrollPercentageHorizontal = scrollLeft / maxScrollLeft || 0;
-        // this.scrollPercentageVertical = scrollTop / maxScrollTop || 0;
+        this._scrollPercentageHorizontal = scrollLeft / maxScrollLeft || 0;
+
+        if (
+            container.scrollHeight - container.scrollTop <=
+            container.offsetHeight + threshold
+        ) {
+            this._autoscrollIsEnabled = true;
+        } else {
+            this._autoscrollIsEnabled = false;
+        }
 
         this.requestUpdate();
     };
 
     updateOverflowIndicators() {
-        const table = this.renderRoot.querySelector(".table-container");
+        const container = this.renderRoot.querySelector('.table-container');
 
-        if (!table) {
+        if (!container) {
             return;
         }
 
         const containerWidth = this.offsetWidth;
-        const tableWidth = table!.scrollWidth;
-        const containerHeight = this.offsetHeight;
-        const tableHeight = table!.scrollHeight;
+        const tableWidth = container?.scrollWidth;
 
         this._isOverflowingToRight = tableWidth > containerWidth;
     }
 
-    updateWidth() {
-        const table = this.shadowRoot?.querySelector("table");
-
-        if (table) {
-            // Calculate the width of the table
-            const tableWidth = table.offsetWidth;
-
-            // Apply the calculated width as fixed value
-            table.style.width = `${tableWidth}px`;
-        }
-    }
-
     handleColumnResizeStart(event: MouseEvent, columnIndex: number) {
         event.preventDefault();
         const startX = event.clientX;
         const table = this.renderRoot.querySelector(
-            "table"
+            'table'
         ) as HTMLTableElement;
 
         const th = table.querySelector(
             `th:nth-child(${columnIndex + 1})`
-        ) as HTMLTableHeaderCellElement;
+        ) as HTMLTableCellElement;
         const startWidth = th.offsetWidth;
 
         this.columnResizeData = {
@@ -242,33 +380,49 @@
 
         const handleColumnResizeEnd = () => {
             this.columnResizeData = null;
-            document.removeEventListener("mousemove", handleColumnResize);
-            document.removeEventListener("mouseup", handleColumnResizeEnd);
+            document.removeEventListener('mousemove', handleColumnResize);
+            document.removeEventListener('mouseup', handleColumnResizeEnd);
         };
 
-        document.addEventListener("mousemove", handleColumnResize);
-        document.addEventListener("mouseup", handleColumnResizeEnd);
+        document.addEventListener('mousemove', handleColumnResize);
+        document.addEventListener('mouseup', handleColumnResizeEnd);
     }
 
     handleColumnResize(event: MouseEvent) {
         if (!this.columnResizeData) return;
         const { columnIndex, startX, startWidth } = this.columnResizeData;
         const table = this.renderRoot.querySelector(
-            "table"
+            'table'
         ) as HTMLTableElement;
+
         const th = table.querySelector(
             `th:nth-child(${columnIndex + 1})`
-        ) as HTMLTableHeaderCellElement;
-        const tds = table.querySelectorAll(
-            `td:nth-child(${columnIndex + 1})`
-        ) as NodeListOf<HTMLTableDataCellElement>;
+        ) as HTMLTableCellElement;
 
         const offsetX = event.clientX - startX;
-        const newWidth = Math.max(startWidth + offsetX, 20); // Minimum width
+        const newWidth = Math.max(startWidth + offsetX, 48); // Minimum width
 
         th.style.width = `${newWidth}px`;
-        for (let i = 0; i < tds.length; i++) {
-            tds[i].style.width = `${newWidth}px`;
+
+        const totalColumns = table.querySelectorAll('th').length;
+        let gridTemplateColumns = '';
+
+        for (let i = 0; i < totalColumns; i++) {
+            if (i === columnIndex) {
+                gridTemplateColumns += `${newWidth}px `;
+            } else {
+                const tableEl = table.querySelector(
+                    `th:nth-child(${i + 1})`
+                ) as HTMLElement;
+                const otherColumnWidth = tableEl?.offsetWidth;
+
+                gridTemplateColumns += `${otherColumnWidth}px `;
+            }
         }
+
+        const rows = table.querySelectorAll('tr');
+        rows.forEach((row) => {
+            row.style.gridTemplateColumns = gridTemplateColumns;
+        });
     }
 }
diff --git a/pw_web/log-viewer/src/components/log-view/log-view.ts b/pw_web/log-viewer/src/components/log-view/log-view.ts
index 6e4a818..7e8baba 100644
--- a/pw_web/log-viewer/src/components/log-view/log-view.ts
+++ b/pw_web/log-viewer/src/components/log-view/log-view.ts
@@ -12,24 +12,44 @@
 // License for the specific language governing permissions and limitations under
 // the License.
 
-import { LitElement, html } from "lit";
-import { customElement, property, state } from "lit/decorators.js";
-import { styles } from "./log-view.styles";
-import { LogEntry } from "../../shared/interfaces";
-
+import { LitElement, html } from 'lit';
+import { customElement, property, state } from 'lit/decorators.js';
+import { styles } from './log-view.styles';
+import { LogEntry } from '../../shared/interfaces';
 
 // Import subcomponents
-import "./log-list/log-list";
-import "./controls/controls";
+import './log-list/log-list';
+import './controls/controls';
+import { LogList } from './log-list/log-list';
+
+interface FilterChangeEvent extends Event {
+    detail: {
+        filterValue: string;
+    };
+}
+
+declare global {
+    interface HTMLElementEventMap {
+        'filter-change': FilterChangeEvent;
+    }
+}
+
+let viewCount = 0;
 
 /**
  * Description of LogView.
  */
-@customElement("log-view")
+@customElement('log-view')
 export class LogView extends LitElement {
     static styles = styles;
 
     /**
+     * Description of id.
+     */
+    @property({ type: String })
+    id: string;
+
+    /**
      * Description of logs.
      */
     @property({ type: Array })
@@ -44,32 +64,62 @@
     /**
      * Fields from some log source
      */
-    @property({ type: Array, reflect: true })
-    _fields: String[] = [];
-
-    /**
-     * Description of selectedHostId.
-     */
-    @state()
-    private _selectedHostId: string = 'example-host';
+    @property({ type: Array })
+    _fields: string[] = [];
 
     /**
      * Description of filter.
      */
     @state()
-    private _filter: (logEntry: LogEntry) => boolean;
+    private _filter: (logEntry: LogEntry) => boolean = () => true;
+
+    /**
+     * Description of filterValue.
+     */
+    @state()
+    private _filterValue = '';
+
+    /**
+     * Description of hideCloseButton.
+     */
+    @property({ type: Boolean })
+    hideCloseButton = false;
 
     constructor() {
         super();
-        this._filter = (logEntry) => logEntry.hostId === this._selectedHostId;
+        this.id = `log-view-${viewCount}`;
     }
 
-    render() {
-        this._filteredLogs = JSON.parse(JSON.stringify(this.logs.filter(this._filter)));
-        this._fields = this.getFields(this.logs);
-        return html`<log-view-controls .fieldKeys=${this._fields} 
-        @field-toggle="${this.handleFieldToggleEvent}"role="toolbar"></log-view-controls>
-                <log-list .logs=${this._filteredLogs}></log-list>`;
+    connectedCallback() {
+        super.connectedCallback();
+        viewCount++;
+        this.addEventListener('filter-change', this.handleFilterChange);
+        this.addEventListener('clear-logs', this.handleClearLogs);
+    }
+
+    disconnectedCallback() {
+        super.disconnectedCallback();
+        this.removeEventListener('filter-change', this.handleFilterChange);
+        this.removeEventListener('clear-logs', this.handleClearLogs);
+    }
+
+    handleFilterChange(event: FilterChangeEvent) {
+        this._filterValue = event.detail.filterValue;
+        const regex = new RegExp(this._filterValue, 'i');
+
+        this._filter = (logEntry) =>
+            logEntry.fields.some((field) => regex.test(field.value.toString()));
+        this.requestUpdate();
+    }
+
+    handleClearLogs() {
+        const timeThreshold = new Date();
+        const existingFilter = this._filter;
+        this._filter = (logEntry) => {
+            return (
+                existingFilter(logEntry) && logEntry.timestamp > timeThreshold
+            );
+        };
     }
 
     /**
@@ -77,9 +127,9 @@
      * @param logs the source logs to extract fields from
      * @return     an array of log fields
      */
-    private getFields(logs: LogEntry[]): String[] {
+    private getFields(logs: LogEntry[]): string[] {
         const log = logs[0];
-        const logFields = [] as String[];
+        const logFields = [] as string[];
         if (log != undefined) {
             log.fields.forEach((field) => {
                 logFields.push(field.key);
@@ -96,38 +146,43 @@
         // should be index to show/hide element
         let index = -1;
 
-        // TODO(b/287285444): surface table element as property of LogList
-        const logListEl = this.renderRoot.querySelector('log-list') as HTMLElement;
-        const tableBodyEl = logListEl?.shadowRoot?.querySelector('tbody') as HTMLElement;
-        const table = Array.from(tableBodyEl?.children) as HTMLElement[];
+        const logList = this.renderRoot.querySelector('log-list') as LogList;
+        const colsHidden = logList.colsHidden;
 
-        // Get index from the source logs to show field back into view
-        if (e.detail.isChecked) {
-            this._fields.forEach((_, i: number) => {
-                if (this.logs[0].fields[i].key == e.detail.field) {
-                    index = i;
-                }
-            });
+        this._fields.forEach((_, i: number) => {
+            if (this.logs[0].fields[i].key == e.detail.field) {
+                index = i;
+            }
+        });
 
-            table.forEach((_, i: number) => {
-                let tableCellEl = table[i].children[index] as HTMLElement;
-                tableCellEl.hidden = false;
-            });
-        }
+        colsHidden[index] = !e.detail.isChecked;
 
-        // Get index from dropdown and hide field from view
-        else {
-            this._fields.forEach((_, i: number) => {
-                if (this._filteredLogs[0].fields[i].key === e.detail.field) {
-                    index = i;
-                }
-            });
+        logList.colsHidden = colsHidden;
+        logList.clearGridTemplateColumns();
+        logList.updateGridTemplateColumns();
+    }
 
-            table.forEach((_, i: number) => {
-                let tableCellEl = table[i].children[index] as HTMLElement;
-                tableCellEl.hidden = true;
-            });
-        }
+    render() {
+        this._filteredLogs = JSON.parse(
+            JSON.stringify(this.logs.filter(this._filter))
+        );
+        this._fields = this.getFields(this.logs);
+
+        const passedFilterValue = this._filterValue;
+
+        return html` <log-view-controls
+                .viewId=${this.id}
+                .fieldKeys=${this._fields}
+                .hideCloseButton=${this.hideCloseButton}
+                @field-toggle="${this.handleFieldToggleEvent}"
+                role="toolbar"
+            >
+            </log-view-controls>
+
+            <log-list
+                .viewId=${this.id}
+                .logs=${this._filteredLogs}
+                .filterValue=${passedFilterValue}
+            ></log-list>`;
     }
 }
-
diff --git a/pw_web/log-viewer/src/components/log-viewer.ts b/pw_web/log-viewer/src/components/log-viewer.ts
index c73e0d2..ff525b5 100644
--- a/pw_web/log-viewer/src/components/log-viewer.ts
+++ b/pw_web/log-viewer/src/components/log-viewer.ts
@@ -12,16 +12,16 @@
 // License for the specific language governing permissions and limitations under
 // the License.
 
-import { LitElement, html } from "lit";
-import { customElement, property } from "lit/decorators.js";
-import { styles } from "./log-viewer.styles";
-import { LogView } from "./log-view/log-view";
-import { LogEntry } from "../shared/interfaces";
+import { LitElement, html } from 'lit';
+import { customElement, property } from 'lit/decorators.js';
+import { styles } from './log-viewer.styles';
+import { LogView } from './log-view/log-view';
+import { LogEntry } from '../shared/interfaces';
 
 /**
  * Description of LogViewer.
  */
-@customElement("log-viewer")
+@customElement('log-viewer')
 export class LogViewer extends LitElement {
     static styles = styles;
 
@@ -43,17 +43,39 @@
         this.logViews = [new LogView()];
     }
 
+    connectedCallback(): void {
+        super.connectedCallback();
+        this.addEventListener('close-view', this.handleCloseView);
+    }
+
+    disconnectedCallback() {
+        super.disconnectedCallback();
+        this.removeEventListener('close-view', this.handleCloseView);
+    }
+
     render() {
         const logsCopy = [...this.logs]; // Trigger an update in <log-view>
+        const hideCloseButton = this.logViews.length <= 1;
 
         return html`
-            <md-outlined-button class="add-button" @click="${this.addLogView}">
+            <md-outlined-button
+                class="add-button"
+                @click="${this.addLogView}"
+                title="Add a view"
+            >
                 Add View
             </md-outlined-button>
 
             <div class="grid-container">
                 ${this.logViews.map(
-                    () => html` <log-view .logs=${logsCopy}></log-view> `
+                    (view) =>
+                        html`
+                            <log-view
+                                id=${view.id}
+                                .logs=${logsCopy}
+                                .hideCloseButton=${hideCloseButton}
+                            ></log-view>
+                        `
                 )}
             </div>
         `;
@@ -62,10 +84,15 @@
     addLogView() {
         this.logViews = [...this.logViews, new LogView()];
     }
+
+    handleCloseView(event: Event) {
+        const viewId = (event as CustomEvent).detail.viewId;
+        this.logViews = this.logViews.filter((view) => view.id !== viewId);
+    }
 }
 
 declare global {
     interface HTMLElementTagNameMap {
-        "log-viewer": LogViewer;
+        'log-viewer': LogViewer;
     }
 }
diff --git a/pw_web/log-viewer/src/custom/mock-log-source.ts b/pw_web/log-viewer/src/custom/mock-log-source.ts
index 088a71e..c3930f3 100644
--- a/pw_web/log-viewer/src/custom/mock-log-source.ts
+++ b/pw_web/log-viewer/src/custom/mock-log-source.ts
@@ -12,33 +12,30 @@
 // License for the specific language governing permissions and limitations under
 // the License.
 
-import { LogSource } from "../log-source";
-
-interface LogEntry {
-    hostId: string;
-    timestamp: string;
-    fields: FieldData[];
-}
-
-interface FieldData {
-    key: string;
-    value: string | number | object;
-}
+import { LogSource } from '../log-source';
+import { LogEntry } from '../shared/interfaces';
 
 export class MockLogSource extends LogSource {
-    private intervalId: NodeJS.Timeout | null;
-    private READ_FREQUENCY = 50;
+    private intervalId: NodeJS.Timeout | null = null;
 
     constructor() {
         super();
-        this.intervalId = null;
     }
 
     start(): void {
-        this.intervalId = setInterval(() => {
+        const getRandomInterval = () => {
+            return Math.floor(Math.random() * (1000 - 50 + 1) + 50);
+        };
+
+        const readLogEntry = () => {
             const logEntry = this.readLogEntryFromHost();
-            this.emitEvent("logEntry", logEntry);
-        }, this.READ_FREQUENCY);
+            this.emitEvent('logEntry', logEntry);
+
+            const nextInterval = getRandomInterval();
+            setTimeout(readLogEntry, nextInterval);
+        };
+
+        readLogEntry();
     }
 
     stop(): void {
@@ -50,36 +47,34 @@
 
     readLogEntryFromHost(): LogEntry {
         // Emulate reading log data from a host device
-        const severities = ["INFO", "WARNING", "ERROR", "DEBUG"];
-        const sources = ["application", "server", "database", "network"];
+        const severities = ['INFO', 'WARNING', 'ERROR', 'DEBUG'];
+        const sources = ['application', 'server', 'database', 'network'];
         const messages = [
-            "Request processed successfully!",
-            "An unexpected error occurred while performing the operation.",
-            "Connection timed out. Please check your network settings.",
-            "Invalid input detected. Please provide valid data.",
-            "Database connection lost. Attempting to reconnect.",
-            "User authentication failed. Invalid credentials provided.",
-            "System reboot initiated. Please wait for the system to come back online.",
-            "File not found. The requested file does not exist.",
-            "Data corruption detected. Initiating recovery process.",
-            "Network congestion detected. Traffic is high, please try again later.",
-            "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam condimentum auctor justo, sit amet condimentum nibh facilisis non. Quisque in quam a urna dignissim cursus. Suspendisse egestas nisl sed massa dictum dictum. In tincidunt arcu nec odio eleifend, vel pharetra justo iaculis. Vivamus quis tellus ac velit vehicula consequat. Nam eu felis sed risus hendrerit faucibus ac id lacus. Vestibulum tincidunt tellus in ex feugiat interdum. Nulla sit amet luctus neque. Mauris et aliquet nunc, vel finibus massa. Curabitur laoreet eleifend nibh eget luctus. Fusce sodales augue nec purus faucibus, vel tristique enim vehicula. Aenean eu magna eros. Fusce accumsan dignissim dui auctor scelerisque. Proin ultricies nunc vel tincidunt facilisis.",
+            'Request processed successfully!',
+            'An unexpected error occurred while performing the operation.',
+            'Connection timed out. Please check your network settings.',
+            'Invalid input detected. Please provide valid data.',
+            'Database connection lost. Attempting to reconnect.',
+            'User authentication failed. Invalid credentials provided.',
+            'System reboot initiated. Please wait for the system to come back online.',
+            'File not found. The requested file does not exist.',
+            'Data corruption detected. Initiating recovery process.',
+            'Network congestion detected. Traffic is high, please try again later.',
+            'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam condimentum auctor justo, sit amet condimentum nibh facilisis non. Quisque in quam a urna dignissim cursus. Suspendisse egestas nisl sed massa dictum dictum. In tincidunt arcu nec odio eleifend, vel pharetra justo iaculis. Vivamus quis tellus ac velit vehicula consequat. Nam eu felis sed risus hendrerit faucibus ac id lacus. Vestibulum tincidunt tellus in ex feugiat interdum. Nulla sit amet luctus neque. Mauris et aliquet nunc, vel finibus massa. Curabitur laoreet eleifend nibh eget luctus. Fusce sodales augue nec purus faucibus, vel tristique enim vehicula. Aenean eu magna eros. Fusce accumsan dignissim dui auctor scelerisque. Proin ultricies nunc vel tincidunt facilisis.',
         ];
-        const timestamp = new Date().toISOString();
+        const timestamp: Date = new Date();
         const getRandomValue = (values: string[]) => {
             const randomIndex = Math.floor(Math.random() * values.length);
             return values[randomIndex];
         };
 
         const logEntry = {
-            hostId: "example-host",
             timestamp: timestamp,
             fields: [
-                { key: "timestamp", value: timestamp },
-                { key: "severity", value: getRandomValue(severities) },
-                { key: "source", value: getRandomValue(sources) },
-                { key: "message", value: getRandomValue(messages) },
-                { key: "second message", value: getRandomValue(messages) },
+                { key: 'timestamp', value: timestamp.toISOString() },
+                { key: 'severity', value: getRandomValue(severities) },
+                { key: 'source', value: getRandomValue(sources) },
+                { key: 'message', value: getRandomValue(messages) },
             ],
         };
 
diff --git a/pw_web/log-viewer/src/index.ts b/pw_web/log-viewer/src/index.ts
index 5fb9046..151fa35 100644
--- a/pw_web/log-viewer/src/index.ts
+++ b/pw_web/log-viewer/src/index.ts
@@ -12,18 +12,26 @@
 // License for the specific language governing permissions and limitations under
 // the License.
 
-import { MockLogSource } from "./custom/mock-log-source";
-import {createLogViewer} from "./components/createLogViewer";
+import { MockLogSource } from './custom/mock-log-source';
+import { createLogViewer } from './components/createLogViewer';
 
 const logSource = new MockLogSource();
-const unsubscribe = createLogViewer(logSource, document.querySelector('#log-viewer-container')!);
+const containerEl = document.querySelector(
+    '#log-viewer-container'
+) as HTMLElement;
 
-const TIMEOUT_DURATION = 5000; // ms
+let unsubscribe: () => void;
+
+if (containerEl) {
+    unsubscribe = createLogViewer(logSource, containerEl);
+}
+
+const TIMEOUT_DURATION = 30_000; // ms
 // Start reading log data
 logSource.start();
 
 // Stop reading log data once timeout duration has elapsed
 setTimeout(() => {
     logSource.stop();
-  unsubscribe();
+    unsubscribe();
 }, TIMEOUT_DURATION);
diff --git a/pw_web/log-viewer/src/shared/interfaces.ts b/pw_web/log-viewer/src/shared/interfaces.ts
index 959194f..6e639f9 100644
--- a/pw_web/log-viewer/src/shared/interfaces.ts
+++ b/pw_web/log-viewer/src/shared/interfaces.ts
@@ -12,12 +12,11 @@
 // License for the specific language governing permissions and limitations under
 // the License.
 export interface LogEntry {
-    hostId: string;
-    timestamp: string;
+    timestamp: Date;
     fields: FieldData[];
 }
 
 export interface FieldData {
     key: string;
-    value: any;
+    value: string | boolean | number | object;
 }
diff --git a/pw_web/log-viewer/tsconfig.json b/pw_web/log-viewer/tsconfig.json
index 0379a19..51fb1aa 100644
--- a/pw_web/log-viewer/tsconfig.json
+++ b/pw_web/log-viewer/tsconfig.json
@@ -17,5 +17,6 @@
         "noFallthroughCasesInSwitch": true,
         "incremental": true
     },
-    "include": ["src/**/*"]
+    "include": ["src/**/*"],
+    "exclude": ["src/legacy/**/*"]
 }