log to webui - initial version

This commit is contained in:
proddy
2021-06-14 21:28:20 +02:00
parent fffed3b411
commit fc2bcd50ca
30 changed files with 607 additions and 611 deletions

View File

@@ -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;

View 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;
}

View File

@@ -15,3 +15,5 @@ export * from './RestController';
export * from './WebSocketFormLoader';
export * from './WebSocketController';
export * from './WindowSize';

View File

@@ -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>
);
}
}

View File

@@ -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} />}

View File

@@ -9,4 +9,13 @@ module.exports = function (app) {
changeOrigin: true
})
);
app.use(
'/es/*',
createProxyMiddleware({
target: 'http://localhost:3090',
secure: false,
changeOrigin: true
})
);
};

View 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;

View 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);

View File

@@ -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

View File

@@ -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;
}