/** * Meldungen. * * Die Seite, die in Phase 12 noch als „noch nicht verfuegbar" stand — mit der * Begruendung, eine leere Liste hiesse „keine Probleme" und wuerde bedeuten „es * wird nicht geprueft". Jetzt gibt es die Pruefung, und die Seite sagt, worauf * geachtet wird. * * Das Regelwerk steht deshalb mit auf der Seite: Eine leere Meldungsliste ist * erst dann eine gute Nachricht, wenn man weiss, was ueberhaupt geprueft wird. */ import { useCallback, useState } from 'react'; import { useApiResource } from '../../api/useApiResource'; import { EmptyState, ErrorState, LoadingState } from '../../components/PageState'; import { acknowledgeAlert, fetchAlertOverview, fetchAlerts, resolveAlert, } from './alertsApi'; import type { Alert, AlertOverview, AlertSeverity } from './alertsApi'; /** Zeigt die Meldungen. */ export function AlertsPage(): React.JSX.Element { const [onlyActive, setOnlyActive] = useState(true); const [actionCounter, setActionCounter] = useState(0); const [actionError, setActionError] = useState(null); const loadAlerts = useCallback( (abortSignal: AbortSignal) => fetchAlerts(onlyActive, abortSignal), [onlyActive], ); const { loadState, data, loadError, reload } = useApiResource( loadAlerts, `${String(onlyActive)}|${String(actionCounter)}`, ); const loadOverview = useCallback( (abortSignal: AbortSignal) => fetchAlertOverview(abortSignal), [], ); const overviewResource = useApiResource(loadOverview, String(actionCounter)); /** Fuehrt eine Handlung an einer Meldung aus und laedt neu. */ async function runAlertAction(action: () => Promise): Promise { try { await action(); setActionError(null); setActionCounter((previousCounter) => previousCounter + 1); } catch (caughtError) { setActionError(caughtError instanceof Error ? caughtError.message : 'Unbekannter Fehler'); } } return (

Meldungen

{overviewResource.data !== null && ( {overviewResource.data.available_rule_count} von {overviewResource.data.rules.length}{' '} Regeln werden geprueft )}
{overviewResource.data !== null && }
{actionError !== null && (

{actionError}

)} {loadState === 'loading' && } {loadState === 'failed' && loadError !== null && ( )} {loadState === 'loaded' && data !== null && data.length === 0 && ( )} {loadState === 'loaded' && data !== null && data.length > 0 && (
    {data.map((alert) => ( void runAlertAction(() => acknowledgeAlert(alert.id, ''))} onResolve={() => void runAlertAction(() => resolveAlert(alert.id, 'Von Hand geschlossen.'))} /> ))}
)} {overviewResource.data !== null && }
); } /** Eigenschaften der Zusammenfassung. */ interface SummaryBarProperties { /** Die Meldungslage samt Regelwerk. */ readonly overview: AlertOverview; } /** Zeigt die Meldungslage in Zahlen. */ function SummaryBar({ overview }: SummaryBarProperties): React.JSX.Element { const { summary } = overview; return (
0 ? 'critical' : undefined} /> 0 ? 'high' : undefined} />
); } /** Eigenschaften einer Kennzahl der Zusammenfassung. */ interface SummaryFigureProperties { /** Beschriftung. */ readonly label: string; /** Der Wert. */ readonly value: number; /** Statusfarbe, sofern der Wert eine ist. */ readonly severity?: AlertSeverity | undefined; } /** Zeigt eine einzelne Kennzahl. */ function SummaryFigure({ label, value, severity }: SummaryFigureProperties): React.JSX.Element { return (
{value} {label}
); } /** Eigenschaften einer Meldungskarte. */ interface AlertCardProperties { /** Die dargestellte Meldung. */ readonly alert: Alert; /** Nimmt die Meldung zur Kenntnis. */ readonly onAcknowledge: () => void; /** Schliesst die Meldung. */ readonly onResolve: () => void; } /** Zeigt eine einzelne Meldung. */ function AlertCard({ alert, onAcknowledge, onResolve }: AlertCardProperties): React.JSX.Element { const isResolved = alert.status === 'resolved'; return (
  • {alert.severity}

    {alert.title}

    {/* Die Wiederholungszahl steht oben: Einmal ist ein Zwischenfall, zwanzigmal ein Zustand. */} {alert.occurrence_count > 1 && ( {alert.occurrence_count}× aufgetreten )}

    {alert.message}

    Seit {formatTimestamp(alert.first_seen_at)} · zuletzt {formatTimestamp(alert.last_seen_at)} · Regel {alert.rule_name}

    {alert.status === 'acknowledged' && (

    Zur Kenntnis genommen am {formatTimestamp(alert.acknowledged_at)}. Die Meldung bleibt offen, bis ihre Ursache verschwindet.

    )} {isResolved && (

    Erledigt am {formatTimestamp(alert.resolved_at)} {alert.resolution_note !== undefined && alert.resolution_note !== '' ? ` — ${alert.resolution_note}` : ''}

    )} {!isResolved && (
    {alert.status === 'open' && ( )}
    )}
  • ); } /** Eigenschaften der Regelliste. */ interface RuleListProperties { /** Die Meldungslage samt Regelwerk. */ readonly overview: AlertOverview; } /** * Zeigt, worauf geachtet wird. * * Ohne diese Liste bliebe offen, ob eine leere Meldungsliste bedeutet „alles in * Ordnung" oder „es wird nichts geprueft". */ function RuleList({ overview }: RuleListProperties): React.JSX.Element { return (

    Worauf geachtet wird

      {overview.rules.map((rule) => (
    • {rule.title} {rule.available ? ( {rule.severity} ) : ( wird nicht geprueft )}

      {rule.available ? rule.description : rule.unavailable_reason}

    • ))}
    ); } /** Bildet einen Schweregrad auf eine Statusfarbe ab. */ function severityClassOf(severity: AlertSeverity): string { switch (severity) { case 'critical': return 'critical'; case 'high': return 'high'; case 'warning': return 'warning'; default: return 'info'; } } /** Schreibt einen Zeitstempel in deutscher Schreibweise. */ function formatTimestamp(isoTimestamp: string | undefined): string { if (isoTimestamp === undefined) { return '—'; } return new Date(isoTimestamp).toLocaleString('de-DE', { dateStyle: 'medium', timeStyle: 'short', }); }