FERS 1.0.0
The Flexible Extensible Radar Simulator
Loading...
Searching...
No Matches
RawLogDrawer.tsx
Go to the documentation of this file.
1// SPDX-License-Identifier: GPL-2.0-only
2// Copyright (c) 2026-present FERS Contributors (see AUTHORS.md).
3
4import CloseIcon from '@mui/icons-material/Close';
5import DeleteSweepIcon from '@mui/icons-material/DeleteSweep';
6import {
7 Box,
8 Button,
9 Divider,
10 IconButton,
11 TextField,
12 Tooltip,
13 Typography,
14} from '@mui/material';
15import type { ChangeEvent } from 'react';
16import { useEffect, useRef } from 'react';
17import {
18 type FersLogEntry,
19 type FersLogLevel,
20 useFersLogStore,
21} from '@/stores/fersLogStore';
22import LogLevelSelect from './LogLevelSelect';
23
24const levelColor = (level: FersLogLevel) => {
25 switch (level) {
26 case 'FATAL':
27 case 'ERROR':
28 return 'error.main';
29 case 'WARNING':
30 return 'secondary.main';
31 case 'TRACE':
32 case 'DEBUG':
33 return 'text.secondary';
34 case 'OFF':
35 return 'text.disabled';
36 default:
37 return 'text.primary';
38 }
39};
40
41const RawLogLine = ({ entry }: { entry: FersLogEntry }) => (
42 <Box
43 component="div"
44 sx={{
45 color: levelColor(entry.level),
46 fontFamily: '"Roboto Mono", Consolas, "Liberation Mono", monospace',
47 fontSize: 12,
48 lineHeight: 1.55,
49 whiteSpace: 'pre-wrap',
50 wordBreak: 'break-word',
51 }}
52 >
53 {entry.line}
54 </Box>
55);
56
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);
66
67 useEffect(() => {
68 if (!shouldAutoScrollRef.current || !scrollRef.current) {
69 return;
70 }
71
72 scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
73 }, [entries.length]);
74
75 const handleScroll = () => {
76 const element = scrollRef.current;
77 if (!element) {
78 return;
79 }
80
81 shouldAutoScrollRef.current =
82 element.scrollHeight - element.scrollTop - element.clientHeight <
83 24;
84 };
85
86 const handleMaxLinesChange = (event: ChangeEvent<HTMLInputElement>) => {
87 const nextValue = Number(event.target.value);
88 if (Number.isFinite(nextValue)) {
89 setMaxLines(nextValue);
90 }
91 };
92
93 return (
94 <Box
95 sx={{
96 position: 'fixed',
97 left: 60,
98 top: 0,
99 bottom: 0,
100 width: 'min(560px, calc(100vw - 60px))',
101 bgcolor: 'background.paper',
102 borderRight: 1,
103 borderColor: 'divider',
104 boxShadow: 8,
105 zIndex: (theme) => theme.zIndex.drawer,
106 display: 'flex',
107 flexDirection: 'column',
108 }}
109 >
110 <Box
111 sx={{
112 p: 2,
113 display: 'flex',
114 alignItems: 'center',
115 flexWrap: 'wrap',
116 gap: 1.5,
117 }}
118 >
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
123 </Typography>
124 </Box>
125 <LogLevelSelect
126 id="raw-log-level"
127 label="Log level"
128 sx={{ width: 150 }}
129 />
130 <TextField
131 label="Max lines"
132 type="number"
133 size="small"
134 value={maxLines}
135 onChange={handleMaxLinesChange}
136 sx={{ width: 120 }}
137 slotProps={{
138 htmlInput: {
139 min: 100,
140 max: 20000,
141 step: 100,
142 },
143 }}
144 />
145 <Tooltip title="Clear logs">
146 <span>
147 <IconButton
148 aria-label="Clear logs"
149 onClick={clearLogs}
150 disabled={entries.length === 0}
151 >
152 <DeleteSweepIcon />
153 </IconButton>
154 </span>
155 </Tooltip>
156 <Tooltip title="Close logs">
157 <IconButton
158 aria-label="Close logs"
159 onClick={() => setOpen(false)}
160 >
161 <CloseIcon />
162 </IconButton>
163 </Tooltip>
164 </Box>
165 <Divider />
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.
170 </Typography>
171 </Box>
172 )}
173 <Box
174 ref={scrollRef}
175 onScroll={handleScroll}
176 sx={{
177 flexGrow: 1,
178 overflow: 'auto',
179 p: 2,
180 bgcolor: 'background.default',
181 }}
182 >
183 {entries.length === 0 ? (
184 <Box
185 sx={{
186 height: '100%',
187 display: 'flex',
188 alignItems: 'center',
189 justifyContent: 'center',
190 textAlign: 'center',
191 px: 3,
192 }}
193 >
194 <Typography variant="body2" color="text.secondary">
195 No FERS logs yet.
196 </Typography>
197 </Box>
198 ) : (
199 entries.map((entry) => (
200 <RawLogLine key={entry.sequence} entry={entry} />
201 ))
202 )}
203 </Box>
204 <Divider />
205 <Box sx={{ p: 1.5, display: 'flex', justifyContent: 'flex-end' }}>
206 <Button size="small" onClick={() => setOpen(false)}>
207 Close
208 </Button>
209 </Box>
210 </Box>
211 );
212}