mirror of
https://github.com/idanoo/autobrr
synced 2025-07-23 08:49:13 +00:00
feat(web): added ability to customize logs view (#236)
* enhancement(frontend/logs): added ability to indent messages, hide wrapped text and ability to turn off "scroll to bottom page on new line". addresses #232 * fix: improved "hide wrapped text" feature
This commit is contained in:
parent
9e5b7b0aa5
commit
4b74a006c8
2 changed files with 161 additions and 90 deletions
|
@ -1,65 +1,129 @@
|
|||
import { useEffect, useRef, useState } from "react";
|
||||
import { ExclamationIcon } from "@heroicons/react/solid";
|
||||
|
||||
import { APIClient } from "../api/APIClient";
|
||||
import {ExclamationIcon} from "@heroicons/react/solid";
|
||||
import { Checkbox } from "../components/Checkbox";
|
||||
import { classNames } from "../utils";
|
||||
import { SettingsContext } from "../utils/Context";
|
||||
|
||||
type LogEvent = {
|
||||
time: string;
|
||||
level: string;
|
||||
message: string;
|
||||
time: string;
|
||||
level: string;
|
||||
message: string;
|
||||
};
|
||||
|
||||
type LogLevel = "TRACE" | "DEBUG" | "INFO" | "ERROR";
|
||||
|
||||
const LogColors: Record<LogLevel, string> = {
|
||||
"TRACE": "text-purple-300",
|
||||
"DEBUG": "text-yellow-500",
|
||||
"INFO": "text-green-500",
|
||||
"ERROR": "text-red-500",
|
||||
};
|
||||
|
||||
export const Logs = () => {
|
||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||
const [logs, setLogs] = useState<LogEvent[]>([]);
|
||||
const [settings, setSettings] = SettingsContext.use();
|
||||
|
||||
const scrollToBottom = () => {
|
||||
messagesEndRef.current?.scrollIntoView({ behavior: "auto" })
|
||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||
const [logs, setLogs] = useState<LogEvent[]>([]);
|
||||
|
||||
const scrollToBottom = () => {
|
||||
messagesEndRef.current?.scrollIntoView({ behavior: "auto" });
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const es = APIClient.events.logs();
|
||||
|
||||
es.onmessage = (event) => {
|
||||
const newData = JSON.parse(event.data) as LogEvent;
|
||||
setLogs((prevState) => [...prevState, newData]);
|
||||
|
||||
if (settings.scrollOnNewLog)
|
||||
scrollToBottom();
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const es = APIClient.events.logs()
|
||||
return () => es.close();
|
||||
}, [setLogs, settings]);
|
||||
|
||||
es.onmessage = (event) => {
|
||||
const d = JSON.parse(event.data) as LogEvent;
|
||||
setLogs(prevState => ([...prevState, d]));
|
||||
scrollToBottom();
|
||||
}
|
||||
return () => {
|
||||
es.close();
|
||||
}
|
||||
}, [setLogs]);
|
||||
const onSetValue = (
|
||||
key: "scrollOnNewLog" | "indentLogLines" | "hideWrappedText",
|
||||
newValue: boolean
|
||||
) => setSettings((prevState) => ({
|
||||
...prevState,
|
||||
[key]: newValue
|
||||
}));
|
||||
|
||||
return (
|
||||
<main>
|
||||
<header className="py-10">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<h1 className="text-3xl font-bold text-black dark:text-white capitalize">Logs</h1>
|
||||
<div className="flex mt-4 justify-center">
|
||||
<ExclamationIcon
|
||||
className="h-5 w-5 text-yellow-400"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<p className="ml-2 text-sm text-gray-800 dark:text-gray-400">This only shows new logs, no history</p>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<div className="max-w-7xl mx-auto pb-12 px-2 sm:px-4 lg:px-8">
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-lg px-2 sm:px-4 py-3 sm:py-4">
|
||||
<div className="overflow-y-auto p-2 rounded-lg min-h-[32rem] lg:min-h-[48rem] min-w-full bg-gray-100 dark:bg-gray-900">
|
||||
{logs.map((a, idx) => (
|
||||
<p key={idx}>
|
||||
<span className="font-mono text-gray-500 dark:text-gray-600 mr-2">{a.time}</span>
|
||||
{a.level === "TRACE" && <span className="font-mono font-semibold text-purple-300">{a.level}</span>}
|
||||
{a.level === "DEBUG" && <span className="font-mono font-semibold text-yellow-500">{a.level}</span>}
|
||||
{a.level === "INFO" && <span className="font-mono font-semibold text-green-500">{a.level} </span>}
|
||||
{a.level === "ERROR" && <span className="font-mono font-semibold text-red-500">{a.level}</span>}
|
||||
<span className="ml-2 text-black dark:text-gray-300">{a.message}</span>
|
||||
</p>
|
||||
))}
|
||||
<div ref={messagesEndRef} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
)
|
||||
return (
|
||||
<main>
|
||||
<header className="py-10">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<h1 className="text-3xl font-bold text-black dark:text-white capitalize">Logs</h1>
|
||||
<div className="flex mt-4 justify-center">
|
||||
<ExclamationIcon
|
||||
className="h-5 w-5 text-yellow-400"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<p className="ml-2 text-sm text-gray-800 dark:text-gray-400">This only shows new logs, no history</p>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<div className="max-w-7xl mx-auto pb-12 px-2 sm:px-4 lg:px-8">
|
||||
<div
|
||||
className="bg-white dark:bg-gray-800 rounded-lg shadow-lg px-2 sm:px-4 pb-3 sm:pb-4"
|
||||
>
|
||||
<Checkbox
|
||||
label="Scroll to bottom on new message"
|
||||
value={settings.scrollOnNewLog}
|
||||
setValue={(newValue) => onSetValue("scrollOnNewLog", newValue)}
|
||||
/>
|
||||
<Checkbox
|
||||
label="Indent log lines"
|
||||
description="Indent each log line according to their respective starting position"
|
||||
value={settings.indentLogLines}
|
||||
setValue={(newValue) => onSetValue("indentLogLines", newValue)}
|
||||
/>
|
||||
<Checkbox
|
||||
label="Hide wrapped text"
|
||||
description="Hides text that is meant to be wrapped"
|
||||
value={settings.hideWrappedText}
|
||||
setValue={(newValue) => onSetValue("hideWrappedText", newValue)}
|
||||
/>
|
||||
<div
|
||||
className="overflow-y-auto p-2 rounded-lg min-h-[32rem] lg:min-h-[48rem] min-w-full bg-gray-100 dark:bg-gray-900"
|
||||
>
|
||||
{logs.map((a, idx) => (
|
||||
<div
|
||||
key={idx}
|
||||
className={classNames(
|
||||
settings.indentLogLines ? "grid justify-start grid-flow-col" : "",
|
||||
settings.hideWrappedText ? "truncate hover:text-ellipsis hover:whitespace-normal" : "",
|
||||
)}
|
||||
>
|
||||
<span
|
||||
className="font-mono text-gray-500 dark:text-gray-600 mr-2 h-full"
|
||||
>
|
||||
{a.time}
|
||||
</span>
|
||||
{a.level in LogColors ? (
|
||||
<span
|
||||
className={classNames(
|
||||
LogColors[a.level as LogLevel],
|
||||
"font-mono font-semibold h-full"
|
||||
)}
|
||||
>
|
||||
{a.level}
|
||||
{' '}
|
||||
</span>
|
||||
) : null}
|
||||
<span className="ml-2 text-black dark:text-gray-300">
|
||||
{a.message}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
<div ref={messagesEndRef} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -2,65 +2,72 @@ import { newRidgeState } from "react-ridge-state";
|
|||
|
||||
|
||||
export const InitializeGlobalContext = () => {
|
||||
const auth_ctx = localStorage.getItem("auth");
|
||||
if (auth_ctx)
|
||||
AuthContext.set(JSON.parse(auth_ctx));
|
||||
const auth_ctx = localStorage.getItem("auth");
|
||||
if (auth_ctx)
|
||||
AuthContext.set(JSON.parse(auth_ctx));
|
||||
|
||||
const settings_ctx = localStorage.getItem("settings");
|
||||
if (settings_ctx) {
|
||||
SettingsContext.set(JSON.parse(settings_ctx));
|
||||
} else {
|
||||
// Only check for light theme, otherwise dark theme is the default
|
||||
SettingsContext.set((state) => ({
|
||||
...state,
|
||||
darkTheme: !(
|
||||
window.matchMedia !== undefined &&
|
||||
window.matchMedia("(prefers-color-scheme: light)").matches
|
||||
)
|
||||
}));
|
||||
}
|
||||
const settings_ctx = localStorage.getItem("settings");
|
||||
if (settings_ctx) {
|
||||
SettingsContext.set(JSON.parse(settings_ctx));
|
||||
} else {
|
||||
// Only check for light theme, otherwise dark theme is the default
|
||||
SettingsContext.set((state) => ({
|
||||
...state,
|
||||
darkTheme: !(
|
||||
window.matchMedia !== undefined &&
|
||||
window.matchMedia("(prefers-color-scheme: light)").matches
|
||||
)
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
interface AuthInfo {
|
||||
username: string;
|
||||
isLoggedIn: boolean;
|
||||
username: string;
|
||||
isLoggedIn: boolean;
|
||||
}
|
||||
|
||||
export const AuthContext = newRidgeState<AuthInfo>(
|
||||
{
|
||||
username: "",
|
||||
isLoggedIn: false
|
||||
},
|
||||
{
|
||||
onSet: (new_state) => {
|
||||
try {
|
||||
localStorage.setItem("auth", JSON.stringify(new_state));
|
||||
} catch (e) {
|
||||
console.log("An error occurred while trying to modify the local auth context state.");
|
||||
console.log("Error:", e);
|
||||
}
|
||||
}
|
||||
{
|
||||
username: "",
|
||||
isLoggedIn: false
|
||||
},
|
||||
{
|
||||
onSet: (new_state) => {
|
||||
try {
|
||||
localStorage.setItem("auth", JSON.stringify(new_state));
|
||||
} catch (e) {
|
||||
console.log("An error occurred while trying to modify the local auth context state.");
|
||||
console.log("Error:", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
interface SettingsType {
|
||||
debug: boolean;
|
||||
darkTheme: boolean;
|
||||
debug: boolean;
|
||||
darkTheme: boolean;
|
||||
scrollOnNewLog: boolean;
|
||||
indentLogLines: boolean;
|
||||
hideWrappedText: boolean;
|
||||
}
|
||||
|
||||
export const SettingsContext = newRidgeState<SettingsType>(
|
||||
{
|
||||
debug: false,
|
||||
darkTheme: true
|
||||
debug: false,
|
||||
darkTheme: true,
|
||||
scrollOnNewLog: false,
|
||||
indentLogLines: false,
|
||||
hideWrappedText: false
|
||||
},
|
||||
{
|
||||
onSet: (new_state) => {
|
||||
try {
|
||||
if (new_state.darkTheme) {
|
||||
document.documentElement.classList.add("dark");
|
||||
document.documentElement.classList.add("dark");
|
||||
} else {
|
||||
document.documentElement.classList.remove("dark");
|
||||
document.documentElement.classList.remove("dark");
|
||||
}
|
||||
|
||||
localStorage.setItem("settings", JSON.stringify(new_state));
|
||||
} catch (e) {
|
||||
console.log("An error occurred while trying to modify the local settings context state.");
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue