diff --git a/components/log-viewer-webui/client/package-lock.json b/components/log-viewer-webui/client/package-lock.json
index fc1a071310..d80e7f03b1 100644
--- a/components/log-viewer-webui/client/package-lock.json
+++ b/components/log-viewer-webui/client/package-lock.json
@@ -15,16 +15,19 @@
"@sinclair/typebox": "^0.34.25",
"antd": "^5.24.5",
"axios": "^1.7.9",
+ "highlight.js": "^11.11.1",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-router": "^7.4.1",
+ "react-syntax-highlighter": "^15.6.1",
"zustand": "^5.0.3"
},
"devDependencies": {
"@types/react": "^19.0.10",
"@types/react-dom": "^19.0.4",
+ "@types/react-syntax-highlighter": "^15.5.13",
"@vitejs/plugin-react": "^4.3.4",
- "eslint-config-yscope": "*",
+ "eslint-config-yscope": "latest",
"typescript": "~5.6.2",
"vite": "^6.2.6"
}
@@ -2241,6 +2244,15 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@types/hast": {
+ "version": "2.3.10",
+ "resolved": "https://registry.npmjs.org/@types/hast/-/hast-2.3.10.tgz",
+ "integrity": "sha512-McWspRw8xx8J9HurkVBfYj0xKoE25tOFlHGdx4MJ5xORQrMGZNqJhVQWaIbm6Oyla5kYOXtDiopzKRJzEOkwJw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^2"
+ }
+ },
"node_modules/@types/json-schema": {
"version": "7.0.15",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
@@ -2289,6 +2301,22 @@
"@types/react": "^19.0.0"
}
},
+ "node_modules/@types/react-syntax-highlighter": {
+ "version": "15.5.13",
+ "resolved": "https://registry.npmjs.org/@types/react-syntax-highlighter/-/react-syntax-highlighter-15.5.13.tgz",
+ "integrity": "sha512-uLGJ87j6Sz8UaBAooU0T6lWJ0dBmjZgN1PZTrj05TNql2/XpC6+4HhMT5syIdFUUt+FASfCeLLv4kBygNU+8qA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/react": "*"
+ }
+ },
+ "node_modules/@types/unist": {
+ "version": "2.0.11",
+ "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz",
+ "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==",
+ "license": "MIT"
+ },
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.19.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.19.0.tgz",
@@ -3366,6 +3394,36 @@
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
+ "node_modules/character-entities": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-1.2.4.tgz",
+ "integrity": "sha512-iBMyeEHxfVnIakwOuDXpVkc54HijNgCyQB2w0VfGQThle6NXn50zU6V/u+LDhxHcDUPojn6Kpga3PTAD8W1bQw==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/character-entities-legacy": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-1.1.4.tgz",
+ "integrity": "sha512-3Xnr+7ZFS1uxeiUDvV02wQ+QDbc55o97tIV5zHScSPJpcLm/r0DFPcoY3tYRp+VZukxuMeKgXYmsXQHO05zQeA==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/character-reference-invalid": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-1.1.4.tgz",
+ "integrity": "sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
"node_modules/classnames": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz",
@@ -3415,6 +3473,16 @@
"node": ">= 0.8"
}
},
+ "node_modules/comma-separated-tokens": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-1.0.8.tgz",
+ "integrity": "sha512-GHuDRO12Sypu2cV70d1dkA2EUmXHgntrzbpvOB+Qy+49ypNfGgFQIC2fhhXbnyrJRynDCAARsT7Ou0M6hirpfw==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
"node_modules/comment-parser": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/comment-parser/-/comment-parser-1.4.1.tgz",
@@ -4587,6 +4655,19 @@
"reusify": "^1.0.4"
}
},
+ "node_modules/fault": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/fault/-/fault-1.0.4.tgz",
+ "integrity": "sha512-CJ0HCB5tL5fYTEA7ToAq5+kTwd++Borf1/bifxd9iT70QcXr4MRrO3Llf8Ifs70q+SJcGHFtnIE/Nw6giCtECA==",
+ "license": "MIT",
+ "dependencies": {
+ "format": "^0.2.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
"node_modules/fdir": {
"version": "6.4.3",
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.3.tgz",
@@ -4730,6 +4811,14 @@
"node": ">= 6"
}
},
+ "node_modules/format": {
+ "version": "0.2.2",
+ "resolved": "https://registry.npmjs.org/format/-/format-0.2.2.tgz",
+ "integrity": "sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==",
+ "engines": {
+ "node": ">=0.4.x"
+ }
+ },
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
@@ -5036,6 +5125,48 @@
"node": ">= 0.4"
}
},
+ "node_modules/hast-util-parse-selector": {
+ "version": "2.2.5",
+ "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-2.2.5.tgz",
+ "integrity": "sha512-7j6mrk/qqkSehsM92wQjdIgWM2/BW61u/53G6xmC8i1OmEdKLHbk419QKQUjz6LglWsfqoiHmyMRkP1BGjecNQ==",
+ "license": "MIT",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/hastscript": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-6.0.0.tgz",
+ "integrity": "sha512-nDM6bvd7lIqDUiYEiu5Sl/+6ReP0BMk/2f4U/Rooccxkj0P5nm+acM5PrGJ/t5I8qPGiqZSE6hVAwZEdZIvP4w==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/hast": "^2.0.0",
+ "comma-separated-tokens": "^1.0.0",
+ "hast-util-parse-selector": "^2.0.0",
+ "property-information": "^5.0.0",
+ "space-separated-tokens": "^1.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/highlight.js": {
+ "version": "11.11.1",
+ "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.11.1.tgz",
+ "integrity": "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=12.0.0"
+ }
+ },
+ "node_modules/highlightjs-vue": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/highlightjs-vue/-/highlightjs-vue-1.0.0.tgz",
+ "integrity": "sha512-PDEfEF102G23vHmPhLyPboFCD+BkMGu+GuJe2d9/eH4FsCwvgBpnc9n0pGE+ffKdph38s6foEZiEjdgHdzp+IA==",
+ "license": "CC0-1.0"
+ },
"node_modules/hoist-non-react-statics": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
@@ -5105,6 +5236,30 @@
"node": ">= 0.4"
}
},
+ "node_modules/is-alphabetical": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-1.0.4.tgz",
+ "integrity": "sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/is-alphanumerical": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-1.0.4.tgz",
+ "integrity": "sha512-UzoZUr+XfVz3t3v4KyGEniVL9BDRoQtY7tOyrRybkVNjDFWyo1yhXNGrrBTQxp3ib9BLAWs7k2YKBQsFRkZG9A==",
+ "license": "MIT",
+ "dependencies": {
+ "is-alphabetical": "^1.0.0",
+ "is-decimal": "^1.0.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
"node_modules/is-array-buffer": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz",
@@ -5263,6 +5418,16 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/is-decimal": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-1.0.4.tgz",
+ "integrity": "sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
"node_modules/is-extglob": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
@@ -5325,6 +5490,16 @@
"node": ">=0.10.0"
}
},
+ "node_modules/is-hexadecimal": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-1.0.4.tgz",
+ "integrity": "sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
"node_modules/is-map": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz",
@@ -5738,6 +5913,29 @@
"loose-envify": "cli.js"
}
},
+ "node_modules/lowlight": {
+ "version": "1.20.0",
+ "resolved": "https://registry.npmjs.org/lowlight/-/lowlight-1.20.0.tgz",
+ "integrity": "sha512-8Ktj+prEb1RoCPkEOrPMYUN/nCggB7qAWe3a7OpMjWQkh3l2RD5wKRQ+o8Q8YuI9RG/xs95waaI/E6ym/7NsTw==",
+ "license": "MIT",
+ "dependencies": {
+ "fault": "^1.0.0",
+ "highlight.js": "~10.7.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/lowlight/node_modules/highlight.js": {
+ "version": "10.7.3",
+ "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz",
+ "integrity": "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": "*"
+ }
+ },
"node_modules/lru-cache": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
@@ -6098,6 +6296,24 @@
"node": ">=6"
}
},
+ "node_modules/parse-entities": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-2.0.0.tgz",
+ "integrity": "sha512-kkywGpCcRYhqQIchaWqZ875wzpS/bMKhz5HnN3p7wveJTkTtyAB/AlnS0f8DFSqYW1T82t6yEAkEcB+A1I3MbQ==",
+ "license": "MIT",
+ "dependencies": {
+ "character-entities": "^1.0.0",
+ "character-entities-legacy": "^1.0.0",
+ "character-reference-invalid": "^1.0.0",
+ "is-alphanumerical": "^1.0.0",
+ "is-decimal": "^1.0.0",
+ "is-hexadecimal": "^1.0.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
"node_modules/parse-imports": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/parse-imports/-/parse-imports-2.2.1.tgz",
@@ -6239,6 +6455,15 @@
"node": ">= 0.8.0"
}
},
+ "node_modules/prismjs": {
+ "version": "1.30.0",
+ "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz",
+ "integrity": "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/prop-types": {
"version": "15.8.1",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
@@ -6256,6 +6481,19 @@
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
"license": "MIT"
},
+ "node_modules/property-information": {
+ "version": "5.6.0",
+ "resolved": "https://registry.npmjs.org/property-information/-/property-information-5.6.0.tgz",
+ "integrity": "sha512-YUHSPk+A30YPv+0Qf8i9Mbfe/C0hdPXk1s1jPVToV8pk8BQtpw10ct89Eo7OWkutrwqvT0eicAxlOg3dOAu8JA==",
+ "license": "MIT",
+ "dependencies": {
+ "xtend": "^4.0.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
@@ -6968,6 +7206,32 @@
}
}
},
+ "node_modules/react-syntax-highlighter": {
+ "version": "15.6.1",
+ "resolved": "https://registry.npmjs.org/react-syntax-highlighter/-/react-syntax-highlighter-15.6.1.tgz",
+ "integrity": "sha512-OqJ2/vL7lEeV5zTJyG7kmARppUjiB9h9udl4qHQjjgEos66z00Ia0OckwYfRxCSFrW8RJIBnsBwQsHZbVPspqg==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.3.1",
+ "highlight.js": "^10.4.1",
+ "highlightjs-vue": "^1.0.0",
+ "lowlight": "^1.17.0",
+ "prismjs": "^1.27.0",
+ "refractor": "^3.6.0"
+ },
+ "peerDependencies": {
+ "react": ">= 0.14.0"
+ }
+ },
+ "node_modules/react-syntax-highlighter/node_modules/highlight.js": {
+ "version": "10.7.3",
+ "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz",
+ "integrity": "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": "*"
+ }
+ },
"node_modules/reflect.getprototypeof": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz",
@@ -6992,6 +7256,30 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/refractor": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/refractor/-/refractor-3.6.0.tgz",
+ "integrity": "sha512-MY9W41IOWxxk31o+YvFCNyNzdkc9M20NoZK5vq6jkv4I/uh2zkWcfudj0Q1fovjUQJrNewS9NMzeTtqPf+n5EA==",
+ "license": "MIT",
+ "dependencies": {
+ "hastscript": "^6.0.0",
+ "parse-entities": "^2.0.0",
+ "prismjs": "~1.27.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/refractor/node_modules/prismjs": {
+ "version": "1.27.0",
+ "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.27.0.tgz",
+ "integrity": "sha512-t13BGPUlFDR7wRB5kQDG4jjl7XeuH6jbJGt11JHPL96qwsEHNX2+68tFXqc1/k+/jALsbSWJKUOT/hcYAZ5LkA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/regenerator-runtime": {
"version": "0.14.1",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
@@ -7419,6 +7707,16 @@
"node": ">=0.10.0"
}
},
+ "node_modules/space-separated-tokens": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-1.1.5.tgz",
+ "integrity": "sha512-q/JSVd1Lptzhf5bkYm4ob4iWPjx0KiRe3sRFBNrVqbJkFaBm5vbbowy1mymoPNLRa52+oadOhJ+K49wsSeSjTA==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
"node_modules/spdx-exceptions": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz",
@@ -8261,6 +8559,15 @@
"node": ">=0.10.0"
}
},
+ "node_modules/xtend": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
+ "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.4"
+ }
+ },
"node_modules/yallist": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
diff --git a/components/log-viewer-webui/client/package.json b/components/log-viewer-webui/client/package.json
index 6397be684f..ea62e92066 100644
--- a/components/log-viewer-webui/client/package.json
+++ b/components/log-viewer-webui/client/package.json
@@ -20,14 +20,17 @@
"@sinclair/typebox": "^0.34.25",
"antd": "^5.24.5",
"axios": "^1.7.9",
+ "highlight.js": "^11.11.1",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-router": "^7.4.1",
+ "react-syntax-highlighter": "^15.6.1",
"zustand": "^5.0.3"
},
"devDependencies": {
"@types/react": "^19.0.10",
"@types/react-dom": "^19.0.4",
+ "@types/react-syntax-highlighter": "^15.5.13",
"@vitejs/plugin-react": "^4.3.4",
"eslint-config-yscope": "latest",
"typescript": "~5.6.2",
diff --git a/components/log-viewer-webui/client/src/pages/SearchPage/SearchControls/index.module.css b/components/log-viewer-webui/client/src/pages/SearchPage/SearchControls/index.module.css
index 3b53313c8f..8119ea54bc 100644
--- a/components/log-viewer-webui/client/src/pages/SearchPage/SearchControls/index.module.css
+++ b/components/log-viewer-webui/client/src/pages/SearchPage/SearchControls/index.module.css
@@ -1,6 +1,4 @@
.searchControlsContainer {
- margin-top: 20px;
- padding: 0 20px;
display: flex;
gap: 10px;
}
diff --git a/components/log-viewer-webui/client/src/pages/SearchPage/SearchResults/SearchResultsTable/Message/LogViewerLink.tsx b/components/log-viewer-webui/client/src/pages/SearchPage/SearchResults/SearchResultsTable/Message/LogViewerLink.tsx
new file mode 100644
index 0000000000..765bab8129
--- /dev/null
+++ b/components/log-viewer-webui/client/src/pages/SearchPage/SearchResults/SearchResultsTable/Message/LogViewerLink.tsx
@@ -0,0 +1,41 @@
+import {LinkOutlined} from "@ant-design/icons";
+import {
+ Tooltip,
+ Typography,
+} from "antd";
+
+import styles from "./index.module.css";
+
+
+const {Link} = Typography;
+
+// eslint-disable-next-line no-warning-comments
+// TODO: Fix link to connect to package log viewer when log viewer setup finished. Also pass
+// proper args to package log viewer.
+const LOG_VIEWER_URL = "https://y-scope.github.io/yscope-log-viewer/";
+
+interface LogViewerLinkProps {
+ filePath: string;
+}
+
+/**
+ * Render a link to the log viewer with open file.
+ *
+ * @param props
+ * @param props.filePath
+ * @return
+ */
+const LogViewerLink = ({filePath}: LogViewerLinkProps) => (
+
+
+
+ {filePath}
+
+
+);
+
+export default LogViewerLink;
diff --git a/components/log-viewer-webui/client/src/pages/SearchPage/SearchResults/SearchResultsTable/Message/index.module.css b/components/log-viewer-webui/client/src/pages/SearchPage/SearchResults/SearchResultsTable/Message/index.module.css
new file mode 100644
index 0000000000..e6140d720b
--- /dev/null
+++ b/components/log-viewer-webui/client/src/pages/SearchPage/SearchResults/SearchResultsTable/Message/index.module.css
@@ -0,0 +1,3 @@
+.linkIcon {
+ margin-right: 4px;
+}
diff --git a/components/log-viewer-webui/client/src/pages/SearchPage/SearchResults/SearchResultsTable/Message/index.tsx b/components/log-viewer-webui/client/src/pages/SearchPage/SearchResults/SearchResultsTable/Message/index.tsx
new file mode 100644
index 0000000000..f9456f4bab
--- /dev/null
+++ b/components/log-viewer-webui/client/src/pages/SearchPage/SearchResults/SearchResultsTable/Message/index.tsx
@@ -0,0 +1,44 @@
+import SyntaxHighlighter from "react-syntax-highlighter";
+
+import {Typography} from "antd";
+
+import LogViewerLink from "./LogViewerLink";
+import {highlighterCustomStyles} from "./utils";
+
+import "highlight.js/styles/intellij-light.css";
+
+
+const {Text} = Typography;
+
+interface MessageProps {
+ message: string;
+ filePath: string;
+}
+
+/**
+ * Renders a message with syntax highlighting and a file path link.
+ *
+ * @param props
+ * @param props.message
+ * @param props.filePath
+ * @return
+ */
+const Message = ({message, filePath}: MessageProps) => {
+ return (
+ <>
+ {/* Parent `Text` component allows syntax highlighter to inherit AntD fonts. */}
+
+
+ {message}
+
+
+
+ >
+ );
+};
+
+export default Message;
diff --git a/components/log-viewer-webui/client/src/pages/SearchPage/SearchResults/SearchResultsTable/Message/utils.ts b/components/log-viewer-webui/client/src/pages/SearchPage/SearchResults/SearchResultsTable/Message/utils.ts
new file mode 100644
index 0000000000..f04be5abfb
--- /dev/null
+++ b/components/log-viewer-webui/client/src/pages/SearchPage/SearchResults/SearchResultsTable/Message/utils.ts
@@ -0,0 +1,11 @@
+/**
+ * Style overrides for the syntax highlighter. "react-syntax-highlighter" lib will not accept
+ * styles directly from CSS modules.
+ */
+export const highlighterCustomStyles: React.CSSProperties = {
+ background: "none",
+ border: "none",
+ fontFamily: "inherit",
+ margin: "0",
+ padding: "0",
+};
diff --git a/components/log-viewer-webui/client/src/pages/SearchPage/SearchResults/SearchResultsTable/index.tsx b/components/log-viewer-webui/client/src/pages/SearchPage/SearchResults/SearchResultsTable/index.tsx
new file mode 100644
index 0000000000..77e6b441de
--- /dev/null
+++ b/components/log-viewer-webui/client/src/pages/SearchPage/SearchResults/SearchResultsTable/index.tsx
@@ -0,0 +1,54 @@
+import {Table} from "antd";
+
+import {
+ SearchResult,
+ searchResultsTableColumns,
+} from "./typings";
+
+
+// eslint-disable-next-line no-warning-comments
+// TODO: Replace with values from database once api implemented.
+const DUMMY_RESULTS: SearchResult[] = [
+ {
+ id: 1,
+ timestamp: "2023-01-01 12:00:00",
+ message: "INFO: User login successful for user 'john.doe'.",
+ filePath: "/var/logs/auth.log",
+ },
+ {
+ id: 2,
+ timestamp: "2023-01-01 12:01:00",
+ message: "ERROR: Failed to connect to database 'logs_db'.",
+ filePath: "/var/logs/db.log",
+ },
+ {
+ id: 3,
+ timestamp: "2023-01-01 12:02:00",
+ message: "WARN: Disk space running low on volume '/var/logs'.",
+ filePath: "/var/logs/system.log",
+ },
+ {
+ id: 4,
+ timestamp: "2023-01-01 12:03:00",
+ message: "DEBUG: Processing request ID 12345.",
+ filePath: "/var/logs/app.log",
+ },
+];
+
+/**
+ * Renders search results in a table.
+ *
+ * @return
+ */
+const SearchResultsTable = () => {
+ return (
+
+ columns={searchResultsTableColumns}
+ dataSource={DUMMY_RESULTS}
+ pagination={false}
+ rowKey={(record) => record.id.toString()}
+ virtual={true}/>
+ );
+};
+
+export default SearchResultsTable;
diff --git a/components/log-viewer-webui/client/src/pages/SearchPage/SearchResults/SearchResultsTable/typings.tsx b/components/log-viewer-webui/client/src/pages/SearchPage/SearchResults/SearchResultsTable/typings.tsx
new file mode 100644
index 0000000000..af10344d45
--- /dev/null
+++ b/components/log-viewer-webui/client/src/pages/SearchPage/SearchResults/SearchResultsTable/typings.tsx
@@ -0,0 +1,41 @@
+import {TableProps} from "antd";
+
+import Message from "./Message";
+
+
+/**
+ * Structure of search results data displayed in the table.
+ */
+interface SearchResult {
+ id: number;
+ timestamp: string;
+ message: string;
+ filePath: string;
+}
+
+/**
+ * Columns configuration for the seach results table.
+ */
+const searchResultsTableColumns: NonNullable["columns"]> = [
+ {
+ dataIndex: "timestamp",
+ key: "timestamp",
+ sorter: true,
+ title: "Timestamp",
+ width: 15,
+ },
+ {
+ dataIndex: "message",
+ key: "message",
+ render: (_, record) => (
+
+ ),
+ title: "Message",
+ width: 85,
+ },
+];
+
+export type {SearchResult};
+export {searchResultsTableColumns};
diff --git a/components/log-viewer-webui/client/src/pages/SearchPage/index.module.css b/components/log-viewer-webui/client/src/pages/SearchPage/index.module.css
new file mode 100644
index 0000000000..b62e697498
--- /dev/null
+++ b/components/log-viewer-webui/client/src/pages/SearchPage/index.module.css
@@ -0,0 +1,6 @@
+.searchPageContainer {
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+ padding: 24px 16px 16px;
+}
diff --git a/components/log-viewer-webui/client/src/pages/SearchPage/index.tsx b/components/log-viewer-webui/client/src/pages/SearchPage/index.tsx
index 429dcd3f98..d14124b4bf 100644
--- a/components/log-viewer-webui/client/src/pages/SearchPage/index.tsx
+++ b/components/log-viewer-webui/client/src/pages/SearchPage/index.tsx
@@ -1,4 +1,6 @@
+import styles from "./index.module.css";
import SearchControls from "./SearchControls";
+import SearchResultsTable from "./SearchResults/SearchResultsTable";
/**
@@ -8,7 +10,10 @@ import SearchControls from "./SearchControls";
*/
const SearchPage = () => {
return (
-
+
+
+
+
);
};