mirror of
https://github.com/emsesp/EMS-ESP32.git
synced 2025-12-07 08:19:52 +03:00
log to webui - initial version
This commit is contained in:
@@ -3,6 +3,7 @@ export const PROJECT_PATH = process.env.REACT_APP_PROJECT_PATH!;
|
||||
|
||||
export const ENDPOINT_ROOT = calculateEndpointRoot('/rest/');
|
||||
export const WEB_SOCKET_ROOT = calculateWebSocketRoot('/ws/');
|
||||
export const EVENT_SOURCE_ROOT = calculateEndpointRoot('/es/');
|
||||
|
||||
function calculateEndpointRoot(endpointPath: string) {
|
||||
const httpRoot = process.env.REACT_APP_HTTP_ROOT;
|
||||
|
||||
14
interface/src/components/WindowSize.tsx
Normal file
14
interface/src/components/WindowSize.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import { useLayoutEffect, useState } from 'react';
|
||||
|
||||
export function 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;
|
||||
}
|
||||
@@ -15,3 +15,5 @@ export * from './RestController';
|
||||
|
||||
export * from './WebSocketFormLoader';
|
||||
export * from './WebSocketController';
|
||||
|
||||
export * from './WindowSize';
|
||||
|
||||
@@ -23,14 +23,12 @@ class EMSESPSettingsController extends Component<EMSESPSettingsControllerProps>
|
||||
|
||||
render() {
|
||||
return (
|
||||
// <Container maxWidth="md" disableGutters>
|
||||
<SectionContent title="" titleGutter>
|
||||
<RestFormLoader
|
||||
{...this.props}
|
||||
render={(formProps) => <EMSESPSettingsForm {...formProps} />}
|
||||
/>
|
||||
</SectionContent>
|
||||
// </Container>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ class EMSESPStatusController extends Component<EMSESPStatusControllerProps> {
|
||||
|
||||
render() {
|
||||
return (
|
||||
<SectionContent title="EMS Status">
|
||||
<SectionContent title="EMS Status" titleGutter>
|
||||
<RestFormLoader
|
||||
{...this.props}
|
||||
render={(formProps) => <EMSESPStatusForm {...formProps} />}
|
||||
|
||||
@@ -9,4 +9,13 @@ module.exports = function (app) {
|
||||
changeOrigin: true
|
||||
})
|
||||
);
|
||||
|
||||
app.use(
|
||||
'/es/*',
|
||||
createProxyMiddleware({
|
||||
target: 'http://localhost:3090',
|
||||
secure: false,
|
||||
changeOrigin: true
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
112
interface/src/system/LogEventConsole.tsx
Normal file
112
interface/src/system/LogEventConsole.tsx
Normal file
@@ -0,0 +1,112 @@
|
||||
import { FC } from 'react';
|
||||
|
||||
import { LogEvent, LogLevel } from './types';
|
||||
import { Theme, makeStyles, Box } from '@material-ui/core';
|
||||
import { useWindowSize } from '../components';
|
||||
|
||||
interface LogEventConsoleProps {
|
||||
events: LogEvent[];
|
||||
}
|
||||
|
||||
interface Offsets {
|
||||
topOffset: () => number;
|
||||
leftOffset: () => number;
|
||||
}
|
||||
|
||||
const topOffset = () =>
|
||||
document.getElementById('log-window')?.getBoundingClientRect().bottom || 0;
|
||||
|
||||
const leftOffset = () =>
|
||||
document.getElementById('log-window')?.getBoundingClientRect().left || 0;
|
||||
|
||||
const useStyles = makeStyles((theme: Theme) => ({
|
||||
console: {
|
||||
padding: theme.spacing(2),
|
||||
position: 'absolute',
|
||||
left: (offsets: Offsets) => offsets.leftOffset(),
|
||||
right: 24,
|
||||
top: (offsets: Offsets) => offsets.topOffset(),
|
||||
bottom: 24,
|
||||
backgroundColor: 'black',
|
||||
overflow: 'auto'
|
||||
},
|
||||
entry: {
|
||||
color: '#bbbbbb',
|
||||
fontFamily: 'Courier New, monospace',
|
||||
fontSize: '14px',
|
||||
letterSpacing: 'normal',
|
||||
whiteSpace: 'nowrap'
|
||||
},
|
||||
debug: {
|
||||
color: '#0000ff'
|
||||
},
|
||||
info: {
|
||||
color: '#00ff00'
|
||||
},
|
||||
notice: {
|
||||
color: '#ffff00'
|
||||
},
|
||||
err: {
|
||||
color: '#ff0000'
|
||||
},
|
||||
unknown: {
|
||||
color: '#ffffff'
|
||||
}
|
||||
}));
|
||||
|
||||
const LogEventConsole: FC<LogEventConsoleProps> = (props) => {
|
||||
useWindowSize();
|
||||
const classes = useStyles({ topOffset, leftOffset });
|
||||
const { events } = props;
|
||||
|
||||
const styleLevel = (level: LogLevel) => {
|
||||
switch (level) {
|
||||
case LogLevel.DEBUG:
|
||||
return classes.debug;
|
||||
case LogLevel.INFO:
|
||||
return classes.info;
|
||||
case LogLevel.NOTICE:
|
||||
return classes.notice;
|
||||
case LogLevel.ERR:
|
||||
return classes.err;
|
||||
default:
|
||||
return classes.unknown;
|
||||
}
|
||||
};
|
||||
|
||||
const levelLabel = (level: LogLevel) => {
|
||||
switch (level) {
|
||||
case LogLevel.DEBUG:
|
||||
return 'DEBUG';
|
||||
case LogLevel.INFO:
|
||||
return 'INFO';
|
||||
case LogLevel.ERR:
|
||||
return 'ERR';
|
||||
case LogLevel.NOTICE:
|
||||
return 'NOTICE';
|
||||
default:
|
||||
return '?';
|
||||
}
|
||||
};
|
||||
|
||||
const paddedLevelLabel = (level: LogLevel) => {
|
||||
const label = levelLabel(level);
|
||||
return label.padStart(7, '\xa0');
|
||||
};
|
||||
|
||||
return (
|
||||
<Box className={classes.console}>
|
||||
{events.map((e) => (
|
||||
<div className={classes.entry}>
|
||||
<span>{e.time} </span>
|
||||
<span className={styleLevel(e.level)}>
|
||||
{paddedLevelLabel(e.level)}{' '}
|
||||
</span>
|
||||
<span>{e.message}</span>
|
||||
</div>
|
||||
))}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default LogEventConsole;
|
||||
109
interface/src/system/LogEventController.tsx
Normal file
109
interface/src/system/LogEventController.tsx
Normal file
@@ -0,0 +1,109 @@
|
||||
import { Component } from 'react';
|
||||
import { FormActions, FormButton } from '../components';
|
||||
|
||||
import {
|
||||
createStyles,
|
||||
WithStyles,
|
||||
withStyles,
|
||||
Typography,
|
||||
Theme,
|
||||
Paper
|
||||
} from '@material-ui/core';
|
||||
|
||||
import { LogEvent } from './types';
|
||||
import { EVENT_SOURCE_ROOT } from '../api/Env';
|
||||
import LogEventConsole from './LogEventConsole';
|
||||
import { addAccessTokenParameter } from '../authentication';
|
||||
|
||||
import SaveIcon from '@material-ui/icons/Save';
|
||||
|
||||
const LOG_EVENT_EVENT_SOURCE_URL = EVENT_SOURCE_ROOT + 'log';
|
||||
|
||||
interface LogEventControllerState {
|
||||
eventSource?: EventSource;
|
||||
events: LogEvent[];
|
||||
}
|
||||
|
||||
const styles = (theme: Theme) =>
|
||||
createStyles({
|
||||
content: {
|
||||
padding: theme.spacing(2),
|
||||
margin: theme.spacing(3)
|
||||
}
|
||||
});
|
||||
|
||||
type LogEventControllerProps = WithStyles<typeof styles>;
|
||||
|
||||
class LogEventController extends Component<
|
||||
LogEventControllerProps,
|
||||
LogEventControllerState
|
||||
> {
|
||||
eventSource?: EventSource;
|
||||
reconnectTimeout?: NodeJS.Timeout;
|
||||
|
||||
constructor(props: LogEventControllerProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
events: []
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.configureEventSource();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.eventSource) {
|
||||
this.eventSource.close();
|
||||
}
|
||||
if (this.reconnectTimeout) {
|
||||
clearTimeout(this.reconnectTimeout);
|
||||
}
|
||||
}
|
||||
|
||||
configureEventSource = () => {
|
||||
this.eventSource = new EventSource(
|
||||
addAccessTokenParameter(LOG_EVENT_EVENT_SOURCE_URL)
|
||||
);
|
||||
this.eventSource.onmessage = this.onMessage;
|
||||
this.eventSource.onerror = this.onError;
|
||||
};
|
||||
|
||||
onError = () => {
|
||||
if (this.eventSource && this.reconnectTimeout) {
|
||||
this.eventSource.close();
|
||||
this.eventSource = undefined;
|
||||
this.reconnectTimeout = setTimeout(this.configureEventSource, 1000);
|
||||
}
|
||||
};
|
||||
|
||||
onMessage = (event: MessageEvent) => {
|
||||
const rawData = event.data;
|
||||
if (typeof rawData === 'string' || rawData instanceof String) {
|
||||
const event = JSON.parse(rawData as string) as LogEvent;
|
||||
this.setState((state) => ({ events: [...state.events, event] }));
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { classes } = this.props;
|
||||
return (
|
||||
<Paper id="log-window" className={classes.content}>
|
||||
<Typography variant="h6">System Log</Typography>
|
||||
<FormActions>
|
||||
<FormButton
|
||||
startIcon={<SaveIcon />}
|
||||
variant="contained"
|
||||
color="secondary"
|
||||
// onClick={this.requestNetworkScan}
|
||||
>
|
||||
Save
|
||||
</FormButton>
|
||||
</FormActions>
|
||||
<LogEventConsole events={this.state.events} />
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withStyles(styles)(LogEventController);
|
||||
@@ -15,6 +15,7 @@ import { MenuAppBar } from '../components';
|
||||
import SystemStatusController from './SystemStatusController';
|
||||
import OTASettingsController from './OTASettingsController';
|
||||
import UploadFirmwareController from './UploadFirmwareController';
|
||||
import LogEventController from './LogEventController';
|
||||
|
||||
type SystemProps = AuthenticatedContextProps &
|
||||
RouteComponentProps &
|
||||
@@ -35,6 +36,7 @@ class System extends Component<SystemProps> {
|
||||
variant="fullWidth"
|
||||
>
|
||||
<Tab value="/system/status" label="System Status" />
|
||||
<Tab value="/system/log" label="System Log" />
|
||||
{features.ota && (
|
||||
<Tab
|
||||
value="/system/ota"
|
||||
@@ -56,6 +58,11 @@ class System extends Component<SystemProps> {
|
||||
path="/system/status"
|
||||
component={SystemStatusController}
|
||||
/>
|
||||
<AuthenticatedRoute
|
||||
exact
|
||||
path="/system/log"
|
||||
component={LogEventController}
|
||||
/>
|
||||
{features.ota && (
|
||||
<AuthenticatedRoute
|
||||
exact
|
||||
|
||||
@@ -36,3 +36,16 @@ export interface OTASettings {
|
||||
port: number;
|
||||
password: string;
|
||||
}
|
||||
|
||||
export enum LogLevel {
|
||||
ERR = 3,
|
||||
NOTICE = 5,
|
||||
INFO = 6,
|
||||
DEBUG = 7
|
||||
}
|
||||
|
||||
export interface LogEvent {
|
||||
time: string;
|
||||
level: LogLevel;
|
||||
message: string;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user