import { Button } from "@/components/ui/button"; import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"; import { LogLine, LogSeverity } from "@/types/log"; import copy from "copy-to-clipboard"; import { useCallback, useMemo, useRef, useState } from "react"; import { IoIosAlert } from "react-icons/io"; import { GoAlertFill } from "react-icons/go"; import { LuCopy } from "react-icons/lu"; import useSWR from "swr"; const logTypes = ["frigate", "go2rtc", "nginx"] as const; type LogType = (typeof logTypes)[number]; const datestamp = /\[[\d\s-:]*]/; const severity = /(DEBUG)|(INFO)|(WARNING)|(ERROR)/; const section = /[\w.]*/; function Logs() { const [logService, setLogService] = useState("frigate"); const { data: frigateLogs } = useSWR("logs/frigate", { refreshInterval: 1000, }); const { data: go2rtcLogs } = useSWR("logs/go2rtc", { refreshInterval: 1000 }); const { data: nginxLogs } = useSWR("logs/nginx", { refreshInterval: 1000 }); // convert to log data const logs = useMemo(() => { if (logService == "frigate") { return frigateLogs; } else if (logService == "go2rtc") { return go2rtcLogs; } else if (logService == "nginx") { return nginxLogs; } else { return "unknown logs"; } }, [logService, frigateLogs, go2rtcLogs, nginxLogs]); const logLines = useMemo(() => { if (logService == "frigate") { if (!frigateLogs) { return []; } return frigateLogs .split("\n") .map((line) => { const match = datestamp.exec(line); if (!match) { return null; } const sectionMatch = section.exec( line.substring(match.index + match[0].length).trim(), ); if (!sectionMatch) { return null; } return { dateStamp: match.toString().slice(1, -1), severity: severity .exec(line) ?.at(0) ?.toString() ?.toLowerCase() as LogSeverity, section: sectionMatch.toString(), content: line .substring(line.indexOf(":", match.index + match[0].length) + 2) .trim(), }; }) .filter((value) => value != null) as LogLine[]; } else if (logService == "go2rtc") { return []; } else if (logService == "nginx") { return []; } else { return []; } }, [logService, frigateLogs, go2rtcLogs, nginxLogs]); //console.log(`the logs are ${JSON.stringify(logLines)}`); const handleCopyLogs = useCallback(() => { copy(logs); }, [logs]); // scroll to bottom button const contentRef = useRef(null); const [endVisible, setEndVisible] = useState(true); const observer = useRef(null); const endLogRef = useCallback( (node: HTMLElement | null) => { if (observer.current) observer.current.disconnect(); try { observer.current = new IntersectionObserver((entries) => { setEndVisible(entries[0].isIntersecting); }); if (node) observer.current.observe(node); } catch (e) { // no op } }, [setEndVisible], ); return (
value ? setLogService(value) : null } // don't allow the severity to be unselected > {Object.values(logTypes).map((item) => (
{`${item} Logs`}
))}
{!endVisible && ( )}
Severity
Timestamp
Tag
Message
{logLines.map((log, idx) => ( ))}
); } type LogLineDataProps = { line: LogLine; offset: number; }; function LogLineData({ line, offset }: LogLineDataProps) { // long log message const contentRef = useRef(null); const [expanded, setExpanded] = useState(false); const contentOverflows = useMemo(() => { if (!contentRef.current) { return false; } return contentRef.current.scrollWidth > contentRef.current.clientWidth; // update on ref change // eslint-disable-next-line react-hooks/exhaustive-deps }, [contentRef.current]); // severity coloring const severityClassName = useMemo(() => { switch (line.severity) { case "info": return "text-secondary-foreground rounded-md"; case "warning": return "text-yellow-400 rounded-md"; case "error": return "text-danger rounded-md"; } }, [line]); return (
{line.severity == "error" ? ( ) : ( )} {line.severity}
{line.dateStamp}
{line.section}
{line.content}
{contentOverflows && ( )}
); } export default Logs;