import DownloadIcon from '@mui/icons-material/GetApp'; import WarningIcon from '@mui/icons-material/Warning'; import { Box, styled, Button, Checkbox, MenuItem, Grid, TextField } from '@mui/material'; import { useRequest } from 'alova'; import { useState, useEffect, useRef } from 'react'; import { toast } from 'react-toastify'; import type { FC } from 'react'; import type { LogSettings, LogEntry } from 'types'; import { addAccessTokenParameter } from 'api/authentication'; import { EVENT_SOURCE_ROOT } from 'api/endpoints'; import * as SystemApi from 'api/system'; import { SectionContent, FormLoader, BlockFormControlLabel, BlockNavigation } from 'components'; import { useI18nContext } from 'i18n/i18n-react'; import { LogLevel } from 'types'; import { updateValueDirty, useRest } from 'utils'; export const LOG_EVENTSOURCE_URL = EVENT_SOURCE_ROOT + 'log'; const LogEntryLine = styled('div')(() => ({ 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.TRACE: return 'TRACE'; default: return ''; } }; const SystemLog: FC = () => { const { LL } = useI18nContext(); const { loadData, data, updateDataValue, origData, dirtyFlags, setDirtyFlags, blocker, saveData, errorMessage } = useRest({ read: SystemApi.readLogSettings, update: SystemApi.updateLogSettings }); // called on page load to reset pointer and fetch all log entries useRequest(SystemApi.fetchLog()); const [logEntries, setLogEntries] = useState([]); const [lastIndex, setLastIndex] = useState(0); const updateFormValue = updateValueDirty(origData, dirtyFlags, setDirtyFlags, updateDataValue); 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 onDownload = () => { let result = ''; for (const i of logEntries) { 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((log) => [...log, logentry]); } } }; const saveSettings = async () => { await saveData(); }; const ref = useRef(null); useEffect(() => { if (logEntries.length) { ref.current?.scrollIntoView({ behavior: 'smooth', block: 'end' }); } }, [logEntries.length]); useEffect(() => { const es = new EventSource(addAccessTokenParameter(LOG_EVENTSOURCE_URL)); es.onmessage = onMessage; es.onerror = () => { es.close(); toast.error('EventSource failed'); }; return () => { es.close(); }; }); const content = () => { if (!data) { return ; } return ( <> OFF ERROR WARNING NOTICE INFO ALL 25 50 75 100 } label={LL.COMPACT()} /> {dirtyFlags && dirtyFlags.length !== 0 && ( )} leftOffset(), top: () => topOffset(), p: 1 }} > {logEntries.map((e) => ( {e.t} {data.compact && {paddedLevelLabel(e.l)} } {!data.compact && {paddedLevelLabel(e.l)} } {paddedIDLabel(e.i)} {paddedNameLabel(e.n)} {e.m} ))}
); }; return ( {blocker ? : null} {content()} ); }; export default SystemLog;