mirror of
https://github.com/emsesp/EMS-ESP32.git
synced 2025-12-06 15:59:52 +03:00
243 lines
7.3 KiB
TypeScript
243 lines
7.3 KiB
TypeScript
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<LogSettings>({
|
|
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<LogEntry[]>([]);
|
|
const [lastIndex, setLastIndex] = useState<number>(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<HTMLDivElement>(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 <FormLoader onRetry={loadData} errorMessage={errorMessage} />;
|
|
}
|
|
|
|
return (
|
|
<>
|
|
<Grid container spacing={3} direction="row" justifyContent="flex-start" alignItems="center">
|
|
<Grid item xs={2}>
|
|
<TextField
|
|
name="level"
|
|
label={LL.LOG_LEVEL()}
|
|
value={data.level}
|
|
fullWidth
|
|
variant="outlined"
|
|
onChange={updateFormValue}
|
|
margin="normal"
|
|
select
|
|
>
|
|
<MenuItem value={-1}>OFF</MenuItem>
|
|
<MenuItem value={3}>ERROR</MenuItem>
|
|
<MenuItem value={4}>WARNING</MenuItem>
|
|
<MenuItem value={5}>NOTICE</MenuItem>
|
|
<MenuItem value={6}>INFO</MenuItem>
|
|
<MenuItem value={9}>ALL</MenuItem>
|
|
</TextField>
|
|
</Grid>
|
|
<Grid item xs={2}>
|
|
<TextField
|
|
name="max_messages"
|
|
label={LL.BUFFER_SIZE()}
|
|
value={data.max_messages}
|
|
fullWidth
|
|
variant="outlined"
|
|
onChange={updateFormValue}
|
|
margin="normal"
|
|
select
|
|
>
|
|
<MenuItem value={25}>25</MenuItem>
|
|
<MenuItem value={50}>50</MenuItem>
|
|
<MenuItem value={75}>75</MenuItem>
|
|
<MenuItem value={100}>100</MenuItem>
|
|
</TextField>
|
|
</Grid>
|
|
<Grid item>
|
|
<BlockFormControlLabel
|
|
control={<Checkbox checked={data.compact} onChange={updateFormValue} name="compact" />}
|
|
label={LL.COMPACT()}
|
|
/>
|
|
</Grid>
|
|
<Box
|
|
sx={{
|
|
'& button, & a, & .MuiCard-root': {
|
|
mt: 3,
|
|
mx: 0.6
|
|
}
|
|
}}
|
|
>
|
|
<Button startIcon={<DownloadIcon />} variant="outlined" color="secondary" onClick={onDownload}>
|
|
{LL.EXPORT()}
|
|
</Button>
|
|
{dirtyFlags && dirtyFlags.length !== 0 && (
|
|
<Button
|
|
startIcon={<WarningIcon color="warning" />}
|
|
variant="contained"
|
|
color="info"
|
|
onClick={saveSettings}
|
|
>
|
|
{LL.APPLY_CHANGES(dirtyFlags.length)}
|
|
</Button>
|
|
)}
|
|
</Box>
|
|
</Grid>
|
|
<Box
|
|
sx={{
|
|
backgroundColor: 'black',
|
|
overflowY: 'scroll',
|
|
position: 'absolute',
|
|
right: 18,
|
|
bottom: 18,
|
|
left: () => leftOffset(),
|
|
top: () => topOffset(),
|
|
p: 1
|
|
}}
|
|
>
|
|
{logEntries.map((e) => (
|
|
<LogEntryLine key={e.i}>
|
|
<span>{e.t}</span>
|
|
{data.compact && <span>{paddedLevelLabel(e.l)} </span>}
|
|
{!data.compact && <span>{paddedLevelLabel(e.l)} </span>}
|
|
<span>{paddedIDLabel(e.i)} </span>
|
|
<span>{paddedNameLabel(e.n)} </span>
|
|
<span>{e.m}</span>
|
|
</LogEntryLine>
|
|
))}
|
|
<div ref={ref} />
|
|
</Box>
|
|
</>
|
|
);
|
|
};
|
|
|
|
return (
|
|
<SectionContent title={LL.LOG_OF(LL.SYSTEM(2))} titleGutter id="log-window">
|
|
{blocker ? <BlockNavigation blocker={blocker} /> : null}
|
|
{content()}
|
|
</SectionContent>
|
|
);
|
|
};
|
|
|
|
export default SystemLog;
|