/* * Copyright (c) 2021 - 2023, Ludvig Lundgren and the autobrr contributors. * SPDX-License-Identifier: GPL-2.0-or-later */ import { Fragment, useEffect, useRef, useState } from "react"; import format from "date-fns/format"; import { DebounceInput } from "react-debounce-input"; import { Cog6ToothIcon, DocumentArrowDownIcon } from "@heroicons/react/24/outline"; import { useQuery } from "@tanstack/react-query"; import { Menu, Transition } from "@headlessui/react"; import { APIClient } from "@api/APIClient"; import { Checkbox } from "@components/Checkbox"; import { classNames, simplifyDate } from "@utils"; import { SettingsContext } from "@utils/Context"; import { EmptySimple } from "@components/emptystates"; 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 = { time: string; level: string; message: string; }; type LogLevel = "TRC" | "DBG" | "INF" | "ERR" | "WRN" | "FTL" | "PNC"; const LogColors: Record = { "TRC": "text-purple-300", "DBG": "text-yellow-500", "INF": "text-green-500", "ERR": "text-red-500", "WRN": "text-yellow-500", "FTL": "text-red-500", "PNC": "text-red-600", }; 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); useEffect(() => { const scrollToBottom = () => { if (messagesEndRef.current) { messagesEndRef.current.scrollTop = messagesEndRef.current.scrollHeight; } }; if (settings.scrollOnNewLog) scrollToBottom(); }, [filteredLogs]); // Add a useEffect to clear logs div when settings.scrollOnNewLog changes to prevent duplicate entries. useEffect(() => { setLogs([]); }, [settings.scrollOnNewLog]); useEffect(() => { const es = APIClient.events.logs(); es.onmessage = (event) => { const newData = JSON.parse(event.data) as LogEvent; setLogs((prevState) => [...prevState, newData]); }; return () => es.close(); }, [setLogs, settings]); useEffect(() => { if (!searchFilter.length) { setFilteredLogs(logs); setIsInvalidRegex(false); return; } 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 (

Logs

{ 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 regex pattern to filter logs by..." /> {isInvalidRegex && (
)}
{filteredLogs.map((entry, idx) => (
{format(new Date(entry.time), "HH:mm:ss")} {entry.level in LogColors ? ( {entry.level} ) : null} {entry.message}
))}
); }; export const LogFiles = () => { const { data } = useQuery({ queryKey: ["log-files"], queryFn: () => APIClient.logs.files(), retry: false, refetchOnWindowFocus: false, onError: err => console.log(err) }); return (

Log files

Download old log files.

{data && data.files.length > 0 ? (
  1. Name
    Last modified
    Size
  2. {data && data.files.map((f, idx) => )}
) : ( )}
); }; interface LogFilesItemProps { file: LogFile; } const LogFilesItem = ({ file }: LogFilesItemProps) => { const [isDownloading, setIsDownloading] = useState(false); 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); const link = document.createElement("a"); link.href = url; link.download = file.filename; link.click(); URL.revokeObjectURL(url); // Dismiss the custom toast after the download is complete toast.dismiss(toastId); setIsDownloading(false); }; return (
  • {file.filename}
    {simplifyDate(file.updated_at)}
    {file.size}
  • ); }; // interface LogsDropdownProps {} const LogsDropdown = () => { const [settings, setSettings] = SettingsContext.use(); const onSetValue = ( key: "scrollOnNewLog" | "indentLogLines" | "hideWrappedText", newValue: boolean ) => setSettings((prevState) => ({ ...prevState, [key]: newValue })); return (
    {() => ( onSetValue("scrollOnNewLog", newValue)} /> )} {() => ( onSetValue("indentLogLines", newValue)} /> )} {() => ( onSetValue("hideWrappedText", newValue)} /> )}
    ); };