u
This commit is contained in:
198
src/components/settings/HealthSettingsPanel.jsx
Normal file
198
src/components/settings/HealthSettingsPanel.jsx
Normal file
@@ -0,0 +1,198 @@
|
||||
import { PureComponent } from 'react';
|
||||
import Alert from '@mui/material/Alert';
|
||||
import Box from '@mui/material/Box';
|
||||
import CircularProgress from '@mui/material/CircularProgress';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import ToggleButton from '@mui/material/ToggleButton';
|
||||
import ToggleButtonGroup from '@mui/material/ToggleButtonGroup';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import healthAutomat from '../../automats/healthAutomat';
|
||||
import { BORDER, PRIMARY, SURFACE_RAISED } from '../../colors';
|
||||
|
||||
const W = 800;
|
||||
const H = 240;
|
||||
const PAD = { l: 48, r: 12, t: 12, b: 28 };
|
||||
|
||||
const fmtMs = (v) => `${v.toFixed(v < 10 ? 2 : 1)} ms`;
|
||||
|
||||
function fmtTime(ts, withDate) {
|
||||
const d = new Date(ts);
|
||||
const t = d.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' });
|
||||
return withDate ? `${d.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit' })} ${t}` : t;
|
||||
}
|
||||
|
||||
/** Minimal dependency-free SVG line chart. series: [{ name, color, points }] */
|
||||
function LineChart({ series, withDate }) {
|
||||
const all = series.flatMap((s) => s.points);
|
||||
if (!all.length) {
|
||||
return (
|
||||
<Typography variant="body2" color="text.secondary" sx={{ py: 4, textAlign: 'center' }}>
|
||||
Keine Daten vorhanden.
|
||||
</Typography>
|
||||
);
|
||||
}
|
||||
const xs = all.map((p) => p.timestamp);
|
||||
const minX = Math.min(...xs);
|
||||
const maxX = Math.max(...xs);
|
||||
const maxY = Math.max(...all.map((p) => p.value)) * 1.1 || 1;
|
||||
const x = (t) => PAD.l + (maxX === minX ? 0.5 : (t - minX) / (maxX - minX)) * (W - PAD.l - PAD.r);
|
||||
const y = (v) => H - PAD.b - (v / maxY) * (H - PAD.t - PAD.b);
|
||||
const yTicks = [0, 0.25, 0.5, 0.75, 1].map((f) => f * maxY);
|
||||
const xTicks = [0, 0.25, 0.5, 0.75, 1].map((f) => minX + f * (maxX - minX));
|
||||
|
||||
return (
|
||||
<svg viewBox={`0 0 ${W} ${H}`} width="100%" style={{ display: 'block' }}>
|
||||
{yTicks.map((v) => (
|
||||
<g key={v}>
|
||||
<line x1={PAD.l} x2={W - PAD.r} y1={y(v)} y2={y(v)} stroke={BORDER} strokeWidth="1" />
|
||||
<text x={PAD.l - 6} y={y(v) + 4} fontSize="11" textAnchor="end" fill="#6b7280">
|
||||
{v.toFixed(v < 10 ? 1 : 0)}
|
||||
</text>
|
||||
</g>
|
||||
))}
|
||||
{xTicks.map((t) => (
|
||||
<text key={t} x={x(t)} y={H - 8} fontSize="11" textAnchor="middle" fill="#6b7280">
|
||||
{fmtTime(t, withDate)}
|
||||
</text>
|
||||
))}
|
||||
{series.map((s) => (
|
||||
<polyline
|
||||
key={s.name}
|
||||
fill="none"
|
||||
stroke={s.color}
|
||||
strokeWidth="1.8"
|
||||
strokeLinejoin="round"
|
||||
points={s.points.map((p) => `${x(p.timestamp)},${y(p.value)}`).join(' ')}
|
||||
/>
|
||||
))}
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function Legend({ series }) {
|
||||
return (
|
||||
<Stack direction="row" spacing={2} sx={{ mb: 1, flexWrap: 'wrap' }}>
|
||||
{series.map((s) => {
|
||||
const last = s.points[s.points.length - 1];
|
||||
return (
|
||||
<Stack key={s.name} direction="row" spacing={0.75} alignItems="center">
|
||||
<Box sx={{ width: 12, height: 3, bgcolor: s.color, borderRadius: 1 }} />
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
{s.name}
|
||||
{last ? `: ${fmtMs(last.value)}` : ''}
|
||||
</Typography>
|
||||
</Stack>
|
||||
);
|
||||
})}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
function ChartCard({ title, series, withDate }) {
|
||||
return (
|
||||
<Box sx={{ border: '1px solid', borderColor: 'divider', borderRadius: 2, bgcolor: SURFACE_RAISED, p: 2, mb: 2 }}>
|
||||
<Typography variant="subtitle2" fontWeight={600} sx={{ mb: 1 }}>
|
||||
{title}
|
||||
</Typography>
|
||||
<Legend series={series} />
|
||||
<LineChart series={series} withDate={withDate} />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default class HealthSettingsPanel extends PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
const { status, error, range, raw, avg, max } = healthAutomat.state;
|
||||
this.state = { status, error, range, raw, avg, max };
|
||||
this.polling = false;
|
||||
this.handleRange = this.handleRange.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.unsub = healthAutomat.subscribe(this, ['status', 'error', 'range', 'raw', 'avg', 'max']);
|
||||
this.syncPolling();
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (prevProps.active !== this.props.active) this.syncPolling();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.unsub?.();
|
||||
if (this.polling) healthAutomat.actions.stopPolling();
|
||||
this.polling = false;
|
||||
}
|
||||
|
||||
// Poll only while the tab is visible.
|
||||
syncPolling() {
|
||||
const active = this.props.active !== false;
|
||||
if (active && !this.polling) {
|
||||
this.polling = true;
|
||||
healthAutomat.actions.startPolling();
|
||||
} else if (!active && this.polling) {
|
||||
this.polling = false;
|
||||
healthAutomat.actions.stopPolling();
|
||||
}
|
||||
}
|
||||
|
||||
handleRange(_e, value) {
|
||||
if (value) healthAutomat.actions.setRange(value);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { status, error, range, raw, avg, max } = this.state;
|
||||
const isAll = range === 'all';
|
||||
return (
|
||||
<Box>
|
||||
<Stack direction="row" alignItems="center" justifyContent="space-between" sx={{ mb: 2 }}>
|
||||
<Typography variant="h6" fontWeight={600}>
|
||||
Gesundheit · Latenz
|
||||
</Typography>
|
||||
<ToggleButtonGroup size="small" exclusive value={range} onChange={this.handleRange}>
|
||||
<ToggleButton value="3h">Letzte 3 Std.</ToggleButton>
|
||||
<ToggleButton value="all">Verlauf (30 Tage)</ToggleButton>
|
||||
</ToggleButtonGroup>
|
||||
</Stack>
|
||||
|
||||
{status === 'failed' && (
|
||||
<Alert severity="error" sx={{ mb: 2 }}>
|
||||
{error || 'Latenzdaten konnten nicht geladen werden.'}
|
||||
</Alert>
|
||||
)}
|
||||
{status === 'loading' && (
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center', py: 4 }}>
|
||||
<CircularProgress size={28} />
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{status !== 'loading' && (
|
||||
<>
|
||||
{!isAll && (
|
||||
<ChartCard
|
||||
title="Latenz pro Minute (letzte 3 Stunden)"
|
||||
series={[{ name: 'Latenz', color: PRIMARY, points: raw }]}
|
||||
/>
|
||||
)}
|
||||
{isAll && (
|
||||
<>
|
||||
<ChartCard
|
||||
title="Rollierender Durchschnitt und Maximum (4 Std.-Fenster)"
|
||||
withDate
|
||||
series={[
|
||||
{ name: 'Durchschnitt', color: PRIMARY, points: avg },
|
||||
{ name: 'Maximum', color: '#d97706', points: max },
|
||||
]}
|
||||
/>
|
||||
<ChartCard
|
||||
title="Letzte 3 Stunden"
|
||||
series={[{ name: 'Latenz', color: PRIMARY, points: raw }]}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -21,11 +21,14 @@ import ImportExportSettingsPanel from './ImportExportSettingsPanel';
|
||||
import PosTerminalSettingsPanel from './PosTerminalSettingsPanel';
|
||||
import McpAuthorizationsPanel from './McpAuthorizationsPanel';
|
||||
import BackupSettingsPanel from './BackupSettingsPanel';
|
||||
import HealthSettingsPanel from './HealthSettingsPanel';
|
||||
import MonitorHeartOutlinedIcon from '@mui/icons-material/MonitorHeartOutlined';
|
||||
import SecurityIcon from '@mui/icons-material/Security';
|
||||
import BackupTableOutlinedIcon from '@mui/icons-material/BackupTableOutlined';
|
||||
import { SURFACE_BASE } from '../../colors';
|
||||
|
||||
const TAB_KEYS = {
|
||||
health: 'health',
|
||||
company: 'company',
|
||||
keys: 'keys',
|
||||
numbers: 'numbers',
|
||||
@@ -37,6 +40,7 @@ const TAB_KEYS = {
|
||||
};
|
||||
|
||||
const TABS = [
|
||||
{ id: TAB_KEYS.health, label: 'Gesundheit', icon: <MonitorHeartOutlinedIcon />, Panel: HealthSettingsPanel },
|
||||
{ id: TAB_KEYS.company, label: 'Unternehmen', icon: <BusinessIcon />, Panel: CompanySettingsPanel },
|
||||
{ id: TAB_KEYS.keys, label: 'Einstellungen', icon: <TuneIcon />, Panel: SettingsKeysPanel },
|
||||
{ id: TAB_KEYS.numbers, label: 'Nummernkreise', icon: <NumbersIcon />, Panel: NumberSettingsPanel },
|
||||
@@ -50,14 +54,14 @@ const TABS = [
|
||||
export default class SettingsDialog extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = { tab: TAB_KEYS.company };
|
||||
this.state = { tab: TAB_KEYS.health };
|
||||
this.handleTabChange = this.handleTabChange.bind(this);
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const { open } = this.props;
|
||||
if (!open && prevProps.open) {
|
||||
this.setState({ tab: TAB_KEYS.company });
|
||||
this.setState({ tab: TAB_KEYS.health });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -181,7 +185,7 @@ export default class SettingsDialog extends Component {
|
||||
display: t.id === tab ? 'block' : 'none',
|
||||
}}
|
||||
>
|
||||
<t.Panel />
|
||||
<t.Panel active={t.id === tab} />
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
|
||||
@@ -7,3 +7,4 @@ export { default as PosTerminalSettingsPanel } from './PosTerminalSettingsPanel'
|
||||
export { default as ImportExportSettingsPanel } from './ImportExportSettingsPanel';
|
||||
export { default as McpAuthorizationsPanel } from './McpAuthorizationsPanel';
|
||||
export { default as BackupSettingsPanel } from './BackupSettingsPanel';
|
||||
export { default as HealthSettingsPanel } from './HealthSettingsPanel';
|
||||
|
||||
Reference in New Issue
Block a user