1// SPDX-License-Identifier: GPL-2.0-only
2// Copyright (c) 2026-present FERS Contributors (see AUTHORS.md).
4import CloseIcon from '@mui/icons-material/Close';
5import DeleteSweepIcon from '@mui/icons-material/DeleteSweep';
14} from '@mui/material';
15import type { ChangeEvent } from 'react';
16import { useEffect, useRef } from 'react';
21} from '@/stores/fersLogStore';
22import LogLevelSelect from './LogLevelSelect';
24const levelColor = (level: FersLogLevel) => {
30 return 'secondary.main';
33 return 'text.secondary';
35 return 'text.disabled';
37 return 'text.primary';
41const RawLogLine = ({ entry }: { entry: FersLogEntry }) => (
45 color: levelColor(entry.level),
46 fontFamily: '"Roboto Mono", Consolas, "Liberation Mono", monospace',
49 whiteSpace: 'pre-wrap',
50 wordBreak: 'break-word',
57export default function RawLogDrawer() {
58 const entries = useFersLogStore((state) => state.entries);
59 const droppedCount = useFersLogStore((state) => state.droppedCount);
60 const maxLines = useFersLogStore((state) => state.maxLines);
61 const clearLogs = useFersLogStore((state) => state.clearLogs);
62 const setMaxLines = useFersLogStore((state) => state.setMaxLines);
63 const setOpen = useFersLogStore((state) => state.setOpen);
64 const scrollRef = useRef<HTMLDivElement | null>(null);
65 const shouldAutoScrollRef = useRef(true);
68 if (!shouldAutoScrollRef.current || !scrollRef.current) {
72 scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
75 const handleScroll = () => {
76 const element = scrollRef.current;
81 shouldAutoScrollRef.current =
82 element.scrollHeight - element.scrollTop - element.clientHeight <
86 const handleMaxLinesChange = (event: ChangeEvent<HTMLInputElement>) => {
87 const nextValue = Number(event.target.value);
88 if (Number.isFinite(nextValue)) {
89 setMaxLines(nextValue);
100 width: 'min(560px, calc(100vw - 60px))',
101 bgcolor: 'background.paper',
103 borderColor: 'divider',
105 zIndex: (theme) => theme.zIndex.drawer,
107 flexDirection: 'column',
114 alignItems: 'center',
119 <Box sx={{ minWidth: 0, flexGrow: 1 }}>
120 <Typography variant="h6">Raw Logs</Typography>
121 <Typography variant="caption" color="text.secondary">
122 {entries.length} lines retained
135 onChange={handleMaxLinesChange}
145 <Tooltip title="Clear logs">
148 aria-label="Clear logs"
150 disabled={entries.length === 0}
156 <Tooltip title="Close logs">
158 aria-label="Close logs"
159 onClick={() => setOpen(false)}
166 {droppedCount > 0 && (
167 <Box sx={{ px: 2, py: 1, bgcolor: 'action.hover' }}>
168 <Typography variant="caption" color="text.secondary">
169 {droppedCount} older log lines dropped.
175 onScroll={handleScroll}
180 bgcolor: 'background.default',
183 {entries.length === 0 ? (
188 alignItems: 'center',
189 justifyContent: 'center',
194 <Typography variant="body2" color="text.secondary">
199 entries.map((entry) => (
200 <RawLogLine key={entry.sequence} entry={entry} />
205 <Box sx={{ p: 1.5, display: 'flex', justifyContent: 'flex-end' }}>
206 <Button size="small" onClick={() => setOpen(false)}>