Add logging with filters
This commit is contained in:
committed by
Gerla, J. (Justin)
parent
b7eb0cb5ec
commit
231d7a5ba1
129
src/components/Logging/Logging.tsx
Normal file
129
src/components/Logging/Logging.tsx
Normal file
@@ -0,0 +1,129 @@
|
||||
import {useEffect, useRef, useState} from "react";
|
||||
import {create} from "zustand";
|
||||
|
||||
import formatDuration from "../../utils/formatDuration.ts";
|
||||
import {type LogFilterPredicate, type LogRecord, useLogs} from "./useLogs.ts";
|
||||
import Filters from "./Filters.tsx";
|
||||
import {type Cell, useCell} from "../../utils/cellStore.ts";
|
||||
|
||||
import styles from "./Logging.module.css";
|
||||
|
||||
type LoggingSettings = {
|
||||
showRelativeTime: boolean;
|
||||
setShowRelativeTime: (showRelativeTime: boolean) => void;
|
||||
scrollToBottom: boolean;
|
||||
setScrollToBottom: (scrollToBottom: boolean) => void;
|
||||
};
|
||||
|
||||
const useLoggingSettings = create<LoggingSettings>((set) => ({
|
||||
showRelativeTime: false,
|
||||
setShowRelativeTime: (showRelativeTime: boolean) => set({ showRelativeTime }),
|
||||
scrollToBottom: true,
|
||||
setScrollToBottom: (scrollToBottom: boolean) => set({ scrollToBottom }),
|
||||
}));
|
||||
|
||||
function LogMessage({
|
||||
recordCell,
|
||||
onUpdate,
|
||||
}: {
|
||||
recordCell: Cell<LogRecord>,
|
||||
onUpdate?: () => void,
|
||||
}) {
|
||||
const { showRelativeTime, setShowRelativeTime } = useLoggingSettings();
|
||||
const record = useCell(recordCell);
|
||||
|
||||
/**
|
||||
* Normalizes the log level number to a multiple of 10, for which there are CSS styles.
|
||||
*/
|
||||
const normalizedLevelNo = (() => {
|
||||
// By default, the highest level is 50 (CRITICAL). Custom levels can be higher, but we don't have more critical color.
|
||||
if (record.levelno >= 50) return 50;
|
||||
|
||||
return Math.round(record.levelno / 10) * 10;
|
||||
})();
|
||||
|
||||
const normalizedName = record.name.split(".").pop() || record.name;
|
||||
|
||||
useEffect(() => {
|
||||
if (onUpdate) onUpdate();
|
||||
}, [record, onUpdate]);
|
||||
|
||||
return <div className={`${styles.logContainer} round-md border-lg flex-row gap-md`}>
|
||||
<div className={`${styles[`accented${normalizedLevelNo}`]} flex-col padding-sm justify-between`}>
|
||||
<span className={"mono bold"}>{record.levelname}</span>
|
||||
<span className={"mono clickable font-small"}
|
||||
onClick={() => setShowRelativeTime(!showRelativeTime)}
|
||||
>{showRelativeTime
|
||||
? formatDuration(record.relativeCreated)
|
||||
: new Date(record.created * 1000).toLocaleTimeString()
|
||||
}</span>
|
||||
</div>
|
||||
<div className={"flex-col flex-1 padding-sm"}>
|
||||
<span className={"mono"}>{normalizedName}</span>
|
||||
<span>{record.message}</span>
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
function LogMessages({ recordCells }: { recordCells: Cell<LogRecord>[] }) {
|
||||
const scrollableRef = useRef<HTMLDivElement>(null);
|
||||
const lastElementRef = useRef<HTMLLIElement>(null)
|
||||
const { scrollToBottom, setScrollToBottom } = useLoggingSettings();
|
||||
|
||||
useEffect(() => {
|
||||
if (!scrollableRef.current) return;
|
||||
const currentScrollableRef = scrollableRef.current;
|
||||
|
||||
const handleScroll = () => setScrollToBottom(false);
|
||||
|
||||
currentScrollableRef.addEventListener("wheel", handleScroll);
|
||||
currentScrollableRef.addEventListener("touchmove", handleScroll);
|
||||
|
||||
return () => {
|
||||
currentScrollableRef.removeEventListener("wheel", handleScroll);
|
||||
currentScrollableRef.removeEventListener("touchmove", handleScroll);
|
||||
}
|
||||
}, [scrollableRef, setScrollToBottom]);
|
||||
|
||||
function scrollLastElementIntoView(force = false) {
|
||||
if ((!scrollToBottom && !force) || !lastElementRef.current) return;
|
||||
lastElementRef.current.scrollIntoView({ behavior: "smooth" });
|
||||
}
|
||||
|
||||
return <div ref={scrollableRef} className={"min-height-0 scroll-y padding-b-md"}>
|
||||
<ol className={`${styles.noNumbers} margin-0 flex-col gap-md`}>
|
||||
{recordCells.map((recordCell, i) => (
|
||||
<li key={`${i}_${recordCell.get().firstRelativeCreated}`}>
|
||||
<LogMessage recordCell={recordCell} onUpdate={scrollLastElementIntoView} />
|
||||
</li>
|
||||
))}
|
||||
<li ref={lastElementRef}></li>
|
||||
</ol>
|
||||
{!scrollToBottom && <button
|
||||
className={styles.floatingButton}
|
||||
onClick={() => {
|
||||
setScrollToBottom(true);
|
||||
scrollLastElementIntoView(true);
|
||||
}}
|
||||
>
|
||||
Scroll to bottom
|
||||
</button>}
|
||||
</div>;
|
||||
}
|
||||
|
||||
export default function Logging() {
|
||||
const [filterPredicates, setFilterPredicates] = useState(new Map<string, LogFilterPredicate>());
|
||||
const { filteredLogs, distinctNames } = useLogs(filterPredicates)
|
||||
|
||||
return <div className={`flex-col gap-lg min-height-0 ${styles.loggingContainer}`}>
|
||||
<div className={"flex-row gap-lg justify-between align-center"}>
|
||||
<h2 className={"margin-0"}>Logs</h2>
|
||||
<Filters
|
||||
filterPredicates={filterPredicates}
|
||||
setFilterPredicates={setFilterPredicates}
|
||||
agentNames={distinctNames}
|
||||
/>
|
||||
</div>
|
||||
<LogMessages recordCells={filteredLogs} />
|
||||
</div>;
|
||||
}
|
||||
Reference in New Issue
Block a user