From 8347d6ded119019b7e3510ae47bc2bb61ecdcb74 Mon Sep 17 00:00:00 2001 From: soup Date: Mon, 8 May 2023 22:56:11 +0200 Subject: [PATCH] feat(logs): improve log search with regex (#920) * improve log search with regex * show empty log if regex invalid * show red icon if regex is invalid --- web/src/screens/Logs.tsx | 54 ++++++++++++++++++++++++---------------- 1 file changed, 33 insertions(+), 21 deletions(-) diff --git a/web/src/screens/Logs.tsx b/web/src/screens/Logs.tsx index 00cd676..3f23706 100644 --- a/web/src/screens/Logs.tsx +++ b/web/src/screens/Logs.tsx @@ -23,6 +23,7 @@ import { baseUrl } from "@utils"; import { RingResizeSpinner } from "@components/Icons"; import { toast } from "react-hot-toast"; import Toast from "@components/notifications/Toast"; +import { ExclamationCircleIcon } from "@heroicons/react/24/solid"; type LogEvent = { @@ -45,10 +46,12 @@ export const Logs = () => { const [settings] = SettingsContext.use(); const messagesEndRef = useRef(null); - + const [logs, setLogs] = useState([]); const [searchFilter, setSearchFilter] = useState(""); + const [regexPattern, setRegexPattern] = useState(null); const [filteredLogs, setFilteredLogs] = useState([]); + const [isInvalidRegex, setIsInvalidRegex] = useState(false); const scrollToBottom = () => { messagesEndRef.current?.scrollIntoView({ behavior: "smooth", block: "end", inline: "end" }); @@ -71,16 +74,21 @@ export const Logs = () => { useEffect(() => { if (!searchFilter.length) { setFilteredLogs(logs); + setIsInvalidRegex(false); return; } - - const newLogs: LogEvent[] = []; - logs.forEach((log) => { - if (log.message.indexOf(searchFilter) !== -1) - newLogs.push(log); - }); - setFilteredLogs(newLogs); + try { + const pattern = new RegExp(searchFilter, "i"); + setRegexPattern(pattern); + const newLogs = logs.filter(log => pattern.test(log.message)); + setFilteredLogs(newLogs); + setIsInvalidRegex(false); + } catch (error) { + // Handle regex errors by showing nothing when the regex pattern is invalid + setFilteredLogs([]); + setIsInvalidRegex(true); + } }, [logs, searchFilter]); return ( @@ -104,17 +112,21 @@ export const Logs = () => { setSearchFilter(event.target.value.toLowerCase().trim())} - id="filter" - type="text" - autoComplete="off" + onChange={(event) => { + const inputValue = event.target.value.toLowerCase().trim(); + setSearchFilter(inputValue); + }} className={classNames( "focus:ring-blue-500 dark:focus:ring-blue-500 focus:border-blue-500 dark:focus:border-blue-500 border-gray-300 dark:border-gray-700", "block w-full dark:bg-gray-900 shadow-sm dark:text-gray-100 sm:text-sm rounded-md" )} - placeholder="Enter a string to filter logs by..." + placeholder="Enter a regex pattern to filter logs by..." /> - + {isInvalidRegex && ( +
+ +
+ )} @@ -178,7 +190,7 @@ export const LogFiles = () => {

Log files

- Download old log files. + Download old log files.

@@ -187,13 +199,13 @@ export const LogFiles = () => {
  1. - Name + Name
    Last modified
    - Size + Size
  2. @@ -219,12 +231,12 @@ const LogFilesItem = ({ file }: LogFilesItemProps) => { const handleDownload = async () => { setIsDownloading(true); - + // Add a custom toast before the download starts const toastId = toast.custom((t) => ( )); - + const response = await fetch(`${baseUrl()}api/logs/files/${file.filename}`); const blob = await response.blob(); const url = URL.createObjectURL(blob); @@ -233,10 +245,10 @@ const LogFilesItem = ({ file }: LogFilesItemProps) => { link.download = file.filename; link.click(); URL.revokeObjectURL(url); - + // Dismiss the custom toast after the download is complete toast.dismiss(toastId); - + setIsDownloading(false); };