// This program has been developed by students from the bachelor Computer Science at Utrecht // University within the Software Project course. // © Copyright Utrecht University (Department of Information and Computing Sciences) import {type ComponentType, useEffect, useRef, useState} from "react"; 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"; import { EXPERIMENT_FILTER_KEY, EXPERIMENT_LOGGER_NAME, type LoggingSettings, type MessageComponentProps } from "./Definitions.ts"; import {create} from "zustand"; /** * Local Zustand store for logging UI preferences. */ const useLoggingSettings = create((set) => ({ showRelativeTime: false, setShowRelativeTime: (showRelativeTime: boolean) => set({ showRelativeTime }), })); /** * Renders a single log message entry with colored level indicators and timestamp formatting. * * This component automatically re-renders when the underlying log record (`recordCell`) * changes. It also triggers the `onUpdate` callback whenever the record updates (e.g., for auto-scrolling). * * @param recordCell - A reactive `Cell` containing a single `LogRecord`. * @param onUpdate - Optional callback triggered when the log entry updates. * @returns A JSX element displaying a formatted log message. */ function LogMessage({ recordCell, onUpdate }: MessageComponentProps) { const { showRelativeTime, setShowRelativeTime } = useLoggingSettings(); const record = useCell(recordCell); /** * Normalizes the log level number to a multiple of 10, * for which there are CSS styles. (e.g., INFO = 20, ERROR = 40). */ 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; })(); /** Simplifies the logger name by showing only the last path segment. */ const normalizedName = record.name.split(".").pop() || record.name; // Notify the parent component (e.g., for scroll updates) when this record changes. useEffect(() => { if (onUpdate) onUpdate(); }, [record, onUpdate]); return
{record.levelname} setShowRelativeTime(!showRelativeTime)}>{ showRelativeTime ? formatDuration(record.relativeCreated) : new Date(record.created * 1000).toLocaleTimeString() }
{normalizedName} {record.message}
; } /** * Displays a scrollable list of log messages. * * Handles: * - Auto-scrolling when new messages arrive. * - Allowing users to scroll manually and disable auto-scroll. * - A floating "Scroll to bottom" button when not at the bottom. * * @param recordCells - Array of reactive log records to display. * @param MessageComponent - A component to use to render each log message entry. * @returns A scrollable log list component. */ export function LogMessages({ recordCells, MessageComponent, }: { recordCells: Cell[], MessageComponent: ComponentType, }) { const scrollableRef = useRef(null); const [scrollToBottom, setScrollToBottom] = useState(true); // Disable auto-scroll if the user manually scrolls. 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]); /** * Scrolls the log messages to the bottom, making the latest messages visible. * * @param force - If true, forces scrolling even if `scrollToBottom` is false. */ function showBottom(force = false) { if ((!scrollToBottom && !force) || !scrollableRef.current) return; scrollableRef.current.scrollTo({top: scrollableRef.current.scrollHeight, left: 0, behavior: "smooth"}); } return
    {recordCells.map((recordCell, i) => (
  1. ))}
{!scrollToBottom && }
; } /** * Top-level logging panel component. * * Combines: * - The `Filters` component for adjusting log visibility. * - The `LogMessages` component for displaying filtered logs. * - Zustand-managed UI settings (auto-scroll, timestamp display). * * This component uses the `useLogs` hook to fetch and filter logs based on * active predicates and re-renders automatically as new logs arrive. * * @returns The complete logging UI as a React element. */ export default function Logging() { // By default, filter experiment logs from this debug logger const [filterPredicates, setFilterPredicates] = useState(new Map([ [ EXPERIMENT_FILTER_KEY, { predicate: (r) => r.name == EXPERIMENT_LOGGER_NAME ? false : null, priority: 999, value: null, } as LogFilterPredicate, ], ])); const { filteredLogs, distinctNames } = useLogs(filterPredicates) distinctNames.delete(EXPERIMENT_LOGGER_NAME); return

Logs

; }