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:
stacksmash76 2022-04-12 16:57:20 +02:00 committed by GitHub
parent 9e5b7b0aa5
commit 4b74a006c8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 161 additions and 90 deletions

View file

@ -1,65 +1,129 @@
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import { ExclamationIcon } from "@heroicons/react/solid";
import { APIClient } from "../api/APIClient"; 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 = { type LogEvent = {
time: string; time: string;
level: string; level: string;
message: 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 = () => { export const Logs = () => {
const messagesEndRef = useRef<HTMLDivElement>(null); const [settings, setSettings] = SettingsContext.use();
const [logs, setLogs] = useState<LogEvent[]>([]);
const scrollToBottom = () => { const messagesEndRef = useRef<HTMLDivElement>(null);
messagesEndRef.current?.scrollIntoView({ behavior: "auto" }) 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(() => { return () => es.close();
const es = APIClient.events.logs() }, [setLogs, settings]);
es.onmessage = (event) => { const onSetValue = (
const d = JSON.parse(event.data) as LogEvent; key: "scrollOnNewLog" | "indentLogLines" | "hideWrappedText",
setLogs(prevState => ([...prevState, d])); newValue: boolean
scrollToBottom(); ) => setSettings((prevState) => ({
} ...prevState,
return () => { [key]: newValue
es.close(); }));
}
}, [setLogs]);
return ( return (
<main> <main>
<header className="py-10"> <header className="py-10">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> <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> <h1 className="text-3xl font-bold text-black dark:text-white capitalize">Logs</h1>
<div className="flex mt-4 justify-center"> <div className="flex mt-4 justify-center">
<ExclamationIcon <ExclamationIcon
className="h-5 w-5 text-yellow-400" className="h-5 w-5 text-yellow-400"
aria-hidden="true" aria-hidden="true"
/> />
<p className="ml-2 text-sm text-gray-800 dark:text-gray-400">This only shows new logs, no history</p> <p className="ml-2 text-sm text-gray-800 dark:text-gray-400">This only shows new logs, no history</p>
</div> </div>
</div> </div>
</header> </header>
<div className="max-w-7xl mx-auto pb-12 px-2 sm:px-4 lg:px-8"> <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
<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"> className="bg-white dark:bg-gray-800 rounded-lg shadow-lg px-2 sm:px-4 pb-3 sm:pb-4"
{logs.map((a, idx) => ( >
<p key={idx}> <Checkbox
<span className="font-mono text-gray-500 dark:text-gray-600 mr-2">{a.time}</span> label="Scroll to bottom on new message"
{a.level === "TRACE" && <span className="font-mono font-semibold text-purple-300">{a.level}</span>} value={settings.scrollOnNewLog}
{a.level === "DEBUG" && <span className="font-mono font-semibold text-yellow-500">{a.level}</span>} setValue={(newValue) => onSetValue("scrollOnNewLog", newValue)}
{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>} <Checkbox
<span className="ml-2 text-black dark:text-gray-300">{a.message}</span> label="Indent log lines"
</p> description="Indent each log line according to their respective starting position"
))} value={settings.indentLogLines}
<div ref={messagesEndRef} /> setValue={(newValue) => onSetValue("indentLogLines", newValue)}
</div> />
</div> <Checkbox
</div> label="Hide wrapped text"
</main> 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>
)
} }

View file

@ -2,65 +2,72 @@ import { newRidgeState } from "react-ridge-state";
export const InitializeGlobalContext = () => { export const InitializeGlobalContext = () => {
const auth_ctx = localStorage.getItem("auth"); const auth_ctx = localStorage.getItem("auth");
if (auth_ctx) if (auth_ctx)
AuthContext.set(JSON.parse(auth_ctx)); AuthContext.set(JSON.parse(auth_ctx));
const settings_ctx = localStorage.getItem("settings"); const settings_ctx = localStorage.getItem("settings");
if (settings_ctx) { if (settings_ctx) {
SettingsContext.set(JSON.parse(settings_ctx)); SettingsContext.set(JSON.parse(settings_ctx));
} else { } else {
// Only check for light theme, otherwise dark theme is the default // Only check for light theme, otherwise dark theme is the default
SettingsContext.set((state) => ({ SettingsContext.set((state) => ({
...state, ...state,
darkTheme: !( darkTheme: !(
window.matchMedia !== undefined && window.matchMedia !== undefined &&
window.matchMedia("(prefers-color-scheme: light)").matches window.matchMedia("(prefers-color-scheme: light)").matches
) )
})); }));
} }
} }
interface AuthInfo { interface AuthInfo {
username: string; username: string;
isLoggedIn: boolean; isLoggedIn: boolean;
} }
export const AuthContext = newRidgeState<AuthInfo>( export const AuthContext = newRidgeState<AuthInfo>(
{ {
username: "", username: "",
isLoggedIn: false isLoggedIn: false
}, },
{ {
onSet: (new_state) => { onSet: (new_state) => {
try { try {
localStorage.setItem("auth", JSON.stringify(new_state)); localStorage.setItem("auth", JSON.stringify(new_state));
} catch (e) { } catch (e) {
console.log("An error occurred while trying to modify the local auth context state."); console.log("An error occurred while trying to modify the local auth context state.");
console.log("Error:", e); console.log("Error:", e);
} }
}
} }
}
); );
interface SettingsType { interface SettingsType {
debug: boolean; debug: boolean;
darkTheme: boolean; darkTheme: boolean;
scrollOnNewLog: boolean;
indentLogLines: boolean;
hideWrappedText: boolean;
} }
export const SettingsContext = newRidgeState<SettingsType>( export const SettingsContext = newRidgeState<SettingsType>(
{ {
debug: false, debug: false,
darkTheme: true darkTheme: true,
scrollOnNewLog: false,
indentLogLines: false,
hideWrappedText: false
}, },
{ {
onSet: (new_state) => { onSet: (new_state) => {
try { try {
if (new_state.darkTheme) { if (new_state.darkTheme) {
document.documentElement.classList.add("dark"); document.documentElement.classList.add("dark");
} else { } else {
document.documentElement.classList.remove("dark"); document.documentElement.classList.remove("dark");
} }
localStorage.setItem("settings", JSON.stringify(new_state)); localStorage.setItem("settings", JSON.stringify(new_state));
} catch (e) { } catch (e) {
console.log("An error occurred while trying to modify the local settings context state."); console.log("An error occurred while trying to modify the local settings context state.");