diff --git a/web/package.json b/web/package.json index 8dee884..540c7be 100644 --- a/web/package.json +++ b/web/package.json @@ -11,6 +11,7 @@ "formik": "^2.2.9", "react": "^17.0.2", "react-cookie": "^4.1.1", + "react-debounce-input": "^3.2.5", "react-dom": "^17.0.2", "react-error-boundary": "^3.1.4", "react-hot-toast": "^2.1.1", diff --git a/web/src/domain/react-table-config.d.ts b/web/src/domain/react-table-config.d.ts index 033971a..6d37589 100644 --- a/web/src/domain/react-table-config.d.ts +++ b/web/src/domain/react-table-config.d.ts @@ -64,7 +64,7 @@ declare module "react-table" { // note that having Record here allows you to add anything to the options, this matches the spirit of the // underlying js library, but might be cleaner if it's replaced by a more specific type that matches your // feature set, this is a safe default. - Record {} + Record {} export interface Hooks = Record> extends UseExpandedHooks, diff --git a/web/src/screens/Logs.tsx b/web/src/screens/Logs.tsx index 8a2a7aa..4e945f0 100644 --- a/web/src/screens/Logs.tsx +++ b/web/src/screens/Logs.tsx @@ -1,5 +1,7 @@ import { useEffect, useRef, useState } from "react"; import { ExclamationIcon } from "@heroicons/react/solid"; +import format from "date-fns/format"; +import { DebounceInput } from "react-debounce-input"; import { APIClient } from "../api/APIClient"; import { Checkbox } from "../components/Checkbox"; @@ -23,9 +25,12 @@ const LogColors: Record = { export const Logs = () => { const [settings, setSettings] = SettingsContext.use(); - + const messagesEndRef = useRef(null); + const [logs, setLogs] = useState([]); + const [searchFilter, setSearchFilter] = useState(""); + const [filteredLogs, setFilteredLogs] = useState([]); const scrollToBottom = () => { messagesEndRef.current?.scrollIntoView({ behavior: "auto" }); @@ -45,6 +50,21 @@ export const Logs = () => { return () => es.close(); }, [setLogs, settings]); + useEffect(() => { + if (!searchFilter.length) { + setFilteredLogs(logs); + return; + } + + const newLogs: LogEvent[] = []; + logs.forEach((log) => { + if (log.message.indexOf(searchFilter) !== -1) + newLogs.push(log); + }); + + setFilteredLogs(newLogs); + }, [logs, searchFilter]); + const onSetValue = ( key: "scrollOnNewLog" | "indentLogLines" | "hideWrappedText", newValue: boolean @@ -58,7 +78,7 @@ export const Logs = () => {

Logs

-
+
+ setSearchFilter(event.target.value.toLowerCase().trim())} + id="filter" + type="text" + autoComplete="off" + className={classNames( + "focus:ring-indigo-500 dark:focus:ring-blue-500 focus:border-indigo-500 dark:focus:border-blue-500 border-gray-300 dark:border-gray-700", + "block w-full dark:bg-gray-800 shadow-sm dark:text-gray-100 sm:text-sm rounded-md" + )} + placeholder="Enter a string to filter logs by..." + /> +
+ {filteredLogs.map((entry, idx) => ( +
+ + {format(new Date(entry.time), "HH:mm:ss.SSS")} + + {entry.level in LogColors ? ( + + {entry.level} + {" "} + + ) : null} + + {entry.message} + +
+ ))} +
+
{ value={settings.hideWrappedText} setValue={(newValue) => onSetValue("hideWrappedText", newValue)} /> -
- {logs.map((a, idx) => ( -
- - {a.time} - - {a.level in LogColors ? ( - - {a.level} - {" "} - - ) : null} - - {a.message} - -
- ))} -
-