u
This commit is contained in:
251
src/devtools/SseMonitor.jsx
Normal file
251
src/devtools/SseMonitor.jsx
Normal file
@@ -0,0 +1,251 @@
|
||||
import { PureComponent, createRef } from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import Chip from '@mui/material/Chip';
|
||||
import Fab from '@mui/material/Fab';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import Paper from '@mui/material/Paper';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import Tooltip from '@mui/material/Tooltip';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import CloseIcon from '@mui/icons-material/Close';
|
||||
import DeleteSweepIcon from '@mui/icons-material/DeleteSweep';
|
||||
import DragIndicatorIcon from '@mui/icons-material/DragIndicator';
|
||||
import SensorsIcon from '@mui/icons-material/Sensors';
|
||||
import sseAutomat from '../automats/sseAutomat';
|
||||
|
||||
const PANEL_WIDTH = 420;
|
||||
const PANEL_HEIGHT = 380;
|
||||
|
||||
function formatEventData(data) {
|
||||
if (!data) return '';
|
||||
try {
|
||||
return JSON.stringify(JSON.parse(data), null, 2);
|
||||
} catch {
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
class SseMonitor extends PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
status: sseAutomat.state.status,
|
||||
companyId: sseAutomat.state.companyId,
|
||||
events: sseAutomat.state.events,
|
||||
open: false,
|
||||
position: null,
|
||||
};
|
||||
this.panelRef = createRef();
|
||||
this.handleToggle = this.handleToggle.bind(this);
|
||||
this.handleKeyDown = this.handleKeyDown.bind(this);
|
||||
this.handlePointerDown = this.handlePointerDown.bind(this);
|
||||
this.handlePointerMove = this.handlePointerMove.bind(this);
|
||||
this.handlePointerUp = this.handlePointerUp.bind(this);
|
||||
this.handleClear = this.handleClear.bind(this);
|
||||
this.handleClose = this.handleClose.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.unsubscribe = sseAutomat.subscribe(this, (state) => ({
|
||||
status: state.status,
|
||||
companyId: state.companyId,
|
||||
events: state.events,
|
||||
}));
|
||||
window.addEventListener('keydown', this.handleKeyDown);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.unsubscribe?.();
|
||||
window.removeEventListener('keydown', this.handleKeyDown);
|
||||
}
|
||||
|
||||
handleKeyDown(event) {
|
||||
if (!event.altKey || !event.shiftKey || event.key?.toLowerCase() !== 's') return;
|
||||
event.preventDefault();
|
||||
this.handleToggle();
|
||||
}
|
||||
|
||||
handleToggle() {
|
||||
this.setState((state) => ({ open: !state.open }));
|
||||
}
|
||||
|
||||
handleClose() {
|
||||
this.setState({ open: false });
|
||||
}
|
||||
|
||||
handleClear() {
|
||||
sseAutomat.actions.clearEvents();
|
||||
}
|
||||
|
||||
handlePointerDown(event) {
|
||||
if (event.target.closest('button')) return;
|
||||
const rect = this.panelRef.current?.getBoundingClientRect();
|
||||
if (!rect) return;
|
||||
event.preventDefault();
|
||||
event.currentTarget.setPointerCapture(event.pointerId);
|
||||
this.drag = {
|
||||
pointerId: event.pointerId,
|
||||
pointerX: event.clientX,
|
||||
pointerY: event.clientY,
|
||||
left: rect.left,
|
||||
top: rect.top,
|
||||
};
|
||||
}
|
||||
|
||||
handlePointerMove(event) {
|
||||
if (!this.drag || event.pointerId !== this.drag.pointerId) return;
|
||||
const maxLeft = Math.max(0, window.innerWidth - PANEL_WIDTH);
|
||||
const maxTop = Math.max(0, window.innerHeight - 120);
|
||||
this.setState({
|
||||
position: {
|
||||
left: Math.max(0, Math.min(maxLeft, this.drag.left + event.clientX - this.drag.pointerX)),
|
||||
top: Math.max(0, Math.min(maxTop, this.drag.top + event.clientY - this.drag.pointerY)),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
handlePointerUp(event) {
|
||||
if (this.drag?.pointerId === event.pointerId) this.drag = null;
|
||||
}
|
||||
|
||||
renderEvent(entry) {
|
||||
const data = formatEventData(entry.data);
|
||||
return (
|
||||
<Box
|
||||
key={entry.id}
|
||||
sx={{ px: 1.25, py: 1, borderBottom: '1px solid', borderColor: 'divider' }}
|
||||
>
|
||||
<Stack direction="row" spacing={1} alignItems="baseline">
|
||||
<Typography variant="caption" color="text.secondary" sx={{ flexShrink: 0 }}>
|
||||
{new Date(entry.time).toLocaleTimeString()}
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ fontWeight: 700, overflowWrap: 'anywhere' }}>
|
||||
{entry.type}
|
||||
</Typography>
|
||||
{entry.lastEventId ? (
|
||||
<Typography variant="caption" color="text.secondary" sx={{ ml: 'auto !important' }}>
|
||||
#{entry.lastEventId}
|
||||
</Typography>
|
||||
) : null}
|
||||
</Stack>
|
||||
{data ? (
|
||||
<Typography
|
||||
component="pre"
|
||||
variant="caption"
|
||||
sx={{
|
||||
m: 0,
|
||||
mt: 0.5,
|
||||
whiteSpace: 'pre-wrap',
|
||||
overflowWrap: 'anywhere',
|
||||
color: 'text.secondary',
|
||||
}}
|
||||
>
|
||||
{data}
|
||||
</Typography>
|
||||
) : null}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { open, position, status, companyId, events } = this.state;
|
||||
const statusColor =
|
||||
status === 'connected' ? 'success' : status === 'error' ? 'error' : status === 'connecting' ? 'warning' : 'default';
|
||||
|
||||
return (
|
||||
<>
|
||||
{!open ? (
|
||||
<Tooltip title="SSE-Monitor (Alt+Shift+S)" placement="left">
|
||||
<Fab
|
||||
size="small"
|
||||
color="primary"
|
||||
aria-label="SSE-Monitor umschalten"
|
||||
aria-pressed={open}
|
||||
onClick={this.handleToggle}
|
||||
sx={{
|
||||
position: 'fixed',
|
||||
right: 16,
|
||||
bottom: 16,
|
||||
zIndex: (theme) => theme.zIndex.tooltip,
|
||||
}}
|
||||
>
|
||||
<SensorsIcon fontSize="small" />
|
||||
</Fab>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<Paper
|
||||
ref={this.panelRef}
|
||||
component="aside"
|
||||
aria-label="SSE-Monitor"
|
||||
elevation={12}
|
||||
sx={{
|
||||
position: 'fixed',
|
||||
...(position ? { left: position.left, top: position.top } : { right: 16, bottom: 16 }),
|
||||
width: `min(${PANEL_WIDTH}px, calc(100vw - 24px))`,
|
||||
height: `min(${PANEL_HEIGHT}px, calc(100vh - 24px))`,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
overflow: 'hidden',
|
||||
zIndex: (theme) => theme.zIndex.tooltip,
|
||||
border: '1px solid',
|
||||
borderColor: 'divider',
|
||||
}}
|
||||
>
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
spacing={1}
|
||||
onPointerDown={this.handlePointerDown}
|
||||
onPointerMove={this.handlePointerMove}
|
||||
onPointerUp={this.handlePointerUp}
|
||||
onPointerCancel={this.handlePointerUp}
|
||||
sx={{
|
||||
px: 1,
|
||||
py: 0.75,
|
||||
borderBottom: '1px solid',
|
||||
borderColor: 'divider',
|
||||
cursor: 'grab',
|
||||
touchAction: 'none',
|
||||
userSelect: 'none',
|
||||
'&:active': { cursor: 'grabbing' },
|
||||
}}
|
||||
>
|
||||
<DragIndicatorIcon fontSize="small" color="action" />
|
||||
<SensorsIcon fontSize="small" color="primary" />
|
||||
<Typography variant="subtitle2" sx={{ fontWeight: 700, flexGrow: 1 }}>
|
||||
SSE-Monitor
|
||||
</Typography>
|
||||
<Chip size="small" color={statusColor} label={status} />
|
||||
<Tooltip title="Ereignisse löschen">
|
||||
<IconButton size="small" aria-label="Ereignisse löschen" onClick={this.handleClear}>
|
||||
<DeleteSweepIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title="Schließen">
|
||||
<IconButton size="small" aria-label="Schließen" onClick={this.handleClose}>
|
||||
<CloseIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Stack>
|
||||
<Box sx={{ px: 1.25, py: 0.75, borderBottom: '1px solid', borderColor: 'divider' }}>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
{companyId ? `Company ${companyId}` : 'Keine aktive Company'} · {events.length} Ereignisse
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box sx={{ flex: 1, minHeight: 0, overflowY: 'auto' }}>
|
||||
{events.length > 0 ? (
|
||||
[...events].reverse().map((entry) => this.renderEvent(entry))
|
||||
) : (
|
||||
<Typography variant="body2" color="text.secondary" sx={{ p: 2 }}>
|
||||
Noch keine SSE-Ereignisse empfangen.
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
</Paper>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default SseMonitor;
|
||||
Reference in New Issue
Block a user