import { FC, useState, useEffect, useCallback, useLayoutEffect } from 'react'; import { Box, styled, Button, Checkbox, MenuItem, Grid, Slider, FormLabel } from '@mui/material'; import * as SystemApi from '../../api/system'; import { addAccessTokenParameter } from '../../api/authentication'; import { SectionContent, FormLoader, BlockFormControlLabel, ValidatedTextField } from '../../components'; import { LogSettings, LogEntry, LogEntries, LogLevel } from '../../types'; import { updateValue, useRest, extractErrorMessage } from '../../utils'; import DownloadIcon from '@mui/icons-material/GetApp'; import { useSnackbar } from 'notistack'; import { EVENT_SOURCE_ROOT } from '../../api/endpoints'; export const LOG_EVENTSOURCE_URL = EVENT_SOURCE_ROOT + 'log'; const useWindowSize = () => { const [size, setSize] = useState([0, 0]); useLayoutEffect(() => { function updateSize() { setSize([window.innerWidth, window.innerHeight]); } window.addEventListener('resize', updateSize); updateSize(); return () => window.removeEventListener('resize', updateSize); }, []); return size; }; const LogEntryLine = styled('div')(({ theme }) => ({ color: '#bbbbbb', fontFamily: 'monospace', fontSize: '14px', letterSpacing: 'normal', whiteSpace: 'nowrap' })); const topOffset = () => document.getElementById('log-window')?.getBoundingClientRect().bottom || 0; const leftOffset = () => document.getElementById('log-window')?.getBoundingClientRect().left || 0; const levelLabel = (level: LogLevel) => { switch (level) { case LogLevel.ERROR: return 'ERROR'; case LogLevel.WARNING: return 'WARNING'; case LogLevel.NOTICE: return 'NOTICE'; case LogLevel.INFO: return 'INFO'; case LogLevel.DEBUG: return 'DEBUG'; case LogLevel.TRACE: return 'TRACE'; default: return ''; } }; const SystemLog: FC = () => { useWindowSize(); const { loadData, data, setData } = useRest({ read: SystemApi.readLogSettings }); const [errorMessage, setErrorMessage] = useState(); const [reconnectTimeout, setReconnectTimeout] = useState(); const [logEntries, setLogEntries] = useState({ events: [] }); const [lastIndex, setLastIndex] = useState(0); const paddedLevelLabel = (level: LogLevel) => { const label = levelLabel(level); return data?.compact ? ' ' + label[0] : label.padStart(8, '\xa0'); }; const paddedNameLabel = (name: string) => { const label = '[' + name + ']'; return data?.compact ? label : label.padEnd(12, '\xa0'); }; const paddedIDLabel = (id: number) => { const label = id + ':'; return data?.compact ? label : label.padEnd(7, '\xa0'); }; const updateFormValue = updateValue(setData); const { enqueueSnackbar } = useSnackbar(); const reloadPage = () => { window.location.reload(); }; const sendSettings = async (new_max_messages: number, new_level: number) => { if (data) { try { const response = await SystemApi.updateLogSettings({ level: new_level, max_messages: new_max_messages, compact: data.compact }); if (response.status !== 200) { enqueueSnackbar('Problem applying log settings', { variant: 'error' }); } } catch (error: unknown) { enqueueSnackbar(extractErrorMessage(error, 'Problem applying log settings'), { variant: 'error' }); } } }; const changeLevel = (event: React.ChangeEvent) => { if (data) { setData({ ...data, level: parseInt(event.target.value) }); sendSettings(data.max_messages, parseInt(event.target.value)); } }; const changeMaxMessages = (event: Event, value: number | number[]) => { if (data) { setData({ ...data, max_messages: value as number }); } }; const onDownload = () => { let result = ''; for (let i of logEntries.events) { result += i.t + ' ' + levelLabel(i.l) + ' ' + i.i + ': [' + i.n + '] ' + i.m + '\n'; } const a = document.createElement('a'); a.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(result)); a.setAttribute('download', 'log.txt'); document.body.appendChild(a); a.click(); document.body.removeChild(a); }; const onMessage = (event: MessageEvent) => { const rawData = event.data; if (typeof rawData === 'string' || rawData instanceof String) { const logentry = JSON.parse(rawData as string) as LogEntry; if (logentry.i > lastIndex) { setLastIndex(logentry.i); setLogEntries((old) => ({ events: [...old.events, logentry] })); } } }; const fetchLog = useCallback(async () => { try { setLogEntries((await SystemApi.readLogEntries()).data); } catch (error: unknown) { setErrorMessage(extractErrorMessage(error, 'Failed to fetch log')); } }, []); useEffect(() => { fetchLog(); }, [fetchLog]); useEffect(() => { const es = new EventSource(addAccessTokenParameter(LOG_EVENTSOURCE_URL)); es.onmessage = onMessage; es.onerror = () => { if (reconnectTimeout) { es.close(); setReconnectTimeout(setTimeout(reloadPage, 1000)); } }; return () => { es.close(); if (reconnectTimeout) { clearTimeout(reconnectTimeout); } }; // eslint-disable-next-line }, [reconnectTimeout]); const content = () => { if (!data) { return ; } return ( <> ERROR WARNING NOTICE INFO DEBUG ALL Buffer size sendSettings(data.max_messages, data.level)} /> } label="Compact" /> leftOffset(), top: () => topOffset(), p: 1 }} > {logEntries && logEntries.events.map((e) => ( {e.t} {data.compact && {paddedLevelLabel(e.l)} } {!data.compact && {paddedLevelLabel(e.l)} } {paddedIDLabel(e.i)} {paddedNameLabel(e.n)} {e.m} ))} ); }; return ( {content()} ); }; export default SystemLog;