u
This commit is contained in:
@@ -1,9 +1,8 @@
|
||||
import { PureComponent } from 'react';
|
||||
import { PureComponent, createRef } from 'react';
|
||||
import Badge from '@mui/material/Badge';
|
||||
import Box from '@mui/material/Box';
|
||||
import ButtonBase from '@mui/material/ButtonBase';
|
||||
import Chip from '@mui/material/Chip';
|
||||
import Drawer from '@mui/material/Drawer';
|
||||
import Fab from '@mui/material/Fab';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import Stack from '@mui/material/Stack';
|
||||
@@ -12,6 +11,7 @@ import Tooltip from '@mui/material/Tooltip';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import BugReportIcon from '@mui/icons-material/BugReport';
|
||||
import ChevronRightIcon from '@mui/icons-material/ChevronRight';
|
||||
import ClearIcon from '@mui/icons-material/Clear';
|
||||
import CloseIcon from '@mui/icons-material/Close';
|
||||
import ContentCopyIcon from '@mui/icons-material/ContentCopy';
|
||||
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
||||
@@ -162,11 +162,175 @@ export function formatState(value) {
|
||||
}
|
||||
}
|
||||
|
||||
/** Maximum number of retained action log entries (ring buffer). */
|
||||
const MAX_LOG = 300;
|
||||
|
||||
/** Marks action functions that already carry the logging proxy. */
|
||||
const LOGGED = Symbol('automatInspectorLogged');
|
||||
|
||||
/** Compact single-line rendering limit for args/results in the log. */
|
||||
const LOG_VALUE_LIMIT = 240;
|
||||
|
||||
/**
|
||||
* Floating dev button plus overlay listing all registered Automats with live
|
||||
* state, readiness flags, subscriber counts and available actions.
|
||||
* Ring buffer of captured action calls, oldest first. Kept at module scope so
|
||||
* entries survive component re-renders (and are shared by every inspector).
|
||||
*
|
||||
* @augments {PureComponent<{}, {open: boolean, names: Array<string>, expanded: Object<string, boolean>, query: string, revision: number}>}
|
||||
* @type {Array<{id: number, time: Date, automat: string, action: string, args: string, status: string, duration: number|null, result: string, error: string|null}>}
|
||||
*/
|
||||
const actionLog = [];
|
||||
|
||||
/** Monotonic id source for log entries. */
|
||||
let logSeq = 0;
|
||||
|
||||
/** @type {Set<() => void>} Listeners notified when the log buffer changes. */
|
||||
const logListeners = new Set();
|
||||
|
||||
/** Notifies every log listener, swallowing individual errors. */
|
||||
function notifyLog() {
|
||||
for (const listener of logListeners) {
|
||||
try {
|
||||
listener();
|
||||
} catch {
|
||||
/* listeners are dev-only; never let one break logging */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends an entry to the ring buffer and notifies listeners.
|
||||
*
|
||||
* @param {object} entry Log entry.
|
||||
*/
|
||||
function pushLog(entry) {
|
||||
actionLog.push(entry);
|
||||
if (actionLog.length > MAX_LOG) actionLog.splice(0, actionLog.length - MAX_LOG);
|
||||
notifyLog();
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a value as a compact single-line string for the log.
|
||||
*
|
||||
* @param {*} value Value to summarize.
|
||||
* @param {number} [limit] Maximum length before truncation.
|
||||
* @returns {string} Compact representation.
|
||||
*/
|
||||
function compact(value, limit = LOG_VALUE_LIMIT) {
|
||||
const text = formatState(value).replace(/\s+/g, ' ').trim();
|
||||
return text.length > limit ? `${text.slice(0, limit)} …` : text;
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats a timestamp as `HH:MM:SS.mmm`.
|
||||
*
|
||||
* @param {Date} date Timestamp.
|
||||
* @returns {string} Formatted time.
|
||||
*/
|
||||
function formatTime(date) {
|
||||
const pad = (n, len = 2) => String(n).padStart(len, '0');
|
||||
return `${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}.${pad(date.getMilliseconds(), 3)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps every action of an automat with a logging proxy that records call
|
||||
* arguments, duration, return value and errors. Idempotent: already wrapped
|
||||
* actions are left untouched, and the original function is always invoked with
|
||||
* the caller's `this` and arguments.
|
||||
*
|
||||
* @param {import('react-automat').Automat} automat Automat instance.
|
||||
*/
|
||||
function installActionLogging(automat) {
|
||||
const actions = automat.actions;
|
||||
if (!actions) return;
|
||||
for (const key of Object.keys(actions)) {
|
||||
const original = actions[key];
|
||||
if (typeof original !== 'function' || original[LOGGED]) continue;
|
||||
|
||||
const wrapped = function loggedAction(...args) {
|
||||
const started = performance.now();
|
||||
const entry = {
|
||||
id: ++logSeq,
|
||||
time: new Date(),
|
||||
automat: automat.name ?? '(anonym)',
|
||||
action: key,
|
||||
args: compact(args),
|
||||
status: 'running',
|
||||
duration: null,
|
||||
result: '',
|
||||
error: null,
|
||||
};
|
||||
pushLog(entry);
|
||||
|
||||
const settle = (status, payload, err) => {
|
||||
entry.status = status;
|
||||
entry.duration = performance.now() - started;
|
||||
if (status === 'error') entry.error = err?.message ?? String(err);
|
||||
else entry.result = compact(payload);
|
||||
notifyLog();
|
||||
};
|
||||
|
||||
let result;
|
||||
try {
|
||||
result = original.apply(this, args);
|
||||
} catch (err) {
|
||||
settle('error', undefined, err);
|
||||
throw err;
|
||||
}
|
||||
|
||||
if (result && typeof result.then === 'function') {
|
||||
return result.then(
|
||||
(value) => {
|
||||
settle('ok', value);
|
||||
return value;
|
||||
},
|
||||
(err) => {
|
||||
settle('error', undefined, err);
|
||||
throw err;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
settle('ok', result);
|
||||
return result;
|
||||
};
|
||||
|
||||
try {
|
||||
Object.defineProperty(wrapped, 'name', { value: key });
|
||||
} catch {
|
||||
/* name is cosmetic */
|
||||
}
|
||||
wrapped[LOGGED] = true;
|
||||
actions[key] = wrapped;
|
||||
}
|
||||
}
|
||||
|
||||
/** Marks the registry Map once its `set` has been wrapped. */
|
||||
const WATCHED = Symbol('automatInspectorWatched');
|
||||
|
||||
/**
|
||||
* Wraps the live registry's `set` so automats created after mount get their
|
||||
* actions instrumented immediately instead of on the next poll tick.
|
||||
*/
|
||||
function watchRegistry() {
|
||||
const registry = window.__AUTOMATS__;
|
||||
if (!(registry instanceof Map) || registry[WATCHED]) return;
|
||||
const originalSet = registry.set.bind(registry);
|
||||
registry.set = (name, automat) => {
|
||||
const result = originalSet(name, automat);
|
||||
installActionLogging(automat);
|
||||
return result;
|
||||
};
|
||||
registry[WATCHED] = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Floating dev button plus a docked side panel listing all registered Automats
|
||||
* with live state, readiness flags, subscriber counts and available actions.
|
||||
*
|
||||
* The panel is rendered in-flow (not as an overlay), so the page stays fully
|
||||
* interactive while the action log is watched live. It relies on being placed
|
||||
* as a flex sibling of the app content (see `main.jsx`).
|
||||
*
|
||||
* @augments {PureComponent<{}, {open: boolean, names: Array<string>, expanded: Object<string, boolean>, query: string, revision: number, logOpen: boolean, log: Array<object>}>}
|
||||
*/
|
||||
class AutomatInspector extends PureComponent {
|
||||
constructor(props) {
|
||||
@@ -177,11 +341,15 @@ class AutomatInspector extends PureComponent {
|
||||
expanded: {},
|
||||
query: '',
|
||||
revision: 0,
|
||||
logOpen: false,
|
||||
log: [],
|
||||
};
|
||||
|
||||
/** @type {Map<string, () => void>} Unsubscribe functions per automat name. */
|
||||
this.unsubs = new Map();
|
||||
this.frame = null;
|
||||
this.logFrame = null;
|
||||
this.logRef = createRef();
|
||||
|
||||
this.handleToggle = this.handleToggle.bind(this);
|
||||
this.handleClose = this.handleClose.bind(this);
|
||||
@@ -193,24 +361,35 @@ class AutomatInspector extends PureComponent {
|
||||
this.handleKeyDown = this.handleKeyDown.bind(this);
|
||||
this.syncRegistry = this.syncRegistry.bind(this);
|
||||
this.scheduleUpdate = this.scheduleUpdate.bind(this);
|
||||
this.scheduleLogUpdate = this.scheduleLogUpdate.bind(this);
|
||||
this.handleToggleLog = this.handleToggleLog.bind(this);
|
||||
this.handleClearLog = this.handleClearLog.bind(this);
|
||||
this.handleCopyLog = this.handleCopyLog.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.syncRegistry();
|
||||
this.registryTimer = window.setInterval(this.syncRegistry, REGISTRY_POLL_MS);
|
||||
window.addEventListener('keydown', this.handleKeyDown);
|
||||
logListeners.add(this.scheduleLogUpdate);
|
||||
this.scheduleLogUpdate();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
window.clearInterval(this.registryTimer);
|
||||
window.removeEventListener('keydown', this.handleKeyDown);
|
||||
logListeners.delete(this.scheduleLogUpdate);
|
||||
this.unsubscribeAll();
|
||||
if (this.frame) cancelAnimationFrame(this.frame);
|
||||
if (this.logFrame) cancelAnimationFrame(this.logFrame);
|
||||
}
|
||||
|
||||
/** Mirrors the registry keys into state and keeps subscriptions in sync. */
|
||||
syncRegistry() {
|
||||
const names = Array.from(getRegistry().keys()).sort((a, b) => a.localeCompare(b));
|
||||
watchRegistry();
|
||||
const registry = getRegistry();
|
||||
for (const automat of registry.values()) installActionLogging(automat);
|
||||
const names = Array.from(registry.keys()).sort((a, b) => a.localeCompare(b));
|
||||
const changed =
|
||||
names.length !== this.state.names.length || names.some((name, i) => name !== this.state.names[i]);
|
||||
if (changed) this.setState({ names });
|
||||
@@ -249,6 +428,58 @@ class AutomatInspector extends PureComponent {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Copies the module-level log ring buffer into state, coalesced per frame so
|
||||
* bursts of action calls do not cause a render each. Skipped while the drawer
|
||||
* is closed; opening it re-syncs via `handleToggle`.
|
||||
*/
|
||||
scheduleLogUpdate() {
|
||||
if (!this.state.open || this.logFrame) return;
|
||||
this.logFrame = requestAnimationFrame(() => {
|
||||
this.logFrame = null;
|
||||
this.setState({ log: actionLog.slice() });
|
||||
});
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
const grew = this.state.log.length > prevState.log.length;
|
||||
const opened = this.state.logOpen && !prevState.logOpen;
|
||||
if (this.state.logOpen && (grew || opened)) {
|
||||
const node = this.logRef.current;
|
||||
if (node) node.scrollTop = node.scrollHeight;
|
||||
}
|
||||
}
|
||||
|
||||
handleToggleLog() {
|
||||
this.setState((prev) => ({ logOpen: !prev.logOpen, log: actionLog.slice() }));
|
||||
}
|
||||
|
||||
handleClearLog() {
|
||||
actionLog.length = 0;
|
||||
notifyLog();
|
||||
}
|
||||
|
||||
/** Copies the whole log buffer to the clipboard as plain text. */
|
||||
async handleCopyLog() {
|
||||
try {
|
||||
if (!navigator.clipboard?.writeText) {
|
||||
throw new Error('Clipboard API nicht verfügbar (sichere Origin erforderlich).');
|
||||
}
|
||||
const text = actionLog
|
||||
.map(
|
||||
(entry) =>
|
||||
`${formatTime(entry.time)} ${entry.automat}.${entry.action}(${entry.args})` +
|
||||
`${entry.status === 'error' ? ` ! ${entry.error}` : ` → ${entry.result}`}` +
|
||||
`${entry.duration != null ? ` [${Math.round(entry.duration)}ms]` : ''}`,
|
||||
)
|
||||
.join('\n');
|
||||
await navigator.clipboard.writeText(text);
|
||||
enqueueSnackbar(`Log kopiert (${actionLog.length} Einträge)`, { variant: 'success' });
|
||||
} catch (err) {
|
||||
notifyError(err);
|
||||
}
|
||||
}
|
||||
|
||||
handleKeyDown(event) {
|
||||
if (!event.altKey || !event.shiftKey) return;
|
||||
if (event.key?.toLowerCase() !== 'a') return;
|
||||
@@ -262,6 +493,7 @@ class AutomatInspector extends PureComponent {
|
||||
if (open) {
|
||||
this.syncRegistry();
|
||||
this.subscribeToAll();
|
||||
this.scheduleLogUpdate();
|
||||
} else {
|
||||
this.unsubscribeAll();
|
||||
}
|
||||
@@ -434,8 +666,116 @@ class AutomatInspector extends PureComponent {
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { open, names, expanded, query } = this.state;
|
||||
/**
|
||||
* Renders one captured action call as a single log line.
|
||||
*
|
||||
* @param {object} entry Log entry.
|
||||
* @returns {React.ReactElement} Log line.
|
||||
*/
|
||||
renderLogLine(entry) {
|
||||
const color =
|
||||
entry.status === 'error' ? 'error.main' : entry.status === 'running' ? 'text.secondary' : 'success.main';
|
||||
return (
|
||||
<Box key={entry.id} component="div">
|
||||
<Box component="span" sx={{ color: 'text.disabled' }}>
|
||||
{formatTime(entry.time)}{' '}
|
||||
</Box>
|
||||
<Box component="span" sx={{ fontWeight: 600 }}>
|
||||
{entry.automat}.{entry.action}
|
||||
</Box>
|
||||
<Box component="span" sx={{ color: 'text.secondary' }}>
|
||||
({entry.args})
|
||||
</Box>
|
||||
{entry.status === 'error' ? (
|
||||
<Box component="span" sx={{ color: 'error.main' }}>
|
||||
{' '}
|
||||
✗ {entry.error}
|
||||
</Box>
|
||||
) : entry.status === 'ok' ? (
|
||||
<Box component="span" sx={{ color: 'text.disabled' }}>
|
||||
{' '}
|
||||
→ {entry.result}
|
||||
</Box>
|
||||
) : null}{' '}
|
||||
<Box component="span" sx={{ color, ml: 0.5 }}>
|
||||
{entry.status === 'running'
|
||||
? '…'
|
||||
: `${entry.status === 'error' ? '✗' : '✓'} ${Math.round(entry.duration)}ms`}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
/** Bottom-docked, collapsible log of every captured automat action call. */
|
||||
renderLog() {
|
||||
const { logOpen, log } = this.state;
|
||||
const errorCount = log.filter((entry) => entry.status === 'error').length;
|
||||
|
||||
return (
|
||||
<Box sx={{ borderTop: '1px solid', borderColor: 'divider', flexShrink: 0 }}>
|
||||
<Stack direction="row" alignItems="center" spacing={0.5} sx={{ px: 1.5, py: 0.5 }}>
|
||||
<ButtonBase
|
||||
component="div"
|
||||
onClick={this.handleToggleLog}
|
||||
sx={{ flexGrow: 1, display: 'flex', alignItems: 'center', gap: 0.5, justifyContent: 'flex-start', py: 0.5 }}
|
||||
>
|
||||
{logOpen ? <ExpandMoreIcon fontSize="small" /> : <ChevronRightIcon fontSize="small" />}
|
||||
<Typography variant="caption" sx={{ fontWeight: 600 }}>
|
||||
Action-Log
|
||||
</Typography>
|
||||
<Chip size="small" variant="outlined" label={log.length} />
|
||||
{errorCount > 0 ? <Chip size="small" color="error" label={`${errorCount} Fehler`} /> : null}
|
||||
</ButtonBase>
|
||||
<Tooltip title="Log als Text kopieren">
|
||||
<span>
|
||||
<IconButton size="small" onClick={this.handleCopyLog} disabled={log.length === 0}>
|
||||
<ContentCopyIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</span>
|
||||
</Tooltip>
|
||||
<Tooltip title="Log leeren">
|
||||
<span>
|
||||
<IconButton size="small" onClick={this.handleClearLog} disabled={log.length === 0}>
|
||||
<ClearIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</span>
|
||||
</Tooltip>
|
||||
</Stack>
|
||||
|
||||
{logOpen ? (
|
||||
<Box
|
||||
ref={this.logRef}
|
||||
component="div"
|
||||
sx={{
|
||||
m: 0,
|
||||
mx: 1.5,
|
||||
mb: 1,
|
||||
p: 1,
|
||||
borderRadius: 1,
|
||||
bgcolor: 'action.hover',
|
||||
fontFamily: 'monospace',
|
||||
fontSize: 11,
|
||||
lineHeight: 1.6,
|
||||
maxHeight: '40vh',
|
||||
overflow: 'auto',
|
||||
}}
|
||||
>
|
||||
{log.length === 0 ? (
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
Noch keine Action-Aufrufe protokolliert.
|
||||
</Typography>
|
||||
) : (
|
||||
log.map((entry) => this.renderLogLine(entry))
|
||||
)}
|
||||
</Box>
|
||||
) : null}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
/** Docked side panel with the automat list, filter and action log. */
|
||||
renderPanel() {
|
||||
const { names, expanded, query } = this.state;
|
||||
const registry = getRegistry();
|
||||
const needle = query.trim().toLowerCase();
|
||||
const entries = names
|
||||
@@ -444,91 +784,108 @@ class AutomatInspector extends PureComponent {
|
||||
const allExpanded = names.length > 0 && names.every((name) => expanded[name]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Badge
|
||||
badgeContent={names.length}
|
||||
color="secondary"
|
||||
overlap="circular"
|
||||
invisible={names.length === 0}
|
||||
sx={{ position: 'fixed', right: 16, bottom: 16, zIndex: (theme) => theme.zIndex.modal + 1 }}
|
||||
<Box
|
||||
component="aside"
|
||||
aria-label="Automat-Inspector"
|
||||
sx={{
|
||||
width: { xs: '100%', sm: 560 },
|
||||
maxWidth: '100%',
|
||||
flexShrink: 0,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
position: 'sticky',
|
||||
top: 0,
|
||||
alignSelf: 'flex-start',
|
||||
height: '100vh',
|
||||
borderLeft: '1px solid',
|
||||
borderColor: 'divider',
|
||||
bgcolor: 'background.paper',
|
||||
zIndex: (theme) => theme.zIndex.appBar,
|
||||
}}
|
||||
>
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
spacing={1}
|
||||
sx={{ px: 2, py: 1.25, borderBottom: '1px solid', borderColor: 'divider' }}
|
||||
>
|
||||
<Tooltip title="Automat-Inspector (Alt+Shift+A)">
|
||||
<Fab
|
||||
size="small"
|
||||
color="primary"
|
||||
aria-label="Automat-Inspector umschalten"
|
||||
aria-pressed={open}
|
||||
onClick={this.handleToggle}
|
||||
>
|
||||
<BugReportIcon fontSize="small" />
|
||||
</Fab>
|
||||
<BugReportIcon fontSize="small" color="primary" />
|
||||
<Typography variant="subtitle1" sx={{ fontWeight: 600, flexGrow: 1 }}>
|
||||
Automat-Inspector
|
||||
</Typography>
|
||||
<Chip size="small" label={`${names.length} registriert`} />
|
||||
<Tooltip title="Registry neu einlesen">
|
||||
<IconButton size="small" onClick={this.handleRefresh}>
|
||||
<RefreshIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Badge>
|
||||
<Tooltip title={allExpanded ? 'Alle einklappen' : 'Alle ausklappen'}>
|
||||
<IconButton size="small" onClick={this.handleToggleAll}>
|
||||
{allExpanded ? <UnfoldLessIcon fontSize="small" /> : <UnfoldMoreIcon fontSize="small" />}
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title="Schließen">
|
||||
<IconButton size="small" onClick={this.handleClose}>
|
||||
<CloseIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Stack>
|
||||
|
||||
<Drawer
|
||||
anchor="right"
|
||||
open={open}
|
||||
onClose={this.handleClose}
|
||||
slotProps={{
|
||||
transition: { timeout: 0 },
|
||||
paper: {
|
||||
sx: {
|
||||
width: { xs: '100%', sm: 560 },
|
||||
maxWidth: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
spacing={1}
|
||||
sx={{ px: 2, py: 1.25, borderBottom: '1px solid', borderColor: 'divider' }}
|
||||
>
|
||||
<BugReportIcon fontSize="small" color="primary" />
|
||||
<Typography variant="subtitle1" sx={{ fontWeight: 600, flexGrow: 1 }}>
|
||||
Automat-Inspector
|
||||
<Box sx={{ px: 2, py: 1 }}>
|
||||
<TextField
|
||||
size="small"
|
||||
fullWidth
|
||||
placeholder="Name filtern …"
|
||||
value={query}
|
||||
onChange={this.handleQueryChange}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ flexGrow: 1, minHeight: 0, overflowY: 'auto', px: 1.5, pb: 2 }}>
|
||||
{entries.length === 0 ? (
|
||||
<Typography variant="body2" color="text.secondary" sx={{ p: 2 }}>
|
||||
{names.length === 0 ? 'Kein Automat registriert.' : 'Kein Automat passt zum Filter.'}
|
||||
</Typography>
|
||||
<Chip size="small" label={`${names.length} registriert`} />
|
||||
<Tooltip title="Registry neu einlesen">
|
||||
<IconButton size="small" onClick={this.handleRefresh}>
|
||||
<RefreshIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title={allExpanded ? 'Alle einklappen' : 'Alle ausklappen'}>
|
||||
<IconButton size="small" onClick={this.handleToggleAll}>
|
||||
{allExpanded ? <UnfoldLessIcon fontSize="small" /> : <UnfoldMoreIcon fontSize="small" />}
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title="Schließen">
|
||||
<IconButton size="small" onClick={this.handleClose}>
|
||||
<CloseIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Stack>
|
||||
) : (
|
||||
<Stack spacing={1}>
|
||||
{entries.map((automat) => this.renderEntry(automat, Boolean(expanded[automat.name])))}
|
||||
</Stack>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<Box sx={{ px: 2, py: 1 }}>
|
||||
<TextField
|
||||
size="small"
|
||||
fullWidth
|
||||
placeholder="Name filtern …"
|
||||
value={query}
|
||||
onChange={this.handleQueryChange}
|
||||
/>
|
||||
</Box>
|
||||
{this.renderLog()}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
<Box sx={{ flexGrow: 1, minHeight: 0, overflowY: 'auto', px: 1.5, pb: 2 }}>
|
||||
{entries.length === 0 ? (
|
||||
<Typography variant="body2" color="text.secondary" sx={{ p: 2 }}>
|
||||
{names.length === 0 ? 'Kein Automat registriert.' : 'Kein Automat passt zum Filter.'}
|
||||
</Typography>
|
||||
) : (
|
||||
<Stack spacing={1}>{entries.map((automat) => this.renderEntry(automat, Boolean(expanded[automat.name])))}</Stack>
|
||||
)}
|
||||
</Box>
|
||||
</Drawer>
|
||||
render() {
|
||||
const { open, names } = this.state;
|
||||
|
||||
return (
|
||||
<>
|
||||
{open ? null : (
|
||||
<Badge
|
||||
badgeContent={names.length}
|
||||
color="secondary"
|
||||
overlap="circular"
|
||||
invisible={names.length === 0}
|
||||
sx={{ position: 'fixed', right: 16, bottom: 16, zIndex: (theme) => theme.zIndex.modal + 1 }}
|
||||
>
|
||||
<Tooltip title="Automat-Inspector (Alt+Shift+A)">
|
||||
<Fab
|
||||
size="small"
|
||||
color="primary"
|
||||
aria-label="Automat-Inspector umschalten"
|
||||
aria-pressed={open}
|
||||
onClick={this.handleToggle}
|
||||
>
|
||||
<BugReportIcon fontSize="small" />
|
||||
</Fab>
|
||||
</Tooltip>
|
||||
</Badge>
|
||||
)}
|
||||
|
||||
{open ? this.renderPanel() : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user